Use GUI telemetry toggle to more clearly show user if they're opted in or not

This commit is contained in:
Saoud Rizwan 2025-02-25 17:34:36 -08:00
parent 8c5f8b253b
commit e14fd78767
10 changed files with 90 additions and 45 deletions

View file

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

View file

@ -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<ApiProvider | undefined>,
this.getGlobalState("apiModelId") as Promise<string | undefined>,
@ -1806,6 +1817,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
this.getGlobalState("previousModeModelInfo") as Promise<ModelInfo | undefined>,
this.getGlobalState("qwenApiLine") as Promise<string | undefined>,
this.getSecret("liteLlmApiKey") as Promise<string | undefined>,
this.getGlobalState("telemetrySetting") as Promise<TelemetrySetting | undefined>,
])
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<boolean>("mcpMarketplace.enabled", true)
const telemetryOptIn = vscode.workspace.getConfiguration("cline").get<boolean | null>("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",
}
}

View file

@ -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<string>("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<boolean | null>("enableTelemetry", null)
this.telemetryEnabled = clineOptIn === true
this.telemetryEnabled = didUserOptIn
}
// Update PostHog client state based on telemetry preference

View file

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

View file

@ -0,0 +1 @@
export type TelemetrySetting = "unset" | "enabled" | "disabled"

View file

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

View file

@ -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 && <TelemetryBanner />}
{telemetrySetting === "unset" && <TelemetryBanner />}
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
<div style={{ padding: "0 20px", flexShrink: 0 }}>

View file

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

View file

@ -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<string | undefined>(undefined)
const [modelIdErrorMessage, setModelIdErrorMessage] = useState<string | undefined>(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) => {
</p>
</div>
<div style={{ marginBottom: 5 }}>
<VSCodeCheckbox
style={{ marginBottom: "5px" }}
checked={telemetrySetting === "enabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
}}>
Allow anonymous error and usage reporting
</VSCodeCheckbox>
<p
style={{
fontSize: "12px",
marginTop: "5px",
color: "var(--vscode-descriptionForeground)",
}}>
Help improve Cline by sending anonymous usage data and error reports. No code, prompts, or personal
information is ever sent. See our{" "}
<VSCodeLink
href="https://github.com/cline/cline/blob/main/docs/PRIVACY.md"
style={{ fontSize: "inherit" }}>
privacy policy
</VSCodeLink>{" "}
for more details.
</p>
</div>
{IS_DEV && (
<>
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>

View file

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