From 568932492b6dbd74380ae5d380c5b41aad100aa9 Mon Sep 17 00:00:00 2001 From: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com> Date: Tue, 25 Feb 2025 15:23:38 -0800 Subject: [PATCH] Add optional opt-in telemetry to help fix bugs and improve product --- docs/PRIVACY.md | 23 +++++- package-lock.json | 17 ++++- package.json | 6 ++ src/core/webview/ClineProvider.ts | 50 ++++++++++--- src/extension.ts | 2 + src/services/telemetry/TelemetryService.ts | 73 +++++++++++++++++++ src/shared/ExtensionMessage.ts | 1 + src/shared/WebviewMessage.ts | 2 + webview-ui/src/components/chat/ChatView.tsx | 5 +- .../src/components/common/TelemetryBanner.tsx | 70 ++++++++++++++++++ .../src/context/ExtensionStateContext.tsx | 1 + 11 files changed, 233 insertions(+), 17 deletions(-) create mode 100644 src/services/telemetry/TelemetryService.ts create mode 100644 webview-ui/src/components/common/TelemetryBanner.tsx diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md index 548948b359..d293dc814a 100644 --- a/docs/PRIVACY.md +++ b/docs/PRIVACY.md @@ -42,8 +42,8 @@ Cline functions solely as a client-side VS Code extension that facilitates commu 1. **Local-Only Processing**: - All operations happen on your local machine - - No central servers or data collection - - No telemetry or usage statistics gathered + - No central servers or data collection by default + - Anonymous telemetry and usage statistics are only collected if you explicitly opt in - No account creation required 2. **API Key Security**: @@ -73,7 +73,8 @@ When you request assistance: - Error logs are processed locally - No automatic error reporting to Cline -- You control what information to include when reporting issues + - Optional anonymous telemetry and error reporting via PostHog if you opt in +- You control what information to include when manually reporting issues ## Children's Privacy @@ -90,6 +91,22 @@ We will post any changes to this policy on our GitHub repository. Significant ch - You can inspect exactly what data is being sent to AI providers - Enterprise users can implement additional access controls through VS Code +## Telemetry & Usage Statistics + +If you choose to opt in to anonymous telemetry: + +- Basic usage statistics and error reports are collected via PostHog +- A stable, anonymous identifier (VS Code's `machineId`) is used to understand unique usage patterns + - This identifier is not linked to any personal information + - It helps us understand how features are used across sessions + - It cannot be used to identify you personally +- All data is anonymized and cannot be linked to individual users +- No code content or sensitive information is ever included +- You can opt out at any time through: + - VS Code Settings > Cline > Enable Telemetry + - VS Code Settings > Telemetry > Telemetry Level (setting this to anything other than "all" will disable Cline's telemetry) +- Collected data helps us improve the extension's functionality and stability + ## Contact Us For privacy-related questions or concerns: diff --git a/package-lock.json b/package-lock.json index 4048df5cbd..41c8953530 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-dev", - "version": "3.4.6", + "version": "3.4.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-dev", - "version": "3.4.6", + "version": "3.4.8", "license": "Apache-2.0", "dependencies": { "@anthropic-ai/bedrock-sdk": "^0.10.2", @@ -40,6 +40,7 @@ "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", + "posthog-node": "^4.7.0", "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "serialize-error": "^11.0.3", @@ -11606,6 +11607,18 @@ "node": ">= 0.4" } }, + "node_modules/posthog-node": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-4.7.0.tgz", + "integrity": "sha512-RgdUKSW8MfMOkjUa8cYVqWndNjPePNuuxlGbrZC6z1WRBsVc6TdGl8caidmC10RW8mu/BOfmrGbP4cRTo2jARg==", + "license": "MIT", + "dependencies": { + "axios": "^1.7.4" + }, + "engines": { + "node": ">=15.0.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/package.json b/package.json index 0951cf6c33..97f6ba9ef8 100644 --- a/package.json +++ b/package.json @@ -186,6 +186,11 @@ "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." } } } @@ -268,6 +273,7 @@ "os-name": "^6.0.0", "p-wait-for": "^5.0.2", "pdf-parse": "^1.1.1", + "posthog-node": "^4.7.0", "puppeteer-chromium-resolver": "^23.0.0", "puppeteer-core": "^23.4.0", "serialize-error": "^11.0.3", diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 7eb93c00c6..228a04230d 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -1,9 +1,9 @@ import { Anthropic } from "@anthropic-ai/sdk" import axios from "axios" -import fs from "fs/promises" -import os from "os" import crypto from "crypto" import { execa } from "execa" +import fs from "fs/promises" +import os from "os" import pWaitFor from "p-wait-for" import * as path from "path" import * as vscode from "vscode" @@ -13,26 +13,25 @@ import { openFile, openImage } from "../../integrations/misc/open-file" import { selectImages } from "../../integrations/misc/process-images" import { getTheme } from "../../integrations/theme/getTheme" import WorkspaceTracker from "../../integrations/workspace/WorkspaceTracker" -import { McpHub } from "../../services/mcp/McpHub" -import { McpDownloadResponse, McpMarketplaceCatalog, McpMarketplaceItem, McpServer } from "../../shared/mcp" import { FirebaseAuthManager, UserInfo } from "../../services/auth/FirebaseAuthManager" +import { McpHub } from "../../services/mcp/McpHub" import { ApiProvider, ModelInfo } from "../../shared/api" import { findLast } from "../../shared/array" +import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings" +import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings" +import { ChatContent } from "../../shared/ChatContent" +import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings" import { ExtensionMessage, ExtensionState, Platform } from "../../shared/ExtensionMessage" import { HistoryItem } from "../../shared/HistoryItem" +import { McpDownloadResponse, McpMarketplaceCatalog, McpServer } from "../../shared/mcp" import { ClineCheckpointRestore, WebviewMessage } from "../../shared/WebviewMessage" import { fileExistsAtPath } from "../../utils/fs" +import { searchCommits } from "../../utils/git" import { Cline } from "../Cline" import { openMention } from "../mentions" import { getNonce } from "./getNonce" import { getUri } from "./getUri" -import { AutoApprovalSettings, DEFAULT_AUTO_APPROVAL_SETTINGS } from "../../shared/AutoApprovalSettings" -import { BrowserSettings, DEFAULT_BROWSER_SETTINGS } from "../../shared/BrowserSettings" -import { ChatSettings, DEFAULT_CHAT_SETTINGS } from "../../shared/ChatSettings" -import { DIFF_VIEW_URI_SCHEME } from "../../integrations/editor/DiffViewProvider" -import { searchCommits } from "../../utils/git" -import { ChatContent } from "../../shared/ChatContent" -import { getShell } from "../../utils/shell" +import { telemetryService } from "../../services/telemetry/TelemetryService" /* https://github.com/microsoft/vscode-webview-ui-toolkit-samples/blob/main/default/weather-webview/src/providers/WeatherViewProvider.ts @@ -280,6 +279,16 @@ export class ClineProvider implements vscode.WebviewViewProvider { task, images, ) + + // New task started + if (telemetryService.isTelemetryEnabled()) { + telemetryService.capture({ + event: "New task started", + properties: { + apiProvider: apiConfiguration.apiProvider, + }, + }) + } } async initClineWithHistoryItem(historyItem: HistoryItem) { @@ -829,6 +838,21 @@ export class ClineProvider implements vscode.WebviewViewProvider { ) break } + // telemetry + case "openTelemetrySettings": { + await vscode.commands.executeCommand( + "workbench.action.openSettings", + "@ext:saoudrizwan.claude-dev cline.telemetryOptIn", + ) + break + } + case "telemetryOptIn": { + if (message.bool !== undefined) { + await vscode.workspace.getConfiguration("cline").update("enableTelemetry", message.bool, true) + await this.postStateToWebview() + } + break + } // Add more switch case statements here as more webview message commands // are created within the webview context (i.e. inside media/main.js) } @@ -1594,6 +1618,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont userInfo, authToken, mcpMarketplaceEnabled, + telemetryOptIn, } = await this.getState() return { @@ -1613,6 +1638,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont isLoggedIn: !!authToken, userInfo, mcpMarketplaceEnabled, + telemetryOptIn, } } @@ -1791,6 +1817,7 @@ 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: { @@ -1847,6 +1874,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont previousModeModelId, previousModeModelInfo, mcpMarketplaceEnabled, + telemetryOptIn, } } diff --git a/src/extension.ts b/src/extension.ts index 991f415a00..e485cd352d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -8,6 +8,7 @@ import { createClineAPI } from "./exports" import "./utils/path" // necessary to have access to String.prototype.toPosix import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider" import assert from "node:assert" +import { telemetryService } from "./services/telemetry/TelemetryService" /* Built using https://github.com/microsoft/vscode-webview-ui-toolkit @@ -189,6 +190,7 @@ export function activate(context: vscode.ExtensionContext) { // This method is called when your extension is deactivated export function deactivate() { + telemetryService.shutdown() Logger.log("Cline extension deactivated") } diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts new file mode 100644 index 0000000000..1dea301c36 --- /dev/null +++ b/src/services/telemetry/TelemetryService.ts @@ -0,0 +1,73 @@ +import { PostHog } from "posthog-node" +import * as vscode from "vscode" + +class PostHogClient { + private static instance: PostHogClient + private client: PostHog + private distinctId: string = vscode.env.machineId + private telemetryEnabled: boolean = false + + private constructor() { + this.client = new PostHog("phc_qfOAGxZw2TL5O8p9KYd9ak3bPBFzfjC8fy5L6jNWY7K", { + 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 { + // 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 + if (globalTelemetryEnabled) { + const clineOptIn = vscode.workspace.getConfiguration("cline").get("enableTelemetry", null) + this.telemetryEnabled = clineOptIn === true + } + + // Update PostHog client state based on telemetry preference + if (this.telemetryEnabled) { + this.client.optIn() + // console.log("Telemetry enabled") + } else { + this.client.optOut() + // console.log("Telemetry disabled") + } + } + + public static getInstance(): PostHogClient { + if (!PostHogClient.instance) { + PostHogClient.instance = new PostHogClient() + } + return PostHogClient.instance + } + + public capture(event: { event: string; properties?: any }): void { + // Only send events if telemetry is enabled + if (this.telemetryEnabled) { + this.client.capture({ distinctId: this.distinctId, event: event.event, properties: event.properties }) + // console.log("Captured event", { distinctId: this.distinctId, event: event.event, properties: event.properties }) + } + } + + public isTelemetryEnabled(): boolean { + return this.telemetryEnabled + } + + public async shutdown(): Promise { + await this.client.shutdown() + } +} + +// Export a single instance +export const telemetryService = PostHogClient.getInstance() diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index 136eb8ac0c..ecba81781d 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -81,6 +81,7 @@ export interface ExtensionState { photoURL: string | null } mcpMarketplaceEnabled?: boolean + telemetryOptIn: boolean | null } export interface ClineMessage { diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index e42cb5860b..62f154c06e 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -50,6 +50,8 @@ export interface WebviewMessage { | "searchCommits" | "showMcpView" | "fetchLatestMcpServersFromHub" + | "telemetryOptIn" + | "openTelemetrySettings" // | "relaunchChromeDebugMode" text?: string disabled?: boolean diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index a78ca0c2fa..a6c7f2dbfa 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -26,6 +26,7 @@ import BrowserSessionRow from "./BrowserSessionRow" import ChatRow from "./ChatRow" import ChatTextArea from "./ChatTextArea" import TaskHeader from "./TaskHeader" +import TelemetryBanner from "../common/TelemetryBanner" interface ChatViewProps { isHidden: boolean @@ -37,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 } = useExtensionState() + const { version, clineMessages: messages, taskHistory, apiConfiguration, telemetryOptIn } = 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) @@ -789,6 +790,8 @@ const ChatView = ({ isHidden, showAnnouncement, hideAnnouncement, showHistoryVie flexDirection: "column", paddingBottom: "10px", }}> + {telemetryOptIn === null && } + {showAnnouncement && }

What can I do for you?

diff --git a/webview-ui/src/components/common/TelemetryBanner.tsx b/webview-ui/src/components/common/TelemetryBanner.tsx new file mode 100644 index 0000000000..5f4455a379 --- /dev/null +++ b/webview-ui/src/components/common/TelemetryBanner.tsx @@ -0,0 +1,70 @@ +import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react" +import { memo, useState } from "react" +import styled from "styled-components" +import { vscode } from "../../utils/vscode" + +const BannerContainer = styled.div` + background-color: var(--vscode-banner-background); + padding: 12px 20px; + display: flex; + flex-direction: column; + gap: 10px; + flex-shrink: 0; +` + +const ButtonContainer = styled.div` + display: flex; + gap: 8px; + width: 100%; + + & > vscode-button { + flex: 1; + } +` + +const TelemetryBanner = () => { + const [hasChosen, setHasChosen] = useState(false) + + const handleAllow = () => { + setHasChosen(true) + vscode.postMessage({ type: "telemetryOptIn", bool: true }) + } + + const handleDeny = () => { + setHasChosen(true) + vscode.postMessage({ type: "telemetryOptIn", bool: false }) + } + + const handleOpenSettings = () => { + vscode.postMessage({ type: "openTelemetrySettings" }) + } + + return ( + +
+ Help Improve Cline +
+ Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or + personal information is ever sent. +
+ You can always change this in{" "} + + settings + + . +
+
+
+ + + Allow + + + Deny + + +
+ ) +} + +export default memo(TelemetryBanner) diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 0b428ef79d..3ad7de6461 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -39,6 +39,7 @@ export const ExtensionStateContextProvider: React.FC<{ chatSettings: DEFAULT_CHAT_SETTINGS, isLoggedIn: false, platform: DEFAULT_PLATFORM, + telemetryOptIn: null, }) const [didHydrateState, setDidHydrateState] = useState(false) const [showWelcome, setShowWelcome] = useState(false)