Skip to content

Commit

Permalink
Add Heap integration [CLI-158] (#302)
Browse files Browse the repository at this point in the history
* Add Heap integration

* Add opt-out mechanism

* Fix linter warning

* Move comment

* Add comment
  • Loading branch information
Widcket authored May 24, 2021
1 parent b9317fe commit c5ff007
Show file tree
Hide file tree
Showing 28 changed files with 1,251 additions and 9 deletions.
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -205,11 +205,24 @@ Failed Login hello 7 minutes ago N/A my awesome app
userid: auth0|QXV0aDAgaXMgaGlyaW5nISBhdXRoMC5jb20vY2FyZWVycyAK
```
## Anonymous Analytics
By default, the CLI tracks some anonymous usage events. This helps us understand how the CLI is being used, so we can continue to improve it. You can opt-out by setting the environment variable `AUTH0_CLI_ANALYTICS` to `false`.
### Data sent
Every event tracked sends the following data along with the event name:
- The CLI version.
- The OS name: as determined by the value of `GOOS`, e.g. `windows`.
- The processor architecture: as determined by the value of `GOARCH`, e.g. `amd64`.
- The install ID: an anonymous UUID that is stored in the CLI's config file.
- A timestamp.

## Contributing

Please check the [contributing guidelines](CONTRIBUTING.md).

## Author

[Auth0](https://auth0.com)
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/golang/mock v1.5.0
github.com/golang/snappy v0.0.3 // indirect
github.com/google/go-cmp v0.5.5
github.com/google/uuid v1.2.0
github.com/guiguan/caster v0.0.0-20191104051807-3736c4464f38
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
github.com/klauspost/compress v1.11.9 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,8 @@ github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hf
github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8=
Expand Down
155 changes: 155 additions & 0 deletions internal/analytics/analytics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
package analytics

import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"runtime"
"strings"
"sync"
"time"

"github.com/auth0/auth0-cli/internal/buildinfo"
"github.com/spf13/cobra"
)

const (
eventNamePrefix = "CLI"
analyticsEndpoint = "https://heapanalytics.com/api/track"
appID = "1279799279"
versionKey = "version"
osKey = "os"
archKey = "arch"
)

type Tracker struct {
wg sync.WaitGroup
}

type event struct {
App string `json:"app_id"`
ID string `json:"identity"`
Event string `json:"event"`
Timestamp int64 `json:"timestamp"`
Properties map[string]string `json:"properties"`
}

func NewTracker() *Tracker {
return &Tracker{}
}

func (t *Tracker) TrackFirstLogin(id string) {
eventName := fmt.Sprintf("%s - Auth0 - First Login", eventNamePrefix)
t.track(eventName, id)
}

func (t *Tracker) TrackCommandRun(cmd *cobra.Command, id string) {
eventName := generateRunEventName(cmd.CommandPath())
t.track(eventName, id)
}

func (t *Tracker) Wait(ctx context.Context) {
ch := make(chan struct{})

go func() {
t.wg.Wait()
close(ch)
}()

select {
case <-ch: // waitgroup is done
return
case <-ctx.Done():
return
}
}

func (t *Tracker) track(eventName string, id string) {
if !shouldTrack() {
return
}

event := newEvent(eventName, id)

t.wg.Add(1)
go t.sendEvent(event)
}

func (t *Tracker) sendEvent(event *event) {
jsonEvent, err := json.Marshal(event)
if err != nil {
return
}

req, err := http.NewRequest("POST", analyticsEndpoint, bytes.NewBuffer(jsonEvent))
if err != nil {
return
}

req.Header.Set("Content-Type", "application/json")

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
println(err.Error())
return
}

// defers execute in LIFO order
defer t.wg.Done()
defer resp.Body.Close()
}

func newEvent(eventName string, id string) *event {
return &event{
App: appID,
ID: id,
Event: eventName,
Timestamp: timestamp(),
Properties: map[string]string{
versionKey: buildinfo.Version,
osKey: runtime.GOOS,
archKey: runtime.GOARCH,
},
}
}

func generateRunEventName(command string) string {
return generateEventName(command, "Run")
}

func generateEventName(command string, action string) string {
commands := strings.Split(command, " ")

for i := range commands {
commands[i] = strings.Title(commands[i])
}

if len(commands) == 1 { // the root command
return fmt.Sprintf("%s - %s - %s", eventNamePrefix, commands[0], action)
} else if len(commands) == 2 { // a top-level command e.g. auth0 apps
return fmt.Sprintf("%s - %s - %s - %s", eventNamePrefix, commands[0], commands[1], action)
} else if len(commands) >= 3 {
return fmt.Sprintf("%s - %s - %s - %s", eventNamePrefix, commands[1], strings.Join(commands[2:], " "), action)
}

return eventNamePrefix
}

func shouldTrack() bool {
if os.Getenv("AUTH0_CLI_ANALYTICS") == "false" || buildinfo.Version == "" { // Do not track debug builds
return false
}

return true
}

func timestamp() int64 {
t := time.Now()
s := t.Unix() * 1e3
ms := int64(t.Nanosecond()) / 1e6
return s + ms
}
74 changes: 74 additions & 0 deletions internal/analytics/analytics_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package analytics

import (
"runtime"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func TestGenerateEventName(t *testing.T) {
t.Run("generates from root command run", func(t *testing.T) {
want := "CLI - Auth0 - Action"
got := generateEventName("auth0", "Action")
assert.Equal(t, want, got)
})

t.Run("generates from top-level command run", func(t *testing.T) {
want := "CLI - Auth0 - Apps - Action"
got := generateEventName("auth0 apps", "Action")
assert.Equal(t, want, got)
})

t.Run("generates from subcommand run", func(t *testing.T) {
want := "CLI - Apps - List - Action"
got := generateEventName("auth0 apps list", "Action")
assert.Equal(t, want, got)
})

t.Run("generates from deep subcommand run", func(t *testing.T) {
want := "CLI - Apis - Scopes List - Action"
got := generateEventName("auth0 apis scopes list", "Action")
assert.Equal(t, want, got)
})
}

func TestGenerateRunEventName(t *testing.T) {
t.Run("generates from root command run", func(t *testing.T) {
want := "CLI - Auth0 - Run"
got := generateRunEventName("auth0")
assert.Equal(t, want, got)
})

t.Run("generates from top-level command run", func(t *testing.T) {
want := "CLI - Auth0 - Apps - Run"
got := generateRunEventName("auth0 apps")
assert.Equal(t, want, got)
})

t.Run("generates from subcommand run", func(t *testing.T) {
want := "CLI - Apps - List - Run"
got := generateRunEventName("auth0 apps list")
assert.Equal(t, want, got)
})

t.Run("generates from deep subcommand run", func(t *testing.T) {
want := "CLI - Apis - Scopes List - Run"
got := generateRunEventName("auth0 apis scopes list")
assert.Equal(t, want, got)
})
}

func TestNewEvent(t *testing.T) {
t.Run("creates a new event instance", func(t *testing.T) {
event := newEvent("event", "id")
// Assert that the interval between the event timestamp and now is within 1 second
assert.WithinDuration(t, time.Now(), time.Unix(0, event.Timestamp * int64(1000000)), 1 * time.Second)
assert.Equal(t, event.App, appID)
assert.Equal(t, event.Event, "event")
assert.Equal(t, event.ID, "id")
assert.Equal(t, event.Properties[osKey], runtime.GOOS)
assert.Equal(t, event.Properties[archKey], runtime.GOARCH)
})
}
19 changes: 17 additions & 2 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import (
"sync"
"time"

"github.com/auth0/auth0-cli/internal/analytics"
"github.com/auth0/auth0-cli/internal/ansi"
"github.com/auth0/auth0-cli/internal/auth"
"github.com/auth0/auth0-cli/internal/auth0"
"github.com/auth0/auth0-cli/internal/buildinfo"
"github.com/auth0/auth0-cli/internal/display"
"github.com/google/uuid"
"github.com/lestrrat-go/jwx/jwt"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand All @@ -34,6 +36,7 @@ const (
// config defines the exact set of tenants, access tokens, which only exists
// for a particular user's machine.
type config struct {
InstallID string `json:"install_id,omitempty"`
DefaultTenant string `json:"default_tenant"`
Tenants map[string]tenant `json:"tenants"`
}
Expand Down Expand Up @@ -72,6 +75,8 @@ type cli struct {
// core primitives exposed to command builders.
api *auth0.API
renderer *display.Renderer
tracker *analytics.Tracker
context context.Context
// set of flags which are user specified.
debug bool
tenant string
Expand Down Expand Up @@ -373,8 +378,18 @@ func (c *cli) setFirstCommandRun(clientID string, command string) error {

c.config.Tenants[tenant.Domain] = tenant

if err := c.persistConfig(); err != nil {
return fmt.Errorf("Unexpected error persisting config: %w", err)
return nil
}

func checkInstallID(c *cli) error {
if c.config.InstallID == "" {
c.config.InstallID = uuid.NewString()

if err := c.persistConfig(); err != nil {
return fmt.Errorf("unexpected error persisting config: %w", err)
}

c.tracker.TrackFirstLogin(c.config.InstallID)
}

return nil
Expand Down
10 changes: 7 additions & 3 deletions internal/cli/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func RunLogin(ctx context.Context, cli *cli, expired bool) (tenant, error) {
a := &auth.Authenticator{}
state, err := a.Start(ctx)
if err != nil {
return tenant{}, fmt.Errorf("could not start the authentication process: %w.", err)
return tenant{}, fmt.Errorf("Could not start the authentication process: %w.", err)
}

fmt.Printf("Your Device Confirmation code is: %s\n\n", ansi.Bold(state.UserCode))
Expand Down Expand Up @@ -88,7 +88,11 @@ func RunLogin(ctx context.Context, cli *cli, expired bool) (tenant, error) {
}
err = cli.addTenant(t)
if err != nil {
return tenant{}, fmt.Errorf("Unexpected error adding tenant to config: %w", err)
return tenant{}, fmt.Errorf("Could not add tenant to config: %w", err)
}

if err := checkInstallID(cli); err != nil {
return tenant{}, fmt.Errorf("Could not update config: %w", err)
}

if cli.config.DefaultTenant != res.Domain {
Expand All @@ -98,7 +102,7 @@ func RunLogin(ctx context.Context, cli *cli, expired bool) (tenant, error) {
}
cli.config.DefaultTenant = res.Domain
if err := cli.persistConfig(); err != nil {
return tenant{}, fmt.Errorf("An error occurred while setting the default tenant: %w", err)
cli.renderer.Warnf("Could not set the default tenant, please try 'auth0 tenants use %s': %w", res.Domain, err)
}
}

Expand Down
Loading

0 comments on commit c5ff007

Please sign in to comment.