@@ -15,6 +15,7 @@ import (
1515 "github.com/akitasoftware/akita-cli/rest"
1616 "github.com/akitasoftware/akita-cli/version"
1717 "github.com/akitasoftware/akita-libs/analytics"
18+ "github.com/akitasoftware/go-utils/maps"
1819)
1920
2021var (
@@ -27,14 +28,20 @@ var (
2728 // Client key; set at link-time with -X flag
2829 defaultAmplitudeKey = ""
2930
30- // Store the distinct ID; run through the process
31+ // Store the user ID and team ID; run through the process
3132 // of getting it only once.
32- userDistinctID string
33- userDistinctOnce sync. Once
33+ userID string
34+ teamID string
3435
3536 // Timeout talking to API.
3637 // Shorter than normal because we don't want the CLI to be slow.
3738 userAPITimeout = 2 * time .Second
39+
40+ // Sync.once to ensure that client is initialized before trying to use it.
41+ initClientOnce sync.Once
42+
43+ // Whether to log client init logs to the console
44+ isLoggingEnabled bool = true
3845)
3946
4047type nullClient struct {}
@@ -52,8 +59,14 @@ func (_ nullClient) Close() error {
5259}
5360
5461// Initialize the telemetry client.
55- // This should be called once at startup either from the root command or from a subcommand that overrides the default PersistentPreRun.
56- func Init (isLoggingEnabled bool ) {
62+ // This should be called once at startup either from the root command
63+ // or from a subcommand that overrides the default PersistentPreRun.
64+ func Init (loggingEnabled bool ) {
65+ isLoggingEnabled = loggingEnabled
66+ initClientOnce .Do (doInit )
67+ }
68+
69+ func doInit () {
5770 // Opt-out mechanism
5871 disableTelemetry := os .Getenv ("AKITA_DISABLE_TELEMETRY" ) + os .Getenv ("POSTMAN_INSIGHTS_AGENT_DISABLE_TELEMETRY" )
5972 if disableTelemetry != "" {
@@ -107,39 +120,50 @@ func Init(isLoggingEnabled bool) {
107120 printer .Infof ("Please send this log message to %s.\n " , consts .SupportEmail )
108121 }
109122 analyticsClient = nullClient {}
110- } else {
111- analyticsEnabled = true
123+ return
124+ }
125+
126+ analyticsEnabled = true
127+
128+ userID , teamID , err = getUserIdentity () // Initialize user ID and team ID
129+ if err != nil {
130+ if isLoggingEnabled {
131+ printer .Infof ("Telemetry unavailable; error getting userID for given API key: %v\n " , err )
132+ printer .Infof ("Postman support will not be able to see any errors you encounter.\n " )
133+ printer .Infof ("Please send this log message to %s.\n " , consts .SupportEmail )
134+ }
135+ analyticsClient = nullClient {}
136+ return
112137 }
138+
139+ // Set up automatic reporting of all API errors
140+ // (rest can't call telemetry directly because we call rest above!)
141+ rest .SetAPIErrorHandler (APIError )
113142}
114143
115- func getDistinctID () string {
116- // If we have a user email, use that!
144+ func getUserIdentity () ( string , string , error ) {
145+ // If we can get user details use userID and teamID
117146 // Otherwise use the configured API Key.
118- // Failing that, try to use the user name and host name?
147+ // Failing that, try to use the user name and host name.
148+ // In latter 2 cases teamID will be empty.
119149
120150 id := os .Getenv ("POSTMAN_ANALYTICS_DISTINCT_ID" )
121151 if id != "" {
122- return id
152+ return id , "" , nil
123153 }
124154
125155 // If there's no credentials configured, skip the API call and
126156 // do not emit a log message.
127157 // Similarly if telemetry is disabled.
128158 if cfg .CredentialsPresent () && analyticsEnabled {
129- // Call the REST API to get the user email associated with the configured
159+ // Call the REST API to get the postman user associated with the configured
130160 // API key.
131161 ctx , cancel := context .WithTimeout (context .Background (), userAPITimeout )
132162 defer cancel ()
133163 frontClient := rest .NewFrontClient (rest .Domain , GetClientID ())
134164 userResponse , err := frontClient .GetUser (ctx )
135165 if err == nil {
136- if userResponse .Email != "" {
137- return userResponse .Email
138- }
139-
140- // Use the user ID if no email is present;
141- // this should be fixed in the current backend.
142- return fmt .Sprint (userResponse .ID )
166+ return fmt .Sprint (userResponse .ID ), fmt .Sprint (userResponse .TeamID ), nil
143167 }
144168
145169 printer .Infof ("Telemetry using temporary ID; GetUser API call failed: %v\n " , err )
@@ -148,44 +172,31 @@ func getDistinctID() string {
148172 }
149173
150174 // Try to derive a distinct ID from the credentials, if present, even
151- // if the /v1/user call failed.
175+ // if the getUser() call failed.
152176 keyID := cfg .DistinctIDFromCredentials ()
153177 if keyID != "" {
154- return keyID
178+ return keyID , "" , nil
155179 }
156180
157181 localUser , err := user .Current ()
158182 if err != nil {
159- return "unknown"
183+ return "" , "" , err
160184 }
161185 localHost , err := os .Hostname ()
162186 if err != nil {
163- return localUser .Username
187+ return localUser .Username , "" , nil
164188 }
165- return localUser .Username + "@" + localHost
166- }
167-
168- func distinctID () string {
169- userDistinctOnce .Do (func () {
170- userDistinctID = getDistinctID ()
171-
172- // Set up automatic reporting of all API errors
173- // (rest can't call telemetry directly because we call rest above!)
174- rest .SetAPIErrorHandler (APIError )
175-
176- printer .Debugf ("Using ID %q for telemetry\n " , userDistinctID )
177- })
178- return userDistinctID
189+ return localUser .Username + "@" + localHost , "" , nil
179190}
180191
181192// Report an error in a particular operation (inContext), including
182193// the text of the error.
183194func Error (inContext string , e error ) {
184- analyticsClient . Track ( distinctID (),
185- fmt . Sprintf ( "Error in %s" , inContext ) ,
195+ tryTrackingEvent (
196+ "Operation - Errored" ,
186197 map [string ]any {
187- "error " : e . Error () ,
188- "type " : "error" ,
198+ "operation " : inContext ,
199+ "error " : e . Error () ,
189200 },
190201 )
191202}
@@ -235,80 +246,80 @@ func RateLimitError(inContext string, e error) {
235246 rateLimitMap .Store (inContext , newRecord )
236247 }
237248
238- analyticsClient . Track ( distinctID (),
239- fmt . Sprintf ( "Error in %s" , inContext ) ,
249+ tryTrackingEvent (
250+ "Operation - Rate Limited" ,
240251 map [string ]any {
241- "error " : e . Error () ,
242- "type " : "error" ,
243- "count" : count ,
252+ "operation " : inContext ,
253+ "error " : e . Error () ,
254+ "count" : count ,
244255 },
245256 )
246257}
247258
248259// Report an error in a particular API, including the text of the error.
249260func APIError (method string , path string , e error ) {
250- analyticsClient . Track ( distinctID (),
251- "Error calling API " ,
261+ tryTrackingEvent (
262+ "API Call - Errored " ,
252263 map [string ]any {
253264 "method" : method ,
254265 "path" : path ,
255266 "error" : e .Error (),
256- "type" : "error" ,
257267 },
258268 )
259269}
260270
261271// Report a failure without a specific error object
262272func Failure (message string ) {
263- analyticsClient . Track ( distinctID (),
264- fmt . Sprintf ( "Unknown Error: %s" , message ) ,
273+ tryTrackingEvent (
274+ "Operation - Errored" ,
265275 map [string ]any {
266- "type " : "error" ,
276+ "error " : message ,
267277 },
268278 )
269279}
270280
271281// Report success of an operation
272282func Success (message string ) {
273- analyticsClient . Track ( distinctID (),
274- fmt . Sprintf ( "Success in %s" , message ) ,
283+ tryTrackingEvent (
284+ "Operation - Succeeded" ,
275285 map [string ]any {
276- "type " : "success" ,
286+ "operation " : message ,
277287 },
278288 )
279289}
280290
281291// Report a step in a multi-part workflow.
282292func WorkflowStep (workflow string , message string ) {
283- analyticsClient . Track ( distinctID (),
284- fmt . Sprintf ( "Executing Step: %s" , message ) ,
293+ tryTrackingEvent (
294+ "Workflow Step - Executed" ,
285295 map [string ]any {
286- "type " : "workflow" ,
296+ "step " : message ,
287297 "workflow" : workflow ,
288298 },
289299 )
290300}
291301
292302// Report command line flags (before any error checking.)
293303func CommandLine (command string , commandLine []string ) {
294- analyticsClient . Track ( distinctID (),
295- fmt . Sprintf ( "Executed %s" , command ) ,
304+ tryTrackingEvent (
305+ "Command - Executed" ,
296306 map [string ]any {
307+ "command" : command ,
297308 "command_line" : commandLine ,
298309 },
299310 )
300311}
301312
302313// Report the platform and version of an attempted integration
303314func InstallIntegrationVersion (integration , arch , platform , version string ) {
304- analyticsClient . Track ( distinctID (),
305- fmt . Sprintf ( "Install %s" , integration ) ,
315+ tryTrackingEvent (
316+ "Integration - Installed" ,
306317 map [string ]any {
318+ "integration" : integration ,
307319 "architecture" : arch ,
308320 "version" : version ,
309321 "platform" : platform ,
310- },
311- )
322+ })
312323}
313324
314325// Flush the telemetry to its endpoint
@@ -321,3 +332,22 @@ func Shutdown() {
321332 printer .Infof ("Please send the CLI output to %s.\n " , consts .SupportEmail )
322333 }
323334}
335+
336+ // Attempts to track an event using the provided event name and properties. It adds the user ID
337+ // and team ID to the event properties, and then sends the event to the analytics client.
338+ // If there is an error sending the event, a warning message is printed.
339+ func tryTrackingEvent (eventName string , eventProperties maps.Map [string , any ]) {
340+ // precondition: analyticsClient is initialized
341+ initClientOnce .Do (doInit )
342+
343+ eventProperties .Upsert ("user_id" , userID , func (v , newV any ) any { return v })
344+
345+ if teamID != "" {
346+ eventProperties .Upsert ("team_id" , teamID , func (v , newV any ) any { return v })
347+ }
348+
349+ err := analyticsClient .Track (userID , eventName , eventProperties )
350+ if err != nil {
351+ printer .Warningf ("Error sending analytics event %q: %v\n " , eventName , err )
352+ }
353+ }
0 commit comments