Skip to content

Commit 2e62397

Browse files
committed
events-saving is also product-aware
1 parent 6d3ae91 commit 2e62397

File tree

1 file changed

+180
-53
lines changed

1 file changed

+180
-53
lines changed

framework/tracking/dx.go

Lines changed: 180 additions & 53 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 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.
338365
type 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.
351377
func 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
404432
func 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 {
442480
func (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.
549650
func (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.
565682
func 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

Comments
 (0)