Skip to content

Commit

Permalink
Add telemetry support for usage analysis
Browse files Browse the repository at this point in the history
  • Loading branch information
ekerfelt committed May 22, 2024
1 parent ec3f5f6 commit 549a530
Show file tree
Hide file tree
Showing 14 changed files with 1,105 additions and 431 deletions.
14 changes: 13 additions & 1 deletion cli/cmd/encore/app/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import (
"encr.dev/cli/cmd/encore/auth"
"encr.dev/cli/cmd/encore/cmdutil"
"encr.dev/cli/internal/platform"
"encr.dev/cli/internal/telemetry"
"encr.dev/internal/conf"
"encr.dev/internal/env"
"encr.dev/internal/version"
Expand Down Expand Up @@ -58,6 +59,15 @@ func init() {

// createApp is the implementation of the "encore app create" command.
func createApp(ctx context.Context, name, template string) (err error) {
var lang language
defer func() {
// We need to send the telemetry synchronously to ensure it's sent before the command exits.
telemetry.SendSync("app.create", map[string]any{
"template": template,
"lang": lang,
"error": err != nil,
})
}()
cyan := color.New(color.FgCyan)
green := color.New(color.FgGreen)
red := color.New(color.FgRed)
Expand All @@ -72,10 +82,12 @@ func createApp(ctx context.Context, name, template string) (err error) {
input = strings.TrimSpace(input)
switch input {
case "Y", "y", "yes", "":
telemetry.Send("app.create.account", map[string]any{"response": true})
if err := auth.DoLogin(auth.AutoFlow); err != nil {
cmdutil.Fatal(err)
}
case "N", "n", "no":
telemetry.Send("app.create.account", map[string]any{"response": false})
// Continue without creating an account.
case "q", "quit", "exit":
os.Exit(1)
Expand All @@ -89,7 +101,7 @@ func createApp(ctx context.Context, name, template string) (err error) {
}

if name == "" || template == "" {
name, template, _ = selectTemplate(name, template, false)
name, template, lang = selectTemplate(name, template, false)
}
// Treat the special name "empty" as the empty app template
// (the rest of the code assumes that's the empty string).
Expand Down
27 changes: 24 additions & 3 deletions cli/cmd/encore/cmdutil/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,27 @@ import (
daemonpb "encr.dev/proto/encore/daemon"
)

// ConnectDaemon sets up the Encore daemon if it isn't already running
// and returns a client connected to it.
func ConnectDaemon(ctx context.Context) daemonpb.DaemonClient {
// DaemonOption is an option for connecting to the Encore daemon.
type DaemonOption func(*daemonOptions)

type daemonOptions struct {
skipStart bool
}

var (
// SkipStart skips starting the daemon if it is not already running.
SkipStart DaemonOption = func(o *daemonOptions) {
o.skipStart = true
}
)

// ConnectDaemon returns a client connection to the Encore daemon.
// By default, it will start the daemon if it is not already running.
func ConnectDaemon(ctx context.Context, opts ...DaemonOption) daemonpb.DaemonClient {
var options daemonOptions
for _, o := range opts {
o(&options)
}
socketPath, err := daemonSockPath()
if err != nil {
fmt.Fprintln(os.Stderr, "fatal: ", err)
Expand Down Expand Up @@ -58,6 +76,9 @@ func ConnectDaemon(ctx context.Context) daemonpb.DaemonClient {
_ = os.Remove(socketPath)
}

if options.skipStart {
return nil
}
// Start the daemon.
if err := StartDaemonInBackground(ctx); err != nil {
Fatal("starting daemon: ", err)
Expand Down
21 changes: 16 additions & 5 deletions cli/cmd/encore/root/rootcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,21 @@ import (
)

var (
verbosity int
Verbosity int
traceFile string

// TraceFile is the file to write trace logs to.
// If nil (the default), trace logs are not written.
TraceFile *string
)

var preRuns []func(cmd *cobra.Command, args []string)

// AddPreRun adds a function to be executed before the command runs.
func AddPreRun(f func(cmd *cobra.Command, args []string)) {
preRuns = append(preRuns, f)
}

var Cmd = &cobra.Command{
Use: "encore",
Short: "encore is the fastest way of developing backend applications",
Expand All @@ -30,20 +37,24 @@ var Cmd = &cobra.Command{
}

level := zerolog.InfoLevel
if verbosity == 1 {
if Verbosity == 1 {
level = zerolog.DebugLevel
} else if verbosity >= 2 {
} else if Verbosity >= 2 {
level = zerolog.TraceLevel
}

if verbosity >= 1 {
if Verbosity >= 1 {
errlist.Verbose = true
}
log.Logger = log.Logger.Level(level)

for _, f := range preRuns {
f(cmd, args)
}
},
}

func init() {
Cmd.PersistentFlags().CountVarP(&verbosity, "verbose", "v", "verbose output")
Cmd.PersistentFlags().CountVarP(&Verbosity, "verbose", "v", "verbose output")
Cmd.PersistentFlags().StringVar(&traceFile, "trace", "", "file to write execution trace data to")
}
119 changes: 119 additions & 0 deletions cli/cmd/encore/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package main

import (
"context"
"fmt"
"os"

"github.com/logrusorgru/aurora/v3"
"github.com/rs/zerolog/log"
"github.com/spf13/cobra"

"encr.dev/cli/cmd/encore/cmdutil"
"encr.dev/cli/cmd/encore/root"
"encr.dev/cli/internal/telemetry"
daemonpb "encr.dev/proto/encore/daemon"
)

var TelemetryDisabledByEnvVar = os.Getenv("DISABLE_ENCORE_TELEMETRY") == "1"
var TelemetryDebugByEnvVar = os.Getenv("ENCORE_TELEMETRY_DEBUG") == "1"

func printTelemetryStatus() {
status := aurora.Green("Enabled")
if !telemetry.IsEnabled() {
status = aurora.Red("Disabled")
}
fmt.Println(aurora.Sprintf("%s\n", aurora.Bold("Encore Telemetry")))
items := [][2]string{
{"Status", status.String()},
}
if root.Verbosity > 0 {
items = append(items, [2]string{"Install ID", telemetry.GetAnonID()})
}
if telemetry.IsDebug() {
items = append(items, [2]string{"Debug", "Enabled"})
}
for _, item := range items {
// add spacing to align the columns
fmt.Printf(" %-12s %s\n", item[0], item[1])
}
fmt.Println(aurora.Sprintf("\nLearn more: %s", aurora.Underline("https://encore.dev/docs/telemetry")))
}

func updateTelemetry(ctx context.Context) {
err := func() error {
//
daemon := cmdutil.ConnectDaemon(ctx, cmdutil.SkipStart)
if daemon != nil {
// Update the telemetry config on the daemon if it is running
_, err := daemon.Telemetry(ctx, &daemonpb.TelemetryConfig{
AnonId: telemetry.GetAnonID(),
Enabled: telemetry.IsEnabled(),
Debug: telemetry.IsDebug(),
})
return err
} else {
// Update the telemetry config locally if the daemon is not running
return telemetry.SaveConfig()
}
}()
if err != nil {
log.Debug().Err(err).Msgf("could not update telemetry: %s", err)
}
}

var telemetryCommand = &cobra.Command{
Use: "telemetry",
Short: "Reports the current telemetry status",

Run: func(cmd *cobra.Command, args []string) {
printTelemetryStatus()
},
}

var telemetryEnableCommand = &cobra.Command{
Use: "enable",
Short: "Enables telemetry reporting",
Run: func(cmd *cobra.Command, args []string) {
if telemetry.SetEnabled(true) {
updateTelemetry(cmd.Context())
}
printTelemetryStatus()
},
}

var telemetryDisableCommand = &cobra.Command{
Use: "disable",
Short: "Disables telemetry reporting",
Run: func(cmd *cobra.Command, args []string) {
if telemetry.SetEnabled(false) {
updateTelemetry(cmd.Context())
}
printTelemetryStatus()
},
}

func init() {
telemetryCommand.AddCommand(telemetryEnableCommand, telemetryDisableCommand)
rootCmd.AddCommand(telemetryCommand)
root.AddPreRun(func(cmd *cobra.Command, args []string) {
update := false
if TelemetryDisabledByEnvVar {
update = telemetry.SetEnabled(false)
}
if cmd.Use == "daemon" {
return
}
update = update || telemetry.SetDebug(TelemetryDebugByEnvVar)
if update {
go updateTelemetry(cmd.Context())
}
if telemetry.ShouldShowWarning() && cmd.Use != "version" {
fmt.Println()
fmt.Println(aurora.Sprintf("%s: This CLI tool collects usage data to help us improve Encore.", aurora.Bold("Note")))
fmt.Println(aurora.Sprintf(" You can disable this by running '%s'.\n", aurora.Yellow("encore telemetry disable")))
telemetry.SetShownWarning()
}
})

}
26 changes: 26 additions & 0 deletions cli/daemon/dash/dash.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"encr.dev/cli/daemon/run"
"encr.dev/cli/internal/browser"
"encr.dev/cli/internal/jsonrpc2"
"encr.dev/cli/internal/telemetry"
"encr.dev/internal/version"
"encr.dev/parser/encoding"
"encr.dev/pkg/editors"
Expand Down Expand Up @@ -73,6 +74,22 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
}

switch r.Method() {
case "telemetry":
type params struct {
Event string `json:"event"`
Properties map[string]interface{} `json:"properties"`
Once bool `json:"once,omitempty"`
}
var p params
if err := unmarshal(&p); err != nil {
return reply(ctx, nil, err)
}
if p.Once {
telemetry.SendOnce(p.Event, p.Properties)
} else {
telemetry.Send(p.Event, p.Properties)
}
return reply(ctx, "ok", nil)
case "version":
type versionResp struct {
Version string `json:"version"`
Expand Down Expand Up @@ -130,6 +147,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
return reply(ctx, apps, nil)

case "traces/list":
telemetry.Send("traces.list")
var params struct {
AppID string `json:"app_id"`
MessageID string `json:"message_id"`
Expand Down Expand Up @@ -157,6 +175,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
return reply(ctx, list, err)

case "traces/get":
telemetry.Send("traces.get")
var params struct {
AppID string `json:"app_id"`
TraceID string `json:"trace_id"`
Expand Down Expand Up @@ -205,6 +224,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
return reply(ctx, status, nil)

case "api-call":
telemetry.Send("api.call")
var params apiCallParams
if err := unmarshal(&params); err != nil {
return reply(ctx, nil, err)
Expand All @@ -227,6 +247,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
}
return reply(ctx, resp, nil)
case "ai/propose-system-design":
telemetry.Send("ai.propose")
log.Debug().Msg("dash: propose-system-design")
var params struct {
AppID string `json:"app_id"`
Expand Down Expand Up @@ -280,6 +301,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
}

case "ai/modify-system-design":
telemetry.Send("ai.modify")
log.Debug().Msg("dash: modify-system-design")
var params struct {
AppID string `json:"app_id"`
Expand All @@ -300,6 +322,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
})
return reply(ctx, task.SubscriptionID, err)
case "ai/define-endpoints":
telemetry.Send("ai.details")
log.Debug().Msg("dash: define-endpoints")
log.Debug().Msg("dash: define-endpoints")
var params struct {
Expand Down Expand Up @@ -351,6 +374,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
results, err := h.ai.UpdateCode(ctx, params.Services, app, params.Overwrite)
return reply(ctx, results, err)
case "ai/preview-files":
telemetry.Send("ai.preview")
log.Debug().Msg("dash: preview-files")
var params struct {
AppID string `json:"app_id"`
Expand All @@ -366,6 +390,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
result, err := h.ai.PreviewFiles(ctx, params.Services, app)
return reply(ctx, result, err)
case "ai/write-files":
telemetry.Send("ai.write")
log.Debug().Msg("dash: write-files")
var params struct {
AppID string `json:"app_id"`
Expand Down Expand Up @@ -403,6 +428,7 @@ func (h *handler) Handle(ctx context.Context, reply jsonrpc2.Replier, r jsonrpc2
}
return reply(ctx, true, err)
case "editors/open":
telemetry.Send("editors.open")
var params struct {
AppID string `json:"app_id"`
Editor editors.EditorName `json:"editor"`
Expand Down
20 changes: 20 additions & 0 deletions cli/daemon/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package daemon

import (
"context"

"google.golang.org/protobuf/types/known/emptypb"

"encr.dev/cli/internal/telemetry"
daemonpb "encr.dev/proto/encore/daemon"
)

func (s *Server) Telemetry(ctx context.Context, req *daemonpb.TelemetryConfig) (*emptypb.Empty, error) {
if telemetry.UpdateConfig(req.AnonId, req.Enabled, req.Debug) {
err := telemetry.SaveConfig()
if err != nil {
return nil, err
}
}
return new(emptypb.Empty), nil
}
Loading

0 comments on commit 549a530

Please sign in to comment.