@@ -22,6 +22,7 @@ import (
2222 "github.com/duneanalytics/cli/cmd/query"
2323 "github.com/duneanalytics/cli/cmd/sim"
2424 "github.com/duneanalytics/cli/cmd/usage"
25+ "github.com/duneanalytics/cli/cmd/whoami"
2526 "github.com/duneanalytics/cli/cmdutil"
2627 "github.com/duneanalytics/cli/tracking"
2728)
@@ -77,6 +78,13 @@ var rootCmd = &cobra.Command{
7778 client := dune .NewDuneClient (env )
7879 cmdutil .SetClient (cmd , client )
7980
81+ // Resolve customer identity for analytics (best-effort, never blocks the CLI).
82+ if tr := cmdutil .TrackerFromCmd (cmd ); tr != nil {
83+ if customerID := resolveCustomerID (client , env .APIKey ); customerID != "" {
84+ tr .SetUserID (customerID )
85+ }
86+ }
87+
8088 return nil
8189 },
8290 PersistentPostRunE : func (cmd * cobra.Command , _ []string ) error {
@@ -93,7 +101,8 @@ var rootCmd = &cobra.Command{
93101 commandPath = parts [1 ]
94102 }
95103
96- tr .Track (commandPath , tracking .StatusSuccess , "" , durationMs )
104+ isSim := strings .HasPrefix (commandPath , "sim" )
105+ tr .Track (commandPath , tracking .StatusSuccess , "" , durationMs , isSim )
97106 return nil
98107 },
99108}
@@ -107,6 +116,7 @@ func init() {
107116 rootCmd .AddCommand (query .NewQueryCmd ())
108117 rootCmd .AddCommand (execution .NewExecutionCmd ())
109118 rootCmd .AddCommand (usage .NewUsageCmd ())
119+ rootCmd .AddCommand (whoami .NewWhoAmICmd ())
110120 rootCmd .AddCommand (sim .NewSimCmd ())
111121}
112122
@@ -115,11 +125,9 @@ func Execute(version, commit, date, amplitudeKey string) {
115125 versionStr := fmt .Sprintf ("%s (commit: %s, built: %s)" , version , commit , date )
116126
117127 telemetryEnabled := duneconfig .IsTelemetryEnabled ()
118- configDir , _ := authconfig .Dir ()
119128 tracker := tracking .New (tracking.Config {
120129 AmplitudeKey : amplitudeKey ,
121130 CLIVersion : version ,
122- ConfigDir : configDir ,
123131 Enabled : telemetryEnabled ,
124132 })
125133 defer tracker .Shutdown ()
@@ -132,7 +140,8 @@ func Execute(version, commit, date, amplitudeKey string) {
132140 ); err != nil {
133141 // Build best-effort command path from os.Args (strip flags).
134142 commandPath := commandPathFromArgs (os .Args )
135- tracker .Track (commandPath , tracking .StatusError , err .Error (), 0 )
143+ isSim := strings .HasPrefix (commandPath , "sim" )
144+ tracker .Track (commandPath , tracking .StatusError , err .Error (), 0 , isSim )
136145 // Flush the event before exiting — os.Exit does not run deferred funcs,
137146 // so defer tracker.Shutdown() above would never fire.
138147 tracker .Shutdown ()
@@ -142,6 +151,34 @@ func Execute(version, commit, date, amplitudeKey string) {
142151 }
143152}
144153
154+ // resolveCustomerID returns the customer_id associated with the given API key.
155+ // The customer_id may represent a user ("user_123") or a team ("team_456").
156+ // It uses a local cache to avoid calling /api/whoami on every invocation.
157+ // On any error it returns "" silently — analytics should never block the CLI.
158+ func resolveCustomerID (client dune.DuneClient , apiKey string ) string {
159+ keyHash := authconfig .HashAPIKey (apiKey )
160+
161+ // Try the cache first.
162+ cached , err := authconfig .LoadIdentity ()
163+ if err == nil && cached != nil && cached .APIKeyHash == keyHash && cached .CustomerID != "" {
164+ return cached .CustomerID
165+ }
166+
167+ // Cache miss or stale — call the API.
168+ resp , err := client .WhoAmI ()
169+ if err != nil || resp == nil || resp .CustomerID == "" {
170+ return ""
171+ }
172+
173+ // Persist for next time (best-effort).
174+ _ = authconfig .SaveIdentity (& authconfig.UserIdentity {
175+ CustomerID : resp .CustomerID ,
176+ APIKeyHash : keyHash ,
177+ })
178+
179+ return resp .CustomerID
180+ }
181+
145182// commandPathFromArgs extracts the subcommand path from os.Args, skipping
146183// the binary name, flags, and flag values so the tracked path is e.g.
147184// "query list" even when invoked as "dune --api-key KEY query list --limit 10".
0 commit comments