diff --git a/docs/CONFIG.md b/docs/CONFIG.md index f15c3f8..bf0b17d 100644 --- a/docs/CONFIG.md +++ b/docs/CONFIG.md @@ -283,8 +283,14 @@ Track coding activity heartbeats: ```toml [codeTracking] enabled = false +# Optional: use a custom API endpoint for heartbeats (defaults to global apiEndpoint) +apiEndpoint = "https://api.custom-heartbeat.com" +# Optional: use a custom token for heartbeats (defaults to global token) +token = "custom-heartbeat-token" ``` +When `apiEndpoint` or `token` is set under `[codeTracking]`, heartbeat data will use these values instead of the global configuration. This allows you to send coding activity to a different server or authenticate with a separate token. + --- ## Advanced Settings @@ -399,6 +405,8 @@ enabled = false [codeTracking] enabled = false +# apiEndpoint = "https://api.custom-heartbeat.com" # Optional: custom endpoint +# token = "custom-heartbeat-token" # Optional: custom token # --- Log Management --- [logCleanup] diff --git a/model/api_heartbeat.go b/model/api_heartbeat.go index 93622b3..05b5e43 100644 --- a/model/api_heartbeat.go +++ b/model/api_heartbeat.go @@ -11,9 +11,22 @@ func SendHeartbeatsToServer(ctx context.Context, cfg ShellTimeConfig, payload He ctx, span := modelTracer.Start(ctx, "api.sendHeartbeats") defer span.End() + // Use custom endpoint/token from CodeTracking if specified, otherwise fall back to global + apiEndpoint := cfg.APIEndpoint + token := cfg.Token + + if cfg.CodeTracking != nil { + if cfg.CodeTracking.APIEndpoint != "" { + apiEndpoint = cfg.CodeTracking.APIEndpoint + } + if cfg.CodeTracking.Token != "" { + token = cfg.CodeTracking.Token + } + } + endpoint := Endpoint{ - Token: cfg.Token, - APIEndpoint: cfg.APIEndpoint, + Token: token, + APIEndpoint: apiEndpoint, } var response HeartbeatResponse diff --git a/model/config_test.go b/model/config_test.go index 0396ac5..38df2e3 100644 --- a/model/config_test.go +++ b/model/config_test.go @@ -301,3 +301,136 @@ enabled = true` require.NotNil(t, config.CodeTracking, "CodeTracking should be present") assert.True(t, *config.CodeTracking.Enabled, "CodeTracking.Enabled should be overridden by local config") } + +func TestCodeTrackingCustomEndpointAndToken(t *testing.T) { + // Create a temporary directory for test configs + tmpDir, err := os.MkdirTemp("", "shelltime-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create config file with custom CodeTracking endpoint and token + baseConfigPath := filepath.Join(tmpDir, "config.toml") + baseConfig := `Token = 'global-token' +APIEndpoint = 'https://api.global.com' + +[codeTracking] +enabled = true +apiEndpoint = 'https://api.custom-heartbeat.com' +token = 'custom-heartbeat-token'` + err = os.WriteFile(baseConfigPath, []byte(baseConfig), 0644) + require.NoError(t, err) + + cs := NewConfigService(baseConfigPath) + config, err := cs.ReadConfigFile(context.Background()) + require.NoError(t, err) + + // Verify global config values + assert.Equal(t, "global-token", config.Token) + assert.Equal(t, "https://api.global.com", config.APIEndpoint) + + // Verify CodeTracking custom values + require.NotNil(t, config.CodeTracking, "CodeTracking should be present") + assert.True(t, *config.CodeTracking.Enabled) + assert.Equal(t, "https://api.custom-heartbeat.com", config.CodeTracking.APIEndpoint) + assert.Equal(t, "custom-heartbeat-token", config.CodeTracking.Token) +} + +func TestCodeTrackingPartialCustomConfig(t *testing.T) { + testCases := []struct { + name string + config string + expectedAPIEndpoint string + expectedToken string + }{ + { + name: "Only custom apiEndpoint", + config: `Token = 'global-token' +APIEndpoint = 'https://api.global.com' + +[codeTracking] +enabled = true +apiEndpoint = 'https://api.custom.com'`, + expectedAPIEndpoint: "https://api.custom.com", + expectedToken: "", // empty, should fall back to global + }, + { + name: "Only custom token", + config: `Token = 'global-token' +APIEndpoint = 'https://api.global.com' + +[codeTracking] +enabled = true +token = 'custom-token'`, + expectedAPIEndpoint: "", // empty, should fall back to global + expectedToken: "custom-token", + }, + { + name: "No custom endpoint or token", + config: `Token = 'global-token' +APIEndpoint = 'https://api.global.com' + +[codeTracking] +enabled = true`, + expectedAPIEndpoint: "", + expectedToken: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "shelltime-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + baseConfigPath := filepath.Join(tmpDir, "config.toml") + err = os.WriteFile(baseConfigPath, []byte(tc.config), 0644) + require.NoError(t, err) + + cs := NewConfigService(baseConfigPath) + config, err := cs.ReadConfigFile(context.Background()) + require.NoError(t, err) + + require.NotNil(t, config.CodeTracking, "CodeTracking should be present") + assert.Equal(t, tc.expectedAPIEndpoint, config.CodeTracking.APIEndpoint) + assert.Equal(t, tc.expectedToken, config.CodeTracking.Token) + }) + } +} + +func TestCodeTrackingMergeEndpointFromLocal(t *testing.T) { + // Create a temporary directory for test configs + tmpDir, err := os.MkdirTemp("", "shelltime-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create base config file with CodeTracking + baseConfigPath := filepath.Join(tmpDir, "config.toml") + baseConfig := `Token = 'base-token' +APIEndpoint = 'https://api.base.com' + +[codeTracking] +enabled = true +apiEndpoint = 'https://api.base-heartbeat.com' +token = 'base-heartbeat-token'` + err = os.WriteFile(baseConfigPath, []byte(baseConfig), 0644) + require.NoError(t, err) + + // Create local config file that overrides CodeTracking endpoint and token + localConfigPath := filepath.Join(tmpDir, "config.local.toml") + localConfig := `[codeTracking] +enabled = true +apiEndpoint = 'https://api.local-heartbeat.com' +token = 'local-heartbeat-token'` + err = os.WriteFile(localConfigPath, []byte(localConfig), 0644) + require.NoError(t, err) + + cs := NewConfigService(baseConfigPath) + config, err := cs.ReadConfigFile(context.Background()) + require.NoError(t, err) + + // Verify local config overrides base CodeTracking + require.NotNil(t, config.CodeTracking, "CodeTracking should be present") + assert.True(t, *config.CodeTracking.Enabled) + assert.Equal(t, "https://api.local-heartbeat.com", config.CodeTracking.APIEndpoint) + assert.Equal(t, "local-heartbeat-token", config.CodeTracking.Token) +} diff --git a/model/types.go b/model/types.go index 77a4516..53c3c6f 100644 --- a/model/types.go +++ b/model/types.go @@ -34,7 +34,9 @@ type CCOtel struct { // CodeTracking configuration for coding activity heartbeat tracking type CodeTracking struct { - Enabled *bool `toml:"enabled"` + Enabled *bool `toml:"enabled"` + APIEndpoint string `toml:"apiEndpoint"` // Custom API endpoint for heartbeats + Token string `toml:"token"` // Custom token for heartbeats } // LogCleanup configuration for automatic log file cleanup