Skip to content

Commit

Permalink
[devbox] Add anonymized telemetry (#40)
Browse files Browse the repository at this point in the history
## 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.
  • Loading branch information
loreto committed Aug 30, 2022
1 parent e749a56 commit 650e8fe
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 7 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ jobs:

release-snapshot:
runs-on: ubuntu-latest
environment: release
needs: tests
if: ${{ inputs.is_snapshot_release || github.event.schedule }}
steps:
Expand All @@ -47,6 +48,7 @@ jobs:
args: release --rm-dist --skip-publish --snapshot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TELEMETRY_KEY: ${{ secrets.TELEMETRY_KEY }}
- name: Determine snapshot tag
run: |
TAG=$(ls dist/*_linux_386.tar.gz | cut -d '_' -f 2 | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+-dev')
Expand All @@ -62,6 +64,7 @@ jobs:
dist/*.tar.gz
release:
runs-on: ubuntu-latest
environment: release
needs: tests
# Only release when there's a tag for the release.
if: startsWith(github.ref, 'refs/tags/')
Expand All @@ -83,3 +86,4 @@ jobs:
args: release --rm-dist
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TELEMETRY_KEY: ${{ secrets.TELEMETRY_KEY }}
1 change: 1 addition & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ builds:
- -s -w -X go.jetpack.io/devbox/build.Version={{.Version}}
- -s -w -X go.jetpack.io/devbox/build.Commit={{.Commit}}
- -s -w -X go.jetpack.io/devbox/build.CommitDate={{.CommitDate}}
- -s -w -X go.jetpack.io/devbox/build.TelemetryKey={{ .Env.TELEMETRY_KEY }}
env:
- CGO_ENABLED=0
- GO111MODULE=on
Expand Down
65 changes: 65 additions & 0 deletions boxcli/midcobra/midcobra.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
// Use of this source code is governed by the license in the LICENSE file.

package midcobra

import (
"context"

"github.com/spf13/cobra"
)

type Executable interface {
AddMiddleware(mids ...Middleware)
Execute(ctx context.Context, args []string) int
}

type Middleware interface {
preRun(cmd *cobra.Command, args []string)
postRun(cmd *cobra.Command, args []string, runErr error)
}

func New(cmd *cobra.Command) Executable {
return &midcobraExecutable{
cmd: cmd,
middlewares: []Middleware{},
}
}

type midcobraExecutable struct {
cmd *cobra.Command
middlewares []Middleware
}

var _ Executable = (*midcobraExecutable)(nil)

func (ex *midcobraExecutable) AddMiddleware(mids ...Middleware) {
ex.middlewares = append(ex.middlewares, mids...)
}

func (ex *midcobraExecutable) Execute(ctx context.Context, args []string) int {
// Ensure cobra uses the same arguments
ex.cmd.SetArgs(args)

// Run the 'pre' hooks
for _, m := range ex.middlewares {
m.preRun(ex.cmd, args)
}

// Execute the cobra command:
err := ex.cmd.ExecuteContext(ctx)

// Run the 'post' hooks. Note that unlike the default PostRun cobra functionality these
// run even if the command resulted in an error. This is useful when we still want to clean up
// before the program exists or we want to log something. The error, if any, gets passed
// to the post hook.
for _, m := range ex.middlewares {
m.postRun(ex.cmd, args, err)
}

if err != nil {
return 1 // Error exit code
} else {
return 0
}
}
130 changes: 130 additions & 0 deletions boxcli/midcobra/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
// Copyright 2022 Jetpack Technologies Inc and contributors. All rights reserved.
// Use of this source code is governed by the license in the LICENSE file.

package midcobra

import (
"fmt"
"os"
"runtime"
"strconv"
"time"

"github.com/denisbrodbeck/machineid"
segment "github.com/segmentio/analytics-go"
"github.com/spf13/cobra"
)

// We collect some light telemetry to be able to improve devbox over time.
// We're aware how important privacy is and value it ourselves, so we have
// the following rules:
// 1. We only collect anonymized data – nothing that is personally identifiable
// 2. Data is only stored in SOC 2 compliant systems, and we are SOC 2 compliant ourselves.
// 3. Users should always have the ability to opt-out.
func Telemetry(opts *TelemetryOpts) Middleware {
doNotTrack, err := strconv.ParseBool(os.Getenv("DO_NOT_TRACK")) // https://consoledonottrack.com/
if err != nil {
doNotTrack = false
}

return &telemetryMiddleware{
opts: *opts,
disabled: doNotTrack || opts.TelemetryKey == "",
}
}

type TelemetryOpts struct {
AppName string
AppVersion string
TelemetryKey string
}
type telemetryMiddleware struct {
// Setup:
opts TelemetryOpts
disabled bool

// Used during execution:
startTime time.Time
}

// telemetryMiddleware implements interface Middleware (compile-time check)
var _ Middleware = (*telemetryMiddleware)(nil)

func (m *telemetryMiddleware) preRun(cmd *cobra.Command, args []string) {
m.startTime = time.Now()
}

func (m *telemetryMiddleware) postRun(cmd *cobra.Command, args []string, runErr error) {
if m.disabled {
return
}

segmentClient := segment.New(m.opts.TelemetryKey)
defer func() {
_ = segmentClient.Close()
}()

subcmd, subargs, parseErr := getSubcommand(cmd, args)
if parseErr != nil {
return // Ignore invalid commands
}

trackEvent(segmentClient, &event{
AppName: m.opts.AppName,
AppVersion: m.opts.AppVersion,
Command: subcmd.CommandPath(),
CommandArgs: subargs,
DeviceID: deviceID(),
Duration: time.Since(m.startTime),
Failed: runErr != nil,
})
}

func deviceID() string {
salt := "64ee464f-9450-4b14-8d9c-014c0012ac1a"
hashedID, _ := machineid.ProtectedID(salt) // Ensure machine id is hashed and non-identifiable
return hashedID
}

func getSubcommand(c *cobra.Command, args []string) (subcmd *cobra.Command, subargs []string, err error) {
if c.TraverseChildren {
subcmd, subargs, err = c.Traverse(args)
} else {
subcmd, subargs, err = c.Find(args)
}
return subcmd, subargs, err
}

type event struct {
AppName string
AppVersion string
Command string
CommandArgs []string
DeviceID string
Duration time.Duration
Failed bool
}

func trackEvent(client segment.Client, evt *event) {
_ = client.Enqueue(segment.Track{ // Ignore errors, telemetry is best effort
AnonymousId: evt.DeviceID, // Use device id instead
Event: fmt.Sprintf("[%s] Command: %s", evt.AppName, evt.Command),
Context: &segment.Context{
Device: segment.DeviceInfo{
Id: evt.DeviceID,
},
App: segment.AppInfo{
Name: evt.AppName,
Version: evt.AppVersion,
},
OS: segment.OSInfo{
Name: runtime.GOOS,
},
},
Properties: segment.NewProperties().
Set("command", evt.Command).
Set("command_args", evt.CommandArgs).
Set("failed", evt.Failed).
Set("duration", evt.Duration.Milliseconds()),
})
}
19 changes: 12 additions & 7 deletions boxcli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import (
"os/exec"

"github.com/spf13/cobra"
"go.jetpack.io/devbox/boxcli/midcobra"
"go.jetpack.io/devbox/build"
)

func RootCmd() *cobra.Command {
Expand Down Expand Up @@ -41,16 +43,19 @@ func RootCmd() *cobra.Command {
return command
}

func Execute(ctx context.Context) error {
cmd := RootCmd()
return cmd.ExecuteContext(ctx)
func Execute(ctx context.Context, args []string) int {
exe := midcobra.New(RootCmd())
exe.AddMiddleware(midcobra.Telemetry(&midcobra.TelemetryOpts{
AppName: "devbox",
AppVersion: build.Version,
TelemetryKey: build.TelemetryKey,
}))
return exe.Execute(ctx, args)
}

func Main() {
err := Execute(context.Background())
if err != nil {
os.Exit(1)
}
code := Execute(context.Background(), os.Args[1:])
os.Exit(code)
}

type runFunc func(cmd *cobra.Command, args []string) error
2 changes: 2 additions & 0 deletions build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ var (
Version = "0.0.0-dev"
Commit = "none"
CommitDate = "unknown"

TelemetryKey = "" // Disabled by default
)
6 changes: 6 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,29 @@ go 1.19

require (
cuelang.org/go v0.4.3
github.com/denisbrodbeck/machineid v1.0.1
github.com/imdario/mergo v0.3.13
github.com/pkg/errors v0.9.1
github.com/samber/lo v1.27.0
github.com/segmentio/analytics-go v3.1.0+incompatible
github.com/spf13/cobra v1.5.0
github.com/stretchr/testify v1.8.0
gopkg.in/yaml.v3 v3.0.1
)

require (
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 // indirect
github.com/cockroachdb/apd/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.2.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/segmentio/backo-go v1.0.1 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b // indirect
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
golang.org/x/text v0.3.7 // indirect
)
12 changes: 12 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo=
cuelang.org/go v0.4.3/go.mod h1:7805vR9H+VoBNdWFdI7jyDR3QLUPp4+naHfbcgp55HI=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY=
github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd/v2 v2.0.1 h1:y1Rh3tEU89D+7Tgbw+lp52T6p/GJLpDmNvr10UWqLTE=
github.com/cockroachdb/apd/v2 v2.0.1/go.mod h1:DDxRlzC2lo3/vSlmSoS7JkqbbrARPuFOGr0B9pvN3Gw=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/denisbrodbeck/machineid v1.0.1 h1:geKr9qtkB876mXguW2X6TU4ZynleN6ezuMSRhl4D7AQ=
github.com/denisbrodbeck/machineid v1.0.1/go.mod h1:dJUwb7PTidGDeYyUBmXZ2GphQBbjJCrnectwCyxcUSI=
github.com/emicklei/proto v1.6.15 h1:XbpwxmuOPrdES97FrSfpyy67SSCV/wBIKXqgJzh6hNw=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4=
Expand All @@ -32,6 +36,10 @@ github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XF
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/samber/lo v1.27.0 h1:GOyDWxsblvqYobqsmUuMddPa2/mMzkKyojlXol4+LaQ=
github.com/samber/lo v1.27.0/go.mod h1:it33p9UtPMS7z72fP4gw/EIfQB2eI8ke7GR2wc6+Rhg=
github.com/segmentio/analytics-go v3.1.0+incompatible h1:IyiOfUgQFVHvsykKKbdI7ZsH374uv3/DfZUo9+G0Z80=
github.com/segmentio/analytics-go v3.1.0+incompatible/go.mod h1:C7CYBtQWk4vRk2RyLu0qOcbHJ18E3F1HV2C/8JvKN48=
github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4=
github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc=
github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU=
github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
Expand All @@ -42,12 +50,16 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/thoas/go-funk v0.9.1 h1:O549iLZqPpTUQ10ykd26sZhzD+rmR5pWhuElrhbC20M=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g=
github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b h1:0mm1VjtFUOIlE1SbDlwjYaDxZVDP2S5ou6y0gSgXHu8=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0=
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
Expand Down

0 comments on commit 650e8fe

Please sign in to comment.