Skip to content

Commit 650e8fe

Browse files
authored
[devbox] Add anonymized telemetry (#40)
## Summary Add anonymized telemetry to understand usage of devbox and further improve it. We allow users to opt-out via an environment variable. To add telemetry I implemented a small "midcobra" framework, that make it possible to add "middlware" to cobra CLIs. The plan is to re-use the midcobra functionality in other binaries like `envsec`. ## How was it tested? Built locally, ran, and checked telemetry logs.
1 parent e749a56 commit 650e8fe

File tree

8 files changed

+232
-7
lines changed

8 files changed

+232
-7
lines changed

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ jobs:
2727

2828
release-snapshot:
2929
runs-on: ubuntu-latest
30+
environment: release
3031
needs: tests
3132
if: ${{ inputs.is_snapshot_release || github.event.schedule }}
3233
steps:
@@ -47,6 +48,7 @@ jobs:
4748
args: release --rm-dist --skip-publish --snapshot
4849
env:
4950
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
51+
TELEMETRY_KEY: ${{ secrets.TELEMETRY_KEY }}
5052
- name: Determine snapshot tag
5153
run: |
5254
TAG=$(ls dist/*_linux_386.tar.gz | cut -d '_' -f 2 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+-dev')
@@ -62,6 +64,7 @@ jobs:
6264
dist/*.tar.gz
6365
release:
6466
runs-on: ubuntu-latest
67+
environment: release
6568
needs: tests
6669
# Only release when there's a tag for the release.
6770
if: startsWith(github.ref, 'refs/tags/')
@@ -83,3 +86,4 @@ jobs:
8386
args: release --rm-dist
8487
env:
8588
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
89+
TELEMETRY_KEY: ${{ secrets.TELEMETRY_KEY }}

.goreleaser.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ builds:
1010
- -s -w -X go.jetpack.io/devbox/build.Version={{.Version}}
1111
- -s -w -X go.jetpack.io/devbox/build.Commit={{.Commit}}
1212
- -s -w -X go.jetpack.io/devbox/build.CommitDate={{.CommitDate}}
13+
- -s -w -X go.jetpack.io/devbox/build.TelemetryKey={{ .Env.TELEMETRY_KEY }}
1314
env:
1415
- CGO_ENABLED=0
1516
- GO111MODULE=on

boxcli/midcobra/midcobra.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package midcobra
5+
6+
import (
7+
"context"
8+
9+
"github.com/spf13/cobra"
10+
)
11+
12+
type Executable interface {
13+
AddMiddleware(mids ...Middleware)
14+
Execute(ctx context.Context, args []string) int
15+
}
16+
17+
type Middleware interface {
18+
preRun(cmd *cobra.Command, args []string)
19+
postRun(cmd *cobra.Command, args []string, runErr error)
20+
}
21+
22+
func New(cmd *cobra.Command) Executable {
23+
return &midcobraExecutable{
24+
cmd: cmd,
25+
middlewares: []Middleware{},
26+
}
27+
}
28+
29+
type midcobraExecutable struct {
30+
cmd *cobra.Command
31+
middlewares []Middleware
32+
}
33+
34+
var _ Executable = (*midcobraExecutable)(nil)
35+
36+
func (ex *midcobraExecutable) AddMiddleware(mids ...Middleware) {
37+
ex.middlewares = append(ex.middlewares, mids...)
38+
}
39+
40+
func (ex *midcobraExecutable) Execute(ctx context.Context, args []string) int {
41+
// Ensure cobra uses the same arguments
42+
ex.cmd.SetArgs(args)
43+
44+
// Run the 'pre' hooks
45+
for _, m := range ex.middlewares {
46+
m.preRun(ex.cmd, args)
47+
}
48+
49+
// Execute the cobra command:
50+
err := ex.cmd.ExecuteContext(ctx)
51+
52+
// Run the 'post' hooks. Note that unlike the default PostRun cobra functionality these
53+
// run even if the command resulted in an error. This is useful when we still want to clean up
54+
// before the program exists or we want to log something. The error, if any, gets passed
55+
// to the post hook.
56+
for _, m := range ex.middlewares {
57+
m.postRun(ex.cmd, args, err)
58+
}
59+
60+
if err != nil {
61+
return 1 // Error exit code
62+
} else {
63+
return 0
64+
}
65+
}

boxcli/midcobra/telemetry.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
2+
// Use of this source code is governed by the license in the LICENSE file.
3+
4+
package midcobra
5+
6+
import (
7+
"fmt"
8+
"os"
9+
"runtime"
10+
"strconv"
11+
"time"
12+
13+
"github.com/denisbrodbeck/machineid"
14+
segment "github.com/segmentio/analytics-go"
15+
"github.com/spf13/cobra"
16+
)
17+
18+
// We collect some light telemetry to be able to improve devbox over time.
19+
// We're aware how important privacy is and value it ourselves, so we have
20+
// the following rules:
21+
// 1. We only collect anonymized data – nothing that is personally identifiable
22+
// 2. Data is only stored in SOC 2 compliant systems, and we are SOC 2 compliant ourselves.
23+
// 3. Users should always have the ability to opt-out.
24+
func Telemetry(opts *TelemetryOpts) Middleware {
25+
doNotTrack, err := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")) // https://consoledonottrack.com/
26+
if err != nil {
27+
doNotTrack = false
28+
}
29+
30+
return &telemetryMiddleware{
31+
opts: *opts,
32+
disabled: doNotTrack || opts.TelemetryKey == "",
33+
}
34+
}
35+
36+
type TelemetryOpts struct {
37+
AppName string
38+
AppVersion string
39+
TelemetryKey string
40+
}
41+
type telemetryMiddleware struct {
42+
// Setup:
43+
opts TelemetryOpts
44+
disabled bool
45+
46+
// Used during execution:
47+
startTime time.Time
48+
}
49+
50+
// telemetryMiddleware implements interface Middleware (compile-time check)
51+
var _ Middleware = (*telemetryMiddleware)(nil)
52+
53+
func (m *telemetryMiddleware) preRun(cmd *cobra.Command, args []string) {
54+
m.startTime = time.Now()
55+
}
56+
57+
func (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
58+
if m.disabled {
59+
return
60+
}
61+
62+
segmentClient := segment.New(m.opts.TelemetryKey)
63+
defer func() {
64+
_ = segmentClient.Close()
65+
}()
66+
67+
subcmd, subargs, parseErr := getSubcommand(cmd, args)
68+
if parseErr != nil {
69+
return // Ignore invalid commands
70+
}
71+
72+
trackEvent(segmentClient, &event{
73+
AppName: m.opts.AppName,
74+
AppVersion: m.opts.AppVersion,
75+
Command: subcmd.CommandPath(),
76+
CommandArgs: subargs,
77+
DeviceID: deviceID(),
78+
Duration: time.Since(m.startTime),
79+
Failed: runErr != nil,
80+
})
81+
}
82+
83+
func deviceID() string {
84+
salt := "64ee464f-9450-4b14-8d9c-014c0012ac1a"
85+
hashedID, _ := machineid.ProtectedID(salt) // Ensure machine id is hashed and non-identifiable
86+
return hashedID
87+
}
88+
89+
func getSubcommand(c *cobra.Command, args []string) (subcmd *cobra.Command, subargs []string, err error) {
90+
if c.TraverseChildren {
91+
subcmd, subargs, err = c.Traverse(args)
92+
} else {
93+
subcmd, subargs, err = c.Find(args)
94+
}
95+
return subcmd, subargs, err
96+
}
97+
98+
type event struct {
99+
AppName string
100+
AppVersion string
101+
Command string
102+
CommandArgs []string
103+
DeviceID string
104+
Duration time.Duration
105+
Failed bool
106+
}
107+
108+
func trackEvent(client segment.Client, evt *event) {
109+
_ = client.Enqueue(segment.Track{ // Ignore errors, telemetry is best effort
110+
AnonymousId: evt.DeviceID, // Use device id instead
111+
Event: fmt.Sprintf("[%s] Command: %s", evt.AppName, evt.Command),
112+
Context: &segment.Context{
113+
Device: segment.DeviceInfo{
114+
Id: evt.DeviceID,
115+
},
116+
App: segment.AppInfo{
117+
Name: evt.AppName,
118+
Version: evt.AppVersion,
119+
},
120+
OS: segment.OSInfo{
121+
Name: runtime.GOOS,
122+
},
123+
},
124+
Properties: segment.NewProperties().
125+
Set("command", evt.Command).
126+
Set("command_args", evt.CommandArgs).
127+
Set("failed", evt.Failed).
128+
Set("duration", evt.Duration.Milliseconds()),
129+
})
130+
}

boxcli/root.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import (
1010
"os/exec"
1111

1212
"github.com/spf13/cobra"
13+
"go.jetpack.io/devbox/boxcli/midcobra"
14+
"go.jetpack.io/devbox/build"
1315
)
1416

1517
func RootCmd() *cobra.Command {
@@ -41,16 +43,19 @@ func RootCmd() *cobra.Command {
4143
return command
4244
}
4345

44-
func Execute(ctx context.Context) error {
45-
cmd := RootCmd()
46-
return cmd.ExecuteContext(ctx)
46+
func Execute(ctx context.Context, args []string) int {
47+
exe := midcobra.New(RootCmd())
48+
exe.AddMiddleware(midcobra.Telemetry(&midcobra.TelemetryOpts{
49+
AppName: "devbox",
50+
AppVersion: build.Version,
51+
TelemetryKey: build.TelemetryKey,
52+
}))
53+
return exe.Execute(ctx, args)
4754
}
4855

4956
func Main() {
50-
err := Execute(context.Background())
51-
if err != nil {
52-
os.Exit(1)
53-
}
57+
code := Execute(context.Background(), os.Args[1:])
58+
os.Exit(code)
5459
}
5560

5661
type runFunc func(cmd *cobra.Command, args []string) error

build/build.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ var (
55
Version = "0.0.0-dev"
66
Commit = "none"
77
CommitDate = "unknown"
8+
9+
TelemetryKey = "" // Disabled by default
810
)

go.mod

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,29 @@ go 1.19
44

55
require (
66
cuelang.org/go v0.4.3
7+
github.com/denisbrodbeck/machineid v1.0.1
78
github.com/imdario/mergo v0.3.13
89
github.com/pkg/errors v0.9.1
910
github.com/samber/lo v1.27.0
11+
github.com/segmentio/analytics-go v3.1.0+incompatible
1012
github.com/spf13/cobra v1.5.0
1113
github.com/stretchr/testify v1.8.0
1214
gopkg.in/yaml.v3 v3.0.1
1315
)
1416

1517
require (
18+
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
1619
github.com/cockroachdb/apd/v2 v2.0.1 // indirect
1720
github.com/davecgh/go-spew v1.1.1 // indirect
1821
github.com/google/uuid v1.2.0 // indirect
1922
github.com/inconshreveable/mousetrap v1.0.0 // indirect
2023
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
2124
github.com/pmezard/go-difflib v1.0.0 // indirect
25+
github.com/segmentio/backo-go v1.0.1 // indirect
2226
github.com/spf13/pflag v1.0.5 // indirect
27+
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
2328
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
2429
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect
30+
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
2531
golang.org/x/text v0.3.7 // indirect
2632
)

go.sum

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo=
22
cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI=
3+
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
4+
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
35
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
46
github.com/cockroachdb/apd/v2 v2.0.1 h1:y1Rh3tEU89D+7Tgbw+lp52T6p/GJLpDmNvr10UWqLTE=
57
github.com/cockroachdb/apd/v2 v2.0.1/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw=
68
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
79
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
810
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
911
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
12+
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
13+
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
1014
github.com/emicklei/proto v1.6.15 h1:XbpwxmuOPrdES97FrSfpyy67SSCV/wBIKXqgJzh6hNw=
1115
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
1216
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
@@ -32,6 +36,10 @@ github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XF
3236
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
3337
github.com/samber/lo v1.27.0 h1:GOyDWxsblvqYobqsmUuMddPa2/mMzkKyojlXol4+LaQ=
3438
github.com/samber/lo v1.27.0/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
39+
github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=
40+
github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
41+
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
42+
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
3543
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
3644
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
3745
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
@@ -42,12 +50,16 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
4250
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
4351
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
4452
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
53+
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
54+
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
4555
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
4656
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
4757
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
4858
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
4959
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
5060
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
61+
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
62+
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
5163
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
5264
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
5365
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=

0 commit comments

Comments
 (0)