Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cmd): add google analytics #3599

Merged
merged 40 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
b42cb73
add googla analytics
Jul 26, 2023
d0f43e4
add doNotTrack env var
Jul 26, 2023
6b5a2c7
add changelog
Jul 26, 2023
80e1da0
add anlytics into a cobra command
Jul 27, 2023
00dcb4d
improve analyticcs
Jul 27, 2023
8257a24
run make format
Jul 27, 2023
878ffb4
fix file check
Jul 27, 2023
d2b8536
fix env var
Jul 27, 2023
ed524fd
Merge branch 'main' into feat/ga-analytics
Pantani Jul 31, 2023
5297f56
fix send metrics in parallel
Aug 1, 2023
e64c8b1
Merge branch 'main' into feat/ga-analytics
Pantani Aug 10, 2023
4a22f7f
move vars to const
Aug 28, 2023
95f5c8c
Update ignite/cmd/ignite/analytics.go
Pantani Aug 28, 2023
02cd167
Update ignite/cmd/ignite/main.go
Pantani Aug 28, 2023
4b59abd
Merge branch 'main' into feat/ga-analytics
Pantani Aug 28, 2023
d741d73
improve analytics msg
Aug 28, 2023
37d3486
Merge branch 'main' into feat/ga-analytics
Pantani Oct 17, 2023
80ed5f6
update ga pkg
Oct 17, 2023
2831465
Merge branch 'main' into feat/ga-analytics
Pantani Oct 18, 2023
7cb728d
Merge branch 'main' into feat/ga-analytics
Pantani Oct 19, 2023
5758000
Merge branch 'main' into feat/ga-analytics
Pantani Oct 19, 2023
23b8e18
Merge branch 'main' into feat/ga-analytics
Pantani Oct 31, 2023
5ccae09
Merge branch 'main' into feat/ga-analytics
Pantani Nov 7, 2023
f90577a
improve analytics init
Nov 7, 2023
0806603
Merge remote-tracking branch 'origin/main' into feat/ga-analytics
Nov 27, 2023
17b9f8c
use http post request instate a libraty
Nov 27, 2023
e1fad03
add GA Client ID
Nov 28, 2023
08bc355
use postform instead newrequest
Nov 28, 2023
458cf15
use set instead literals for url.Values
Nov 28, 2023
5ab6089
add ga4 http metric send
Nov 29, 2023
ac742e8
fix url and values
Nov 29, 2023
39ad038
chore: small improvements gacli (#3789)
julienrbrt Nov 29, 2023
bf6f3c4
remove sensitive data
Nov 30, 2023
19b47e0
remove sensitive data and improve the parameters
Nov 30, 2023
c9e3b0f
refactor: good default http client for ga (#3791)
julienrbrt Nov 30, 2023
f30ba51
remove `Pallinder/go-randomdata` pkg
Nov 30, 2023
ae79f49
set the ignite telemetry endpoint
Nov 30, 2023
caa2d07
remove the url from the pkg and change some var names
Dec 1, 2023
c14c612
move telemetry to the internal package
Dec 1, 2023
bbdd493
use snake case for anon identity file
Dec 1, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [#3561](https://github.com/ignite/cli/pull/3561) Add GetChainInfo method to plugin system API
- [#3626](https://github.com/ignite/cli/pull/3626) Add logging levels to relayer
- [#3476](https://github.com/ignite/cli/pull/3476) Use `buf.build` binary to code generate from proto files
- [#3599](https://github.com/ignite/cli/pull/3599) Add google analytics
- [#3614](https://github.com/ignite/cli/pull/3614) feat: use DefaultBaseappOptions for app.New method
- [#3536](https://github.com/ignite/cli/pull/3536) Change app.go to v2 and add AppWiring feature
- [#3659](https://github.com/ignite/cli/pull/3659) cosmos-sdk `v0.50.x`
Expand Down
26 changes: 20 additions & 6 deletions ignite/cmd/ignite/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ import (
"errors"
"fmt"
"os"
"sync"

ignitecmd "github.com/ignite/cli/ignite/cmd"
chainconfig "github.com/ignite/cli/ignite/config/chain"
"github.com/ignite/cli/ignite/internal/analytics"
"github.com/ignite/cli/ignite/pkg/clictx"
"github.com/ignite/cli/ignite/pkg/cliui/colors"
"github.com/ignite/cli/ignite/pkg/cliui/icons"
Expand All @@ -20,12 +22,22 @@ func main() {
}

func run() int {
const (
exitCodeOK = 0
exitCodeError = 1
)
ctx := clictx.From(context.Background())
const exitCodeOK, exitCodeError = 0, 1
var wg sync.WaitGroup

defer func() {
if r := recover(); r != nil {
analytics.SendMetric(&wg, os.Args, analytics.WithError(fmt.Errorf("%v", r)))
fmt.Println(r)
os.Exit(exitCodeError)
}
}()

if len(os.Args) > 1 {
analytics.SendMetric(&wg, os.Args)
}

ctx := clictx.From(context.Background())
cmd, cleanUp, err := ignitecmd.New(ctx)
if err != nil {
fmt.Printf("%v\n", err)
Expand All @@ -34,7 +46,6 @@ func run() int {
defer cleanUp()

err = cmd.ExecuteContext(ctx)

if errors.Is(ctx.Err(), context.Canceled) || errors.Is(err, context.Canceled) {
fmt.Println("aborted")
return exitCodeOK
Expand Down Expand Up @@ -64,5 +75,8 @@ func run() int {

return exitCodeError
}

wg.Wait() // waits for all metrics to be sent

return exitCodeOK
}
153 changes: 153 additions & 0 deletions ignite/internal/analytics/analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package analytics

import (
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"sync"

"github.com/manifoldco/promptui"

"github.com/ignite/cli/ignite/pkg/gacli"
"github.com/ignite/cli/ignite/pkg/randstr"
"github.com/ignite/cli/ignite/version"
)

const (
telemetryEndpoint = "https://telemetry-cli.ignite.com"
envDoNotTrack = "DO_NOT_TRACK"
igniteDir = ".ignite"
igniteAnonIdentity = "anon_identity.json"
)

var gaclient gacli.Client

type (
// metric represents an analytics metric.
options struct {
// err sets metrics type as an error metric.
err error
}

// anonIdentity represents an analytics identity file.
anonIdentity struct {
// name represents the username.
Name string `json:"name" yaml:"name"`
// doNotTrack represents the user track choice.
DoNotTrack bool `json:"doNotTrack" yaml:"doNotTrack"`
}
)

func init() {
gaclient = gacli.New(telemetryEndpoint)
}

// Option configures ChainCmd.
type Option func(*options)

// WithError with application command error.
func WithError(error error) Option {
return func(m *options) {
m.err = error
}
}

func SendMetric(wg *sync.WaitGroup, args []string, opts ...Option) {
// only the app name
if len(args) <= 1 {
return
}

// apply analytics options.
var opt options
for _, o := range opts {
o(&opt)
}

envDoNotTrackVar := os.Getenv(envDoNotTrack)
if envDoNotTrackVar == "1" || strings.ToLower(envDoNotTrackVar) == "true" {
return
}

if args[1] == "version" {
return
}

fullCmd := strings.Join(args[1:], " ")

dntInfo, err := checkDNT()
if err != nil || dntInfo.DoNotTrack {
return
}

met := gacli.Metric{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
FullCmd: fullCmd,
SessionID: dntInfo.Name,
Version: version.Version,
}

switch {
case opt.err == nil:
met.Status = "success"
case opt.err != nil:
met.Status = "error"
met.Error = opt.err.Error()
}
met.Cmd = args[1]

wg.Add(1)
go func() {
defer wg.Done()
_ = gaclient.SendMetric(met)
}()
}

func checkDNT() (anonIdentity, error) {
home, err := os.UserHomeDir()
if err != nil {
return anonIdentity{}, err
}
if err := os.Mkdir(filepath.Join(home, igniteDir), 0o700); err != nil && !os.IsExist(err) {
return anonIdentity{}, err
}
identityPath := filepath.Join(home, igniteDir, igniteAnonIdentity)
data, err := os.ReadFile(identityPath)
if err != nil && !os.IsNotExist(err) {
return anonIdentity{}, err
}

var i anonIdentity
if err := json.Unmarshal(data, &i); err == nil {
return i, nil
}

i.Name = randstr.Runes(10)
i.DoNotTrack = false

prompt := promptui.Select{
Label: "Ignite collects metrics about command usage. " +
"All data is anonymous and helps to improve Ignite. " +
"Ignite respect the DNT rules (consoledonottrack.com). " +
"Would you agree to share these metrics with us?",
Items: []string{"Yes", "No"},
}
resultID, _, err := prompt.Run()
if err != nil {
return anonIdentity{}, err
}

if resultID != 0 {
i.DoNotTrack = true
}

data, err = json.Marshal(&i)
if err != nil {
return i, err
}

return i, os.WriteFile(identityPath, data, 0o700)
}
2 changes: 2 additions & 0 deletions ignite/pkg/gacli/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package gacli is a client for Google Analytics to send data points for hint-type=event.
package gacli
134 changes: 96 additions & 38 deletions ignite/pkg/gacli/gacli.go
Original file line number Diff line number Diff line change
@@ -1,63 +1,121 @@
// Package gacli is a client for Google Analytics to send data points for hint-type=event.
package gacli

import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)

const (
endpoint = "https://www.google-analytics.com/collect"
type (
// Client is an analytics client.
Client struct {
endpoint string
measurementID string // Google Analytics measurement ID.
apiSecret string // Google Analytics API secret.
httpClient http.Client
}
// Body analytics metrics body.
Body struct {
ClientID string `json:"client_id"`
Events []Event `json:"events"`
}
// Event analytics event.
Event struct {
Name string `json:"name"`
Params Metric `json:"params"`
}
// Metric represents a data point.
Metric struct {
Status string `json:"status,omitempty"`
OS string `json:"os,omitempty"`
Arch string `json:"arch,omitempty"`
FullCmd string `json:"full_command,omitempty"`
Cmd string `json:"command,omitempty"`
Error string `json:"error,omitempty"`
Version string `json:"version,omitempty"`
SessionID string `json:"session_id,omitempty"`
EngagementTimeMsec string `json:"engagement_time_msec,omitempty"`
}
)

// Client is an analytics client.
type Client struct {
id string // Google Analytics ID
// Option configures code generation.
type Option func(*Client)

// WithMeasurementID adds an analytics measurement ID.
func WithMeasurementID(measurementID string) Option {
return func(c *Client) {
c.measurementID = measurementID
}
}

// New creates a new analytics client for Segment.io with Segment's
// endpoint and access key.
func New(id string) *Client {
return &Client{
id: id,
// WithAPISecret adds an analytics API secret.
func WithAPISecret(secret string) Option {
return func(c *Client) {
c.apiSecret = secret
}
}

// Metric represents a data point.
type Metric struct {
Category string
Action string
Label string
Value string
User string
Version string
// New creates a new analytics client with
// measure id and secret key.
func New(endpoint string, opts ...Option) Client {
c := Client{
endpoint: endpoint,
httpClient: http.Client{
Timeout: 1500 * time.Millisecond,
},
}
// apply analytics options.
for _, o := range opts {
o(&c)
}
return c
}

// Send sends metrics to GA.
func (c *Client) Send(metric Metric) error {
v := url.Values{
"v": {"1"},
"tid": {c.id},
"cid": {metric.User},
"t": {"event"},
"ec": {metric.Category},
"ea": {metric.Action},
"ua": {"Opera/9.80 (Windows NT 6.0) Presto/2.12.388 Version/12.14"},
// Send sends metrics to analytics.
func (c Client) Send(body Body) error {
// encode body
encoded, err := json.Marshal(body)
if err != nil {
return err
}
if metric.Label != "" {
v.Set("el", metric.Label)

requestURL, err := url.Parse(c.endpoint)
if err != nil {
return err
}
if metric.Value != "" {
v.Set("ev", metric.Value)
v := requestURL.Query()
if c.measurementID != "" {
v.Set("measurement_id", c.measurementID)
}
if metric.Version != "" {
v.Set("an", metric.Version)
v.Set("av", metric.Version)
if c.apiSecret != "" {
v.Set("api_secret", c.apiSecret)
}
resp, err := http.PostForm(endpoint, v)
requestURL.RawQuery = v.Encode()

// Create an HTTP request with the payload
resp, err := c.httpClient.Post(requestURL.String(), "application/json", bytes.NewBuffer(encoded))
if err != nil {
return err
return fmt.Errorf("error creating HTTP request: %w", err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK &&
resp.StatusCode != http.StatusNoContent {
return fmt.Errorf("error sending event. Status code: %d", resp.StatusCode)
}
return nil
}

func (c Client) SendMetric(metric Metric) error {
metric.EngagementTimeMsec = "100"
return c.Send(Body{
ClientID: metric.SessionID,
Events: []Event{{
Name: metric.Cmd,
Params: metric,
}},
})
}
Loading