Skip to content

Commit 7953ae1

Browse files
committed
events-saving is also product-aware
1 parent 6d3ae91 commit 7953ae1

File tree

1 file changed

+181
-52
lines changed

1 file changed

+181
-52
lines changed

framework/tracking/dx.go

Lines changed: 181 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ func (t *NoOpTracker) Track(event string, metadata map[string]any) error {
5151
type 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.
351378
func 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
404433
func 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 {
442481
func (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.
549651
func (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.
565683
func 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

Comments
 (0)