forked from ignite/cli
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
290 additions
and
46 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,154 @@ | ||
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 | ||
} | ||
} | ||
|
||
// SendMetric send command metrics to analytics. | ||
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) | ||
} | ||
|
||
if args[1] == "version" { | ||
return | ||
} | ||
|
||
dntInfo, err := checkDNT() | ||
if err != nil || dntInfo.DoNotTrack { | ||
return | ||
} | ||
|
||
met := gacli.Metric{ | ||
OS: runtime.GOOS, | ||
Arch: runtime.GOARCH, | ||
FullCmd: strings.Join(args[1:], " "), | ||
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) | ||
}() | ||
} | ||
|
||
// checkDNT check if the user allow to track data or if the DO_NOT_TRACK | ||
// env var is set https://consoledonottrack.com/ | ||
func checkDNT() (anonIdentity, error) { | ||
envDoNotTrackVar := os.Getenv(envDoNotTrack) | ||
if envDoNotTrackVar == "1" || strings.ToLower(envDoNotTrackVar) == "true" { | ||
return anonIdentity{DoNotTrack: true}, nil | ||
} | ||
|
||
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,122 @@ | ||
// 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 metric event 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 | ||
} | ||
|
||
// SendMetric build the metrics and send to analytics. | ||
func (c Client) SendMetric(metric Metric) error { | ||
metric.EngagementTimeMsec = "100" | ||
return c.Send(Body{ | ||
ClientID: metric.SessionID, | ||
Events: []Event{{ | ||
Name: metric.Cmd, | ||
Params: metric, | ||
}}, | ||
}) | ||
} |
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
Oops, something went wrong.