-
Notifications
You must be signed in to change notification settings - Fork 551
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add googla analytics * add doNotTrack env var * add changelog * add anlytics into a cobra command * improve analyticcs * run make format * fix file check * fix env var * fix send metrics in parallel * move vars to const * Update ignite/cmd/ignite/analytics.go Co-authored-by: Jerónimo Albi <[email protected]> * Update ignite/cmd/ignite/main.go Co-authored-by: Jerónimo Albi <[email protected]> * improve analytics msg * update ga pkg * improve analytics init * use http post request instate a libraty * add GA Client ID * use postform instead newrequest * use set instead literals for url.Values * add ga4 http metric send * fix url and values * chore: small improvements gacli (#3789) * chore: small improvements gacli * duplicate * remove sensitive data * remove sensitive data and improve the parameters * refactor: good default http client for ga (#3791) * refactor: good default http client * use client * ilker feedback * lint * remove `Pallinder/go-randomdata` pkg * set the ignite telemetry endpoint * remove the url from the pkg and change some var names * move telemetry to the internal package * use snake case for anon identity file --------- Co-authored-by: Pantani <Pantani> Co-authored-by: Jerónimo Albi <[email protected]> Co-authored-by: Julien Robert <[email protected]>
- Loading branch information
1 parent
e510a29
commit c7cc3a5
Showing
5 changed files
with
272 additions
and
44 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}}, | ||
}) | ||
} |