Add optional opt-in telemetry to help fix bugs and improve product

This commit is contained in:
Saoud Rizwan 2025-02-25 15:23:38 -08:00
parent 35a64a3666
commit 568932492b
11 changed files with 233 additions and 17 deletions

View file

@ -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:

17
package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<boolean>("mcpMarketplace.enabled", true)
const telemetryOptIn = vscode.workspace.getConfiguration("cline").get<boolean | null>("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,
}
}

View file

@ -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")
}

View file

@ -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<string>("telemetryLevel", "all")
const globalTelemetryEnabled = telemetryLevel === "all"
// Only check Cline setting if global telemetry is enabled
if (globalTelemetryEnabled) {
const clineOptIn = vscode.workspace.getConfiguration("cline").get<boolean | null>("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<void> {
await this.client.shutdown()
}
}
// Export a single instance
export const telemetryService = PostHogClient.getInstance()

View file

@ -81,6 +81,7 @@ export interface ExtensionState {
photoURL: string | null
}
mcpMarketplaceEnabled?: boolean
telemetryOptIn: boolean | null
}
export interface ClineMessage {

View file

@ -50,6 +50,8 @@ export interface WebviewMessage {
| "searchCommits"
| "showMcpView"
| "fetchLatestMcpServersFromHub"
| "telemetryOptIn"
| "openTelemetrySettings"
// | "relaunchChromeDebugMode"
text?: string
disabled?: boolean

View file

@ -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 && <TelemetryBanner />}
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>
<h2>What can I do for you?</h2>

View file

@ -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 (
<BannerContainer>
<div>
<strong>Help Improve Cline</strong>
<div style={{ marginTop: 4 }}>
Send anonymous error and usage data to help us fix bugs and improve the extension. No code, prompts, or
personal information is ever sent.
<div style={{ marginTop: 4 }}>
You can always change this in{" "}
<VSCodeLink href="#" onClick={handleOpenSettings}>
settings
</VSCodeLink>
.
</div>
</div>
</div>
<ButtonContainer>
<VSCodeButton appearance="primary" onClick={handleAllow} disabled={hasChosen}>
Allow
</VSCodeButton>
<VSCodeButton appearance="secondary" onClick={handleDeny} disabled={hasChosen}>
Deny
</VSCodeButton>
</ButtonContainer>
</BannerContainer>
)
}
export default memo(TelemetryBanner)

View file

@ -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)