Skip to content

Commit 18eca38

Browse files
authored
[telemetry] Add uid, org_id, and nix build event (#2018)
## Summary This PR improves segment logging by: * Adding orgID if user is logged in. * Using userID if user is logged in. * Removing anonymousID if userID is non zero. (Segment will remove userID if we pass an anonymousID) * Add new event to track `nix build` This allows us to: * Use orgID to segment data, potentially allowing individual orgs to export/use their own data as needed. * Track logged in users. * Track which packages are slow to build. Private Notes: * We still respect `DO_NOT_TRACK` even if user is logged in. * userID and orgID data is only sent if user is logged in. cc: @Lagoja ## How was it tested? Logged to development segment/amplitude, built a quick table showing nix average build times: <img width="1208" alt="image" src="https://github.com/jetify-com/devbox/assets/544948/4ec2d49e-c886-4032-a5c6-9dff6986d780">
1 parent a55a74b commit 18eca38

File tree

6 files changed

+84
-21
lines changed

6 files changed

+84
-21
lines changed

internal/boxcli/auth.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ func loginCmd() *cobra.Command {
4646
if err != nil {
4747
return err
4848
}
49+
// TODO: all uses of IDClaims() are broken when using a static
50+
// non-expiring token (i.e. API_TOKEN)
4951
fmt.Fprintf(cmd.ErrOrStderr(), "Logged in as: %s\n", t.IDClaims().Email)
5052
return nil
5153
},

internal/boxcli/log.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,14 @@ func doLogCommand(cmd *cobra.Command, args []string) error {
3333
return usererr.New("expected a start-time argument for logging the shell-ready event")
3434
}
3535
telemetry.Event(telemetry.EventShellReady, telemetry.Metadata{
36-
CommandStart: telemetry.ParseShellStart(args[1]),
36+
EventStart: telemetry.ParseShellStart(args[1]),
3737
})
3838
case "shell-interactive":
3939
if len(args) < 2 {
4040
return usererr.New("expected a start-time argument for logging the shell-interactive event")
4141
}
4242
telemetry.Event(telemetry.EventShellInteractive, telemetry.Metadata{
43-
CommandStart: telemetry.ParseShellStart(args[1]),
43+
EventStart: telemetry.ParseShellStart(args[1]),
4444
})
4545
}
4646
return usererr.New("unrecognized event-name %s for command: %s", args[0], cmd.CommandPath())

internal/devbox/packages.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"runtime/trace"
1515
"slices"
1616
"strings"
17+
"time"
1718

1819
"github.com/fatih/color"
1920
"github.com/pkg/errors"
@@ -26,6 +27,7 @@ import (
2627
"go.jetpack.io/devbox/internal/lock"
2728
"go.jetpack.io/devbox/internal/redact"
2829
"go.jetpack.io/devbox/internal/shellgen"
30+
"go.jetpack.io/devbox/internal/telemetry"
2931

3032
"go.jetpack.io/devbox/internal/boxcli/usererr"
3133
"go.jetpack.io/devbox/internal/debug"
@@ -485,12 +487,17 @@ func (d *Devbox) installNixPackagesToStore(ctx context.Context, mode installMode
485487
}
486488
}
487489
for _, installable := range installables {
490+
eventStart := time.Now()
488491
err = nix.Build(ctx, args, installable)
489492
if err != nil {
490493
fmt.Fprintf(d.stderr, "%s: ", stepMsg)
491494
color.New(color.FgRed).Fprintf(d.stderr, "Fail\n")
492495
return packageInstallErrorHandler(err, pkg, installable)
493496
}
497+
telemetry.Event(telemetry.EventNixBuildSuccess, telemetry.Metadata{
498+
EventStart: eventStart,
499+
Packages: []string{pkg.Raw},
500+
})
494501
}
495502

496503
fmt.Fprintf(d.stderr, "%s: ", stepMsg)

internal/devbox/providers/identity/identity.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import (
1515

1616
var scopes = []string{"openid", "offline_access", "email", "profile"}
1717

18-
type Provider struct{}
18+
type Provider struct {
19+
cachedAccessTokenFromAPIToken *session.Token
20+
}
1921

2022
var singleton *Provider = &Provider{}
2123

@@ -24,7 +26,7 @@ func Get() *Provider {
2426
}
2527

2628
func (p *Provider) GenSession(ctx context.Context) (*session.Token, error) {
27-
if t, err := p.getTokenFromPAT(ctx); err != nil || t != nil {
29+
if t, err := p.getAccessTokenFromAPIToken(ctx); err != nil || t != nil {
2830
return t, err
2931
}
3032

@@ -35,6 +37,27 @@ func (p *Provider) GenSession(ctx context.Context) (*session.Token, error) {
3537
return c.GetSession(ctx)
3638
}
3739

40+
func (p *Provider) Peek() (*session.Token, error) {
41+
if p.cachedAccessTokenFromAPIToken != nil {
42+
return p.cachedAccessTokenFromAPIToken, nil
43+
}
44+
45+
c, err := p.AuthClient()
46+
if err != nil {
47+
return nil, err
48+
}
49+
tokens, err := c.GetSessions()
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
if len(tokens) == 0 {
55+
return nil, auth.ErrNotLoggedIn
56+
}
57+
58+
return tokens[0].Peek(), nil
59+
}
60+
3861
func (p *Provider) AuthClient() (*auth.Client, error) {
3962
return auth.NewClient(
4063
build.Issuer(),
@@ -45,7 +68,9 @@ func (p *Provider) AuthClient() (*auth.Client, error) {
4568
)
4669
}
4770

48-
func (p *Provider) getTokenFromPAT(ctx context.Context) (*session.Token, error) {
71+
func (p *Provider) getAccessTokenFromAPIToken(
72+
ctx context.Context,
73+
) (*session.Token, error) {
4974
pat := os.Getenv("DEVBOX_ACCESS_TOKEN")
5075
if pat == "" {
5176
return nil, nil
@@ -64,9 +89,11 @@ func (p *Provider) getTokenFromPAT(ctx context.Context) (*session.Token, error)
6489

6590
// This is not the greatest. This token is missing id, refresh, etc.
6691
// It may be better to change api.NewClient() to take a token string instead.
67-
return &session.Token{
92+
p.cachedAccessTokenFromAPIToken = &session.Token{
6893
Token: oauth2.Token{
6994
AccessToken: response.AccessToken,
7095
},
71-
}, nil
96+
}
97+
98+
return p.cachedAccessTokenFromAPIToken, nil
7299
}

internal/telemetry/segment.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"path/filepath"
1111
"time"
1212

13+
"github.com/samber/lo"
1314
segment "github.com/segmentio/analytics-go"
1415
"go.jetpack.io/devbox/internal/nix"
1516

@@ -39,14 +40,17 @@ func newTrackMessage(name string, meta Metadata) *segment.Track {
3940
}
4041

4142
dur := time.Since(procStartTime)
42-
if !meta.CommandStart.IsZero() {
43-
dur = time.Since(meta.CommandStart)
43+
if !meta.EventStart.IsZero() {
44+
dur = time.Since(meta.EventStart)
4445
}
46+
uid := userID()
4547
return &segment.Track{
46-
MessageId: newEventID(),
47-
Type: "track",
48-
AnonymousId: deviceID,
49-
UserId: userID,
48+
MessageId: newEventID(),
49+
Type: "track",
50+
// Only set anonymous ID if user ID is not set. Otherwise segment will
51+
// drop the UserId.
52+
AnonymousId: lo.Ternary(uid == "", deviceID, ""),
53+
UserId: uid,
5054
Timestamp: time.Now(),
5155
Event: name,
5256
Context: &segment.Context{
@@ -66,6 +70,7 @@ func newTrackMessage(name string, meta Metadata) *segment.Track {
6670
"command": meta.Command,
6771
"command_args": meta.CommandFlags,
6872
"duration": dur.Milliseconds(),
73+
"org_id": orgID(),
6974
"packages": meta.Packages,
7075
"shell": os.Getenv(envir.Shell),
7176
"shell_access": shellAccess(),

internal/telemetry/telemetry.go

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/google/uuid"
2525
"github.com/pkg/errors"
2626
segment "github.com/segmentio/analytics-go"
27+
"go.jetpack.io/devbox/internal/devbox/providers/identity"
2728
"go.jetpack.io/devbox/internal/nix"
2829

2930
"go.jetpack.io/devbox/internal/build"
@@ -40,11 +41,11 @@ const (
4041
EventCommandSuccess EventName = iota
4142
EventShellInteractive
4243
EventShellReady
44+
EventNixBuildSuccess
4345
)
4446

4547
var (
4648
deviceID string
47-
userID string
4849

4950
// procStartTime records the start time of the current process.
5051
procStartTime = time.Now()
@@ -61,16 +62,33 @@ func Start() {
6162
const deviceSalt = "64ee464f-9450-4b14-8d9c-014c0012ac1a"
6263
deviceID, _ = machineid.ProtectedID(deviceSalt)
6364

64-
username := os.Getenv(envir.GitHubUsername)
65-
if username != "" {
65+
started = true
66+
}
67+
68+
func userID() string {
69+
// TODO, once we add access token parsing, use that instead of id token.
70+
// that will work with API_TOKEN as well.
71+
if tok, err := identity.Get().Peek(); err == nil && tok.IDClaims() != nil {
72+
return tok.IDClaims().Subject
73+
}
74+
if username := os.Getenv(envir.GitHubUsername); username != "" {
6675
const uidSalt = "d6134cd5-347d-4b7c-a2d0-295c0f677948"
6776
const githubPrefix = "github:"
6877

6978
// userID is a v5 UUID which is basically a SHA hash of the username.
7079
// See https://www.uuidtools.com/uuid-versions-explained for a comparison of UUIDs.
71-
userID = uuid.NewSHA1(uuid.MustParse(uidSalt), []byte(githubPrefix+username)).String()
80+
return uuid.NewSHA1(uuid.MustParse(uidSalt), []byte(githubPrefix+username)).String()
7281
}
73-
started = true
82+
return ""
83+
}
84+
85+
func orgID() string {
86+
// TODO, once we add access token parsing, use that instead of id token.
87+
// that will work with API_TOKEN as well.
88+
if tok, err := identity.Get().Peek(); err == nil && tok.IDClaims() != nil {
89+
return tok.IDClaims().OrgID
90+
}
91+
return ""
7492
}
7593

7694
// Stop stops gathering telemetry and flushes buffered events to disk.
@@ -103,6 +121,10 @@ func Event(e EventName, meta Metadata) {
103121
name := fmt.Sprintf("[%s] Shell Event: ready", appName)
104122
msg := newTrackMessage(name, meta)
105123
bufferSegmentMessage(msg.MessageId, msg)
124+
case EventNixBuildSuccess:
125+
name := fmt.Sprintf("[%s] Nix Build Event: success", appName)
126+
msg := newTrackMessage(name, meta)
127+
bufferSegmentMessage(msg.MessageId, msg)
106128
}
107129
}
108130

@@ -163,8 +185,8 @@ func Error(err error, meta Metadata) {
163185

164186
// Prefer using the user ID instead of the device ID when it's
165187
// available.
166-
if userID != "" {
167-
event.User.ID = userID
188+
if uid := userID(); uid != "" {
189+
event.User.ID = uid
168190
}
169191
bufferSentryEvent(event)
170192

@@ -177,7 +199,7 @@ func Error(err error, meta Metadata) {
177199
type Metadata struct {
178200
Command string
179201
CommandFlags []string
180-
CommandStart time.Time
202+
EventStart time.Time
181203
FeatureFlags map[string]bool
182204

183205
InShell bool

0 commit comments

Comments
 (0)