Compare commits

...
Author SHA1 Message Date
Daniel SteigmanandCopilot f1ddde625f Update src/services/telemetry/providers/posthog/PostHogTelemetryProvider.ts
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-10-28 15:17:55 -07:00
Daniel Steigman bcf1c3b097 Merge branch 'Nighttrek/telemetry-settings-2-config' into Nighttrek/telemetry-settings-3-interface 2025-10-28 15:17:32 -07:00
Daniel Steigman 0e759ec6af Merge branch 'main' into Nighttrek/telemetry-settings-3-interface 2025-10-28 15:17:02 -07:00
Daniel Steigman 8dc59eee82 Merge branch 'main' into Nighttrek/telemetry-settings-2-config 2025-10-28 14:50:11 -07:00
NightTrek d28120e1c5 fix: eliminate circular dependency in otel-config.ts
- Remove require() call to StateManager that caused circular dependency
- Implement dependency injection pattern with OtelSettingsProvider interface
- Update TelemetryProviderFactory, OpenTelemetryClientProvider, and TelemetryService to pass settings provider
- Maintain backward compatibility with environment variable fallback
- All builds and tests passing
2025-10-28 13:20:22 -07:00
Daniel Steigman 898e0351cd Merge branch 'main' into Nighttrek/telemetry-settings-2-config 2025-10-21 21:44:47 -10:00
NightTrek 66e26ee944 chore: add changeset for provider reinitialization interface 2025-10-14 03:29:15 -07:00
NightTrek 3eb4e3754e feat: Add telemetry provider reinitialization interface (3/5)
- Add reinitializeIfNeeded() method to ITelemetryProvider interface
- Implement in NoOpTelemetryProvider (returns false)
- Implement in PostHogTelemetryProvider (config tracking, logs but doesn't reinitialize)
- Add reinitializeAllProviders() orchestration method to TelemetryService
- Sets up contract for dynamic provider reinitialization

Part 3 of 5 in the telemetry settings refactor series.
2025-10-14 03:28:35 -07:00
NightTrek 94d6241a99 chore: add changeset for config resolution 2025-10-14 03:27:27 -07:00
NightTrek 455897e551 feat: Implement OpenTelemetry config resolution with precedence (2/5)
- Implement configuration precedence: build-time env vars < runtime env vars < settings
- Fix critical bug in getOtelConfig() to use StateManager getter methods
- Add clearOtelConfigCache() function for dynamic config updates
- Update documentation to reflect new precedence model

Part 2 of 5 in the telemetry settings refactor series.
2025-10-14 03:25:41 -07:00
8 changed files with 201 additions and 49 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Added support for reinitializing the telemetry service and all providers inside of it.
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
Fixed a bug and set up how environmental variables are preceded and overwritten by the remote configuration settings.
@@ -1,4 +1,4 @@
import { getValidOpenTelemetryConfig } from "@/shared/services/config/otel-config"
import { getValidOpenTelemetryConfig, OtelSettingsProvider } from "@/shared/services/config/otel-config"
import { isPostHogConfigValid, posthogConfig } from "@/shared/services/config/posthog-config"
import { Logger } from "../logging/Logger"
import type { ITelemetryProvider } from "./providers/ITelemetryProvider"
@@ -27,10 +27,11 @@ export class TelemetryProviderFactory {
/**
* Creates multiple telemetry providers based on configuration
* Supports dual tracking during transition period
* @param settingsProvider Optional settings provider for OpenTelemetry configuration
* @returns Array of ITelemetryProvider instances
*/
public static async createProviders(): Promise<ITelemetryProvider[]> {
const configs = TelemetryProviderFactory.getDefaultConfigs()
public static async createProviders(settingsProvider?: OtelSettingsProvider): Promise<ITelemetryProvider[]> {
const configs = TelemetryProviderFactory.getDefaultConfigs(settingsProvider)
const providers: ITelemetryProvider[] = await Promise.all(configs.map((c) => TelemetryProviderFactory.createProvider(c)))
// Fallback to no-op if no providers available
@@ -79,15 +80,15 @@ export class TelemetryProviderFactory {
/**
* Gets the default telemetry provider configuration
* @returns Default configuration using available providers
* @param settingsProvider Optional settings provider for OpenTelemetry configuration
* @returns Default configuration using available providers
*/
public static getDefaultConfigs(): TelemetryProviderConfig[] {
public static getDefaultConfigs(settingsProvider?: OtelSettingsProvider): TelemetryProviderConfig[] {
const configs: TelemetryProviderConfig[] = []
if (isPostHogConfigValid(posthogConfig)) {
configs.push({ type: "posthog", ...posthogConfig })
}
const otelConfig = getValidOpenTelemetryConfig()
const otelConfig = getValidOpenTelemetryConfig(settingsProvider)
if (otelConfig) {
configs.push({ type: "opentelemetry", ...otelConfig })
}
@@ -131,6 +132,11 @@ export class NoOpTelemetryProvider implements ITelemetryProvider {
}
}
public async reinitializeIfNeeded(): Promise<boolean> {
Logger.info("[NoOpTelemetryProvider] reinitializeIfNeeded called (no-op)")
return false
}
public async dispose(): Promise<void> {
Logger.info("[NoOpTelemetryProvider] Disposing")
}
+47 -1
View File
@@ -3,12 +3,15 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
import { ShowMessageType } from "@shared/proto/host/window"
import type { TaskFeedbackType } from "@shared/WebviewMessage"
import * as os from "os"
import { StateManager } from "@/core/storage/StateManager"
import { ClineAccountUserInfo } from "@/services/auth/AuthService"
import { Setting } from "@/shared/proto/index.host"
import { OtelSettingsProvider } from "@/shared/services/config/otel-config"
import { Mode } from "@/shared/storage/types"
import { version as extensionVersion } from "../../../package.json"
import { setDistinctId } from "../logging/distinctId"
import type { ITelemetryProvider, TelemetryProperties } from "./providers/ITelemetryProvider"
import { OpenTelemetryClientProvider } from "./providers/opentelemetry/OpenTelemetryClientProvider"
import { TelemetryProviderFactory } from "./TelemetryProviderFactory"
/**
@@ -218,7 +221,26 @@ export class TelemetryService {
}
public static async create(): Promise<TelemetryService> {
const providers = await TelemetryProviderFactory.createProviders()
// Get StateManager instance to pass as settings provider
let settingsProvider: OtelSettingsProvider | undefined
try {
const stateManager = StateManager.get()
// Create a settings provider adapter that implements OtelSettingsProvider interface
settingsProvider = {
getGlobalSettingsKey: <K extends string>(key: K) => {
return stateManager.getGlobalSettingsKey(key as any)
},
}
} catch (error) {
// StateManager not initialized yet - will use environment variables only
console.log("[TelemetryService] StateManager not available, using environment config only")
}
// Initialize OpenTelemetry client provider with settings
OpenTelemetryClientProvider.initialize(settingsProvider)
// Create telemetry providers with settings
const providers = await TelemetryProviderFactory.createProviders(settingsProvider)
const hostVersion = await HostProvider.env.getHostVersion({})
const metadata: TelemetryMetadata = {
extension_version: extensionVersion,
@@ -1582,6 +1604,30 @@ export class TelemetryService {
})
}
/**
* Reinitialize all telemetry providers if their configuration has changed.
* Each provider decides internally whether reinitialization is needed.
*
* @returns Promise<void>
*/
public async reinitializeAllProviders(): Promise<void> {
console.log("[TelemetryService] Checking all providers for reinitialization...")
const results = await Promise.all(
this.providers.map(async (provider) => {
try {
return await provider.reinitializeIfNeeded()
} catch (error) {
console.error("[TelemetryService] Error reinitializing provider:", error)
return false
}
}),
)
const reinitializedCount = results.filter((r) => r).length
console.log(`[TelemetryService] Reinitialized ${reinitializedCount} of ${this.providers.length} provider(s)`)
}
/**
* Clean up resources when the service is disposed
*/
@@ -104,6 +104,15 @@ export interface ITelemetryProvider {
*/
recordHistogram?(name: string, value: number, attributes?: TelemetryProperties): void
/**
* Reinitialize the provider if configuration has changed.
* Provider internally compares current config with new config
* and only reinitializes if necessary.
*
* @returns Promise<boolean> - true if reinitialized, false if no change needed
*/
reinitializeIfNeeded(): Promise<boolean>
/**
* Clean up resources when the provider is disposed
*/
@@ -5,7 +5,11 @@ import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs
import { MeterProvider } from "@opentelemetry/sdk-metrics"
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"
import { ExtensionRegistryInfo } from "@/registry"
import { getValidOpenTelemetryConfig, OpenTelemetryClientValidConfig } from "@/shared/services/config/otel-config"
import {
getValidOpenTelemetryConfig,
OpenTelemetryClientValidConfig,
OtelSettingsProvider,
} from "@/shared/services/config/otel-config"
import {
createConsoleLogExporter,
createConsoleMetricReader,
@@ -19,10 +23,23 @@ import {
*/
export class OpenTelemetryClientProvider {
private static _instance: OpenTelemetryClientProvider | null = null
private static _settingsProvider: OtelSettingsProvider | undefined = undefined
/**
* Initialize the OpenTelemetry client provider with an optional settings provider
* @param settingsProvider Optional settings provider for configuration
*/
public static initialize(settingsProvider?: OtelSettingsProvider): void {
OpenTelemetryClientProvider._settingsProvider = settingsProvider
// Reset instance to force re-initialization with new settings provider
if (OpenTelemetryClientProvider._instance) {
OpenTelemetryClientProvider._instance = null
}
}
public static getInstance(): OpenTelemetryClientProvider {
if (!OpenTelemetryClientProvider._instance) {
OpenTelemetryClientProvider._instance = new OpenTelemetryClientProvider()
OpenTelemetryClientProvider._instance = new OpenTelemetryClientProvider(OpenTelemetryClientProvider._settingsProvider)
}
return OpenTelemetryClientProvider._instance
}
@@ -47,8 +64,8 @@ export class OpenTelemetryClientProvider {
return process.env.TEL_DEBUG_DIAGNOSTICS === "true" || process.env.IS_DEV === "true"
}
private constructor() {
this.config = getValidOpenTelemetryConfig()
private constructor(settingsProvider?: OtelSettingsProvider) {
this.config = getValidOpenTelemetryConfig(settingsProvider)
if (!this.config) {
console.log("[OTEL DEBUG] OpenTelemetry is disabled or not configured")
@@ -14,6 +14,7 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
private client: PostHog
private telemetrySettings: TelemetrySettings
private isSharedClient: boolean
private currentConfig: typeof posthogConfig = null
constructor(sharedClient?: PostHog) {
this.isSharedClient = !!sharedClient
@@ -139,6 +140,23 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
// no-op
}
public async reinitializeIfNeeded(): Promise<boolean> {
// Get new config from posthog-config
const newConfig = posthogConfig
// Compare with current config
if (JSON.stringify(this.currentConfig) === JSON.stringify(newConfig)) {
return false
}
// Update current config
this.currentConfig = { ...newConfig }
// For now, just log - actual reinitialization can be added later
console.log("[PostHogTelemetryProvider] Config changed, would reinitialize")
return false
}
public async dispose(): Promise<void> {
// Only shut down the client if it's not shared (we own it)
if (!this.isSharedClient) {
+84 -38
View File
@@ -85,19 +85,16 @@ export interface OpenTelemetryClientValidConfig extends OpenTelemetryClientConfi
const isTestEnv = process.env.E2E_TEST === "true" || process.env.IS_TEST === "true"
/**
* Cached OpenTelemetry configuration.
* Lazily initialized on first access to avoid race conditions with environment variable loading.
* Interface for providing settings overrides to OpenTelemetry configuration.
* This allows StateManager to provide settings without creating a circular dependency.
*/
let otelConfig: OpenTelemetryClientConfig | null = null
export interface OtelSettingsProvider {
getGlobalSettingsKey<K extends keyof OpenTelemetryClientConfig>(key: K): OpenTelemetryClientConfig[K] | undefined
}
/**
* Gets or creates the OpenTelemetry configuration from environment variables.
* Configuration is cached after first access for performance.
*
* Configuration Sources:
* - **Production Build**: Environment variables injected by esbuild at build time
* via .github/workflows/publish.yml
* - **Development**: Environment variables from .env file loaded by VSCode
* Gets the base OpenTelemetry configuration from environment variables.
* This is the lowest precedence configuration source.
*
* Supported Environment Variables:
* - OTEL_TELEMETRY_ENABLED: "1" to enable OpenTelemetry (default: off)
@@ -119,34 +116,82 @@ let otelConfig: OpenTelemetryClientConfig | null = null
* @see .env.example for development setup
* @see .github/workflows/publish.yml for production environment variable injection
*/
function getOtelConfig(): OpenTelemetryClientConfig {
if (!otelConfig) {
otelConfig = {
enabled: process.env.OTEL_TELEMETRY_ENABLED === "1",
metricsExporter: process.env.OTEL_METRICS_EXPORTER,
logsExporter: process.env.OTEL_LOGS_EXPORTER,
otlpProtocol: process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
otlpMetricsProtocol: process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
otlpMetricsEndpoint: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
otlpLogsProtocol: process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
otlpLogsEndpoint: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
metricExportInterval: process.env.OTEL_METRIC_EXPORT_INTERVAL
? parseInt(process.env.OTEL_METRIC_EXPORT_INTERVAL, 10)
: undefined,
otlpInsecure: process.env.OTEL_EXPORTER_OTLP_INSECURE === "true",
logBatchSize: process.env.OTEL_LOG_BATCH_SIZE
? Math.max(1, parseInt(process.env.OTEL_LOG_BATCH_SIZE, 10))
: undefined,
logBatchTimeout: process.env.OTEL_LOG_BATCH_TIMEOUT
? Math.max(1, parseInt(process.env.OTEL_LOG_BATCH_TIMEOUT, 10))
: undefined,
logMaxQueueSize: process.env.OTEL_LOG_MAX_QUEUE_SIZE
? Math.max(1, parseInt(process.env.OTEL_LOG_MAX_QUEUE_SIZE, 10))
: undefined,
function getBaseOtelConfig(): OpenTelemetryClientConfig {
return {
enabled: process.env.OTEL_TELEMETRY_ENABLED === "1",
metricsExporter: process.env.OTEL_METRICS_EXPORTER,
logsExporter: process.env.OTEL_LOGS_EXPORTER,
otlpProtocol: process.env.OTEL_EXPORTER_OTLP_PROTOCOL,
otlpEndpoint: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
otlpMetricsProtocol: process.env.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
otlpMetricsEndpoint: process.env.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
otlpLogsProtocol: process.env.OTEL_EXPORTER_OTLP_LOGS_PROTOCOL,
otlpLogsEndpoint: process.env.OTEL_EXPORTER_OTLP_LOGS_ENDPOINT,
metricExportInterval: process.env.OTEL_METRIC_EXPORT_INTERVAL
? parseInt(process.env.OTEL_METRIC_EXPORT_INTERVAL, 10)
: undefined,
otlpInsecure: process.env.OTEL_EXPORTER_OTLP_INSECURE === "true",
logBatchSize: process.env.OTEL_LOG_BATCH_SIZE ? Math.max(1, parseInt(process.env.OTEL_LOG_BATCH_SIZE, 10)) : undefined,
logBatchTimeout: process.env.OTEL_LOG_BATCH_TIMEOUT
? Math.max(1, parseInt(process.env.OTEL_LOG_BATCH_TIMEOUT, 10))
: undefined,
logMaxQueueSize: process.env.OTEL_LOG_MAX_QUEUE_SIZE
? Math.max(1, parseInt(process.env.OTEL_LOG_MAX_QUEUE_SIZE, 10))
: undefined,
}
}
/**
* Gets OpenTelemetry configuration with proper precedence.
*
* Configuration Precedence (lowest to highest):
* 1. **Build-time**: Environment variables injected by esbuild at build time via .github/workflows/publish.yml
* 2. **Runtime Environment Variables**: Environment variables from .env file or startup
* 3. **Settings**: Values from settingsProvider (typically StateManager with UI or remote config)
*
* @param settingsProvider Optional provider for settings overrides (typically StateManager)
* @returns OpenTelemetry configuration with all precedence levels applied
* @see .env.example for development setup
* @see .github/workflows/publish.yml for production environment variable injection
*/
function getOtelConfig(settingsProvider?: OtelSettingsProvider): OpenTelemetryClientConfig {
// Start with base environment variable config (lowest precedence)
const baseConfig = getBaseOtelConfig()
// If no settings provider, return base config
if (!settingsProvider) {
return baseConfig
}
// Apply settings overrides from provider (highest precedence)
const settingsOverrides: Partial<OpenTelemetryClientConfig> = {}
const settingsKeys: Array<keyof OpenTelemetryClientConfig> = [
"enabled",
"metricsExporter",
"logsExporter",
"otlpProtocol",
"otlpEndpoint",
"otlpMetricsProtocol",
"otlpMetricsEndpoint",
"otlpLogsProtocol",
"otlpLogsEndpoint",
"metricExportInterval",
"otlpInsecure",
"logBatchSize",
"logBatchTimeout",
"logMaxQueueSize",
]
for (const key of settingsKeys) {
const value = settingsProvider.getGlobalSettingsKey(key)
if (value !== undefined) {
settingsOverrides[key] = value as any
}
}
return otelConfig
// Merge base config with settings overrides
return { ...baseConfig, ...settingsOverrides }
}
export function isOpenTelemetryConfigValid(config: OpenTelemetryClientConfig): config is OpenTelemetryClientValidConfig {
@@ -170,10 +215,11 @@ export function isOpenTelemetryConfigValid(config: OpenTelemetryClientConfig): c
*
* Configuration does not change at runtime - requires VSCode reload to pick up new values.
*
* @param settingsProvider Optional provider for settings overrides (typically StateManager)
* @returns Valid OpenTelemetry configuration or null if disabled/invalid
* @see .env.example for configuration options
*/
export function getValidOpenTelemetryConfig(): OpenTelemetryClientValidConfig | null {
const config = getOtelConfig()
export function getValidOpenTelemetryConfig(settingsProvider?: OtelSettingsProvider): OpenTelemetryClientValidConfig | null {
const config = getOtelConfig(settingsProvider)
return isOpenTelemetryConfigValid(config) ? config : null
}