mirror of
https://github.com/cline/cline.git
synced 2026-09-15 13:02:17 +08:00
Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a75e71a00f | ||
|
|
e2a306ccff | ||
|
|
f8dd797a74 | ||
|
|
2fd9e3209f | ||
|
|
1c6a58dff2 | ||
|
|
098b312392 | ||
|
|
c8144055f2 | ||
|
|
43223234fe | ||
|
|
b9dc6dec43 | ||
|
|
b642b29ed4 | ||
|
|
5e147522a5 | ||
|
|
ed3b5cdd9c |
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"claude-dev": patch
|
||||
---
|
||||
|
||||
Refactored the Telemetry service to support multiple providers for a future where we support Otel
|
||||
@@ -0,0 +1,48 @@
|
||||
# Cline Development Environment Variables
|
||||
# Copy this file to .env and fill in your actual values
|
||||
# Values should be obtained from 1Password shared vault for development
|
||||
|
||||
# ============================================================================
|
||||
# DEVELOPMENT FLAGS
|
||||
# Recomend not changing these unless you know what you're doing they are set by the launch.json normally
|
||||
# ============================================================================
|
||||
# IS_DEV=true
|
||||
# CLINE_ENVIRONMENT=local
|
||||
|
||||
# ============================================================================
|
||||
# POSTHOG TELEMETRY (Existing)
|
||||
# ============================================================================
|
||||
# Get these values from 1Password shared vault
|
||||
TELEMETRY_SERVICE_API_KEY=your-posthog-telemetry-api-key
|
||||
ERROR_SERVICE_API_KEY=your-posthog-error-tracking-api-key
|
||||
|
||||
# ============================================================================
|
||||
# TELEMETRY PROVIDER CONTROL
|
||||
# ============================================================================
|
||||
# Control which telemetry providers are active
|
||||
POSTHOG_TELEMETRY_ENABLED=true # Enable PostHog telemetry (default: true)
|
||||
# Set to false to disable Telemetry completely
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL DEVELOPMENT SETTINGS
|
||||
# ============================================================================
|
||||
# Uncomment and modify as needed for development
|
||||
|
||||
# Multi-root workspace debugging
|
||||
# MULTI_ROOT_TRACE=true
|
||||
|
||||
# gRPC recorder for testing
|
||||
# GRPC_RECORDER_ENABLED=true
|
||||
# GRPC_RECORDER_FILE_NAME=test-recording
|
||||
|
||||
# Test mode
|
||||
# E2E_TEST=true
|
||||
# IS_TEST=true
|
||||
|
||||
# ============================================================================
|
||||
# USAGE INSTRUCTIONS
|
||||
# ============================================================================
|
||||
# 1. Copy this file: cp .env.example .env
|
||||
# 2. Get PostHog keys from 1Password shared vault
|
||||
# 3. Update the values in .env
|
||||
# 4. The .env file is gitignored for security
|
||||
+2
-1
@@ -26,6 +26,7 @@ coverage-unit
|
||||
!.github/scripts/coverage/
|
||||
|
||||
*evals.env
|
||||
.env
|
||||
|
||||
## Generated files ##
|
||||
src/generated/
|
||||
@@ -36,4 +37,4 @@ webview-ui/src/services/grpc-client.ts
|
||||
test-results
|
||||
|
||||
## CLI pre-release ##
|
||||
/cli
|
||||
/cli
|
||||
|
||||
Vendored
+6
@@ -19,6 +19,7 @@
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
@@ -39,6 +40,7 @@
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
@@ -59,6 +61,7 @@
|
||||
"${workspaceFolder}/dist/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}",
|
||||
@@ -84,6 +87,7 @@
|
||||
"preLaunchTask": "clean-tmp-user",
|
||||
"internalConsoleOptions": "openOnSessionStart",
|
||||
"postDebugTask": "stop",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"IS_DEV": "true",
|
||||
"TEMP_PROFILE": "true",
|
||||
@@ -114,6 +118,7 @@
|
||||
"tsx"
|
||||
],
|
||||
"program": "scripts/test-standalone-core-api-server.ts",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"PROTOBUS_PORT": "26040",
|
||||
"HOSTBRIDGE_PORT": "26041",
|
||||
@@ -151,6 +156,7 @@
|
||||
"--exit",
|
||||
"${file}"
|
||||
],
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"env": {
|
||||
"TS_NODE_PROJECT": "./tsconfig.unit-test.json",
|
||||
"NODE_ENV": "test",
|
||||
|
||||
@@ -139,6 +139,10 @@ if (process.env.TELEMETRY_SERVICE_API_KEY) {
|
||||
if (process.env.ERROR_SERVICE_API_KEY) {
|
||||
buildEnvVars["process.env.ERROR_SERVICE_API_KEY"] = JSON.stringify(process.env.ERROR_SERVICE_API_KEY)
|
||||
}
|
||||
|
||||
if (process.env.POSTHOG_TELEMETRY_ENABLED) {
|
||||
buildEnvVars["process.env.POSTHOG_TELEMETRY_ENABLED"] = JSON.stringify(process.env.POSTHOG_TELEMETRY_ENABLED)
|
||||
}
|
||||
// Base configuration shared between extension and standalone builds
|
||||
const baseConfig = {
|
||||
bundle: true,
|
||||
|
||||
Generated
+14
@@ -106,6 +106,7 @@
|
||||
"c8": "^10.1.3",
|
||||
"chai": "^4.3.10",
|
||||
"chalk": "5.6.2",
|
||||
"dotenv": "^17.2.2",
|
||||
"esbuild": "^0.25.0",
|
||||
"grpc-tools": "^1.13.0",
|
||||
"husky": "^9.1.7",
|
||||
@@ -7541,6 +7542,19 @@
|
||||
"url": "https://github.com/fb55/domutils?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "17.2.2",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
|
||||
"integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://dotenvx.com"
|
||||
}
|
||||
},
|
||||
"node_modules/dprint-node": {
|
||||
"version": "1.0.8",
|
||||
"dev": true,
|
||||
|
||||
+1
-1
@@ -17,8 +17,8 @@ import { audioRecordingService } from "./services/dictation/AudioRecordingServic
|
||||
import { ErrorService } from "./services/error"
|
||||
import { featureFlagsService } from "./services/feature-flags"
|
||||
import { initializeDistinctId } from "./services/logging/distinctId"
|
||||
import { PostHogClientProvider } from "./services/posthog/PostHogClientProvider"
|
||||
import { telemetryService } from "./services/telemetry"
|
||||
import { PostHogClientProvider } from "./services/telemetry/providers/posthog/PostHogClientProvider"
|
||||
import { ShowMessageType } from "./shared/proto/host/window"
|
||||
import { getLatestAnnouncementId } from "./utils/announcements"
|
||||
/**
|
||||
|
||||
@@ -2,7 +2,7 @@ import { PostHog } from "posthog-node"
|
||||
import * as vscode from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getDistinctId } from "@/services/logging/distinctId"
|
||||
import { PostHogClientProvider } from "@/services/posthog/PostHogClientProvider"
|
||||
import { PostHogClientProvider } from "@/services/telemetry/providers/posthog/PostHogClientProvider"
|
||||
import { Setting } from "@/shared/proto/index.host"
|
||||
import * as pkg from "../../../../package.json"
|
||||
import { PostHogClientValidConfig } from "../../../shared/services/config/posthog-config"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { isPostHogConfigValid, posthogConfig } from "@/shared/services/config/posthog-config"
|
||||
import { Logger } from "../logging/Logger"
|
||||
import { PostHogClientProvider } from "../posthog/PostHogClientProvider"
|
||||
import { PostHogClientProvider } from "../telemetry/providers/posthog/PostHogClientProvider"
|
||||
import type { IFeatureFlagsProvider } from "./providers/IFeatureFlagsProvider"
|
||||
import { PostHogFeatureFlagsProvider } from "./providers/PostHogFeatureFlagsProvider"
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { isPostHogConfigValid, posthogConfig } from "@/shared/services/config/posthog-config"
|
||||
import { Logger } from "../logging/Logger"
|
||||
import { PostHogClientProvider } from "../posthog/PostHogClientProvider"
|
||||
import type { ITelemetryProvider } from "./providers/ITelemetryProvider"
|
||||
import { PostHogTelemetryProvider } from "./providers/PostHogTelemetryProvider"
|
||||
import { PostHogClientProvider } from "./providers/posthog/PostHogClientProvider"
|
||||
import { PostHogTelemetryProvider } from "./providers/posthog/PostHogTelemetryProvider"
|
||||
|
||||
/**
|
||||
* Supported telemetry provider types
|
||||
@@ -22,15 +22,45 @@ export interface TelemetryProviderConfig {
|
||||
*/
|
||||
export class TelemetryProviderFactory {
|
||||
/**
|
||||
* Creates a telemetry provider based on the provided configuration
|
||||
* Creates multiple telemetry providers based on configuration
|
||||
* Supports dual tracking during transition period
|
||||
* @returns Array of ITelemetryProvider instances
|
||||
*/
|
||||
public static async createProviders(): Promise<ITelemetryProvider[]> {
|
||||
const providers: ITelemetryProvider[] = []
|
||||
|
||||
// Add PostHog if enabled and configured
|
||||
if (isPostHogConfigValid(posthogConfig)) {
|
||||
try {
|
||||
const sharedClient = PostHogClientProvider.getClient()
|
||||
if (sharedClient) {
|
||||
const posthogProvider = await new PostHogTelemetryProvider(sharedClient).initialize()
|
||||
providers.push(posthogProvider)
|
||||
Logger.info("TelemetryProviderFactory: PostHog provider initialized")
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("TelemetryProviderFactory: Failed to initialize PostHog provider:", error)
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to no-op if no providers available
|
||||
if (providers.length === 0) {
|
||||
providers.push(new NoOpTelemetryProvider())
|
||||
Logger.info("TelemetryProviderFactory: Using NoOp provider (no valid configs)")
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a single telemetry provider based on the provided configuration
|
||||
* @param config Configuration for the telemetry provider
|
||||
* @returns ITelemetryProvider instance
|
||||
* @deprecated Use createProviders() for multi-provider support
|
||||
*/
|
||||
public static async createProvider(config: TelemetryProviderConfig): Promise<ITelemetryProvider> {
|
||||
// Get the shared PostHog client from PostHogClientProvider
|
||||
switch (config.type) {
|
||||
case "posthog": {
|
||||
// Get the shared PostHog client from PostHogClientProvider
|
||||
const sharedClient = PostHogClientProvider.getClient()
|
||||
if (sharedClient) {
|
||||
return await new PostHogTelemetryProvider(sharedClient).initialize()
|
||||
@@ -45,13 +75,13 @@ export class TelemetryProviderFactory {
|
||||
|
||||
/**
|
||||
* Gets the default telemetry provider configuration
|
||||
* @returns Default configuration using PostHog
|
||||
* @returns Default configuration using available providers
|
||||
*/
|
||||
public static getDefaultConfig(): TelemetryProviderConfig {
|
||||
const hasValidConfig = isPostHogConfigValid(posthogConfig)
|
||||
return {
|
||||
type: hasValidConfig ? "posthog" : "no-op",
|
||||
if (isPostHogConfigValid(posthogConfig)) {
|
||||
return { type: "posthog" }
|
||||
}
|
||||
return { type: "no-op" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +96,10 @@ export class NoOpTelemetryProvider implements ITelemetryProvider {
|
||||
Logger.log(`[NoOpTelemetryProvider] ${event}: ${JSON.stringify(properties)}`)
|
||||
}
|
||||
|
||||
public logRequired(event: string, properties?: Record<string, unknown>): void {
|
||||
Logger.log(`[NoOpTelemetryProvider] REQUIRED ${event}: ${JSON.stringify(properties)}`)
|
||||
}
|
||||
|
||||
public identifyUser(userInfo: any, properties?: Record<string, unknown>): void {
|
||||
Logger.info(`[NoOpTelemetryProvider] identifyUser - ${JSON.stringify(userInfo)} - ${JSON.stringify(properties)}`)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Tests for the abstracted telemetry system
|
||||
* This demonstrates how easy it is to switch between providers
|
||||
* and validates the NoOpTelemetryProvider functionality
|
||||
* Tests for the abstracted multi-provider telemetry system
|
||||
* This demonstrates the multi-provider architecture that supports dual tracking,
|
||||
* validates provider switching capabilities, and ensures NoOpTelemetryProvider functionality
|
||||
*/
|
||||
|
||||
import * as assert from "assert"
|
||||
@@ -48,7 +48,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
const logSpy = sinon.spy(noOpProvider, "log")
|
||||
const identifyUserSpy = sinon.spy(noOpProvider, "identifyUser")
|
||||
|
||||
const telemetryService = new TelemetryService(noOpProvider, MOCK_METADATA)
|
||||
const telemetryService = new TelemetryService([noOpProvider], MOCK_METADATA)
|
||||
|
||||
// Reset the spy to ignore the initial telemetry event from constructor
|
||||
logSpy.resetHistory()
|
||||
@@ -88,6 +88,69 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
|
||||
await noOpProvider.dispose()
|
||||
})
|
||||
|
||||
it("should support multi-provider telemetry for dual tracking", async () => {
|
||||
// Create multiple providers for dual tracking scenario
|
||||
const noOpProvider1 = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
const noOpProvider2 = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
|
||||
// Spy on both providers to verify they both receive events
|
||||
const logSpy1 = sinon.spy(noOpProvider1, "log")
|
||||
const logSpy2 = sinon.spy(noOpProvider2, "log")
|
||||
const identifyUserSpy1 = sinon.spy(noOpProvider1, "identifyUser")
|
||||
const identifyUserSpy2 = sinon.spy(noOpProvider2, "identifyUser")
|
||||
|
||||
// Create TelemetryService with multiple providers
|
||||
const telemetryService = new TelemetryService([noOpProvider1, noOpProvider2], MOCK_METADATA)
|
||||
|
||||
// Reset spies to ignore constructor events
|
||||
logSpy1.resetHistory()
|
||||
logSpy2.resetHistory()
|
||||
|
||||
// Test that events are sent to both providers
|
||||
telemetryService.captureTaskCreated("multi-task-123", "anthropic")
|
||||
|
||||
// Verify both providers received the event
|
||||
assert.ok(logSpy1.calledOnce, "First provider should receive the event")
|
||||
assert.ok(logSpy2.calledOnce, "Second provider should receive the event")
|
||||
|
||||
// Verify event content is correct for both providers
|
||||
const [eventName1, properties1] = logSpy1.firstCall.args
|
||||
const [eventName2, properties2] = logSpy2.firstCall.args
|
||||
|
||||
assert.strictEqual(eventName1, "task.created", "First provider should receive correct event name")
|
||||
assert.strictEqual(eventName2, "task.created", "Second provider should receive correct event name")
|
||||
|
||||
const expectedProperties = {
|
||||
ulid: "multi-task-123",
|
||||
apiProvider: "anthropic",
|
||||
...MOCK_METADATA,
|
||||
}
|
||||
assert.deepStrictEqual(properties1, expectedProperties, "First provider should receive correct properties")
|
||||
assert.deepStrictEqual(properties2, expectedProperties, "Second provider should receive correct properties")
|
||||
|
||||
// Test user identification with multiple providers
|
||||
telemetryService.identifyAccount(MOCK_USER_INFO)
|
||||
|
||||
assert.ok(identifyUserSpy1.calledOnce, "First provider should receive user identification")
|
||||
assert.ok(identifyUserSpy2.calledOnce, "Second provider should receive user identification")
|
||||
|
||||
// Verify provider count
|
||||
const providers = telemetryService.getProviders()
|
||||
assert.strictEqual(providers.length, 2, "Should have exactly 2 providers")
|
||||
|
||||
// Cleanup
|
||||
logSpy1.restore()
|
||||
logSpy2.restore()
|
||||
identifyUserSpy1.restore()
|
||||
identifyUserSpy2.restore()
|
||||
await noOpProvider1.dispose()
|
||||
await noOpProvider2.dispose()
|
||||
})
|
||||
})
|
||||
describe("PostHog Provider", () => {
|
||||
it("should create PostHog provider and track events", async () => {
|
||||
@@ -96,7 +159,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
type: "posthog",
|
||||
})
|
||||
|
||||
const posthogTelemetryService = new TelemetryService(posthogProvider, MOCK_METADATA)
|
||||
const posthogTelemetryService = new TelemetryService([posthogProvider], MOCK_METADATA)
|
||||
|
||||
// Test various telemetry methods
|
||||
posthogTelemetryService.captureTaskCreated("task-123", "anthropic")
|
||||
@@ -127,7 +190,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
type: "no-op",
|
||||
})
|
||||
|
||||
const noOpTelemetryService = new TelemetryService(noOpProvider, MOCK_METADATA)
|
||||
const noOpTelemetryService = new TelemetryService([noOpProvider], MOCK_METADATA)
|
||||
|
||||
// Test various telemetry methods - should all be no-ops
|
||||
noOpTelemetryService.captureTaskCreated("task-789", "google")
|
||||
@@ -190,7 +253,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
)
|
||||
|
||||
// Should handle all operations safely
|
||||
const telemetryService = new TelemetryService(unsupportedProvider, MOCK_METADATA)
|
||||
const telemetryService = new TelemetryService([unsupportedProvider], MOCK_METADATA)
|
||||
telemetryService.captureTaskCreated("task-456", "test")
|
||||
telemetryService.identifyAccount(MOCK_USER_INFO)
|
||||
|
||||
@@ -224,7 +287,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
const posthogProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "posthog",
|
||||
})
|
||||
let telemetryService = new TelemetryService(posthogProvider, MOCK_METADATA)
|
||||
let telemetryService = new TelemetryService([posthogProvider], MOCK_METADATA)
|
||||
|
||||
telemetryService.captureTaskCreated("task-switch-1", "anthropic")
|
||||
console.log("Captured event with PostHog provider")
|
||||
@@ -235,7 +298,7 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
const noOpProvider = await TelemetryProviderFactory.createProvider({
|
||||
type: "no-op",
|
||||
})
|
||||
telemetryService = new TelemetryService(noOpProvider, MOCK_METADATA)
|
||||
telemetryService = new TelemetryService([noOpProvider], MOCK_METADATA)
|
||||
|
||||
telemetryService.captureTaskCreated("task-switch-2", "openai")
|
||||
console.log("Captured event with No-Op provider")
|
||||
|
||||
@@ -8,7 +8,12 @@ import { Setting } from "@/shared/proto/index.host"
|
||||
import { Mode } from "@/shared/storage/types"
|
||||
import { version as extensionVersion } from "../../../package.json"
|
||||
import { setDistinctId } from "../logging/distinctId"
|
||||
import type { ITelemetryProvider } from "./providers/ITelemetryProvider"
|
||||
import { BrowserEvents } from "./events/BrowserEvents"
|
||||
import { DictationEvents } from "./events/DictationEvents"
|
||||
import { TaskEvents } from "./events/TaskEvents"
|
||||
import { UIEvents } from "./events/UIEvents"
|
||||
import { WorkspaceEvents } from "./events/WorkspaceEvents"
|
||||
import type { ITelemetryProvider, TelemetryProperties } from "./providers/ITelemetryProvider"
|
||||
import { TelemetryProviderFactory } from "./TelemetryProviderFactory"
|
||||
|
||||
/**
|
||||
@@ -202,9 +207,7 @@ export class TelemetryService {
|
||||
}
|
||||
|
||||
public static async create(): Promise<TelemetryService> {
|
||||
const provider = await TelemetryProviderFactory.createProvider({
|
||||
type: "posthog",
|
||||
})
|
||||
const providers = await TelemetryProviderFactory.createProviders()
|
||||
const hostVersion = await HostProvider.env.getHostVersion({})
|
||||
const metadata: TelemetryMetadata = {
|
||||
extension_version: extensionVersion,
|
||||
@@ -214,19 +217,19 @@ export class TelemetryService {
|
||||
os_version: os.version(),
|
||||
is_dev: process.env.IS_DEV,
|
||||
}
|
||||
return new TelemetryService(provider, metadata)
|
||||
return new TelemetryService(providers, metadata)
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor that accepts a PostHogClientProvider instance
|
||||
* @param provider PostHogClientProvider instance for sending analytics events
|
||||
* Constructor that accepts multiple telemetry providers for dual tracking
|
||||
* @param providers Array of telemetry providers for dual/multi tracking
|
||||
*/
|
||||
constructor(
|
||||
private provider: ITelemetryProvider,
|
||||
private providers: ITelemetryProvider[],
|
||||
private telemetryMetadata: TelemetryMetadata,
|
||||
) {
|
||||
this.capture({ event: TelemetryService.EVENTS.USER.TELEMETRY_ENABLED })
|
||||
console.info("[TelemetryService] Initialized with telemetry provider")
|
||||
console.info(`[TelemetryService] Initialized with ${providers.length} telemetry provider(s)`)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -261,30 +264,59 @@ export class TelemetryService {
|
||||
}
|
||||
}
|
||||
|
||||
this.provider.setOptIn(didUserOptIn)
|
||||
}
|
||||
|
||||
private addProperties(properties: any): any {
|
||||
return {
|
||||
...properties,
|
||||
...this.telemetryMetadata,
|
||||
}
|
||||
// Update all providers
|
||||
this.providers.forEach((provider) => {
|
||||
provider.setOptIn(didUserOptIn)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures a telemetry event if telemetry is enabled
|
||||
* @param event The event to capture with its properties
|
||||
*/
|
||||
public capture(event: { event: string; properties?: unknown }): void {
|
||||
const propertiesWithVersion = this.addProperties(event.properties)
|
||||
public capture(event: { event: string; properties?: TelemetryProperties }): void {
|
||||
const propertiesWithMetadata: TelemetryProperties = {
|
||||
...(event.properties || {}),
|
||||
...this.telemetryMetadata,
|
||||
}
|
||||
this.captureToProviders(event.event, propertiesWithMetadata, false)
|
||||
}
|
||||
|
||||
// Use the provider's log method
|
||||
this.provider.log(event.event, propertiesWithVersion)
|
||||
/**
|
||||
* Captures a required telemetry event that bypasses user opt-out settings
|
||||
* @param event The event name to capture
|
||||
* @param properties Optional properties to attach to the event
|
||||
*/
|
||||
public captureRequired(event: string, properties?: TelemetryProperties): void {
|
||||
const propertiesWithMetadata: TelemetryProperties = {
|
||||
...(properties || {}),
|
||||
...this.telemetryMetadata,
|
||||
}
|
||||
this.captureToProviders(event, propertiesWithMetadata, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal method to capture events to all providers with error isolation
|
||||
* @param event The event name
|
||||
* @param properties Event properties (must be JSON-serializable)
|
||||
* @param required Whether this is a required event
|
||||
*/
|
||||
private captureToProviders(event: string, properties: TelemetryProperties, required: boolean): void {
|
||||
this.providers.forEach((provider) => {
|
||||
try {
|
||||
if (required) {
|
||||
provider.logRequired(event, properties)
|
||||
} else {
|
||||
provider.log(event, properties)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`[TelemetryService] Provider failed for event ${event}:`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
public captureExtensionActivated() {
|
||||
// Use provider's log method for the activation event
|
||||
this.provider.log(TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED)
|
||||
this.captureToProviders(TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED, {}, false)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -292,9 +324,19 @@ export class TelemetryService {
|
||||
* @param userInfo The user's information
|
||||
*/
|
||||
public identifyAccount(userInfo: ClineAccountUserInfo) {
|
||||
const propertiesWithVersion = this.addProperties({})
|
||||
// Use the provider's log method instead of direct client capture
|
||||
this.provider.identifyUser(userInfo, propertiesWithVersion)
|
||||
const propertiesWithMetadata: TelemetryProperties = {
|
||||
...this.telemetryMetadata,
|
||||
}
|
||||
|
||||
// Update all providers with error isolation
|
||||
this.providers.forEach((provider) => {
|
||||
try {
|
||||
provider.identifyUser(userInfo, propertiesWithMetadata)
|
||||
} catch (error) {
|
||||
console.error(`[TelemetryService] Provider failed for user identification:`, error)
|
||||
}
|
||||
})
|
||||
|
||||
if (userInfo.id) {
|
||||
setDistinctId(userInfo.id)
|
||||
}
|
||||
@@ -309,15 +351,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("dictation")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.DICTATION.RECORDING_STARTED,
|
||||
properties: {
|
||||
taskId,
|
||||
platform: platform ?? process.platform,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
DictationEvents.captureVoiceRecordingStarted(this, taskId, platform)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -331,17 +365,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("dictation")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.DICTATION.RECORDING_STOPPED,
|
||||
properties: {
|
||||
taskId,
|
||||
durationMs,
|
||||
success,
|
||||
platform: platform ?? process.platform,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
DictationEvents.captureVoiceRecordingStopped(this, taskId, durationMs, success, platform)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -353,15 +377,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("dictation")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.DICTATION.TRANSCRIPTION_STARTED,
|
||||
properties: {
|
||||
taskId,
|
||||
language,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
DictationEvents.captureVoiceTranscriptionStarted(this, taskId, language)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -382,18 +398,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("dictation")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.DICTATION.TRANSCRIPTION_COMPLETED,
|
||||
properties: {
|
||||
taskId,
|
||||
transcriptionLength,
|
||||
durationMs,
|
||||
language,
|
||||
accountType: isOrgAccount ? "organization" : "personal",
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
DictationEvents.captureVoiceTranscriptionCompleted(this, taskId, transcriptionLength, durationMs, language, isOrgAccount)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -407,17 +412,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("dictation")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.DICTATION.TRANSCRIPTION_ERROR,
|
||||
properties: {
|
||||
taskId,
|
||||
errorType,
|
||||
errorMessage,
|
||||
durationMs,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
DictationEvents.captureVoiceTranscriptionError(this, taskId, errorType, errorMessage, durationMs)
|
||||
}
|
||||
// Task events
|
||||
/**
|
||||
@@ -426,10 +421,7 @@ export class TelemetryService {
|
||||
* @param apiProvider Optional API provider
|
||||
*/
|
||||
public captureTaskCreated(ulid: string, apiProvider?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.CREATED,
|
||||
properties: { ulid, apiProvider },
|
||||
})
|
||||
TaskEvents.captureTaskCreated(this, ulid, apiProvider)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -438,10 +430,7 @@ export class TelemetryService {
|
||||
* @param apiProvider Optional API provider
|
||||
*/
|
||||
public captureTaskRestarted(ulid: string, apiProvider?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.RESTARTED,
|
||||
properties: { ulid, apiProvider },
|
||||
})
|
||||
TaskEvents.captureTaskRestarted(this, ulid, apiProvider)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -449,10 +438,7 @@ export class TelemetryService {
|
||||
* @param ulid Unique identifier for the task
|
||||
*/
|
||||
public captureTaskCompleted(ulid: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.COMPLETED,
|
||||
properties: { ulid },
|
||||
})
|
||||
TaskEvents.captureTaskCompleted(this, ulid)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -476,25 +462,7 @@ export class TelemetryService {
|
||||
totalCost?: number
|
||||
} = {},
|
||||
) {
|
||||
// Ensure required parameters are provided
|
||||
if (!ulid || !provider || !model || !source) {
|
||||
console.warn("TelemetryService: Missing required parameters for message capture")
|
||||
return
|
||||
}
|
||||
|
||||
const properties: Record<string, unknown> = {
|
||||
ulid,
|
||||
provider,
|
||||
model,
|
||||
source,
|
||||
timestamp: new Date().toISOString(), // Add timestamp for message sequencing
|
||||
...tokenUsage,
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.CONVERSATION_TURN,
|
||||
properties,
|
||||
})
|
||||
TaskEvents.captureConversationTurnEvent(this, ulid, provider, model, source, tokenUsage)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -505,15 +473,7 @@ export class TelemetryService {
|
||||
* @param model The model used for token calculation
|
||||
*/
|
||||
public captureTokenUsage(ulid: string, tokensIn: number, tokensOut: number, model: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TOKEN_USAGE,
|
||||
properties: {
|
||||
ulid,
|
||||
tokensIn,
|
||||
tokensOut,
|
||||
model,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureTokenUsage(this, ulid, tokensIn, tokensOut, model)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,13 +482,7 @@ export class TelemetryService {
|
||||
* @param mode The mode being switched to (plan or act)
|
||||
*/
|
||||
public captureModeSwitch(ulid: string, mode: Mode) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.MODE_SWITCH,
|
||||
properties: {
|
||||
ulid,
|
||||
mode,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureModeSwitch(this, ulid, mode)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -539,15 +493,7 @@ export class TelemetryService {
|
||||
* @param maxContextWindow Maximum context window size for the model
|
||||
*/
|
||||
public captureSummarizeTask(ulid: string, modelId: string, currentTokens: number, maxContextWindow: number) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.AUTO_COMPACT,
|
||||
properties: {
|
||||
ulid,
|
||||
modelId,
|
||||
currentTokens,
|
||||
maxContextWindow,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureSummarizeTask(this, ulid, modelId, currentTokens, maxContextWindow)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -556,17 +502,7 @@ export class TelemetryService {
|
||||
* @param feedbackType The type of feedback ("thumbs_up" or "thumbs_down")
|
||||
*/
|
||||
public captureTaskFeedback(ulid: string, feedbackType: TaskFeedbackType) {
|
||||
console.info("TelemetryService: Capturing task feedback", {
|
||||
ulid,
|
||||
feedbackType,
|
||||
})
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.FEEDBACK,
|
||||
properties: {
|
||||
ulid,
|
||||
feedbackType,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureTaskFeedback(this, ulid, feedbackType)
|
||||
}
|
||||
|
||||
// Tool events
|
||||
@@ -578,16 +514,7 @@ export class TelemetryService {
|
||||
* @param success Whether the tool execution was successful
|
||||
*/
|
||||
public captureToolUsage(ulid: string, tool: string, modelId: string, autoApproved: boolean, success: boolean) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TOOL_USED,
|
||||
properties: {
|
||||
ulid,
|
||||
tool,
|
||||
autoApproved,
|
||||
success,
|
||||
modelId,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureToolUsage(this, ulid, tool, modelId, autoApproved, success)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -611,17 +538,7 @@ export class TelemetryService {
|
||||
errorMessage?: string,
|
||||
argumentKeys?: string[],
|
||||
) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.MCP_TOOL_CALLED,
|
||||
properties: {
|
||||
ulid,
|
||||
serverName,
|
||||
toolName,
|
||||
status,
|
||||
errorMessage,
|
||||
argumentKeys,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureMcpToolCall(this, ulid, serverName, toolName, status, errorMessage, argumentKeys)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -638,15 +555,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("checkpoints")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.CHECKPOINT_USED,
|
||||
properties: {
|
||||
ulid,
|
||||
action,
|
||||
durationMs,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureCheckpointUsage(this, ulid, action, durationMs)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -655,14 +564,7 @@ export class TelemetryService {
|
||||
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
|
||||
*/
|
||||
public captureDiffEditFailure(ulid: string, modelId: string, errorType?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.DIFF_EDIT_FAILED,
|
||||
properties: {
|
||||
ulid,
|
||||
errorType,
|
||||
modelId,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureDiffEditFailure(this, ulid, modelId, errorType)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -672,14 +574,7 @@ export class TelemetryService {
|
||||
* @param ulid Optional task identifier if model was selected during a task
|
||||
*/
|
||||
public captureModelSelected(model: string, provider: string, ulid?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.UI.MODEL_SELECTED,
|
||||
properties: {
|
||||
model,
|
||||
provider,
|
||||
ulid,
|
||||
},
|
||||
})
|
||||
UIEvents.captureModelSelected(this, model, provider, ulid)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -691,17 +586,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_START,
|
||||
properties: {
|
||||
ulid,
|
||||
viewport: browserSettings.viewport,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
BrowserEvents.captureBrowserToolStart(this, ulid, browserSettings)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -720,17 +605,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_END,
|
||||
properties: {
|
||||
ulid,
|
||||
actionCount: stats.actionCount,
|
||||
duration: stats.duration,
|
||||
actions: stats.actions,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
BrowserEvents.captureBrowserToolEnd(this, ulid, stats)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -748,23 +623,15 @@ export class TelemetryService {
|
||||
action?: string
|
||||
url?: string
|
||||
isRemote?: boolean
|
||||
[key: string]: unknown
|
||||
remoteBrowserHost?: string
|
||||
endpoint?: string
|
||||
[key: string]: string | number | boolean | undefined
|
||||
},
|
||||
) {
|
||||
if (!this.isCategoryEnabled("browser")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.BROWSER_ERROR,
|
||||
properties: {
|
||||
ulid,
|
||||
errorType,
|
||||
errorMessage,
|
||||
context,
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
BrowserEvents.captureBrowserError(this, ulid, errorType, errorMessage, context)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -774,14 +641,7 @@ export class TelemetryService {
|
||||
* @param mode The mode in which the option was selected ("plan" or "act")
|
||||
*/
|
||||
public captureOptionSelected(ulid: string, qty: number, mode: Mode) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.OPTION_SELECTED,
|
||||
properties: {
|
||||
ulid,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureOptionSelected(this, ulid, qty, mode)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -791,14 +651,7 @@ export class TelemetryService {
|
||||
* @param mode The mode in which the custom response was provided ("plan" or "act")
|
||||
*/
|
||||
public captureOptionsIgnored(ulid: string, qty: number, mode: Mode) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.OPTIONS_IGNORED,
|
||||
properties: {
|
||||
ulid,
|
||||
qty,
|
||||
mode,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureOptionsIgnored(this, ulid, qty, mode)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -823,14 +676,7 @@ export class TelemetryService {
|
||||
throughputTokensPerSec?: number
|
||||
},
|
||||
) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.GEMINI_API_PERFORMANCE,
|
||||
properties: {
|
||||
ulid,
|
||||
modelId,
|
||||
...data,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureGeminiApiPerformance(this, ulid, modelId, data)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -839,23 +685,11 @@ export class TelemetryService {
|
||||
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
|
||||
*/
|
||||
public captureModelFavoritesUsage(model: string, isFavorited: boolean) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
|
||||
properties: {
|
||||
model,
|
||||
isFavorited,
|
||||
},
|
||||
})
|
||||
UIEvents.captureModelFavoritesUsage(this, model, isFavorited)
|
||||
}
|
||||
|
||||
public captureButtonClick(button: string, ulid?: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.UI.BUTTON_CLICKED,
|
||||
properties: {
|
||||
button,
|
||||
ulid,
|
||||
},
|
||||
})
|
||||
UIEvents.captureButtonClick(this, button, ulid)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -875,14 +709,7 @@ export class TelemetryService {
|
||||
errorStatus?: number | undefined
|
||||
requestId?: string | undefined
|
||||
}) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.PROVIDER_API_ERROR,
|
||||
properties: {
|
||||
...args,
|
||||
errorMessage: args.errorMessage.substring(0, MAX_ERROR_MESSAGE_LENGTH), // Truncate long error messages
|
||||
timestamp: new Date().toISOString(),
|
||||
},
|
||||
})
|
||||
TaskEvents.captureProviderApiError(this, args)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -893,13 +720,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("focus_chain")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: enabled ? TelemetryService.EVENTS.TASK.FOCUS_CHAIN_ENABLED : TelemetryService.EVENTS.TASK.FOCUS_CHAIN_DISABLED,
|
||||
properties: {
|
||||
enabled,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureFocusChainToggle(this, enabled)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -911,14 +732,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("focus_chain")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.FOCUS_CHAIN_PROGRESS_FIRST,
|
||||
properties: {
|
||||
ulid,
|
||||
totalItems,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureFocusChainProgressFirst(this, ulid, totalItems)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -931,16 +745,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("focus_chain")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.FOCUS_CHAIN_PROGRESS_UPDATE,
|
||||
properties: {
|
||||
ulid,
|
||||
totalItems,
|
||||
completedItems,
|
||||
completionPercentage: totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureFocusChainProgressUpdate(this, ulid, totalItems, completedItems)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -959,17 +764,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("focus_chain")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.FOCUS_CHAIN_INCOMPLETE_ON_COMPLETION,
|
||||
properties: {
|
||||
ulid,
|
||||
totalItems,
|
||||
completedItems,
|
||||
incompleteItems,
|
||||
completionPercentage: totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureFocusChainIncompleteOnCompletion(this, ulid, totalItems, completedItems, incompleteItems)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -980,13 +775,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("focus_chain")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.FOCUS_CHAIN_LIST_OPENED,
|
||||
properties: {
|
||||
ulid,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureFocusChainListOpened(this, ulid)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -997,13 +786,7 @@ export class TelemetryService {
|
||||
if (!this.isCategoryEnabled("focus_chain")) {
|
||||
return
|
||||
}
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.FOCUS_CHAIN_LIST_WRITTEN,
|
||||
properties: {
|
||||
ulid,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureFocusChainListWritten(this, ulid)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1013,14 +796,7 @@ export class TelemetryService {
|
||||
* @param commandType Whether it's a built-in command or custom workflow
|
||||
*/
|
||||
public captureSlashCommandUsed(ulid: string, commandName: string, commandType: "builtin" | "workflow") {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.SLASH_COMMAND_USED,
|
||||
properties: {
|
||||
ulid,
|
||||
commandName,
|
||||
commandType,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureSlashCommandUsed(this, ulid, commandName, commandType)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1031,18 +807,7 @@ export class TelemetryService {
|
||||
* @param isGlobal Whether this is a global rule or workspace-specific rule
|
||||
*/
|
||||
public captureClineRuleToggled(ulid: string, ruleFileName: string, enabled: boolean, isGlobal: boolean) {
|
||||
// Sanitize filename to remove any path information for privacy
|
||||
const sanitizedFileName = ruleFileName.split("/").pop() || ruleFileName.split("\\").pop() || ruleFileName
|
||||
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.RULE_TOGGLED,
|
||||
properties: {
|
||||
ulid,
|
||||
ruleFileName: sanitizedFileName,
|
||||
enabled,
|
||||
isGlobal,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureClineRuleToggled(this, ulid, ruleFileName, enabled, isGlobal)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1052,14 +817,7 @@ export class TelemetryService {
|
||||
* @param modelId The model ID being used when the toggle occurred
|
||||
*/
|
||||
public captureAutoCondenseToggle(ulid: string, enabled: boolean, modelId: string) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.AUTO_CONDENSE_TOGGLED,
|
||||
properties: {
|
||||
ulid,
|
||||
enabled,
|
||||
modelId,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureAutoCondenseToggle(this, ulid, enabled, modelId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1068,13 +826,7 @@ export class TelemetryService {
|
||||
* @param enabled Whether yolo mode was enabled (true) or disabled (false)
|
||||
*/
|
||||
public captureYoloModeToggle(ulid: string, enabled: boolean) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.YOLO_MODE_TOGGLED,
|
||||
properties: {
|
||||
ulid,
|
||||
enabled,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureYoloModeToggle(this, ulid, enabled)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1085,25 +837,14 @@ export class TelemetryService {
|
||||
* @param hasCheckpoints Whether checkpoints are enabled for this task
|
||||
*/
|
||||
public captureTaskInitialization(ulid: string, taskId: string, durationMs: number, hasCheckpoints: boolean) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.INITIALIZATION,
|
||||
properties: {
|
||||
ulid,
|
||||
taskId,
|
||||
durationMs,
|
||||
hasCheckpoints,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureTaskInitialization(this, ulid, taskId, durationMs, hasCheckpoints)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the rules menu button is clicked to open the rules/workflows modal
|
||||
*/
|
||||
public captureRulesMenuOpened() {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.UI.RULES_MENU_OPENED,
|
||||
properties: {},
|
||||
})
|
||||
UIEvents.captureRulesMenuOpened(this)
|
||||
}
|
||||
|
||||
// Terminal telemetry methods
|
||||
@@ -1114,13 +855,7 @@ export class TelemetryService {
|
||||
* @param method The method used to capture output ("shell_integration" | "clipboard" | "none")
|
||||
*/
|
||||
public captureTerminalExecution(success: boolean, method: "shell_integration" | "clipboard" | "none") {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TERMINAL_EXECUTION,
|
||||
properties: {
|
||||
success,
|
||||
method,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureTerminalExecution(this, success, method)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1128,12 +863,7 @@ export class TelemetryService {
|
||||
* @param reason The reason for failure
|
||||
*/
|
||||
public captureTerminalOutputFailure(reason: TerminalOutputFailureReason) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TERMINAL_OUTPUT_FAILURE,
|
||||
properties: {
|
||||
reason,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureTerminalOutputFailure(this, reason)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1141,12 +871,7 @@ export class TelemetryService {
|
||||
* @param action The user action
|
||||
*/
|
||||
public captureTerminalUserIntervention(action: TerminalUserInterventionAction) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TERMINAL_USER_INTERVENTION,
|
||||
properties: {
|
||||
action,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureTerminalUserIntervention(this, action)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1154,12 +879,7 @@ export class TelemetryService {
|
||||
* @param stage Where the hang occurred
|
||||
*/
|
||||
public captureTerminalHang(stage: TerminalHangStage) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.TASK.TERMINAL_HANG,
|
||||
properties: {
|
||||
stage,
|
||||
},
|
||||
})
|
||||
TaskEvents.captureTerminalHang(this, stage)
|
||||
}
|
||||
|
||||
// Workspace telemetry methods
|
||||
@@ -1177,18 +897,7 @@ export class TelemetryService {
|
||||
initDurationMs?: number,
|
||||
featureFlagEnabled?: boolean,
|
||||
) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.WORKSPACE.INITIALIZED,
|
||||
properties: {
|
||||
root_count: rootCount,
|
||||
vcs_types: vcsTypes,
|
||||
is_multi_root: rootCount > 1,
|
||||
has_git: vcsTypes.includes("Git"),
|
||||
has_mercurial: vcsTypes.includes("Mercurial"),
|
||||
init_duration_ms: initDurationMs,
|
||||
feature_flag_enabled: featureFlagEnabled,
|
||||
},
|
||||
})
|
||||
WorkspaceEvents.captureWorkspaceInitialized(this, rootCount, vcsTypes, initDurationMs, featureFlagEnabled)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1198,15 +907,7 @@ export class TelemetryService {
|
||||
* @param workspaceCount Number of workspace folders detected
|
||||
*/
|
||||
public captureWorkspaceInitError(error: Error, fallbackMode: boolean, workspaceCount?: number) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.WORKSPACE.INIT_ERROR,
|
||||
properties: {
|
||||
error_type: error.constructor.name,
|
||||
error_message: error.message.substring(0, MAX_ERROR_MESSAGE_LENGTH),
|
||||
fallback_to_single_root: fallbackMode,
|
||||
workspace_count: workspaceCount ?? 0,
|
||||
},
|
||||
})
|
||||
WorkspaceEvents.captureWorkspaceInitError(this, error, fallbackMode, workspaceCount)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1226,18 +927,7 @@ export class TelemetryService {
|
||||
failureCount: number,
|
||||
durationMs?: number,
|
||||
) {
|
||||
this.capture({
|
||||
event: TelemetryService.EVENTS.WORKSPACE.MULTI_ROOT_CHECKPOINT,
|
||||
properties: {
|
||||
ulid,
|
||||
action,
|
||||
root_count: rootCount,
|
||||
success_count: successCount,
|
||||
failure_count: failureCount,
|
||||
success_rate: rootCount > 0 ? successCount / rootCount : 0,
|
||||
duration_ms: durationMs,
|
||||
},
|
||||
})
|
||||
WorkspaceEvents.captureMultiRootCheckpoint(this, ulid, action, rootCount, successCount, failureCount, durationMs)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1251,33 +941,40 @@ export class TelemetryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the telemetry provider instance
|
||||
* @returns The current telemetry provider
|
||||
* Get the telemetry provider instances
|
||||
* @returns The array of telemetry providers
|
||||
*/
|
||||
public getProvider(): ITelemetryProvider {
|
||||
return this.provider
|
||||
public getProviders(): ITelemetryProvider[] {
|
||||
return [...this.providers]
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if telemetry is currently enabled
|
||||
* @returns Boolean indicating whether telemetry is enabled
|
||||
* @returns Boolean indicating whether any provider is enabled
|
||||
*/
|
||||
public isEnabled(): boolean {
|
||||
return this.provider.isEnabled()
|
||||
return this.providers.some((provider) => provider.isEnabled())
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current telemetry settings
|
||||
* Get current telemetry settings from the first provider
|
||||
* @returns Current telemetry settings
|
||||
*/
|
||||
public getSettings() {
|
||||
return this.provider.getSettings()
|
||||
return this.providers.length > 0
|
||||
? this.providers[0].getSettings()
|
||||
: {
|
||||
extensionEnabled: false,
|
||||
hostEnabled: false,
|
||||
level: "off" as const,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clean up resources when the service is disposed
|
||||
*/
|
||||
public async dispose(): Promise<void> {
|
||||
await this.provider.dispose()
|
||||
const disposePromises = this.providers.map((provider) => provider.dispose())
|
||||
await Promise.allSettled(disposePromises)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
import type { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import type { TelemetryProperties } from "../providers/ITelemetryProvider"
|
||||
import type { TelemetryService } from "../TelemetryService"
|
||||
import { EventHandlerBase } from "./EventHandlerBase"
|
||||
|
||||
/**
|
||||
* Property types for browser telemetry events
|
||||
*/
|
||||
|
||||
export interface BrowserToolStartProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
viewport?: { width: number; height: number }
|
||||
isRemote: boolean
|
||||
remoteBrowserHost?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface BrowserErrorProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
errorType: string
|
||||
errorMessage: string
|
||||
action?: string
|
||||
url?: string
|
||||
isRemote?: boolean
|
||||
remoteBrowserHost?: string
|
||||
endpoint?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface BrowserToolEndProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
actionCount: number
|
||||
duration: number
|
||||
actions?: string[]
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for browser-related telemetry events
|
||||
*/
|
||||
export class BrowserEvents extends EventHandlerBase {
|
||||
static override readonly prefix = "browser"
|
||||
|
||||
/**
|
||||
* Records when the browser tool is started
|
||||
* @param service The telemetry service instance
|
||||
* @param ulid Unique identifier for the task
|
||||
* @param browserSettings The browser settings being used
|
||||
*/
|
||||
static captureBrowserToolStart(service: TelemetryService, ulid: string, browserSettings: BrowserSettings): void {
|
||||
const properties: BrowserToolStartProperties = {
|
||||
ulid,
|
||||
viewport: browserSettings.viewport,
|
||||
isRemote: !!browserSettings.remoteBrowserEnabled,
|
||||
remoteBrowserHost: browserSettings.remoteBrowserHost,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
BrowserEvents.capture(service, "task.browser_tool_start", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when browser errors occur during a task
|
||||
* @param service The telemetry service instance
|
||||
* @param ulid Unique identifier for the task
|
||||
* @param errorType Type of error that occurred
|
||||
* @param errorMessage The error message
|
||||
* @param context Additional context about where the error occurred
|
||||
*/
|
||||
static captureBrowserError(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
errorType: string,
|
||||
errorMessage: string,
|
||||
context?: Partial<BrowserErrorProperties>,
|
||||
): void {
|
||||
const properties: BrowserErrorProperties = {
|
||||
ulid,
|
||||
errorType,
|
||||
errorMessage,
|
||||
timestamp: new Date().toISOString(),
|
||||
...context,
|
||||
}
|
||||
BrowserEvents.capture(service, "task.browser_error", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the browser tool is completed
|
||||
* @param service The telemetry service instance
|
||||
* @param ulid Unique identifier for the task
|
||||
* @param stats Statistics about the browser session
|
||||
*/
|
||||
static captureBrowserToolEnd(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
stats: {
|
||||
actionCount: number
|
||||
duration: number
|
||||
actions?: string[]
|
||||
},
|
||||
): void {
|
||||
const properties: BrowserToolEndProperties = {
|
||||
ulid,
|
||||
actionCount: stats.actionCount,
|
||||
duration: stats.duration,
|
||||
actions: stats.actions,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
BrowserEvents.capture(service, "task.browser_tool_end", properties)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import type { TelemetryProperties } from "../providers/ITelemetryProvider"
|
||||
import type { TelemetryService } from "../TelemetryService"
|
||||
import { EventHandlerBase } from "./EventHandlerBase"
|
||||
|
||||
/**
|
||||
* Property types for dictation/voice telemetry events
|
||||
*/
|
||||
|
||||
export interface VoiceRecordingStartedProperties extends TelemetryProperties {
|
||||
taskId?: string
|
||||
platform: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface VoiceRecordingStoppedProperties extends TelemetryProperties {
|
||||
taskId?: string
|
||||
durationMs?: number
|
||||
success?: boolean
|
||||
platform: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface VoiceTranscriptionStartedProperties extends TelemetryProperties {
|
||||
taskId?: string
|
||||
language?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface VoiceTranscriptionCompletedProperties extends TelemetryProperties {
|
||||
taskId?: string
|
||||
transcriptionLength?: number
|
||||
durationMs?: number
|
||||
language?: string
|
||||
accountType: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface VoiceTranscriptionErrorProperties extends TelemetryProperties {
|
||||
taskId?: string
|
||||
errorType?: string
|
||||
errorMessage?: string
|
||||
durationMs?: number
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for dictation/voice-related telemetry events
|
||||
*/
|
||||
export class DictationEvents extends EventHandlerBase {
|
||||
static override readonly prefix = "dictation"
|
||||
|
||||
/**
|
||||
* Records when voice recording is started
|
||||
* @param service The telemetry service instance
|
||||
* @param taskId Optional task identifier if recording was started during a task
|
||||
* @param platform The platform where recording is happening
|
||||
*/
|
||||
static captureVoiceRecordingStarted(service: TelemetryService, taskId?: string, platform?: string): void {
|
||||
const properties: VoiceRecordingStartedProperties = {
|
||||
taskId,
|
||||
platform: platform ?? process.platform,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
DictationEvents.capture(service, "voice.recording_started", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when voice recording is stopped
|
||||
* @param service The telemetry service instance
|
||||
* @param taskId Optional task identifier
|
||||
* @param durationMs Duration of the recording in milliseconds
|
||||
* @param success Whether the recording was successful
|
||||
* @param platform The platform where recording happened
|
||||
*/
|
||||
static captureVoiceRecordingStopped(
|
||||
service: TelemetryService,
|
||||
taskId?: string,
|
||||
durationMs?: number,
|
||||
success?: boolean,
|
||||
platform?: string,
|
||||
): void {
|
||||
const properties: VoiceRecordingStoppedProperties = {
|
||||
taskId,
|
||||
durationMs,
|
||||
success,
|
||||
platform: platform ?? process.platform,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
DictationEvents.capture(service, "voice.recording_stopped", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when voice transcription is started
|
||||
* @param service The telemetry service instance
|
||||
* @param taskId Optional task identifier
|
||||
* @param language Language hint provided for transcription
|
||||
*/
|
||||
static captureVoiceTranscriptionStarted(service: TelemetryService, taskId?: string, language?: string): void {
|
||||
const properties: VoiceTranscriptionStartedProperties = {
|
||||
taskId,
|
||||
language,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
DictationEvents.capture(service, "voice.transcription_started", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when voice transcription is completed successfully
|
||||
* @param service The telemetry service instance
|
||||
* @param taskId Optional task identifier
|
||||
* @param transcriptionLength Length of the transcribed text
|
||||
* @param durationMs Time taken for transcription in milliseconds
|
||||
* @param language Language used for transcription
|
||||
* @param isOrgAccount Whether the transcription was done using an organization account
|
||||
*/
|
||||
static captureVoiceTranscriptionCompleted(
|
||||
service: TelemetryService,
|
||||
taskId?: string,
|
||||
transcriptionLength?: number,
|
||||
durationMs?: number,
|
||||
language?: string,
|
||||
isOrgAccount?: boolean,
|
||||
): void {
|
||||
const properties: VoiceTranscriptionCompletedProperties = {
|
||||
taskId,
|
||||
transcriptionLength,
|
||||
durationMs,
|
||||
language,
|
||||
accountType: isOrgAccount ? "organization" : "personal",
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
DictationEvents.capture(service, "voice.transcription_completed", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when voice transcription fails
|
||||
* @param service The telemetry service instance
|
||||
* @param taskId Optional task identifier
|
||||
* @param errorType Type of error that occurred
|
||||
* @param errorMessage The error message
|
||||
* @param durationMs Time taken before failure in milliseconds
|
||||
*/
|
||||
static captureVoiceTranscriptionError(
|
||||
service: TelemetryService,
|
||||
taskId?: string,
|
||||
errorType?: string,
|
||||
errorMessage?: string,
|
||||
durationMs?: number,
|
||||
): void {
|
||||
const properties: VoiceTranscriptionErrorProperties = {
|
||||
taskId,
|
||||
errorType,
|
||||
errorMessage,
|
||||
durationMs,
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
DictationEvents.capture(service, "voice.transcription_error", properties)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { TelemetryService } from "../TelemetryService"
|
||||
|
||||
/**
|
||||
* Base class for telemetry event handlers
|
||||
* Provides common functionality for event capture with metadata enrichment
|
||||
*/
|
||||
export abstract class EventHandlerBase {
|
||||
/** Event prefix for this handler (e.g., "task", "ui", "dictation") */
|
||||
static readonly prefix: string
|
||||
|
||||
/**
|
||||
* Capture a regular telemetry event
|
||||
* @param service The telemetry service instance
|
||||
* @param event The full event name (e.g., "task.created")
|
||||
* @param properties Event properties
|
||||
* @param required Whether this is a required event that bypasses user preferences
|
||||
*/
|
||||
protected static capture(
|
||||
service: TelemetryService,
|
||||
event: string,
|
||||
properties?: import("../providers/ITelemetryProvider").TelemetryProperties,
|
||||
required: boolean = false,
|
||||
): void {
|
||||
if (required) {
|
||||
service.captureRequired(event, properties)
|
||||
} else {
|
||||
service.capture({ event, properties })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Capture a required event that bypasses telemetry opt-out settings
|
||||
* @param service The telemetry service instance
|
||||
* @param event The full event name
|
||||
* @param properties Event properties
|
||||
*/
|
||||
protected static captureRequired(
|
||||
service: TelemetryService,
|
||||
event: string,
|
||||
properties?: import("../providers/ITelemetryProvider").TelemetryProperties,
|
||||
): void {
|
||||
service.captureRequired(event, properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if telemetry is enabled for regular events
|
||||
* @param service The telemetry service instance
|
||||
* @returns Whether telemetry is enabled
|
||||
*/
|
||||
protected static isEnabled(service: TelemetryService): boolean {
|
||||
return service.isEnabled()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Registry for automatic event handler registration
|
||||
*/
|
||||
export class EventHandlerRegistry {
|
||||
private static handlers: Map<string, typeof EventHandlerBase> = new Map()
|
||||
private static registered = false
|
||||
|
||||
/**
|
||||
* Register an event handler for a specific prefix
|
||||
* @param prefix The event prefix (e.g., "task", "ui")
|
||||
* @param handlerClass The handler class
|
||||
*/
|
||||
static register(prefix: string, handlerClass: typeof EventHandlerBase): void {
|
||||
EventHandlerRegistry.handlers.set(prefix, handlerClass)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a registered event handler by prefix
|
||||
* @param prefix The event prefix
|
||||
* @returns The handler class or undefined
|
||||
*/
|
||||
static getHandler(prefix: string): typeof EventHandlerBase | undefined {
|
||||
return EventHandlerRegistry.handlers.get(prefix)
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-register all event handlers
|
||||
* This is called once during TelemetryService initialization
|
||||
*/
|
||||
static registerAll(): void {
|
||||
if (EventHandlerRegistry.registered) return
|
||||
|
||||
// Import and register all handlers
|
||||
// This will be populated as we create the specific handlers
|
||||
EventHandlerRegistry.registered = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered prefixes
|
||||
* @returns Array of registered event prefixes
|
||||
*/
|
||||
static getRegisteredPrefixes(): string[] {
|
||||
return Array.from(EventHandlerRegistry.handlers.keys())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,571 @@
|
||||
import type { TaskFeedbackType } from "@shared/WebviewMessage"
|
||||
import { Mode } from "@/shared/storage/types"
|
||||
import type { TelemetryProperties } from "../providers/ITelemetryProvider"
|
||||
import type { TelemetryService } from "../TelemetryService"
|
||||
import { EventHandlerBase } from "./EventHandlerBase"
|
||||
|
||||
/**
|
||||
* Property types for task telemetry events
|
||||
*/
|
||||
|
||||
export interface TaskCreatedProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
apiProvider?: string
|
||||
}
|
||||
|
||||
export interface TaskCompletedProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
}
|
||||
|
||||
export interface TaskFeedbackProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
feedbackType: TaskFeedbackType
|
||||
}
|
||||
|
||||
export interface ConversationTurnProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
provider: string
|
||||
model: string
|
||||
source: "user" | "assistant"
|
||||
timestamp: string
|
||||
tokensIn?: number
|
||||
tokensOut?: number
|
||||
cacheWriteTokens?: number
|
||||
cacheReadTokens?: number
|
||||
totalCost?: number
|
||||
}
|
||||
|
||||
export interface TokenUsageProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
tokensIn: number
|
||||
tokensOut: number
|
||||
model: string
|
||||
}
|
||||
|
||||
export interface ModeSwitchProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
mode: Mode
|
||||
}
|
||||
|
||||
export interface ToolUsageProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
tool: string
|
||||
modelId: string
|
||||
autoApproved: boolean
|
||||
success: boolean
|
||||
}
|
||||
|
||||
export interface McpToolCallProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
serverName: string
|
||||
toolName: string
|
||||
status: "started" | "success" | "error"
|
||||
errorMessage?: string
|
||||
argumentKeys?: string[]
|
||||
}
|
||||
|
||||
export interface CheckpointUsageProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
action: "shadow_git_initialized" | "commit_created" | "restored" | "diff_generated"
|
||||
durationMs?: number
|
||||
}
|
||||
|
||||
export interface DiffEditFailureProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
modelId: string
|
||||
errorType?: string
|
||||
}
|
||||
|
||||
export interface OptionSelectedProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
qty: number
|
||||
mode: Mode
|
||||
}
|
||||
|
||||
export interface GeminiApiPerformanceProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
modelId: string
|
||||
ttftSec?: number
|
||||
totalDurationSec?: number
|
||||
promptTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheHit: boolean
|
||||
cacheHitPercentage?: number
|
||||
apiSuccess: boolean
|
||||
apiError?: string
|
||||
throughputTokensPerSec?: number
|
||||
}
|
||||
|
||||
export interface ProviderApiErrorProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
model: string
|
||||
errorMessage: string
|
||||
provider?: string
|
||||
errorStatus?: number
|
||||
requestId?: string
|
||||
timestamp: string
|
||||
}
|
||||
|
||||
export interface SummarizeTaskProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
modelId: string
|
||||
currentTokens: number
|
||||
maxContextWindow: number
|
||||
}
|
||||
|
||||
export interface SlashCommandUsedProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
commandName: string
|
||||
commandType: "builtin" | "workflow"
|
||||
}
|
||||
|
||||
export interface ClineRuleToggledProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
ruleFileName: string
|
||||
enabled: boolean
|
||||
isGlobal: boolean
|
||||
}
|
||||
|
||||
export interface AutoCondenseToggleProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
enabled: boolean
|
||||
modelId: string
|
||||
}
|
||||
|
||||
export interface YoloModeToggleProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface TaskInitializationProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
taskId: string
|
||||
durationMs: number
|
||||
hasCheckpoints: boolean
|
||||
}
|
||||
|
||||
export interface TerminalExecutionProperties extends TelemetryProperties {
|
||||
success: boolean
|
||||
method: "shell_integration" | "clipboard" | "none"
|
||||
}
|
||||
|
||||
export interface TerminalOutputFailureProperties extends TelemetryProperties {
|
||||
reason: string
|
||||
}
|
||||
|
||||
export interface TerminalUserInterventionProperties extends TelemetryProperties {
|
||||
action: string
|
||||
}
|
||||
|
||||
export interface TerminalHangProperties extends TelemetryProperties {
|
||||
stage: string
|
||||
}
|
||||
|
||||
export interface FocusChainToggleProperties extends TelemetryProperties {
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface FocusChainProgressFirstProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
totalItems: number
|
||||
}
|
||||
|
||||
export interface FocusChainProgressUpdateProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
totalItems: number
|
||||
completedItems: number
|
||||
completionPercentage: number
|
||||
}
|
||||
|
||||
export interface FocusChainIncompleteOnCompletionProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
totalItems: number
|
||||
completedItems: number
|
||||
incompleteItems: number
|
||||
completionPercentage: number
|
||||
}
|
||||
|
||||
export interface FocusChainListOpenedProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
}
|
||||
|
||||
export interface FocusChainListWrittenProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for task-related telemetry events
|
||||
*/
|
||||
export class TaskEvents extends EventHandlerBase {
|
||||
static override readonly prefix = "task"
|
||||
|
||||
/**
|
||||
* Records when a new task/conversation is started
|
||||
*/
|
||||
static captureTaskCreated(service: TelemetryService, ulid: string, apiProvider?: string): void {
|
||||
const properties: TaskCreatedProperties = { ulid, apiProvider }
|
||||
TaskEvents.capture(service, "task.created", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a task/conversation is restarted
|
||||
*/
|
||||
static captureTaskRestarted(service: TelemetryService, ulid: string, apiProvider?: string): void {
|
||||
const properties: TaskCreatedProperties = { ulid, apiProvider }
|
||||
TaskEvents.capture(service, "task.restarted", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when cline calls the task completion_result tool
|
||||
*/
|
||||
static captureTaskCompleted(service: TelemetryService, ulid: string): void {
|
||||
const properties: TaskCompletedProperties = { ulid }
|
||||
TaskEvents.capture(service, "task.completed", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records user feedback on completed tasks
|
||||
*/
|
||||
static captureTaskFeedback(service: TelemetryService, ulid: string, feedbackType: TaskFeedbackType): void {
|
||||
const properties: TaskFeedbackProperties = { ulid, feedbackType }
|
||||
TaskEvents.capture(service, "task.feedback", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures that a message was sent
|
||||
*/
|
||||
static captureConversationTurnEvent(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
provider: string,
|
||||
model: string,
|
||||
source: "user" | "assistant",
|
||||
tokenUsage: {
|
||||
tokensIn?: number
|
||||
tokensOut?: number
|
||||
cacheWriteTokens?: number
|
||||
cacheReadTokens?: number
|
||||
totalCost?: number
|
||||
} = {},
|
||||
): void {
|
||||
const properties: ConversationTurnProperties = {
|
||||
ulid,
|
||||
provider,
|
||||
model,
|
||||
source,
|
||||
timestamp: new Date().toISOString(),
|
||||
...tokenUsage,
|
||||
}
|
||||
TaskEvents.capture(service, "task.conversation_turn", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records token usage metrics
|
||||
*/
|
||||
static captureTokenUsage(service: TelemetryService, ulid: string, tokensIn: number, tokensOut: number, model: string): void {
|
||||
const properties: TokenUsageProperties = { ulid, tokensIn, tokensOut, model }
|
||||
TaskEvents.capture(service, "task.tokens", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a task switches between plan and act modes
|
||||
*/
|
||||
static captureModeSwitch(service: TelemetryService, ulid: string, mode: Mode): void {
|
||||
const properties: ModeSwitchProperties = { ulid, mode }
|
||||
TaskEvents.capture(service, "task.mode", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a tool is used during task execution
|
||||
*/
|
||||
static captureToolUsage(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
tool: string,
|
||||
modelId: string,
|
||||
autoApproved: boolean,
|
||||
success: boolean,
|
||||
): void {
|
||||
const properties: ToolUsageProperties = { ulid, tool, modelId, autoApproved, success }
|
||||
TaskEvents.capture(service, "task.tool_used", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when an MCP tool is called
|
||||
*/
|
||||
static captureMcpToolCall(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
serverName: string,
|
||||
toolName: string,
|
||||
status: "started" | "success" | "error",
|
||||
errorMessage?: string,
|
||||
argumentKeys?: string[],
|
||||
): void {
|
||||
const properties: McpToolCallProperties = { ulid, serverName, toolName, status, errorMessage, argumentKeys }
|
||||
TaskEvents.capture(service, "task.mcp_tool_called", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records interactions with the git-based checkpoint system
|
||||
*/
|
||||
static captureCheckpointUsage(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
action: "shadow_git_initialized" | "commit_created" | "restored" | "diff_generated",
|
||||
durationMs?: number,
|
||||
): void {
|
||||
const properties: CheckpointUsageProperties = { ulid, action, durationMs }
|
||||
TaskEvents.capture(service, "task.checkpoint_used", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a diff edit operation fails
|
||||
*/
|
||||
static captureDiffEditFailure(service: TelemetryService, ulid: string, modelId: string, errorType?: string): void {
|
||||
const properties: DiffEditFailureProperties = { ulid, modelId, errorType }
|
||||
TaskEvents.capture(service, "task.diff_edit_failed", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a user selects an option from AI-generated followup questions
|
||||
*/
|
||||
static captureOptionSelected(service: TelemetryService, ulid: string, qty: number, mode: Mode): void {
|
||||
const properties: OptionSelectedProperties = { ulid, qty, mode }
|
||||
TaskEvents.capture(service, "task.option_selected", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a user types a custom response instead of selecting an option
|
||||
*/
|
||||
static captureOptionsIgnored(service: TelemetryService, ulid: string, qty: number, mode: Mode): void {
|
||||
const properties: OptionSelectedProperties = { ulid, qty, mode }
|
||||
TaskEvents.capture(service, "task.options_ignored", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Captures Gemini API performance metrics
|
||||
*/
|
||||
static captureGeminiApiPerformance(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
modelId: string,
|
||||
data: {
|
||||
ttftSec?: number
|
||||
totalDurationSec?: number
|
||||
promptTokens: number
|
||||
outputTokens: number
|
||||
cacheReadTokens: number
|
||||
cacheHit: boolean
|
||||
cacheHitPercentage?: number
|
||||
apiSuccess: boolean
|
||||
apiError?: string
|
||||
throughputTokensPerSec?: number
|
||||
},
|
||||
): void {
|
||||
const properties: GeminiApiPerformanceProperties = { ulid, modelId, ...data }
|
||||
TaskEvents.capture(service, "task.gemini_api_performance", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when an API provider returns an error
|
||||
*/
|
||||
static captureProviderApiError(
|
||||
service: TelemetryService,
|
||||
args: {
|
||||
ulid: string
|
||||
model: string
|
||||
errorMessage: string
|
||||
provider?: string
|
||||
errorStatus?: number
|
||||
requestId?: string
|
||||
},
|
||||
): void {
|
||||
const properties: ProviderApiErrorProperties = {
|
||||
...args,
|
||||
errorMessage: args.errorMessage.substring(0, 500),
|
||||
timestamp: new Date().toISOString(),
|
||||
}
|
||||
TaskEvents.capture(service, "task.provider_api_error", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when context summarization is triggered
|
||||
*/
|
||||
static captureSummarizeTask(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
modelId: string,
|
||||
currentTokens: number,
|
||||
maxContextWindow: number,
|
||||
): void {
|
||||
const properties: SummarizeTaskProperties = { ulid, modelId, currentTokens, maxContextWindow }
|
||||
TaskEvents.capture(service, "task.summarize_task", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when slash commands or workflows are activated
|
||||
*/
|
||||
static captureSlashCommandUsed(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
commandName: string,
|
||||
commandType: "builtin" | "workflow",
|
||||
): void {
|
||||
const properties: SlashCommandUsedProperties = { ulid, commandName, commandType }
|
||||
TaskEvents.capture(service, "task.slash_command_used", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when individual Cline rules are toggled on/off
|
||||
*/
|
||||
static captureClineRuleToggled(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
ruleFileName: string,
|
||||
enabled: boolean,
|
||||
isGlobal: boolean,
|
||||
): void {
|
||||
const sanitizedFileName = ruleFileName.split("/").pop() || ruleFileName.split("\\").pop() || ruleFileName
|
||||
const properties: ClineRuleToggledProperties = { ulid, ruleFileName: sanitizedFileName, enabled, isGlobal }
|
||||
TaskEvents.capture(service, "task.rule_toggled", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when auto condense is enabled/disabled
|
||||
*/
|
||||
static captureAutoCondenseToggle(service: TelemetryService, ulid: string, enabled: boolean, modelId: string): void {
|
||||
const properties: AutoCondenseToggleProperties = { ulid, enabled, modelId }
|
||||
TaskEvents.capture(service, "task.auto_condense_toggled", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when yolo mode is enabled/disabled
|
||||
*/
|
||||
static captureYoloModeToggle(service: TelemetryService, ulid: string, enabled: boolean): void {
|
||||
const properties: YoloModeToggleProperties = { ulid, enabled }
|
||||
TaskEvents.capture(service, "task.yolo_mode_toggled", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records task initialization timing
|
||||
*/
|
||||
static captureTaskInitialization(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
taskId: string,
|
||||
durationMs: number,
|
||||
hasCheckpoints: boolean,
|
||||
): void {
|
||||
const properties: TaskInitializationProperties = { ulid, taskId, durationMs, hasCheckpoints }
|
||||
TaskEvents.capture(service, "task.initialization", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records terminal command execution outcomes
|
||||
*/
|
||||
static captureTerminalExecution(
|
||||
service: TelemetryService,
|
||||
success: boolean,
|
||||
method: "shell_integration" | "clipboard" | "none",
|
||||
): void {
|
||||
const properties: TerminalExecutionProperties = { success, method }
|
||||
TaskEvents.capture(service, "task.terminal_execution", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when terminal output capture fails
|
||||
*/
|
||||
static captureTerminalOutputFailure(service: TelemetryService, reason: string): void {
|
||||
const properties: TerminalOutputFailureProperties = { reason }
|
||||
TaskEvents.capture(service, "task.terminal_output_failure", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when user has to intervene with terminal execution
|
||||
*/
|
||||
static captureTerminalUserIntervention(service: TelemetryService, action: string): void {
|
||||
const properties: TerminalUserInterventionProperties = { action }
|
||||
TaskEvents.capture(service, "task.terminal_user_intervention", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when terminal execution hangs
|
||||
*/
|
||||
static captureTerminalHang(service: TelemetryService, stage: string): void {
|
||||
const properties: TerminalHangProperties = { stage }
|
||||
TaskEvents.capture(service, "task.terminal_hang", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when focus chain is enabled/disabled
|
||||
*/
|
||||
static captureFocusChainToggle(service: TelemetryService, enabled: boolean): void {
|
||||
const properties: FocusChainToggleProperties = { enabled }
|
||||
const event = enabled ? "task.focus_chain_enabled" : "task.focus_chain_disabled"
|
||||
TaskEvents.capture(service, event, properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a task progress list is returned for the first time
|
||||
*/
|
||||
static captureFocusChainProgressFirst(service: TelemetryService, ulid: string, totalItems: number): void {
|
||||
const properties: FocusChainProgressFirstProperties = { ulid, totalItems }
|
||||
TaskEvents.capture(service, "task.focus_chain_progress_first", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a task progress list is updated
|
||||
*/
|
||||
static captureFocusChainProgressUpdate(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
totalItems: number,
|
||||
completedItems: number,
|
||||
): void {
|
||||
const properties: FocusChainProgressUpdateProperties = {
|
||||
ulid,
|
||||
totalItems,
|
||||
completedItems,
|
||||
completionPercentage: totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0,
|
||||
}
|
||||
TaskEvents.capture(service, "task.focus_chain_progress_update", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a task ends but the task progress list is not complete
|
||||
*/
|
||||
static captureFocusChainIncompleteOnCompletion(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
totalItems: number,
|
||||
completedItems: number,
|
||||
incompleteItems: number,
|
||||
): void {
|
||||
const properties: FocusChainIncompleteOnCompletionProperties = {
|
||||
ulid,
|
||||
totalItems,
|
||||
completedItems,
|
||||
incompleteItems,
|
||||
completionPercentage: totalItems > 0 ? Math.round((completedItems / totalItems) * 100) : 0,
|
||||
}
|
||||
TaskEvents.capture(service, "task.focus_chain_incomplete_on_completion", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when users click to open the focus chain markdown file
|
||||
*/
|
||||
static captureFocusChainListOpened(service: TelemetryService, ulid: string): void {
|
||||
const properties: FocusChainListOpenedProperties = { ulid }
|
||||
TaskEvents.capture(service, "task.focus_chain_list_opened", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when users save and write to the focus chain markdown file
|
||||
*/
|
||||
static captureFocusChainListWritten(service: TelemetryService, ulid: string): void {
|
||||
const properties: FocusChainListWrittenProperties = { ulid }
|
||||
TaskEvents.capture(service, "task.focus_chain_list_written", properties)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import type { TelemetryProperties } from "../providers/ITelemetryProvider"
|
||||
import type { TelemetryService } from "../TelemetryService"
|
||||
import { EventHandlerBase } from "./EventHandlerBase"
|
||||
|
||||
/**
|
||||
* Property types for UI telemetry events
|
||||
*/
|
||||
|
||||
export interface ModelSelectedProperties extends TelemetryProperties {
|
||||
model: string
|
||||
provider: string
|
||||
ulid?: string
|
||||
}
|
||||
|
||||
export interface ModelFavoriteToggledProperties extends TelemetryProperties {
|
||||
model: string
|
||||
isFavorited: boolean
|
||||
}
|
||||
|
||||
export interface ButtonClickedProperties extends TelemetryProperties {
|
||||
button: string
|
||||
ulid?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for UI-related telemetry events
|
||||
*/
|
||||
export class UIEvents extends EventHandlerBase {
|
||||
static override readonly prefix = "ui"
|
||||
|
||||
/**
|
||||
* Records when a different model is selected for use
|
||||
* @param service The telemetry service instance
|
||||
* @param model Name of the selected model
|
||||
* @param provider Provider of the selected model
|
||||
* @param ulid Optional task identifier if model was selected during a task
|
||||
*/
|
||||
static captureModelSelected(service: TelemetryService, model: string, provider: string, ulid?: string): void {
|
||||
const properties: ModelSelectedProperties = {
|
||||
model,
|
||||
provider,
|
||||
ulid,
|
||||
}
|
||||
UIEvents.capture(service, "ui.model_selected", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the user uses the model favorite button in the model picker
|
||||
* @param service The telemetry service instance
|
||||
* @param model The name of the model the user has interacted with
|
||||
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
|
||||
*/
|
||||
static captureModelFavoritesUsage(service: TelemetryService, model: string, isFavorited: boolean): void {
|
||||
const properties: ModelFavoriteToggledProperties = {
|
||||
model,
|
||||
isFavorited,
|
||||
}
|
||||
UIEvents.capture(service, "ui.model_favorite_toggled", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when a button is clicked
|
||||
* @param service The telemetry service instance
|
||||
* @param button The button identifier
|
||||
* @param ulid Optional task identifier
|
||||
*/
|
||||
static captureButtonClick(service: TelemetryService, button: string, ulid?: string): void {
|
||||
const properties: ButtonClickedProperties = {
|
||||
button,
|
||||
ulid,
|
||||
}
|
||||
UIEvents.capture(service, "ui.button_clicked", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the rules menu button is clicked to open the rules/workflows modal
|
||||
* @param service The telemetry service instance
|
||||
*/
|
||||
static captureRulesMenuOpened(service: TelemetryService): void {
|
||||
UIEvents.capture(service, "ui.rules_menu_opened", {})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { TelemetryProperties } from "../providers/ITelemetryProvider"
|
||||
import type { TelemetryService } from "../TelemetryService"
|
||||
import { EventHandlerBase } from "./EventHandlerBase"
|
||||
|
||||
/**
|
||||
* Property types for user telemetry events
|
||||
*/
|
||||
|
||||
export interface UserOptOutProperties extends TelemetryProperties {}
|
||||
|
||||
export interface TelemetryEnabledProperties extends TelemetryProperties {}
|
||||
|
||||
export interface ExtensionActivatedProperties extends TelemetryProperties {}
|
||||
|
||||
/**
|
||||
* Event handler for user-related telemetry events
|
||||
*/
|
||||
export class UserEvents extends EventHandlerBase {
|
||||
static override readonly prefix = "user"
|
||||
|
||||
/**
|
||||
* Records when a user opts out of telemetry
|
||||
* @param service The telemetry service instance
|
||||
*/
|
||||
static captureUserOptOut(service: TelemetryService): void {
|
||||
const properties: UserOptOutProperties = {}
|
||||
UserEvents.captureRequired(service, "user.opt_out", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when telemetry is enabled
|
||||
* @param service The telemetry service instance
|
||||
*/
|
||||
static captureTelemetryEnabled(service: TelemetryService): void {
|
||||
const properties: TelemetryEnabledProperties = {}
|
||||
UserEvents.capture(service, "user.telemetry_enabled", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records when the extension is activated
|
||||
* @param service The telemetry service instance
|
||||
*/
|
||||
static captureExtensionActivated(service: TelemetryService): void {
|
||||
const properties: ExtensionActivatedProperties = {}
|
||||
UserEvents.capture(service, "user.extension_activated", properties)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type { TelemetryProperties } from "../providers/ITelemetryProvider"
|
||||
import type { TelemetryService } from "../TelemetryService"
|
||||
import { EventHandlerBase } from "./EventHandlerBase"
|
||||
|
||||
/**
|
||||
* Property types for workspace telemetry events
|
||||
*/
|
||||
|
||||
export interface WorkspaceInitializedProperties extends TelemetryProperties {
|
||||
root_count: number
|
||||
vcs_types: string[]
|
||||
is_multi_root: boolean
|
||||
has_git: boolean
|
||||
has_mercurial: boolean
|
||||
init_duration_ms?: number
|
||||
feature_flag_enabled?: boolean
|
||||
}
|
||||
|
||||
export interface WorkspaceInitErrorProperties extends TelemetryProperties {
|
||||
error_type: string
|
||||
error_message: string
|
||||
fallback_to_single_root: boolean
|
||||
workspace_count: number
|
||||
}
|
||||
|
||||
export interface MultiRootCheckpointProperties extends TelemetryProperties {
|
||||
ulid: string
|
||||
action: "initialized" | "committed" | "restored"
|
||||
root_count: number
|
||||
success_count: number
|
||||
failure_count: number
|
||||
success_rate: number
|
||||
duration_ms?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler for workspace-related telemetry events
|
||||
*/
|
||||
export class WorkspaceEvents extends EventHandlerBase {
|
||||
static override readonly prefix = "workspace"
|
||||
|
||||
/**
|
||||
* Records when workspace is initialized
|
||||
* @param service The telemetry service instance
|
||||
* @param rootCount Number of workspace roots
|
||||
* @param vcsTypes Array of VCS types detected
|
||||
* @param initDurationMs Time taken to initialize in milliseconds
|
||||
* @param featureFlagEnabled Whether multi-root feature flag is enabled
|
||||
*/
|
||||
static captureWorkspaceInitialized(
|
||||
service: TelemetryService,
|
||||
rootCount: number,
|
||||
vcsTypes: string[],
|
||||
initDurationMs?: number,
|
||||
featureFlagEnabled?: boolean,
|
||||
): void {
|
||||
const properties: WorkspaceInitializedProperties = {
|
||||
root_count: rootCount,
|
||||
vcs_types: vcsTypes,
|
||||
is_multi_root: rootCount > 1,
|
||||
has_git: vcsTypes.includes("Git"),
|
||||
has_mercurial: vcsTypes.includes("Mercurial"),
|
||||
init_duration_ms: initDurationMs,
|
||||
feature_flag_enabled: featureFlagEnabled,
|
||||
}
|
||||
WorkspaceEvents.capture(service, "workspace.initialized", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records workspace initialization errors
|
||||
* @param service The telemetry service instance
|
||||
* @param error The error that occurred
|
||||
* @param fallbackMode Whether system fell back to single-root mode
|
||||
* @param workspaceCount Number of workspace folders detected
|
||||
*/
|
||||
static captureWorkspaceInitError(
|
||||
service: TelemetryService,
|
||||
error: Error,
|
||||
fallbackMode: boolean,
|
||||
workspaceCount?: number,
|
||||
): void {
|
||||
const properties: WorkspaceInitErrorProperties = {
|
||||
error_type: error.constructor.name,
|
||||
error_message: error.message.substring(0, 500), // Truncate long error messages
|
||||
fallback_to_single_root: fallbackMode,
|
||||
workspace_count: workspaceCount ?? 0,
|
||||
}
|
||||
WorkspaceEvents.capture(service, "workspace.init_error", properties)
|
||||
}
|
||||
|
||||
/**
|
||||
* Records multi-root checkpoint operations
|
||||
* @param service The telemetry service instance
|
||||
* @param ulid Task identifier
|
||||
* @param action Type of checkpoint action
|
||||
* @param rootCount Number of roots being checkpointed
|
||||
* @param successCount Number of successful checkpoints
|
||||
* @param failureCount Number of failed checkpoints
|
||||
* @param durationMs Total operation duration in milliseconds
|
||||
*/
|
||||
static captureMultiRootCheckpoint(
|
||||
service: TelemetryService,
|
||||
ulid: string,
|
||||
action: "initialized" | "committed" | "restored",
|
||||
rootCount: number,
|
||||
successCount: number,
|
||||
failureCount: number,
|
||||
durationMs?: number,
|
||||
): void {
|
||||
const properties: MultiRootCheckpointProperties = {
|
||||
ulid,
|
||||
action,
|
||||
root_count: rootCount,
|
||||
success_count: successCount,
|
||||
failure_count: failureCount,
|
||||
success_rate: rootCount > 0 ? successCount / rootCount : 0,
|
||||
duration_ms: durationMs,
|
||||
}
|
||||
WorkspaceEvents.capture(service, "workspace.multi_root_checkpoint", properties)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Barrel export for all telemetry event handlers
|
||||
*/
|
||||
|
||||
export * from "./BrowserEvents"
|
||||
export * from "./DictationEvents"
|
||||
export * from "./EventHandlerBase"
|
||||
export * from "./TaskEvents"
|
||||
export * from "./UIEvents"
|
||||
export * from "./UserEvents"
|
||||
export * from "./WorkspaceEvents"
|
||||
@@ -2,7 +2,7 @@ export type {
|
||||
ITelemetryProvider,
|
||||
TelemetrySettings,
|
||||
} from "./providers/ITelemetryProvider"
|
||||
export { PostHogTelemetryProvider } from "./providers/PostHogTelemetryProvider"
|
||||
export { PostHogTelemetryProvider } from "./providers/posthog/PostHogTelemetryProvider"
|
||||
export {
|
||||
type TelemetryProviderConfig,
|
||||
TelemetryProviderFactory,
|
||||
|
||||
@@ -5,6 +5,32 @@
|
||||
|
||||
import type { ClineAccountUserInfo } from "../../auth/AuthService"
|
||||
|
||||
/**
|
||||
* JSON-serializable primitive types for telemetry properties
|
||||
*/
|
||||
export type TelemetryPrimitive = string | number | boolean | null | undefined
|
||||
|
||||
/**
|
||||
* JSON-serializable value types for telemetry properties
|
||||
* Ensures all telemetry data can be properly serialized
|
||||
*/
|
||||
export type TelemetryValue = TelemetryPrimitive | TelemetryObject | TelemetryArray
|
||||
|
||||
/**
|
||||
* JSON-serializable object for telemetry properties
|
||||
*/
|
||||
export type TelemetryObject = { [key: string]: TelemetryValue }
|
||||
|
||||
/**
|
||||
* JSON-serializable array for telemetry properties
|
||||
*/
|
||||
export type TelemetryArray = Array<TelemetryValue>
|
||||
|
||||
/**
|
||||
* Properties that can be safely passed to telemetry providers
|
||||
*/
|
||||
export type TelemetryProperties = TelemetryObject
|
||||
|
||||
/**
|
||||
* Telemetry settings that control when and how telemetry is collected
|
||||
*/
|
||||
@@ -25,16 +51,24 @@ export interface ITelemetryProvider {
|
||||
/**
|
||||
* Log an event with optional properties
|
||||
* @param event The event name to log
|
||||
* @param properties Optional properties to attach to the event
|
||||
* @param properties Optional JSON-serializable properties to attach to the event
|
||||
*/
|
||||
log(event: string, properties?: Record<string, unknown>): void
|
||||
log(event: string, properties?: TelemetryProperties): void
|
||||
|
||||
/**
|
||||
* Log a required event that bypasses telemetry opt-out settings
|
||||
* Required events are critical for system health and error monitoring
|
||||
* @param event The event name to log
|
||||
* @param properties Optional JSON-serializable properties to attach to the event
|
||||
*/
|
||||
logRequired(event: string, properties?: TelemetryProperties): void
|
||||
|
||||
/**
|
||||
* Identify a user for tracking
|
||||
* @param userInfo The user's information
|
||||
* @param properties Optional additional properties
|
||||
* @param properties Optional additional JSON-serializable properties
|
||||
*/
|
||||
identifyUser(userInfo: ClineAccountUserInfo, properties?: Record<string, unknown>): void
|
||||
identifyUser(userInfo: ClineAccountUserInfo, properties?: TelemetryProperties): void
|
||||
|
||||
/**
|
||||
* Update telemetry opt-in/out status
|
||||
|
||||
+2
-1
@@ -1,5 +1,5 @@
|
||||
import { EventMessage, PostHog } from "posthog-node"
|
||||
import { posthogConfig } from "../../shared/services/config/posthog-config"
|
||||
import { posthogConfig } from "@/shared/services/config/posthog-config"
|
||||
|
||||
export class PostHogClientProvider {
|
||||
private static _instance: PostHogClientProvider | null = null
|
||||
@@ -31,6 +31,7 @@ export class PostHogClientProvider {
|
||||
/**
|
||||
* Filters PostHog events before they are sent.
|
||||
* For exceptions, we only capture those from the Cline extension.
|
||||
* this is specifically to avoid capturing errors from anything other than Cline
|
||||
*/
|
||||
static eventFilter(event: EventMessage | null) {
|
||||
if (!event || event?.event !== "$exception") {
|
||||
+16
-5
@@ -3,9 +3,9 @@ import * as vscode from "vscode"
|
||||
import { HostProvider } from "@/hosts/host-provider"
|
||||
import { getDistinctId, setDistinctId } from "@/services/logging/distinctId"
|
||||
import { Setting } from "@/shared/proto/index.host"
|
||||
import { posthogConfig } from "../../../shared/services/config/posthog-config"
|
||||
import type { ClineAccountUserInfo } from "../../auth/AuthService"
|
||||
import type { ITelemetryProvider, TelemetrySettings } from "./ITelemetryProvider"
|
||||
import { posthogConfig } from "../../../../shared/services/config/posthog-config"
|
||||
import type { ClineAccountUserInfo } from "../../../auth/AuthService"
|
||||
import type { ITelemetryProvider, TelemetryProperties, TelemetrySettings } from "../ITelemetryProvider"
|
||||
/**
|
||||
* PostHog implementation of the telemetry provider interface
|
||||
* Handles PostHog-specific analytics tracking
|
||||
@@ -60,7 +60,7 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
return this
|
||||
}
|
||||
|
||||
public log(event: string, properties?: Record<string, unknown>): void {
|
||||
public log(event: string, properties?: TelemetryProperties): void {
|
||||
if (!this.isEnabled() || this.telemetrySettings.level === "off") {
|
||||
return
|
||||
}
|
||||
@@ -79,7 +79,18 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
})
|
||||
}
|
||||
|
||||
public identifyUser(userInfo: ClineAccountUserInfo, properties: Record<string, unknown> = {}): void {
|
||||
public logRequired(event: string, properties?: TelemetryProperties): void {
|
||||
this.client.capture({
|
||||
distinctId: getDistinctId(),
|
||||
event,
|
||||
properties: {
|
||||
...properties,
|
||||
_required: true, // Mark as required event
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
public identifyUser(userInfo: ClineAccountUserInfo, properties: TelemetryProperties = {}): void {
|
||||
const distinctId = getDistinctId()
|
||||
// Only identify user if telemetry is enabled and user ID is different than the currently set distinct ID
|
||||
if (this.isEnabled() && userInfo && userInfo?.id !== distinctId) {
|
||||
Reference in New Issue
Block a user