diff --git a/package.json b/package.json index eb67881cc7..7d62bcb74d 100644 --- a/package.json +++ b/package.json @@ -186,11 +186,6 @@ "type": "boolean", "default": true, "description": "Controls whether the MCP Marketplace is enabled." - }, - "cline.enableTelemetry": { - "type": "boolean", - "default": null, - "markdownDescription": "Allow anonymous usage and error reporting to help improve Cline. No code, prompts, or personal information is ever sent. See our [privacy policy](https://github.com/cline/cline/blob/main/docs/PRIVACY.md) for details." } } } diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 8483d0cd8c..a042e92363 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -32,6 +32,7 @@ import { openMention } from "../mentions" import { getNonce } from "./getNonce" import { getUri } from "./getUri" import { telemetryService } from "../../services/telemetry/TelemetryService" +import { TelemetrySetting } from "../../shared/TelemetrySetting" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -93,6 +94,7 @@ type GlobalStateKey = | "requestyModelId" | "togetherModelId" | "mcpMarketplaceCatalog" + | "telemetrySetting" export const GlobalFileNames = { apiConversationHistory: "api_conversation_history.json", @@ -454,6 +456,13 @@ export class ClineProvider implements vscode.WebviewViewProvider { } }) + // If user already opted in to telemetry, enable telemetry service + this.getStateToPostToWebview().then((state) => { + const { telemetrySetting } = state + const isOptedIn = telemetrySetting === "enabled" + telemetryService.updateTelemetryState(isOptedIn) + }) + break case "newTask": // Code that should run in response to the hello message command @@ -849,18 +858,19 @@ export class ClineProvider implements vscode.WebviewViewProvider { break } // telemetry - case "openTelemetrySettings": { - await vscode.commands.executeCommand( - "workbench.action.openSettings", - "@ext:saoudrizwan.claude-dev cline.telemetryOptIn", - ) + case "openSettings": { + await this.postMessageToWebview({ + type: "action", + action: "settingsButtonClicked", + }) break } - case "telemetryOptIn": { - if (message.bool !== undefined) { - await vscode.workspace.getConfiguration("cline").update("enableTelemetry", message.bool, true) - await this.postStateToWebview() - } + case "telemetrySetting": { + const telemetrySetting = message.text as TelemetrySetting + await this.updateGlobalState("telemetrySetting", telemetrySetting) + const isOptedIn = telemetrySetting === "enabled" + telemetryService.updateTelemetryState(isOptedIn) + await this.postStateToWebview() break } // Add more switch case statements here as more webview message commands @@ -1628,7 +1638,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont userInfo, authToken, mcpMarketplaceEnabled, - telemetryOptIn, + telemetrySetting, } = await this.getState() return { @@ -1648,7 +1658,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont isLoggedIn: !!authToken, userInfo, mcpMarketplaceEnabled, - telemetryOptIn, + telemetrySetting, } } @@ -1755,6 +1765,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont previousModeModelInfo, qwenApiLine, liteLlmApiKey, + telemetrySetting, ] = await Promise.all([ this.getGlobalState("apiProvider") as Promise, this.getGlobalState("apiModelId") as Promise, @@ -1806,6 +1817,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont this.getGlobalState("previousModeModelInfo") as Promise, this.getGlobalState("qwenApiLine") as Promise, this.getSecret("liteLlmApiKey") as Promise, + this.getGlobalState("telemetrySetting") as Promise, ]) let apiProvider: ApiProvider @@ -1827,7 +1839,6 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont .get("reasoningEffort", "medium") const mcpMarketplaceEnabled = vscode.workspace.getConfiguration("cline").get("mcpMarketplace.enabled", true) - const telemetryOptIn = vscode.workspace.getConfiguration("cline").get("enableTelemetry", null) return { apiConfiguration: { @@ -1884,7 +1895,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont previousModeModelId, previousModeModelInfo, mcpMarketplaceEnabled, - telemetryOptIn, + telemetrySetting: telemetrySetting || "unset", } } diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts index 29fcaec6a8..19dcf0829b 100644 --- a/src/services/telemetry/TelemetryService.ts +++ b/src/services/telemetry/TelemetryService.ts @@ -12,29 +12,18 @@ class PostHogClient { host: "https://us.i.posthog.com", enableExceptionAutocapture: false, }) - - // Initialize telemetry state based on user settings - this.updateTelemetryState() - - // Listen for settings changes - vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration("cline.enableTelemetry") || e.affectsConfiguration("telemetry.telemetryLevel")) { - this.updateTelemetryState() - } - }) } - private updateTelemetryState(): void { + public updateTelemetryState(didUserOptIn: boolean): void { this.telemetryEnabled = false // First check global telemetry level - telemetry should only be enabled when level is "all" const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get("telemetryLevel", "all") const globalTelemetryEnabled = telemetryLevel === "all" - // Only check Cline setting if global telemetry is enabled + // We only enable telemetry if global vscode telemetry is enabled if (globalTelemetryEnabled) { - const clineOptIn = vscode.workspace.getConfiguration("cline").get("enableTelemetry", null) - this.telemetryEnabled = clineOptIn === true + this.telemetryEnabled = didUserOptIn } // Update PostHog client state based on telemetry preference diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index ecba81781d..7ee199fffb 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -7,6 +7,7 @@ import { BrowserSettings } from "./BrowserSettings" import { ChatSettings } from "./ChatSettings" import { HistoryItem } from "./HistoryItem" import { McpServer, McpMarketplaceCatalog, McpMarketplaceItem, McpDownloadResponse } from "./mcp" +import { TelemetrySetting } from "./TelemetrySetting" // webview will hold state export interface ExtensionMessage { @@ -81,7 +82,7 @@ export interface ExtensionState { photoURL: string | null } mcpMarketplaceEnabled?: boolean - telemetryOptIn: boolean | null + telemetrySetting: TelemetrySetting } export interface ClineMessage { diff --git a/src/shared/TelemetrySetting.ts b/src/shared/TelemetrySetting.ts new file mode 100644 index 0000000000..61444b5a09 --- /dev/null +++ b/src/shared/TelemetrySetting.ts @@ -0,0 +1 @@ +export type TelemetrySetting = "unset" | "enabled" | "disabled" diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 62f154c06e..cd4e877604 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -50,8 +50,8 @@ export interface WebviewMessage { | "searchCommits" | "showMcpView" | "fetchLatestMcpServersFromHub" - | "telemetryOptIn" - | "openTelemetrySettings" + | "telemetrySetting" + | "openSettings" // | "relaunchChromeDebugMode" text?: string disabled?: boolean diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index a6c7f2dbfa..afe84601ed 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -38,7 +38,7 @@ interface ChatViewProps { export const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryView }: ChatViewProps) => { - const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetryOptIn } = useExtensionState() + const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetrySetting } = useExtensionState() //const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined const task = useMemo(() => messages.at(0), [messages]) // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see Cline.abort) @@ -790,7 +790,7 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flexDirection: "column", paddingBottom: "10px", }}> - {telemetryOptIn === null && } + {telemetrySetting === "unset" && } {showAnnouncement && }
diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx index 5f4455a379..8ddc749cad 100644 --- a/webview-ui/src/components/common/TelemetryBanner.tsx +++ b/webview-ui/src/components/common/TelemetryBanner.tsx @@ -2,6 +2,7 @@ import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" import { memo, useState } from "react" import styled from "styled-components" import { vscode } from "../../utils/vscode" +import { TelemetrySetting } from "../../../../src/shared/TelemetrySetting" const BannerContainer = styled.div` background-color: var(--vscode-banner-background); @@ -10,6 +11,7 @@ const BannerContainer = styled.div` flex-direction: column; gap: 10px; flex-shrink: 0; + margin-bottom: 6px; ` const ButtonContainer = styled.div` @@ -27,16 +29,16 @@ const TelemetryBanner = () => { const handleAllow = () => { setHasChosen(true) - vscode.postMessage({ type: "telemetryOptIn", bool: true }) + vscode.postMessage({ type: "telemetrySetting", text: "enabled" satisfies TelemetrySetting }) } const handleDeny = () => { setHasChosen(true) - vscode.postMessage({ type: "telemetryOptIn", bool: false }) + vscode.postMessage({ type: "telemetrySetting", text: "disabled" satisfies TelemetrySetting }) } const handleOpenSettings = () => { - vscode.postMessage({ type: "openTelemetrySettings" }) + vscode.postMessage({ type: "openSettings" }) } return ( diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 994aab161f..144a598fd0 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -1,10 +1,10 @@ -import { VSCodeButton, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" +import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextArea } from "@vscode/webview-ui-toolkit/react" import { memo, useEffect, useState } from "react" import { useExtensionState } from "../../context/ExtensionStateContext" import { validateApiConfiguration, validateModelId } from "../../utils/validate" import { vscode } from "../../utils/vscode" -import ApiOptions from "./ApiOptions" import SettingsButton from "../common/SettingsButton" +import ApiOptions from "./ApiOptions" const { IS_DEV } = process.env type SettingsViewProps = { @@ -12,7 +12,15 @@ type SettingsViewProps = { } const SettingsView = ({ onDone }: SettingsViewProps) => { - const { apiConfiguration, version, customInstructions, setCustomInstructions, openRouterModels } = useExtensionState() + const { + apiConfiguration, + version, + customInstructions, + setCustomInstructions, + openRouterModels, + telemetrySetting, + setTelemetrySetting, + } = useExtensionState() const [apiErrorMessage, setApiErrorMessage] = useState(undefined) const [modelIdErrorMessage, setModelIdErrorMessage] = useState(undefined) @@ -29,6 +37,10 @@ const SettingsView = ({ onDone }: SettingsViewProps) => { type: "customInstructions", text: customInstructions, }) + vscode.postMessage({ + type: "telemetrySetting", + text: telemetrySetting, + }) onDone() } } @@ -114,6 +126,33 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {

+
+ { + const checked = e.target.checked === true + setTelemetrySetting(checked ? "enabled" : "disabled") + }}> + Allow anonymous error and usage reporting + +

+ Help improve Cline by sending anonymous usage data and error reports. No code, prompts, or personal + information is ever sent. See our{" "} + + privacy policy + {" "} + for more details. +

+
+ {IS_DEV && ( <>
Debug
diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 3ad7de6461..f3d0619183 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -9,6 +9,7 @@ import { convertTextMateToHljs } from "../utils/textMateToHljs" import { vscode } from "../utils/vscode" import { DEFAULT_BROWSER_SETTINGS } from "../../../src/shared/BrowserSettings" import { DEFAULT_CHAT_SETTINGS } from "../../../src/shared/ChatSettings" +import { TelemetrySetting } from "../../../src/shared/TelemetrySetting" interface ExtensionStateContextType extends ExtensionState { didHydrateState: boolean @@ -21,6 +22,7 @@ interface ExtensionStateContextType extends ExtensionState { filePaths: string[] setApiConfiguration: (config: ApiConfiguration) => void setCustomInstructions: (value?: string) => void + setTelemetrySetting: (value: TelemetrySetting) => void setShowAnnouncement: (value: boolean) => void } @@ -39,7 +41,7 @@ export const ExtensionStateContextProvider: React.FC<{ chatSettings: DEFAULT_CHAT_SETTINGS, isLoggedIn: false, platform: DEFAULT_PLATFORM, - telemetryOptIn: null, + telemetrySetting: "unset", }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false) @@ -158,6 +160,11 @@ export const ExtensionStateContextProvider: React.FC<{ ...prevState, customInstructions: value, })), + setTelemetrySetting: (value) => + setState((prevState) => ({ + ...prevState, + telemetrySetting: value, + })), setShowAnnouncement: (value) => setState((prevState) => ({ ...prevState,