Skip to content
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
1 change: 1 addition & 0 deletions .github/workflows/release.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ jobs:
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
AMPLITUDE_API_KEY: ${{ secrets.AMPLITUDE_API_KEY }}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is currently no GH secret for this, so it will be blank


- name: Mark release as pre-release until verified
env:
Expand Down
33 changes: 31 additions & 2 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
"github.com/datarobot/cli/internal/config"
"github.com/datarobot/cli/internal/log"
internalPlugin "github.com/datarobot/cli/internal/plugin"
"github.com/datarobot/cli/internal/telemetry"
internalVersion "github.com/datarobot/cli/internal/version"
"github.com/datarobot/cli/tui"
"github.com/spf13/cobra"
Expand All @@ -43,6 +44,9 @@ import (

var configFilePath string

// telemetryClientKey is used to store the telemetry client in context
type telemetryClientKey struct{}

// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: internalVersion.CliName,
Expand Down Expand Up @@ -71,10 +75,33 @@ using pre-built templates. Get from idea to production in minutes, not hours.

log.Start()

return initializeConfig(cmd)
err := initializeConfig(cmd)
if err != nil {
return err
}

// Initialize telemetry client
// Check if enabled first to avoid unnecessary filesystem and network I/O.
var props *telemetry.CommonProperties
if telemetry.IsEnabled() {
props = telemetry.CollectCommonProperties()
}
client := telemetry.NewClient(props)
Comment thread
cursor[bot] marked this conversation as resolved.

// Store telemetry client in context for use by commands
cmd.SetContext(context.WithValue(cmd.Context(), telemetryClientKey{}, client))

return nil
},
PersistentPostRun: func(_ *cobra.Command, _ []string) {
PersistentPostRunE: func(cmd *cobra.Command, _ []string) error {
// Flush telemetry events before exit
if client, ok := cmd.Context().Value(telemetryClientKey{}).(*telemetry.Client); ok {
client.Flush(3 * time.Second)
}

log.Stop()

return nil
},
}

Expand Down Expand Up @@ -105,6 +132,7 @@ func init() {
RootCmd.PersistentFlags().Bool("skip-auth", false, "skip authentication checks (for advanced users)")
RootCmd.PersistentFlags().Bool("force-interactive", false, "force setup wizards to run even if already completed")
RootCmd.PersistentFlags().Duration("plugin-discovery-timeout", 2*time.Second, "timeout for plugin discovery (0s disables)")
RootCmd.PersistentFlags().Bool("disable-telemetry", false, "disable anonymous usage telemetry")

// Make some of these flags available via Viper
_ = viper.BindPFlag("config", RootCmd.PersistentFlags().Lookup("config"))
Expand All @@ -113,6 +141,7 @@ func init() {
_ = viper.BindPFlag("skip-auth", RootCmd.PersistentFlags().Lookup("skip-auth"))
_ = viper.BindPFlag("force-interactive", RootCmd.PersistentFlags().Lookup("force-interactive"))
_ = viper.BindPFlag("plugin-discovery-timeout", RootCmd.PersistentFlags().Lookup("plugin-discovery-timeout"))
_ = viper.BindPFlag("disable-telemetry", RootCmd.PersistentFlags().Lookup("disable-telemetry"))

// Add command groups (plugin group added conditionally by registerPluginCommands)
RootCmd.AddGroup(
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.25.8

require (
github.com/Masterminds/semver/v3 v3.4.0
github.com/amplitude/analytics-go v1.3.0
github.com/charmbracelet/bubbles v1.0.0
github.com/charmbracelet/bubbletea v1.3.10
github.com/charmbracelet/glamour v1.0.0
Expand Down Expand Up @@ -45,6 +46,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/go-logfmt/logfmt v0.6.1 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/h2non/filetype v1.1.3 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/alecthomas/chroma/v2 v2.20.0 h1:sfIHpxPyR07/Oylvmcai3X/exDlE8+FA820NT
github.com/alecthomas/chroma/v2 v2.20.0/go.mod h1:e7tViK0xh/Nf4BYHl00ycY6rV7b8iXBksI9E359yNmA=
github.com/alecthomas/repr v0.5.1 h1:E3G4t2QbHTSNpPKBgMTln5KLkZHLOcU7r37J4pXBuIg=
github.com/alecthomas/repr v0.5.1/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/amplitude/analytics-go v1.3.0 h1:Lgj31fWThQ6hdDHO0RPxQfy/D7d8K+aqWsBa+IGTxQk=
github.com/amplitude/analytics-go v1.3.0/go.mod h1:kAQG8OQ6aPOxZrEZ3+/NFCfxdYSyjqXZhgkjWFD3/vo=
github.com/arduino/go-paths-helper v1.12.1 h1:WkxiVUxBjKWlLMiMuYy8DcmVrkxdP7aKxQOAq7r2lVM=
github.com/arduino/go-paths-helper v1.12.1/go.mod h1:jcpW4wr0u69GlXhTYydsdsqAjLaYK5n7oWHfKqOG6LM=
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
Expand Down Expand Up @@ -70,6 +72,8 @@ github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9L
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/h2non/filetype v1.1.3 h1:FKkx9QbD7HR/zjK1Ia5XiBsq9zdLi5Kf3zGyFTAFkGg=
Expand Down Expand Up @@ -153,6 +157,8 @@ github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3A
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
Expand Down
2 changes: 2 additions & 0 deletions goreleaser.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ builds:
-X "github.com/datarobot/cli/internal/version.Version={{ .Tag }}"
-X "github.com/datarobot/cli/internal/version.GitCommit={{ .ShortCommit }}"
-X "github.com/datarobot/cli/internal/version.BuildDate={{ .CommitDate }}"
-X "github.com/datarobot/cli/internal/telemetry.AmplitudeAPIKey={{ .Env.AMPLITUDE_API_KEY }}"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GoReleaser build breaks without AMPLITUDE_API_KEY env var

Medium Severity

The ldflags template {{ .Env.AMPLITUDE_API_KEY }} requires the environment variable to exist at GoReleaser execution time. In CI, ${{ secrets.AMPLITUDE_API_KEY }} resolves to an empty string when the secret doesn't exist (and the reviewer confirmed the secret doesn't exist yet), so the env var is set but empty. However, any local goreleaser release invocation without explicitly exporting AMPLITUDE_API_KEY will fail with a template error because .Env requires the variable to be present. A default value via GoReleaser's env section or envOr template function would prevent this.

Additional Locations (1)
Fix in Cursor Fix in Web

-X "github.com/datarobot/cli/internal/telemetry.InstallMethod=release"

goarch:
- amd64
Expand Down
7 changes: 7 additions & 0 deletions internal/drapi/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ func Get(url, info string) (*http.Response, error) {
return resp, err
}

// GetUserID returns a dummy user ID for telemetry.
// TODO: Discuss with the team whether /api/v2/userinfo/ is a valid endpoint
// and the appropriate way to fetch the user ID for telemetry.
func GetUserID(ctx context.Context) (string, error) {
return "unknown", nil
}

func GetJSON(url, info string, v any) error {
resp, err := Get(url, info)
if err != nil {
Expand Down
169 changes: 169 additions & 0 deletions internal/telemetry/events.go
Copy link
Copy Markdown
Contributor Author

@ajalon1 ajalon1 Mar 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seeing these generated for me, I wonder if it make sense to have a single event type (NewCLICommandEvent) and then a property for the actual event. That would, I think, decrease our Amplitude costs. Or to top-level command events like dr_plugin, dr_start, dr_dotenv ...

Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
// Copyright 2026 DataRobot, Inc. and its affiliates.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package telemetry

import "github.com/amplitude/analytics-go/amplitude/types"

// This file defines typed constructors for all 14 CLI telemetry events.
// Each function returns an amplitude.Event with the correct EventType
// and expected property keys. Call-site wiring happens in PR 2.

// NewDrStartEvent creates a "dr start execute" event.
func NewDrStartEvent(templateName string) types.Event {
return types.Event{
EventType: "dr start execute",
EventProperties: map[string]any{
"template_name": templateName,
},
}
}

// NewDrRunEvent creates a "dr run execute" event.
func NewDrRunEvent(templateName, taskName string) types.Event {
return types.Event{
EventType: "dr run execute",
EventProperties: map[string]any{
"template_name": templateName,
"task_name": taskName,
},
}
}

// NewDrTaskEvent creates a "dr task execute" event.
func NewDrTaskEvent(templateName, taskName string) types.Event {
return types.Event{
EventType: "dr task execute",
EventProperties: map[string]any{
"template_name": templateName,
"task_name": taskName,
},
}
}

// NewDrPluginUpdateEvent creates a "dr plugin update" event.
func NewDrPluginUpdateEvent(pluginName, versionNumber string) types.Event {
return types.Event{
EventType: "dr plugin update",
EventProperties: map[string]any{
"plugin_name": pluginName,
"version_number": versionNumber,
},
}
}

// NewDrTemplateSetupEvent creates a "dr template setup" event.
func NewDrTemplateSetupEvent(templateName string) types.Event {
return types.Event{
EventType: "dr template setup",
EventProperties: map[string]any{
"template_name": templateName,
},
}
}

// NewDrComponentAddEvent creates a "dr component add" event.
func NewDrComponentAddEvent(componentName, templateName string) types.Event {
return types.Event{
EventType: "dr component add",
EventProperties: map[string]any{
"component_name": componentName,
"template_name": templateName,
},
}
}

// NewDrComponentUpdateEvent creates a "dr component update" event.
func NewDrComponentUpdateEvent(componentName, templateName string) types.Event {
return types.Event{
EventType: "dr component update",
EventProperties: map[string]any{
"component_name": componentName,
"template_name": templateName,
},
}
}

// NewDrPluginExecuteEvent creates a "dr plugin execute" event.
func NewDrPluginExecuteEvent(pluginName, pluginVersion string) types.Event {
return types.Event{
EventType: "dr plugin execute",
EventProperties: map[string]any{
"plugin_name": pluginName,
"plugin_version": pluginVersion,
},
}
}

// NewDrPluginInstallEvent creates a "dr plugin install" event.
func NewDrPluginInstallEvent(pluginName, pluginVersion string) types.Event {
return types.Event{
EventType: "dr plugin install",
EventProperties: map[string]any{
"plugin_name": pluginName,
"plugin_version": pluginVersion,
},
}
}

// NewDrPluginUninstallEvent creates a "dr plugin uninstall" event.
func NewDrPluginUninstallEvent(pluginName, pluginVersion string) types.Event {
return types.Event{
EventType: "dr plugin uninstall",
EventProperties: map[string]any{
"plugin_name": pluginName,
"plugin_version": pluginVersion,
},
}
}

// NewDrAuthSetURLEvent creates a "dr auth set-url" event.
func NewDrAuthSetURLEvent(url string) types.Event {
return types.Event{
EventType: "dr auth set-url",
EventProperties: map[string]any{
"url": url,
},
}
}

// NewDrDotenvUpdateEvent creates a "dr dotenv update" event.
func NewDrDotenvUpdateEvent(templateName string) types.Event {
return types.Event{
EventType: "dr dotenv update",
EventProperties: map[string]any{
"template_name": templateName,
},
}
}

// NewDrDotenvSetupEvent creates a "dr dotenv setup" event.
func NewDrDotenvSetupEvent(templateName string) types.Event {
return types.Event{
EventType: "dr dotenv setup",
EventProperties: map[string]any{
"template_name": templateName,
},
}
}

// NewDrDotenvValidateEvent creates a "dr dotenv validate" event.
func NewDrDotenvValidateEvent(templateName string) types.Event {
return types.Event{
EventType: "dr dotenv validate",
EventProperties: map[string]any{
"template_name": templateName,
},
}
}
Loading