@@ -51,6 +51,7 @@ func (t *NoOpTracker) Track(event string, metadata map[string]any) error {
5151type DxTracker struct {
5252 mode Mode
5353 testMode bool
54+ product Product
5455
5556 logger zerolog.Logger
5657
@@ -61,7 +62,7 @@ type DxTracker struct {
6162// NewDxTracker initializes a tracker with automatic GitHub CLI integration for authentication.
6263// APITokenVariableName is the name of the GitHub repository variable containing the DX API token.
6364// Each DX project has its own token for tracking purposes.
64- func NewDxTracker (APITokenVariableName , product string ) (Tracker , error ) {
65+ func NewDxTracker (APITokenVariableName string , product Product ) (Tracker , error ) {
6566 t := & DxTracker {}
6667
6768 lvlStr := os .Getenv (EnvVarLogLevel )
@@ -77,7 +78,6 @@ func NewDxTracker(APITokenVariableName, product string) (Tracker, error) {
7778
7879 if os .Getenv (EnvVarDisableTracking ) == "true" {
7980 t .logger .Debug ().Msg ("Tracking disabled by environment variable" )
80-
8181 return & NoOpTracker {}, nil
8282 }
8383
@@ -94,6 +94,7 @@ func NewDxTracker(APITokenVariableName, product string) (Tracker, error) {
9494 return nil , errors .New ("product is required. Each DX project has its own token for tracking purposes" )
9595 }
9696 product = strings .ToLower (product )
97+ t .product = product
9798
9899 c , isConfigAvailable , configErr := openConfig ()
99100 if configErr != nil {
@@ -109,7 +110,7 @@ func NewDxTracker(APITokenVariableName, product string) (Tracker, error) {
109110 // and if so, try to configure tracker with it
110111 if t .checkIfGhCLIAvailable () {
111112 var configErr error
112- c , configErr = t .buildConfigWithGhCLI (APITokenVariableName , product )
113+ c , configErr = t .buildConfigWithGhCLI (APITokenVariableName , product , c )
113114 if configErr != nil {
114115 t .mode = ModeOffline
115116 t .logger .Warn ().Msgf ("Failed to build config with GH CLI: %s" , configErr .Error ())
@@ -139,6 +140,14 @@ func NewDxTracker(APITokenVariableName, product string) (Tracker, error) {
139140 if sendErr != nil {
140141 log .Debug ().Msgf ("Failed to send saved events: %s\n " , sendErr )
141142 }
143+
144+ // also try to send legacy events if product is local_CRE
145+ if product == "local_cre" {
146+ legacySendErr := t .sendSavedLegacyEvents ()
147+ if legacySendErr != nil {
148+ log .Debug ().Msgf ("Failed to send saved legacy events: %s\n " , legacySendErr )
149+ }
150+ }
142151 }()
143152 }
144153
@@ -147,20 +156,39 @@ func NewDxTracker(APITokenVariableName, product string) (Tracker, error) {
147156 return t , nil
148157}
149158
150- func (t * DxTracker ) buildConfigWithGhCLI (APITokenVariableName , product string ) (* config , error ) {
159+ func (t * DxTracker ) buildConfigWithGhCLI (APITokenVariableName , product string , existinConfig * config ) (* config , error ) {
151160 var userNameErr error
152- c := & config {}
153- c .GithubUsername , userNameErr = t .readGHUsername ()
154- if userNameErr != nil {
155- return nil , errors .Wrap (userNameErr , "failed to read github username" )
161+ var c * config
162+
163+ if existinConfig != nil {
164+ c = existinConfig
165+ } else {
166+ c = & config {}
167+ }
168+
169+ if c .GithubUsername == "" {
170+ c .GithubUsername , userNameErr = t .readGHUsername ()
171+ if userNameErr != nil {
172+ return nil , errors .Wrap (userNameErr , "failed to read github username" )
173+ }
156174 }
157175
158176 apiToken , apiTokenErr := t .readDXAPIToken (APITokenVariableName )
159177 if apiTokenErr != nil {
160178 return nil , errors .Wrap (apiTokenErr , "failed to read DX API token" )
161179 }
162- c .APITokens = map [Product ]string {
163- product : apiToken ,
180+
181+ if c .APITokens == nil || len (c .APITokens ) == 0 {
182+ c .APITokens = map [Product ]string {
183+ product : apiToken ,
184+ }
185+ } else {
186+ c .APITokens [product ] = apiToken
187+ }
188+
189+ // just in case
190+ if ! isConfigValid (c , product ) {
191+ return nil , errors .New ("incomplete config, missing API token or GitHub username" )
164192 }
165193
166194 saveErr := saveConfig (c )
@@ -347,7 +375,6 @@ type config struct {
347375}
348376
349377// openConfig attempts to load existing configuration from the user's home directory.
350- // If a legacy config is found, it is migrated to the new format.
351378func openConfig () (* config , bool , error ) {
352379 configPath , pathErr := configPath ()
353380 if pathErr != nil {
@@ -363,36 +390,37 @@ func openConfig() (*config, bool, error) {
363390 return nil , false , errors .Wrap (readErr , "failed to read config file" )
364391 }
365392
366- var legacyConfig legacyConfig
367- legacyUnmarshalErr := json .Unmarshal (configContent , & legacyConfig )
368- if legacyUnmarshalErr != nil {
369- return nil , false , errors .Wrap (legacyUnmarshalErr , "failed to unmarshal legacy config file" )
393+ var localConfig config
394+ unmarshalErr := json .Unmarshal (configContent , & localConfig )
395+ if unmarshalErr != nil {
396+ return nil , false , errors .Wrap (unmarshalErr , "failed to unmarshal config file" )
370397 }
371398
372- if legacyConfig .DxAPIToken != "" && legacyConfig .GithubUsername != "" {
373- newConfig := config {
374- GithubUsername : legacyConfig .GithubUsername ,
375- APITokens : map [Product ]string {
376- // it is the only product that uses tracking at the moment
377- "local_CRE" : legacyConfig .DxAPIToken ,
378- },
379- }
399+ return & localConfig , true , nil
400+ }
380401
381- saveErr := saveConfig (& newConfig )
382- if saveErr != nil {
383- return nil , false , errors .Wrap (saveErr , "failed to save new config" )
384- }
402+ func openLegacyConfig () (* legacyConfig , bool , error ) {
403+ configPath , pathErr := configPath ()
404+ if pathErr != nil {
405+ return nil , false , errors .Wrap (pathErr , "failed to get config path" )
406+ }
385407
386- return & newConfig , true , nil
408+ if _ , statErr := os .Stat (configPath ); os .IsNotExist (statErr ) {
409+ return nil , false , nil
387410 }
388411
389- var localConfig config
390- unmarshalErr := json .Unmarshal (configContent , & localConfig )
391- if unmarshalErr != nil {
392- return nil , false , errors .Wrap (unmarshalErr , "failed to unmarshal config file" )
412+ configContent , readErr := os .ReadFile (configPath )
413+ if readErr != nil {
414+ return nil , false , errors .Wrap (readErr , "failed to read config file" )
393415 }
394416
395- return & localConfig , true , nil
417+ var legacyConfig legacyConfig
418+ legacyUnmarshalErr := json .Unmarshal (configContent , & legacyConfig )
419+ if legacyUnmarshalErr != nil {
420+ return nil , false , errors .Wrap (legacyUnmarshalErr , "failed to unmarshal legacy config file" )
421+ }
422+
423+ return & legacyConfig , true , nil
396424}
397425
398426// isConfigValid ensures both API token and GitHub username are present.
@@ -401,13 +429,24 @@ func isConfigValid(c *config, product string) bool {
401429}
402430
403431// saveConfig persists configuration to the user's home directory with proper permissions.
432+ // it also migrates legacy config if available
404433func saveConfig (c * config ) error {
434+ legacyConfig , isLegacyConfigAvailable , legacyConfigErr := openLegacyConfig ()
435+ if legacyConfigErr != nil {
436+ return errors .Wrap (legacyConfigErr , "failed to open legacy config" )
437+ }
438+
439+ if isLegacyConfigAvailable && legacyConfig .DxAPIToken != "" && legacyConfig .GithubUsername != "" {
440+ // local CRE is the only product that used legacy config
441+ c .APITokens ["local_cre" ] = legacyConfig .DxAPIToken
442+ }
443+
405444 configPath , pathErr := configPath ()
406445 if pathErr != nil {
407446 return errors .Wrap (pathErr , "failed to get config path" )
408447 }
409448
410- mkdirErr := os .MkdirAll (filepath .Dir (configPath ), 0755 )
449+ mkdirErr := os .MkdirAll (filepath .Dir (configPath ), 0o755 )
411450 if mkdirErr != nil {
412451 return errors .Wrap (mkdirErr , "failed to create config directory" )
413452 }
@@ -442,12 +481,12 @@ type event struct {
442481func (t * DxTracker ) saveEvent (name string , timestamp int64 , metadata map [string ]any ) error {
443482 t .logger .Debug ().Msgf ("Saving event. Name: %s, Timestamp: %d, Metadata: %v" , name , timestamp , metadata )
444483
445- storagePath , pathErr := storagePath ()
484+ storagePath , pathErr := storagePath (t . product )
446485 if pathErr != nil {
447486 return errors .Wrap (pathErr , "failed to get storage path" )
448487 }
449488
450- mkdirErr := os .MkdirAll (filepath .Dir (storagePath ), 0755 )
489+ mkdirErr := os .MkdirAll (filepath .Dir (storagePath ), 0o755 )
451490 if mkdirErr != nil {
452491 return errors .Wrap (mkdirErr , "failed to create storage directory" )
453492 }
@@ -476,40 +515,54 @@ func (t *DxTracker) saveEvent(name string, timestamp int64, metadata map[string]
476515 return errors .Wrap (err , "failed to marshal events to JSON" )
477516 }
478517
479- if err := os .WriteFile (storagePath , jsonData , 0600 ); err != nil {
518+ if err := os .WriteFile (storagePath , jsonData , 0o600 ); err != nil {
480519 return errors .Wrap (err , "failed to write event to storage file" )
481520 }
482521
483522 return nil
484523}
485524
486- // sendSavedEvents attempts to send all queued events and clears the queue on success.
487- func (t * DxTracker ) sendSavedEvents () error {
488- storagePath , pathErr := storagePath ()
489- if pathErr != nil {
490- return errors .Wrap (pathErr , "failed to get storage path" )
491- }
492-
493- stats , statErr := os .Stat (storagePath )
525+ func readEvents (path string ) ([]event , error ) {
526+ stats , statErr := os .Stat (path )
494527 if os .IsNotExist (statErr ) {
495- return nil
528+ return nil , nil
496529 }
497530
498531 if stats .Size () == 0 {
499- return nil
532+ return nil , nil
500533 }
501534
502- storageFile , storageErr := os .OpenFile (storagePath , os .O_RDONLY , 0644 )
535+ storageFile , storageErr := os .OpenFile (path , os .O_RDONLY , 0644 )
503536 if storageErr != nil {
504- return errors .Wrap (storageErr , "failed to open storage file" )
537+ return nil , errors .Wrap (storageErr , "failed to open storage file" )
505538 }
506539 defer storageFile .Close ()
507540
508541 var events []event
509542
510543 decoderErr := json .NewDecoder (storageFile ).Decode (& events )
511544 if decoderErr != nil {
512- return errors .Wrap (decoderErr , "failed to decode events from storage file" )
545+ return nil , errors .Wrap (decoderErr , "failed to decode events from storage file" )
546+ }
547+
548+ return events , nil
549+ }
550+
551+ // sendSavedEvents attempts to send all queued events and clears the queue on success.
552+ func (t * DxTracker ) sendSavedEvents () error {
553+ storagePath , pathErr := storagePath (t .product )
554+ if pathErr != nil {
555+ return errors .Wrap (pathErr , "failed to get storage path" )
556+ }
557+
558+ events , readErr := readEvents (storagePath )
559+ if readErr != nil {
560+ return errors .Wrap (readErr , "failed to read saved events" )
561+ }
562+
563+ if len (events ) == 0 {
564+ t .logger .Debug ().Msg ("No saved events to send" )
565+ return nil
513566 }
514567
515568 t .logger .Debug ().Msgf ("Sending %d saved events" , len (events ))
@@ -545,9 +598,58 @@ func (t *DxTracker) sendSavedEvents() error {
545598 return nil
546599}
547600
601+ func (t * DxTracker ) sendSavedLegacyEvents () error {
602+ storagePath , pathErr := legacyStoragePath ()
603+ if pathErr != nil {
604+ return errors .Wrap (pathErr , "failed to get storage path" )
605+ }
606+
607+ events , readErr := readEvents (storagePath )
608+ if readErr != nil {
609+ return errors .Wrap (readErr , "failed to read saved events" )
610+ }
611+
612+ if len (events ) == 0 {
613+ t .logger .Debug ().Msg ("No saved legacy events to send" )
614+ return nil
615+ }
616+
617+ t .logger .Debug ().Msgf ("Sending %d saved legacy events" , len (events ))
618+
619+ failedEvents := []event {}
620+
621+ for _ , event := range events {
622+ sendErr := t .sendEvent (event .Name , event .Timestamp , event .Metadata )
623+ if sendErr != nil {
624+ failedEvents = append (failedEvents , event )
625+ t .logger .Debug ().Msgf ("Failed to send event: %s" , sendErr .Error ())
626+ }
627+ }
628+
629+ clearErr := t .clearSavedLegacyEvents ()
630+ if clearErr != nil {
631+ return errors .Wrap (clearErr , "failed to clear saved legacy events" )
632+ }
633+
634+ // if there are failed events, save them to the storage file to try again later
635+ if len (failedEvents ) > 0 {
636+ t .logger .Warn ().Msgf ("Failed to send %d legacy events" , len (failedEvents ))
637+ for _ , event := range failedEvents {
638+ saveErr := t .saveEvent (event .Name , event .Timestamp , event .Metadata )
639+ if saveErr != nil {
640+ t .logger .Warn ().Msgf ("Failed to save failed legacy event: %s" , saveErr .Error ())
641+ }
642+ }
643+ } else {
644+ t .logger .Debug ().Msg ("All saved legacy events sent successfully" )
645+ }
646+
647+ return nil
648+ }
649+
548650// clearSavedEvents removes all queued events after successful transmission.
549651func (t * DxTracker ) clearSavedEvents () error {
550- storagePath , pathErr := storagePath ()
652+ storagePath , pathErr := storagePath (t . product )
551653 if pathErr != nil {
552654 return errors .Wrap (pathErr , "failed to get storage path" )
553655 }
@@ -561,6 +663,22 @@ func (t *DxTracker) clearSavedEvents() error {
561663 return nil
562664}
563665
666+ // clearSavedLegacyEvents removes all queued events after successful transmission.
667+ func (t * DxTracker ) clearSavedLegacyEvents () error {
668+ storagePath , pathErr := legacyStoragePath ()
669+ if pathErr != nil {
670+ return errors .Wrap (pathErr , "failed to get legacy storage path" )
671+ }
672+
673+ storageFile , openErr := os .OpenFile (storagePath , os .O_TRUNC | os .O_CREATE | os .O_WRONLY , 0644 )
674+ if openErr != nil {
675+ return errors .Wrap (openErr , "failed to truncate storage file" )
676+ }
677+ defer storageFile .Close ()
678+
679+ return nil
680+ }
681+
564682// validateEvent ensures all required event fields are present and non-empty.
565683func validateEvent (event string , timestamp int64 , metadata map [string ]any ) error {
566684 if event == "" {
@@ -579,7 +697,18 @@ func validateEvent(event string, timestamp int64, metadata map[string]any) error
579697}
580698
581699// storagePath returns the path to the events queue file in the user's home directory.
582- func storagePath () (string , error ) {
700+ func storagePath (product Product ) (string , error ) {
701+ homeDir , err := os .UserHomeDir ()
702+ if err != nil {
703+ return "" , errors .Wrap (err , "failed to get user home directory" )
704+ }
705+
706+ return filepath .Join (homeDir , ".local" , "share" , "dx" , product + "_events.json" ), nil
707+ }
708+
709+ // legacyStoragePath returns the path to the events queue file in the user's home directory.
710+ // deprecated: legacyStoragePath is used to read old storage files and migrate them to the new format. Use storagePath instead.
711+ func legacyStoragePath () (string , error ) {
583712 homeDir , err := os .UserHomeDir ()
584713 if err != nil {
585714 return "" , errors .Wrap (err , "failed to get user home directory" )
0 commit comments