diff --git a/internal/telemetry/client.go b/internal/telemetry/client.go index 03bfce2..fded5c9 100644 --- a/internal/telemetry/client.go +++ b/internal/telemetry/client.go @@ -103,7 +103,7 @@ func Get(opts ...GetOption) Client { analyticsCfg, err := loadConfigFromFile(configPath) if errors.Is(err, os.ErrNotExist) { // file not found, create a new one - analyticsCfg = Config{UserUUID: NewUUID()} + analyticsCfg = Config{AnalyticsID: NewUUID()} if err := writeConfigToFile(configPath, analyticsCfg); err != nil { return analyticsCfg, fmt.Errorf("could not write file to %s: %w", configPath, err) } @@ -113,8 +113,8 @@ func Get(opts ...GetOption) Client { } // if a file exists but doesn't have a uuid, create a new uuid - if analyticsCfg.UserUUID.IsZero() { - analyticsCfg.UserUUID = NewUUID() + if analyticsCfg.AnalyticsID.IsZero() { + analyticsCfg.AnalyticsID = NewUUID() if err := writeConfigToFile(configPath, analyticsCfg); err != nil { return analyticsCfg, fmt.Errorf("could not write file to %s: %w", configPath, err) } diff --git a/internal/telemetry/client_test.go b/internal/telemetry/client_test.go index 2c0c7d0..22c1bb4 100644 --- a/internal/telemetry/client_test.go +++ b/internal/telemetry/client_test.go @@ -29,12 +29,12 @@ func TestGet(t *testing.T) { t.Error("expected config file to contain 'Airbyte'") } - if !strings.Contains(string(data), "anonymous_user_uuid") { - t.Error("expected config file to contain 'anonymous_user_uuid'") + if !strings.Contains(string(data), fieldAnalyticsID) { + t.Error(fmt.Sprintf("expected config file to contain '%s'", fieldAnalyticsID)) } - if strings.Contains(string(data), "anonymous_user_id") { - t.Error("config file should not contain 'anonymous_user_id'") + if strings.Contains(string(data), fieldUserID) { + t.Error(fmt.Sprintf("config file should not contain '%s'", fieldUserID)) } } @@ -64,12 +64,12 @@ func TestGet_WithExistingULID(t *testing.T) { t.Error("expected config file to contain 'Airbyte'") } - if !strings.Contains(string(data), "anonymous_user_uuid") { - t.Error("expected config file to contain 'anonymous_user_uuid'") + if !strings.Contains(string(data), fieldAnalyticsID) { + t.Error(fmt.Sprintf("expected config file to contain '%s'", fieldAnalyticsID)) } - if !strings.Contains(string(data), "anonymous_user_id") { - t.Error("expected config file to contain 'anonymous_user_id'") + if !strings.Contains(string(data), fieldUserID) { + t.Error(fmt.Sprintf("config file should not contain '%s'", fieldUserID)) } } diff --git a/internal/telemetry/config.go b/internal/telemetry/config.go index b07edae..b560c85 100644 --- a/internal/telemetry/config.go +++ b/internal/telemetry/config.go @@ -15,6 +15,12 @@ const ( Anonymous usage reporting is currently enabled. For more information, please see https://docs.airbyte.com/telemetry` ) +// fields +const ( + fieldAnalyticsID = "analytics_id" + fieldUserID = "anonymous_user_id" +) + var ConfigFile = filepath.Join(".airbyte", "analytics.yml") // UUID is a wrapper around uuid.UUID so that we can implement the yaml interfaces. @@ -104,8 +110,9 @@ func (u ULID) IsZero() bool { // Config represents the analytics.yaml file. type Config struct { - UserID ULID `yaml:"anonymous_user_id,omitempty"` - UserUUID UUID `yaml:"anonymous_user_uuid,omitempty"` + UserID ULID `yaml:"anonymous_user_id,omitempty"` + AnalyticsID UUID `yaml:"analytics_id,omitempty"` + Other map[string]interface{} `yaml:",inline"` } // permissions sets the file and directory permission level for the telemetry files that may be created. @@ -126,9 +133,27 @@ func loadConfigFromFile(path string) (Config, error) { var c Config - if err := yaml.Unmarshal(analytics, &c); err != nil { + if err := yaml.Unmarshal(analytics, &c.Other); err != nil { return Config{}, fmt.Errorf("could not unmarshal yaml: %w", err) } + if v, ok := c.Other[fieldUserID]; ok { + if parsed, err := ulid.Parse(v.(string)); err != nil { + return Config{}, fmt.Errorf("could not parse ulid (%s): %w", v, err) + } else { + c.UserID = ULID(parsed) + } + } + + if v, ok := c.Other[fieldAnalyticsID]; ok { + if parsed, err := uuid.Parse(v.(string)); err != nil { + return Config{}, fmt.Errorf("could not parse uuid (%s): %w", v, err) + } else { + c.AnalyticsID = UUID(parsed) + } + } + + delete(c.Other, fieldUserID) + delete(c.Other, fieldAnalyticsID) return c, nil } diff --git a/internal/telemetry/config_test.go b/internal/telemetry/config_test.go index 8d4cd75..e3de221 100644 --- a/internal/telemetry/config_test.go +++ b/internal/telemetry/config_test.go @@ -29,12 +29,12 @@ anonymous_user_id: ` + ulidID.String()); err != nil { t.Fatal("could not write to temp file", err) } - cnf, err := loadConfigFromFile(f.Name()) + cfg, err := loadConfigFromFile(f.Name()) if d := cmp.Diff(nil, err); d != "" { t.Error("failed to load file", d) } - if d := cmp.Diff(ulidID.String(), cnf.UserID.String()); d != "" { + if d := cmp.Diff(ulidID.String(), cfg.UserID.String()); d != "" { t.Error("id is incorrect", d) } }) @@ -94,21 +94,63 @@ func TestLoadConfigWithUUID(t *testing.T) { } defer f.Close() - if _, err := f.WriteString(`# comments -anonymous_user_uuid: ` + uuidID.String()); err != nil { + cfgData := fmt.Sprintf(`# comments +%s: %s`, fieldAnalyticsID, uuidID.String()) + + if _, err := f.WriteString(cfgData); err != nil { t.Fatal("could not write to temp file", err) } - cnf, err := loadConfigFromFile(f.Name()) + cfg, err := loadConfigFromFile(f.Name()) if d := cmp.Diff(nil, err); d != "" { t.Error("failed to load file", d) } - if d := cmp.Diff(uuidID.String(), cnf.UserUUID.String()); d != "" { + if d := cmp.Diff(uuidID.String(), cfg.AnalyticsID.String()); d != "" { t.Error("id is incorrect", d) } }) + t.Run("happy path with extra fields", func(t *testing.T) { + f, err := os.CreateTemp(t.TempDir(), "analytics-") + if err != nil { + t.Fatal("could not create temp file", err) + } + defer f.Close() + + cfgData := fmt.Sprintf(`# comments +%s: %s +extra_field: extra_value +another_field: false +total: 300`, + fieldAnalyticsID, uuidID.String()) + + if _, err := f.WriteString(cfgData); err != nil { + t.Fatal("could not write to temp file", err) + } + + cfg, err := loadConfigFromFile(f.Name()) + if d := cmp.Diff(nil, err); d != "" { + t.Error("failed to load file", d) + } + + if d := cmp.Diff(uuidID.String(), cfg.AnalyticsID.String()); d != "" { + t.Error("id is incorrect", d) + } + + if d := cmp.Diff("extra_value", cfg.Other["extra_field"]); d != "" { + t.Error("extra_field is incorrect", d) + } + + if d := cmp.Diff(false, cfg.Other["another_field"]); d != "" { + t.Error("another_field is incorrect", d) + } + + if d := cmp.Diff(300, cfg.Other["total"]); d != "" { + t.Error("total is incorrect", d) + } + }) + t.Run("no file returns err", func(t *testing.T) { _, err := loadConfigFromFile(filepath.Join(t.TempDir(), "dne.yml")) if err == nil { @@ -171,8 +213,8 @@ func TestWriteConfig(t *testing.T) { t.Error("failed to read file", err) } - exp := fmt.Sprintf(`%sanonymous_user_id: %s -`, header, ulidID.String()) + exp := fmt.Sprintf(`%s%s: %s +`, header, fieldUserID, ulidID.String()) if d := cmp.Diff(exp, string(contents)); d != "" { t.Error("contents do not match", d) @@ -182,7 +224,34 @@ func TestWriteConfig(t *testing.T) { t.Run("uuid", func(t *testing.T) { path := filepath.Join(t.TempDir(), "nested", "deeply", ConfigFile) - cfg := Config{UserUUID: UUID(uuidID)} + cfg := Config{AnalyticsID: UUID(uuidID)} + + if err := writeConfigToFile(path, cfg); err != nil { + t.Error("failed to create file", err) + } + + contents, err := os.ReadFile(path) + if err != nil { + t.Error("failed to read file", err) + } + + exp := fmt.Sprintf(`%s%s: %s +`, header, fieldAnalyticsID, uuidID.String()) + + if d := cmp.Diff(exp, string(contents)); d != "" { + t.Error("contents do not match", d) + } + }) + + t.Run("uuid and other", func(t *testing.T) { + path := filepath.Join(t.TempDir(), "nested", "deeply", ConfigFile) + + cfg := Config{ + AnalyticsID: UUID(uuidID), + Other: map[string]interface{}{ + "another_field": "another_value", + }, + } if err := writeConfigToFile(path, cfg); err != nil { t.Error("failed to create file", err) @@ -193,8 +262,9 @@ func TestWriteConfig(t *testing.T) { t.Error("failed to read file", err) } - exp := fmt.Sprintf(`%sanonymous_user_uuid: %s -`, header, uuidID.String()) + exp := fmt.Sprintf(`%s%s: %s +another_field: another_value +`, header, fieldAnalyticsID, uuidID.String()) if d := cmp.Diff(exp, string(contents)); d != "" { t.Error("contents do not match", d) @@ -205,8 +275,38 @@ func TestWriteConfig(t *testing.T) { path := filepath.Join(t.TempDir(), "nested", "deeply", ConfigFile) cfg := Config{ - UserID: ULID(ulidID), - UserUUID: UUID(uuidID), + UserID: ULID(ulidID), + AnalyticsID: UUID(uuidID), + } + + if err := writeConfigToFile(path, cfg); err != nil { + t.Error("failed to create file", err) + } + + contents, err := os.ReadFile(path) + if err != nil { + t.Error("failed to read file", err) + } + + exp := fmt.Sprintf(`%s%s: %s +%s: %s +`, header, fieldUserID, ulidID.String(), fieldAnalyticsID, uuidID.String()) + + if d := cmp.Diff(exp, string(contents)); d != "" { + t.Error("contents do not match", d) + } + }) + + t.Run("ulid, uuid, and other", func(t *testing.T) { + path := filepath.Join(t.TempDir(), ConfigFile) + + cfg := Config{ + UserID: ULID(ulidID), + AnalyticsID: UUID(uuidID), + Other: map[string]interface{}{ + "field": "value is here", + "count": 100, + }, } if err := writeConfigToFile(path, cfg); err != nil { @@ -219,8 +319,10 @@ func TestWriteConfig(t *testing.T) { } exp := fmt.Sprintf(`%sanonymous_user_id: %s -anonymous_user_uuid: %s -`, header, ulidID.String(), uuidID.String()) +%s: %s +count: 100 +field: value is here +`, header, ulidID.String(), fieldAnalyticsID, uuidID.String()) if d := cmp.Diff(exp, string(contents)); d != "" { t.Error("contents do not match", d) diff --git a/internal/telemetry/segment.go b/internal/telemetry/segment.go index b4eec42..f846a03 100644 --- a/internal/telemetry/segment.go +++ b/internal/telemetry/segment.go @@ -81,7 +81,7 @@ func (s *SegmentClient) Attr(key, val string) { } func (s *SegmentClient) User() uuid.UUID { - return s.cfg.UserUUID.toUUID() + return s.cfg.AnalyticsID.toUUID() } const ( @@ -109,7 +109,7 @@ func (s *SegmentClient) send(ctx context.Context, es EventState, et EventType, e } body := body{ - ID: s.cfg.UserUUID.String(), + ID: s.cfg.AnalyticsID.String(), Event: string(et), Properties: properties, Timestamp: time.Now().UTC().Format(time.RFC3339), diff --git a/internal/telemetry/segment_test.go b/internal/telemetry/segment_test.go index 0c40fa2..330c038 100644 --- a/internal/telemetry/segment_test.go +++ b/internal/telemetry/segment_test.go @@ -53,7 +53,7 @@ func TestSegmentClient_Start(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) ctx := context.Background() @@ -155,7 +155,7 @@ func TestSegmentClient_StartWithAttr(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) cli.Attr("key1", "val1") cli.Attr("key2", "val2") @@ -264,7 +264,7 @@ func TestSegmentClient_StartErr(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) ctx := context.Background() @@ -289,7 +289,7 @@ func TestSegmentClient_Success(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) ctx := context.Background() @@ -391,7 +391,7 @@ func TestSegmentClient_SuccessWithAttr(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) cli.Attr("key1", "val1") cli.Attr("key2", "val2") @@ -500,7 +500,7 @@ func TestSegmentClient_SuccessErr(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) ctx := context.Background() @@ -525,7 +525,7 @@ func TestSegmentClient_Failure(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) ctx := context.Background() failure := errors.New("failure reason") @@ -628,7 +628,7 @@ func TestSegmentClient_FailureWithAttr(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) cli.Attr("key1", "val1") cli.Attr("key2", "val2") @@ -738,7 +738,7 @@ func TestSegmentClient_FailureErr(t *testing.T) { WithHTTPClient(mDoer), } - cli := NewSegmentClient(Config{UserUUID: UUID(userID)}, opts...) + cli := NewSegmentClient(Config{AnalyticsID: UUID(userID)}, opts...) ctx := context.Background() failure := errors.New("failure reason")