Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add telemetry support for usage analysis #1222

Merged
merged 3 commits into from
May 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
22 changes: 20 additions & 2 deletions cli/cmd/encore/cmdutil/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,26 @@ 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 IsDaemonRunning(ctx context.Context) bool {
socketPath, err := daemonSockPath()
if err != nil {
return false
}
if _, err := xos.SocketStat(socketPath); err == nil {
// The socket exists; check that it is responsive.
if cc, err := dialDaemon(ctx, socketPath); err == nil {
_ = cc.Close()
return true
}
// socket is not responding, remove it
_ = os.Remove(socketPath)
}
return false

}

// 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) daemonpb.DaemonClient {
socketPath, err := daemonSockPath()
if err != nil {
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")
}
118 changes: 118 additions & 0 deletions cli/cmd/encore/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

import (
"context"
"fmt"
"os"
"strings"

"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"
"encr.dev/pkg/fns"
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").String()
if !telemetry.IsEnabled() {
status = aurora.Red("Disabled").String()
}
fmt.Println(aurora.Sprintf("%s\n", aurora.Bold("Encore Telemetry")))
items := [][2]string{
{"Status", status},
}
if root.Verbosity > 0 {
items = append(items, [2]string{"Install ID", telemetry.GetAnonID()})
}
if telemetry.IsDebug() {
items = append(items, [2]string{"Debug", aurora.Green("Enabled").String()})
}
maxKeyLen := fns.Max(items, func(entry [2]string) int { return len(entry[0]) })
for _, item := range items {
spacing := strings.Repeat(" ", maxKeyLen-len(item[0]))
fmt.Printf("%s: %s%s\n", item[0], spacing, item[1])
}
fmt.Println(aurora.Sprintf("\nLearn more: %s", aurora.Underline("https://encore.dev/docs/telemetry")))
}

func updateTelemetry(ctx context.Context) {
// Update the telemetry config on the daemon if it is running
if cmdutil.IsDaemonRunning(ctx) {
daemon := cmdutil.ConnectDaemon(ctx)
_, err := daemon.Telemetry(ctx, &daemonpb.TelemetryConfig{
AnonId: telemetry.GetAnonID(),
Enabled: telemetry.IsEnabled(),
Debug: telemetry.IsDebug(),
})
if err != nil {
log.Debug().Err(err).Msgf("could not update daemon telemetry: %s", err)
}
}
if err := telemetry.SaveConfig(); err != nil {
log.Debug().Err(err).Msgf("could not save 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
15 changes: 15 additions & 0 deletions cli/daemon/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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) {
telemetry.UpdateConfig(req.AnonId, req.Enabled, req.Debug)
return new(emptypb.Empty), nil
}
Loading
Loading