mirror of
https://github.com/cline/cline.git
synced 2026-09-13 09:50:12 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a645c6a981 | ||
|
|
f873b6194a |
@@ -345,23 +345,7 @@ export class Controller {
|
||||
}
|
||||
|
||||
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
|
||||
// Get previous setting to detect state changes
|
||||
const previousSetting = this.stateManager.getGlobalSettingsKey("telemetrySetting")
|
||||
const wasOptedIn = previousSetting !== "disabled"
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
|
||||
// Capture opt-out event BEFORE updating (so it gets sent while telemetry is still enabled)
|
||||
if (wasOptedIn && !isOptedIn) {
|
||||
telemetryService.captureUserOptOut()
|
||||
}
|
||||
|
||||
this.stateManager.setGlobalState("telemetrySetting", telemetrySetting)
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
|
||||
// Capture opt-in event AFTER updating (so telemetry is enabled to receive it)
|
||||
if (!wasOptedIn && isOptedIn) {
|
||||
telemetryService.captureUserOptIn()
|
||||
}
|
||||
await telemetryService.updateTelemetryState(telemetrySetting)
|
||||
|
||||
await this.postStateToWebview()
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Empty, EmptyRequest } from "@shared/proto/cline/common"
|
||||
import { OpenRouterCompatibleModelInfo } from "@shared/proto/cline/models"
|
||||
import { readMcpMarketplaceCatalogFromCache } from "@/core/storage/disk"
|
||||
import { telemetryService } from "@/services/telemetry"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { GlobalStateAndSettings } from "@/shared/storage/state-keys"
|
||||
import type { Controller } from "../index"
|
||||
@@ -217,13 +216,6 @@ export async function initializeWebview(controller: Controller, _request: EmptyR
|
||||
// Silently refresh MCP marketplace catalog
|
||||
controller.refreshMcpMarketplace(true /* sendCatalogEvent */)
|
||||
|
||||
// Initialize telemetry service with user's current setting
|
||||
controller.getStateToPostToWebview().then((state) => {
|
||||
const { telemetrySetting } = state
|
||||
const isOptedIn = telemetrySetting !== "disabled"
|
||||
telemetryService.updateTelemetryState(isOptedIn)
|
||||
})
|
||||
|
||||
return Empty.create({})
|
||||
} catch (error) {
|
||||
Logger.error("Failed to initialize webview:", error)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ClineEndpoint } from "@/config"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import {
|
||||
getValidOpenTelemetryConfig,
|
||||
getValidRuntimeOpenTelemetryConfig,
|
||||
@@ -146,16 +147,34 @@ export class NoOpTelemetryProvider implements ITelemetryProvider {
|
||||
private isOptIn = true
|
||||
|
||||
log(_event: string, _properties?: TelemetryProperties): void {
|
||||
Logger.log(`[NoOpTelemetryProvider] ${_event}: ${JSON.stringify(_properties)}`)
|
||||
if (this.isEnabled()) {
|
||||
Logger.log(`[NoOpTelemetryProvider] ${_event}: ${JSON.stringify(_properties)}`)
|
||||
}
|
||||
}
|
||||
logRequired(_event: string, _properties?: TelemetryProperties): void {
|
||||
Logger.log(`[NoOpTelemetryProvider] REQUIRED ${_event}: ${JSON.stringify(_properties)}`)
|
||||
}
|
||||
identifyUser(_userInfo: any, _properties?: TelemetryProperties): void {
|
||||
Logger.info(`[NoOpTelemetryProvider] identifyUser - ${JSON.stringify(_userInfo)} - ${JSON.stringify(_properties)}`)
|
||||
if (this.isEnabled()) {
|
||||
Logger.info(`[NoOpTelemetryProvider] identifyUser - ${JSON.stringify(_userInfo)} - ${JSON.stringify(_properties)}`)
|
||||
}
|
||||
}
|
||||
isEnabled(): boolean {
|
||||
return false
|
||||
const isOptedIn = StateManager.get().getGlobalSettingsKey("telemetrySetting") !== "disabled"
|
||||
const wasOptedIn = this.isOptIn
|
||||
try {
|
||||
if (isOptedIn && !wasOptedIn) {
|
||||
Logger.info("User opted in to PostHog telemetry")
|
||||
}
|
||||
if (!isOptedIn && wasOptedIn) {
|
||||
Logger.info("User opted out to PostHog telemetry")
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error("Failed to update the PostHog telemetry state", err)
|
||||
}
|
||||
this.isOptIn = isOptedIn
|
||||
|
||||
return isOptedIn
|
||||
}
|
||||
getSettings(): TelemetrySettings {
|
||||
return {
|
||||
|
||||
@@ -176,8 +176,16 @@ describe("Telemetry system is abstracted and can easily switch between providers
|
||||
const isEnabled = posthogProvider.isEnabled()
|
||||
const settings = posthogProvider.getSettings()
|
||||
|
||||
console.log("PostHog Provider enabled:", isEnabled)
|
||||
console.log("PostHog Provider settings:", settings)
|
||||
// Check that PostHog provider is enabled based on configuration
|
||||
assert.strictEqual(isEnabled, true, "PostHog provider should be enabled based on configuration")
|
||||
assert.deepStrictEqual(
|
||||
settings,
|
||||
{
|
||||
hostEnabled: true,
|
||||
level: "info",
|
||||
},
|
||||
"PostHog provider should return correct settings",
|
||||
)
|
||||
|
||||
await posthogProvider.dispose()
|
||||
})
|
||||
|
||||
@@ -3,10 +3,12 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
|
||||
import { ShowMessageType } from "@shared/proto/host/window"
|
||||
import type { TaskFeedbackType } from "@shared/WebviewMessage"
|
||||
import * as os from "os"
|
||||
import { StateManager } from "@/core/storage/StateManager"
|
||||
import { ClineAccountUserInfo } from "@/services/auth/AuthService"
|
||||
import { Setting } from "@/shared/proto/index.host"
|
||||
import { Logger } from "@/shared/services/Logger"
|
||||
import { Mode } from "@/shared/storage/types"
|
||||
import { TelemetrySetting } from "@/shared/TelemetrySetting"
|
||||
import { version as extensionVersion } from "../../../package.json"
|
||||
import { setDistinctId } from "../logging/distinctId"
|
||||
import type { ITelemetryProvider, TelemetryProperties } from "./providers/ITelemetryProvider"
|
||||
@@ -365,35 +367,53 @@ export class TelemetryService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the telemetry state based on user preferences and VSCode settings
|
||||
* Updates the telemetry state based on user preferences and client settings
|
||||
* Only enables telemetry if both VSCode global telemetry is enabled and user has opted in
|
||||
* @param didUserOptIn Whether the user has explicitly opted into telemetry
|
||||
*/
|
||||
public async updateTelemetryState(didUserOptIn: boolean): Promise<void> {
|
||||
public async updateTelemetryState(telemetrySetting: TelemetrySetting): Promise<void> {
|
||||
// First check global telemetry level - telemetry should only be enabled when level is "all"
|
||||
|
||||
// We only enable telemetry if global host telemetry is enabled
|
||||
const hostSetting = await HostProvider.env.getTelemetrySettings({})
|
||||
if (hostSetting.isEnabled === Setting.DISABLED) {
|
||||
// Only show warning if user has opted in to Cline telemetry but host telemetry is disabled
|
||||
if (didUserOptIn) {
|
||||
void HostProvider.window
|
||||
.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message:
|
||||
"Anonymous Cline error and usage reporting is enabled, but IDE telemetry is disabled. To enable error and usage reporting for this extension, enable telemetry in IDE settings.",
|
||||
options: {
|
||||
items: ["Open Settings"],
|
||||
},
|
||||
})
|
||||
.then((response: { selectedOption?: string }) => {
|
||||
if (response.selectedOption === "Open Settings") {
|
||||
void HostProvider.window.openSettings({
|
||||
query: "telemetry.telemetryLevel",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const isHostTelemetryDisabled = hostSetting.isEnabled === Setting.DISABLED
|
||||
const stateManager = StateManager.get()
|
||||
|
||||
// Get previous setting to detect state changes
|
||||
const previousSetting = stateManager.getGlobalSettingsKey("telemetrySetting")
|
||||
const wasOptedIn = previousSetting !== "disabled"
|
||||
const didUserOptIn = telemetrySetting !== "disabled"
|
||||
|
||||
// Capture opt-in/out events based on changes
|
||||
if (wasOptedIn && !didUserOptIn) {
|
||||
this.captureUserOptOut()
|
||||
} else if (!wasOptedIn && didUserOptIn) {
|
||||
this.captureUserOptIn()
|
||||
}
|
||||
|
||||
// Update storage only if setting has changed
|
||||
if (previousSetting !== telemetrySetting) {
|
||||
stateManager.setGlobalState("telemetrySetting", telemetrySetting)
|
||||
}
|
||||
|
||||
// Only show warning if user has opted in to Cline telemetry but host telemetry is disabled
|
||||
if (isHostTelemetryDisabled && didUserOptIn) {
|
||||
void HostProvider.window
|
||||
.showMessage({
|
||||
type: ShowMessageType.WARNING,
|
||||
message:
|
||||
"Anonymous Cline error and usage reporting is enabled, but IDE telemetry is disabled. To enable error and usage reporting for this extension, enable telemetry in IDE settings.",
|
||||
options: {
|
||||
items: ["Open Settings"],
|
||||
},
|
||||
})
|
||||
.then((response: { selectedOption?: string }) => {
|
||||
if (response.selectedOption === "Open Settings") {
|
||||
void HostProvider.window.openSettings({
|
||||
query: "telemetry.telemetryLevel",
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -404,6 +424,9 @@ export class TelemetryService {
|
||||
*/
|
||||
public captureUserOptOut(): void {
|
||||
this.captureRequired(TelemetryService.EVENTS.USER.OPT_OUT, {})
|
||||
this.providers.forEach((provider) => {
|
||||
provider.setOptIn(false)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -411,7 +434,10 @@ export class TelemetryService {
|
||||
* Should only be called on explicit user action, not on init/sync.
|
||||
*/
|
||||
public captureUserOptIn(): void {
|
||||
this.capture({ event: TelemetryService.EVENTS.USER.OPT_IN })
|
||||
this.captureRequired(TelemetryService.EVENTS.USER.OPT_IN, {})
|
||||
this.providers.forEach(async (provider) => {
|
||||
provider.setOptIn(true)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -73,6 +73,8 @@ export interface ITelemetryProvider {
|
||||
*/
|
||||
isEnabled(): boolean
|
||||
|
||||
setOptIn(optIn: boolean): Promise<void>
|
||||
|
||||
/**
|
||||
* Get current telemetry settings
|
||||
*/
|
||||
|
||||
@@ -19,6 +19,7 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
private meter: Meter | null = null
|
||||
private logger: OTELLogger | null = null
|
||||
private telemetrySettings: TelemetrySettings
|
||||
private optInCache: boolean
|
||||
private userAttributes: Record<string, string> = {}
|
||||
// Lazy instrument caches for metrics
|
||||
private counters = new Map<string, ReturnType<Meter["createCounter"]>>()
|
||||
@@ -43,6 +44,8 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
level: "all",
|
||||
}
|
||||
|
||||
this.optInCache = StateManager.get().getGlobalSettingsKey("telemetrySetting") !== "disabled"
|
||||
|
||||
if (meterProvider) {
|
||||
this.meter = meterProvider.getMeter("cline")
|
||||
}
|
||||
@@ -71,17 +74,16 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
onResponse: (event: { isEnabled: Setting }) => {
|
||||
const hostEnabled = event.isEnabled === Setting.ENABLED || event.isEnabled === Setting.UNSUPPORTED
|
||||
this.telemetrySettings.hostEnabled = hostEnabled
|
||||
this.telemetrySettings.level = hostEnabled ? this.telemetrySettings.level : "off"
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Check host-specific telemetry setting (e.g. VS Code setting)
|
||||
const hostSettings = await HostProvider.env.getTelemetrySettings({})
|
||||
if (hostSettings.isEnabled === Setting.DISABLED) {
|
||||
this.telemetrySettings.hostEnabled = false
|
||||
}
|
||||
this.telemetrySettings.hostEnabled = hostSettings.isEnabled !== Setting.DISABLED
|
||||
this.telemetrySettings.level = getErrorLevelFromString(hostSettings.errorLevel)
|
||||
|
||||
this.telemetrySettings.level = await this.getTelemetryLevel()
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -166,10 +168,18 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
return (
|
||||
this.bypassUserSettings ||
|
||||
(StateManager.get().getGlobalSettingsKey("telemetrySetting") !== "disabled" && this.telemetrySettings.hostEnabled)
|
||||
)
|
||||
if (this.telemetrySettings.hostEnabled === false || this.telemetrySettings.level === "off") {
|
||||
return false
|
||||
}
|
||||
|
||||
return this.bypassUserSettings || this.optInCache
|
||||
}
|
||||
|
||||
public async setOptIn(optIn: boolean): Promise<void> {
|
||||
this.optInCache = optIn
|
||||
// OpenTelemetry does not have built-in opt-in/out handling.
|
||||
// This method is a no-op to satisfy the interface.
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
public getSettings(): TelemetrySettings {
|
||||
@@ -305,21 +315,6 @@ export class OpenTelemetryTelemetryProvider implements ITelemetryProvider {
|
||||
// Individual providers don't need to do anything
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current telemetry level from VS Code settings
|
||||
*/
|
||||
private async getTelemetryLevel(): Promise<TelemetrySettings["level"]> {
|
||||
if (this.bypassUserSettings) {
|
||||
return "all"
|
||||
}
|
||||
|
||||
const hostSettings = await HostProvider.env.getTelemetrySettings({})
|
||||
if (hostSettings.isEnabled === Setting.DISABLED) {
|
||||
return "off"
|
||||
}
|
||||
return getErrorLevelFromString(hostSettings.errorLevel)
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten nested properties into dot-notation strings for OpenTelemetry attributes.
|
||||
* OpenTelemetry attributes must be primitives (string, number, boolean).
|
||||
|
||||
@@ -16,7 +16,6 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
private client: PostHog
|
||||
private telemetrySettings: TelemetrySettings
|
||||
private isSharedClient: boolean
|
||||
private optInCache: boolean
|
||||
|
||||
readonly name = "PostHogTelemetryProvider"
|
||||
|
||||
@@ -33,11 +32,11 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
}
|
||||
this.client = new PostHog(posthogConfig.apiKey, {
|
||||
host: posthogConfig.host,
|
||||
defaultOptIn: StateManager.get().getGlobalSettingsKey("telemetrySetting") !== "disabled",
|
||||
})
|
||||
}
|
||||
|
||||
// Initialize telemetry settings
|
||||
this.optInCache = true
|
||||
this.telemetrySettings = {
|
||||
hostEnabled: true,
|
||||
level: "all",
|
||||
@@ -51,17 +50,16 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
onResponse: (event: { isEnabled: Setting }) => {
|
||||
const hostEnabled = event.isEnabled === Setting.ENABLED || event.isEnabled === Setting.UNSUPPORTED
|
||||
this.telemetrySettings.hostEnabled = hostEnabled
|
||||
this.telemetrySettings.level = hostEnabled ? this.telemetrySettings.level : "off"
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Check host-specific telemetry setting (e.g. VS Code setting)
|
||||
const hostSettings = await HostProvider.env.getTelemetrySettings({})
|
||||
if (hostSettings.isEnabled === Setting.DISABLED) {
|
||||
this.telemetrySettings.hostEnabled = false
|
||||
}
|
||||
this.telemetrySettings.hostEnabled = hostSettings.isEnabled !== Setting.DISABLED
|
||||
this.telemetrySettings.level = getErrorLevelFromString(hostSettings.errorLevel)
|
||||
|
||||
this.telemetrySettings.level = await this.getTelemetryLevel()
|
||||
return this
|
||||
}
|
||||
|
||||
@@ -113,22 +111,24 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public isEnabled(): boolean {
|
||||
const isOptedIn = StateManager.get().getGlobalSettingsKey("telemetrySetting") !== "disabled"
|
||||
const wasOptedIn = this.optInCache
|
||||
public async setOptIn(optIn: boolean): Promise<void> {
|
||||
try {
|
||||
if (isOptedIn && !wasOptedIn) {
|
||||
this.client.optIn()
|
||||
const wasOptedIn = !this.client.optedOut
|
||||
if (wasOptedIn && !optIn) {
|
||||
await this.client.optIn()
|
||||
Logger.info("[PostHogTelemetryProvider] Client opted in")
|
||||
}
|
||||
if (!isOptedIn && wasOptedIn) {
|
||||
this.client.optOut()
|
||||
if (!wasOptedIn && optIn) {
|
||||
await this.client.optOut()
|
||||
Logger.info("[PostHogTelemetryProvider] Client opted out")
|
||||
}
|
||||
} catch (err) {
|
||||
Logger.error("Failed to update the PostHog telemetry state", err)
|
||||
}
|
||||
this.optInCache = isOptedIn
|
||||
}
|
||||
|
||||
return isOptedIn && this.telemetrySettings.hostEnabled
|
||||
public isEnabled(): boolean {
|
||||
return !this.client.optedOut && this.telemetrySettings.hostEnabled && this.telemetrySettings.level !== "off"
|
||||
}
|
||||
|
||||
public getSettings(): TelemetrySettings {
|
||||
@@ -141,8 +141,8 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
*/
|
||||
public recordCounter(
|
||||
name: string,
|
||||
value: number,
|
||||
attributes?: TelemetryProperties,
|
||||
_value: number,
|
||||
_attributes?: TelemetryProperties,
|
||||
_description?: string,
|
||||
required = false,
|
||||
): void {
|
||||
@@ -205,15 +205,4 @@ export class PostHogTelemetryProvider implements ITelemetryProvider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current telemetry level from VS Code settings
|
||||
*/
|
||||
private async getTelemetryLevel(): Promise<TelemetrySettings["level"]> {
|
||||
const hostSettings = await HostProvider.env.getTelemetrySettings({})
|
||||
if (hostSettings.isEnabled === Setting.DISABLED) {
|
||||
return "off"
|
||||
}
|
||||
return getErrorLevelFromString(hostSettings.errorLevel)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user