@@ -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 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 )
@@ -334,7 +362,6 @@ func (t *DxTracker) readDXAPIToken(APITokenVariableName string) (string, error)
334362}
335363
336364// config stores authentication credentials for the DX API.
337- // deprecated: legacyConfig is used to read old config files and migrate them to the new format. Use config instead.
338365type legacyConfig struct {
339366 DxAPIToken string `json:"dx_api_token"`
340367 GithubUsername string `json:"github_username"`
@@ -347,7 +374,6 @@ type config struct {
347374}
348375
349376// 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.
351377func openConfig () (* config , bool , error ) {
352378 configPath , pathErr := configPath ()
353379 if pathErr != nil {
@@ -363,36 +389,37 @@ func openConfig() (*config, bool, error) {
363389 return nil , false , errors .Wrap (readErr , "failed to read config file" )
364390 }
365391
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" )
392+ var localConfig config
393+ unmarshalErr := json .Unmarshal (configContent , & localConfig )
394+ if unmarshalErr != nil {
395+ return nil , false , errors .Wrap (unmarshalErr , "failed to unmarshal config file" )
370396 }
371397
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- }
398+ return & localConfig , true , nil
399+ }
380400
381- saveErr := saveConfig (& newConfig )
382- if saveErr != nil {
383- return nil , false , errors .Wrap (saveErr , "failed to save new config" )
384- }
401+ func openLegacyConfig () (* legacyConfig , bool , error ) {
402+ configPath , pathErr := configPath ()
403+ if pathErr != nil {
404+ return nil , false , errors .Wrap (pathErr , "failed to get config path" )
405+ }
385406
386- return & newConfig , true , nil
407+ if _ , statErr := os .Stat (configPath ); os .IsNotExist (statErr ) {
408+ return nil , false , nil
387409 }
388410
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" )
411+ configContent , readErr := os .ReadFile (configPath )
412+ if readErr != nil {
413+ return nil , false , errors .Wrap (readErr , "failed to read config file" )
393414 }
394415
395- return & localConfig , true , nil
416+ var legacyConfig legacyConfig
417+ legacyUnmarshalErr := json .Unmarshal (configContent , & legacyConfig )
418+ if legacyUnmarshalErr != nil {
419+ return nil , false , errors .Wrap (legacyUnmarshalErr , "failed to unmarshal legacy config file" )
420+ }
421+
422+ return & legacyConfig , true , nil
396423}
397424
398425// isConfigValid ensures both API token and GitHub username are present.
@@ -401,13 +428,24 @@ func isConfigValid(c *config, product string) bool {
401428}
402429
403430// saveConfig persists configuration to the user's home directory with proper permissions.
431+ // it also migrates legacy config if available
404432func saveConfig (c * config ) error {
433+ legacyConfig , isLegacyConfigAvailable , legacyConfigErr := openLegacyConfig ()
434+ if legacyConfigErr != nil {
435+ return errors .Wrap (legacyConfigErr , "failed to open legacy config" )
436+ }
437+
438+ if isLegacyConfigAvailable && legacyConfig .DxAPIToken != "" && legacyConfig .GithubUsername != "" {
439+ // local CRE is the only product that used legacy config
440+ c .APITokens ["local_cre" ] = legacyConfig .DxAPIToken
441+ }
442+
405443 configPath , pathErr := configPath ()
406444 if pathErr != nil {
407445 return errors .Wrap (pathErr , "failed to get config path" )
408446 }
409447
410- mkdirErr := os .MkdirAll (filepath .Dir (configPath ), 0755 )
448+ mkdirErr := os .MkdirAll (filepath .Dir (configPath ), 0o755 )
411449 if mkdirErr != nil {
412450 return errors .Wrap (mkdirErr , "failed to create config directory" )
413451 }
@@ -442,12 +480,12 @@ type event struct {
442480func (t * DxTracker ) saveEvent (name string , timestamp int64 , metadata map [string ]any ) error {
443481 t .logger .Debug ().Msgf ("Saving event. Name: %s, Timestamp: %d, Metadata: %v" , name , timestamp , metadata )
444482
445- storagePath , pathErr := storagePath ()
483+ storagePath , pathErr := storagePath (t . product )
446484 if pathErr != nil {
447485 return errors .Wrap (pathErr , "failed to get storage path" )
448486 }
449487
450- mkdirErr := os .MkdirAll (filepath .Dir (storagePath ), 0755 )
488+ mkdirErr := os .MkdirAll (filepath .Dir (storagePath ), 0o755 )
451489 if mkdirErr != nil {
452490 return errors .Wrap (mkdirErr , "failed to create storage directory" )
453491 }
@@ -476,40 +514,54 @@ func (t *DxTracker) saveEvent(name string, timestamp int64, metadata map[string]
476514 return errors .Wrap (err , "failed to marshal events to JSON" )
477515 }
478516
479- if err := os .WriteFile (storagePath , jsonData , 0600 ); err != nil {
517+ if err := os .WriteFile (storagePath , jsonData , 0o600 ); err != nil {
480518 return errors .Wrap (err , "failed to write event to storage file" )
481519 }
482520
483521 return nil
484522}
485523
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 )
524+ func readEvents (path string ) ([]event , error ) {
525+ stats , statErr := os .Stat (path )
494526 if os .IsNotExist (statErr ) {
495- return nil
527+ return nil , nil
496528 }
497529
498530 if stats .Size () == 0 {
499- return nil
531+ return nil , nil
500532 }
501533
502- storageFile , storageErr := os .OpenFile (storagePath , os .O_RDONLY , 0644 )
534+ storageFile , storageErr := os .OpenFile (path , os .O_RDONLY , 0644 )
503535 if storageErr != nil {
504- return errors .Wrap (storageErr , "failed to open storage file" )
536+ return nil , errors .Wrap (storageErr , "failed to open storage file" )
505537 }
506538 defer storageFile .Close ()
507539
508540 var events []event
509541
510542 decoderErr := json .NewDecoder (storageFile ).Decode (& events )
511543 if decoderErr != nil {
512- return errors .Wrap (decoderErr , "failed to decode events from storage file" )
544+ return nil , errors .Wrap (decoderErr , "failed to decode events from storage file" )
545+ }
546+
547+ return events , nil
548+ }
549+
550+ // sendSavedEvents attempts to send all queued events and clears the queue on success.
551+ func (t * DxTracker ) sendSavedEvents () error {
552+ storagePath , pathErr := storagePath (t .product )
553+ if pathErr != nil {
554+ return errors .Wrap (pathErr , "failed to get storage path" )
555+ }
556+
557+ events , readErr := readEvents (storagePath )
558+ if readErr != nil {
559+ return errors .Wrap (readErr , "failed to read saved events" )
560+ }
561+
562+ if len (events ) == 0 {
563+ t .logger .Debug ().Msg ("No saved events to send" )
564+ return nil
513565 }
514566
515567 t .logger .Debug ().Msgf ("Sending %d saved events" , len (events ))
@@ -545,9 +597,58 @@ func (t *DxTracker) sendSavedEvents() error {
545597 return nil
546598}
547599
600+ func (t * DxTracker ) sendSavedLegacyEvents () error {
601+ storagePath , pathErr := legacyStoragePath ()
602+ if pathErr != nil {
603+ return errors .Wrap (pathErr , "failed to get storage path" )
604+ }
605+
606+ events , readErr := readEvents (storagePath )
607+ if readErr != nil {
608+ return errors .Wrap (readErr , "failed to read saved events" )
609+ }
610+
611+ if len (events ) == 0 {
612+ t .logger .Debug ().Msg ("No saved legacy events to send" )
613+ return nil
614+ }
615+
616+ t .logger .Debug ().Msgf ("Sending %d saved legacy events" , len (events ))
617+
618+ failedEvents := []event {}
619+
620+ for _ , event := range events {
621+ sendErr := t .sendEvent (event .Name , event .Timestamp , event .Metadata )
622+ if sendErr != nil {
623+ failedEvents = append (failedEvents , event )
624+ t .logger .Debug ().Msgf ("Failed to send event: %s" , sendErr .Error ())
625+ }
626+ }
627+
628+ clearErr := t .clearSavedLegacyEvents ()
629+ if clearErr != nil {
630+ return errors .Wrap (clearErr , "failed to clear saved legacy events" )
631+ }
632+
633+ // if there are failed events, save them to the storage file to try again later
634+ if len (failedEvents ) > 0 {
635+ t .logger .Warn ().Msgf ("Failed to send %d legacy events" , len (failedEvents ))
636+ for _ , event := range failedEvents {
637+ saveErr := t .saveEvent (event .Name , event .Timestamp , event .Metadata )
638+ if saveErr != nil {
639+ t .logger .Warn ().Msgf ("Failed to save failed legacy event: %s" , saveErr .Error ())
640+ }
641+ }
642+ } else {
643+ t .logger .Debug ().Msg ("All saved legacy events sent successfully" )
644+ }
645+
646+ return nil
647+ }
648+
548649// clearSavedEvents removes all queued events after successful transmission.
549650func (t * DxTracker ) clearSavedEvents () error {
550- storagePath , pathErr := storagePath ()
651+ storagePath , pathErr := storagePath (t . product )
551652 if pathErr != nil {
552653 return errors .Wrap (pathErr , "failed to get storage path" )
553654 }
@@ -561,6 +662,22 @@ func (t *DxTracker) clearSavedEvents() error {
561662 return nil
562663}
563664
665+ // clearSavedLegacyEvents removes all queued events after successful transmission.
666+ func (t * DxTracker ) clearSavedLegacyEvents () error {
667+ storagePath , pathErr := legacyStoragePath ()
668+ if pathErr != nil {
669+ return errors .Wrap (pathErr , "failed to get legacy storage path" )
670+ }
671+
672+ storageFile , openErr := os .OpenFile (storagePath , os .O_TRUNC | os .O_CREATE | os .O_WRONLY , 0644 )
673+ if openErr != nil {
674+ return errors .Wrap (openErr , "failed to truncate storage file" )
675+ }
676+ defer storageFile .Close ()
677+
678+ return nil
679+ }
680+
564681// validateEvent ensures all required event fields are present and non-empty.
565682func validateEvent (event string , timestamp int64 , metadata map [string ]any ) error {
566683 if event == "" {
@@ -579,7 +696,17 @@ func validateEvent(event string, timestamp int64, metadata map[string]any) error
579696}
580697
581698// storagePath returns the path to the events queue file in the user's home directory.
582- func storagePath () (string , error ) {
699+ func storagePath (product Product ) (string , error ) {
700+ homeDir , err := os .UserHomeDir ()
701+ if err != nil {
702+ return "" , errors .Wrap (err , "failed to get user home directory" )
703+ }
704+
705+ return filepath .Join (homeDir , ".local" , "share" , "dx" , product + "_events.json" ), nil
706+ }
707+
708+ // legacyStoragePath returns the path to the events queue file in the user's home directory.
709+ func legacyStoragePath () (string , error ) {
583710 homeDir , err := os .UserHomeDir ()
584711 if err != nil {
585712 return "" , errors .Wrap (err , "failed to get user home directory" )
0 commit comments