Compare commits

...

20 Commits

Author SHA1 Message Date
Dennis Bartlett 2141a3ff10 WIP: Updating Dev Exp 2025-03-12 17:06:37 -07:00
frostbournesb 1edf1c7d9f Add IS_DEV ext build script 2025-03-12 14:20:41 -07:00
pashpashpash 5aefaaeede throwing behind is dev 2025-03-12 14:07:38 -07:00
pashpashpash b36f33eb28 changeset 2025-03-12 04:11:13 -07:00
pashpashpash 56783d3255 wow that was harder than i thought 2025-03-12 03:50:44 -07:00
pashpashpash 46fd9894d7 curly braces after if 2025-03-12 01:13:04 -07:00
pashpashpash b169852039 cleanup step is important 2025-03-12 01:12:02 -07:00
pashpashpash 48a77a2064 dont forget the key 2025-03-12 00:51:48 -07:00
pashpashpash 4f497d5b4a logs 2025-03-12 00:24:54 -07:00
pashpashpash 69be886106 better dates 2025-03-12 00:21:47 -07:00
pashpashpash decd7e846c explicit comments 2025-03-11 23:30:31 -07:00
pashpashpash 3944cdd464 explicit comments 2025-03-11 23:24:59 -07:00
pashpashpash cdd5cf4eda system prompt llm observability 2025-03-11 23:22:28 -07:00
pashpashpash 054c6249a6 checkbox instead of dropdown 2025-03-11 23:03:36 -07:00
pashpashpash 18b620afb1 Merge remote-tracking branch 'origin/main' into pashpashpash/conversation-telemetry 2025-03-11 22:49:39 -07:00
pashpashpash d6ba8282d9 merge conflicts 2 minutes later 2025-03-11 20:24:17 -07:00
pashpashpash caa494aa95 moved to advanced settings 2025-03-11 20:22:48 -07:00
pashpashpash a6cb18ed8d merge conflicts resolved 2025-03-11 19:53:55 -07:00
pashpashpash 7bc5ff7509 also tracing responses 2025-03-11 19:23:40 -07:00
pashpashpash 488eea0688 conversation telemetry 2025-03-11 18:16:19 -07:00
15 changed files with 14983 additions and 467 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": minor
---
opt-in llm observability
+1 -1
View File
@@ -13,7 +13,7 @@
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"preLaunchTask": "${defaultBuildTask}",
"env": {
"IS_DEV": "true",
"NODE_ENV": "debug",
"DEV_WORKSPACE_FOLDER": "${workspaceFolder}"
}
}
+5
View File
@@ -79,6 +79,11 @@ const extensionConfig = {
platform: "node",
outfile: "dist/extension.js",
external: ["vscode"],
define: {
"process.env": JSON.stringify({
NODE_ENV: process.env.NODE_ENV,
}),
},
}
async function main() {
+14462 -450
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -211,6 +211,11 @@
"type": "boolean",
"default": true,
"description": "Controls whether the MCP Marketplace is enabled."
},
"cline.conversationData": {
"type": "boolean",
"default": false,
"description": "Allow storing conversation data to improve Cline. Your conversations will be used to make Cline better. No personal information is ever shared with third parties."
}
}
}
@@ -222,6 +227,7 @@
"watch:esbuild": "node esbuild.js --watch",
"watch:tsc": "tsc --noEmit --watch --project tsconfig.json",
"package": "npm run build:webview && npm run check-types && npm run lint && node esbuild.js --production",
"dev:package": "NODE_ENV=development vsce package",
"compile-tests": "tsc -p ./tsconfig.test.json --outDir out",
"watch-tests": "tsc -p . -w --outDir out",
"pretest": "npm run compile-tests && npm run compile && npm run lint",
@@ -253,6 +259,7 @@
"@typescript-eslint/parser": "^7.11.0",
"@vscode/test-cli": "^0.0.9",
"@vscode/test-electron": "^2.4.0",
"@vscode/vsce": "^3.2.2",
"chai": "^4.3.10",
"esbuild": "^0.25.0",
"eslint": "^8.57.0",
@@ -271,6 +278,12 @@
"@google/generative-ai": "^0.18.0",
"@mistralai/mistralai": "^1.5.0",
"@modelcontextprotocol/sdk": "^1.0.1",
"@opentelemetry/api": "^1.4.1",
"@opentelemetry/exporter-trace-otlp-http": "^0.39.1",
"@opentelemetry/resources": "^1.30.1",
"@opentelemetry/sdk-node": "^0.39.1",
"@opentelemetry/sdk-trace-node": "^1.30.1",
"@opentelemetry/semantic-conventions": "^1.30.0",
"@types/clone-deep": "^4.0.4",
"@types/get-folder-size": "^3.0.4",
"@types/pdf-parse": "^1.1.4",
+117
View File
@@ -62,6 +62,7 @@ import { ClineHandler } from "../api/providers/cline"
import { ClineProvider, GlobalFileNames } from "./webview/ClineProvider"
import { DEFAULT_LANGUAGE_SETTINGS, getLanguageKey, LanguageDisplay, LanguageKey } from "../shared/Languages"
import { telemetryService } from "../services/telemetry/TelemetryService"
import { conversationTelemetryService, TelemetryChatMessage } from "../services/telemetry/ConversationTelemetryService"
const cwd = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0) ?? path.join(os.homedir(), "Desktop") // may or may not exist but fs checking existence would immediately ask for permission which would be bad UX, need to come up with a better solution
@@ -1347,6 +1348,26 @@ export class Cline {
)
}
// Capture system prompt for telemetry,
// ONLY if user is opted in, in advanced settings
if (this.apiProvider && this.api.getModel().id) {
const metadata = {
apiProvider: this.apiProvider,
model: this.api.getModel().id,
tokensIn: 0,
tokensOut: 0,
}
const systemMessage: TelemetryChatMessage = {
role: "system",
content: systemPrompt,
ts: Date.now(), // we dont uniquely identify system messages, so we use the timestamp as the id
}
// no need for timeout here, as there's no timestamp to compare to
conversationTelemetryService.captureMessage(this.taskId, systemMessage, metadata)
}
// If the previous API request's total token usage is close to the context window, truncate the conversation history to free up space for the new request
if (previousApiReqIndex >= 0) {
const previousRequest = this.clineMessages[previousApiReqIndex]
@@ -3120,6 +3141,42 @@ export class Cline {
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "user")
// Capture message data for telemetry,
// ONLY if user is opted in, in advanced settings
// This is done after a timeout to ensure the message is added to the conversation history
setTimeout(() => {
if (this.apiProvider && this.api.getModel().id) {
const metadata = {
apiProvider: this.apiProvider,
model: this.api.getModel().id,
tokensIn: 0,
tokensOut: 0,
}
// Get the last message from apiConversationHistory
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
// Get the corresponding timestamp from clineMessages
// The last message in clineMessages should be the one we just added
const lastClineMessage = this.clineMessages[this.clineMessages.length - 1]
const ts = lastClineMessage.ts
// Add the timestamp to the message object for telemetry
const messageWithTs = {
...lastMessage,
ts,
}
// Send individual message to telemetry
conversationTelemetryService.captureMessage(this.taskId, messageWithTs, metadata)
// Send entire conversation history to cleanup endpoint
// This ensures deleted messages are properly handled in telemetry
conversationTelemetryService.cleanupTask(this.taskId, this.clineMessages)
}
}, 5)
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
this.clineMessages[lastApiReqIndex].text = JSON.stringify({
@@ -3197,6 +3254,38 @@ export class Cline {
telemetryService.captureConversationTurnEvent(this.taskId, this.apiProvider, this.api.getModel().id, "assistant")
// Capture message data for telemetry after assistant response
// ONLY if user is opted in, in advanced settings
if (this.apiProvider && this.api.getModel().id) {
const metadata = {
apiProvider: this.apiProvider,
model: this.api.getModel().id,
tokensIn: inputTokens,
tokensOut: outputTokens,
}
// Get the last message from apiConversationHistory
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
// Find the corresponding timestamp from clineMessages
// For assistant messages, we need to find the most recent "text" message
const lastTextMessage = findLast(this.clineMessages, (m) => m.say === "text")
// Add the timestamp to the message object for telemetry
if (!lastTextMessage) {
console.error("No text message found in clineMessages")
} else {
const messageWithTs = {
...lastMessage,
ts: lastTextMessage.ts,
}
setTimeout(() => {
conversationTelemetryService.captureMessage(this.taskId, messageWithTs, metadata)
}, 5)
}
}
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.didFinishAbortingStream = true
}
@@ -3327,6 +3416,34 @@ export class Cline {
content: [{ type: "text", text: assistantMessage }],
})
// Capture message data for telemetry after assistant response,
// ONLY if user is opted in, in advanced settings
if (this.apiProvider && this.api.getModel().id) {
const metadata = {
apiProvider: this.apiProvider,
model: this.api.getModel().id,
tokensIn: inputTokens,
tokensOut: outputTokens,
}
// Get the last message from apiConversationHistory
const lastMessage = this.apiConversationHistory[this.apiConversationHistory.length - 1]
// Find the corresponding timestamp from clineMessages
const lastTextMessage = findLast(this.clineMessages, (m) => m.say === "text")
if (lastTextMessage) {
const messageWithTs = {
...lastMessage,
ts: lastTextMessage.ts,
}
setTimeout(() => {
conversationTelemetryService.captureMessage(this.taskId, messageWithTs, metadata)
})
}
}
// NOTE: this comment is here for future reference - this was a workaround for userMessageContent not getting set to true. It was due to it not recursively calling for partial blocks when didRejectTool, so it would get stuck waiting for a partial block to complete before it could continue.
// in case the content blocks finished
// it may be the api stream finished after the last parsed content block was executed, so we are able to detect out of bounds and set userMessageContentReady to true (note you should not call presentAssistantMessage since if the last block is completed it will be presented again)
+16 -5
View File
@@ -33,7 +33,8 @@ import { openMention } from "../mentions"
import { getNonce } from "./getNonce"
import { getUri } from "./getUri"
import { telemetryService } from "../../services/telemetry/TelemetryService"
import { TelemetrySetting } from "../../shared/TelemetrySetting"
import { conversationTelemetryService } from "../../services/telemetry/ConversationTelemetryService"
import { TelemetrySetting, ConversationDataSetting } from "../../shared/TelemetrySetting"
import { cleanupLegacyCheckpoints } from "../../integrations/checkpoints/CheckpointMigration"
import CheckpointTracker from "../../integrations/checkpoints/CheckpointTracker"
@@ -103,6 +104,7 @@ type GlobalStateKey =
| "togetherModelId"
| "mcpMarketplaceCatalog"
| "telemetrySetting"
| "conversationDataSetting"
| "asksageApiUrl"
| "thinkingBudgetTokens"
| "planActSeparateModelsSetting"
@@ -526,10 +528,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
})
// If user already opted in to telemetry, enable telemetry service
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
this.getStateToPostToWebview().then(async (state) => {
const { telemetrySetting, conversationDataSetting, apiConfiguration } = state
const clineApiKey = apiConfiguration?.clineApiKey
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
// conversation telemetry is only enabled if user has opted in to conversation data telemetry
const isConversationDataEnabled = conversationDataSetting === "enabled"
conversationTelemetryService.updateTelemetryState(isConversationDataEnabled, clineApiKey)
})
break
case "newTask":
@@ -867,7 +874,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
}
break
}
// telemetry
case "openSettings": {
await this.postMessageToWebview({
type: "action",
@@ -919,7 +925,6 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.disposables,
)
}
async updateTelemetrySetting(telemetrySetting: TelemetrySetting) {
await this.updateGlobalState("telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting === "enabled"
@@ -1845,6 +1850,11 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
planActSeparateModelsSetting,
} = await this.getState()
// Get conversationDataSetting separately
const conversationDataSetting = (await this.getGlobalState("conversationDataSetting")) as
| ConversationDataSetting
| undefined
return {
version: this.context.extension?.packageJSON?.version ?? "",
apiConfiguration,
@@ -1865,6 +1875,7 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
userInfo,
mcpMarketplaceEnabled,
telemetrySetting,
conversationDataSetting: conversationDataSetting || "unset",
planActSeparateModelsSetting,
vscMachineId: vscode.env.machineId,
}
+61 -2
View File
@@ -9,6 +9,8 @@ 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"
import { conversationTelemetryService } from "./services/telemetry/ConversationTelemetryService"
import { TelemetrySetting, ConversationDataSetting } from "./shared/TelemetrySetting"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -30,6 +32,60 @@ export function activate(context: vscode.ExtensionContext) {
Logger.initialize(outputChannel)
Logger.log("Cline extension activated")
// Initialize telemetry services
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
const globalTelemetryEnabled = telemetryLevel === "all"
// Get user settings
const telemetrySetting = context.globalState.get<TelemetrySetting>("telemetrySetting") || "unset"
const conversationDataSetting = context.globalState.get<ConversationDataSetting>("conversationDataSetting") || "unset"
// Update telemetry services
const telemetryEnabled = globalTelemetryEnabled && telemetrySetting === "enabled"
const conversationDataEnabled = globalTelemetryEnabled && conversationDataSetting === "enabled"
telemetryService.updateTelemetryState(telemetryEnabled)
context.secrets.get("clineApiKey").then((clineApiKey) => {
conversationTelemetryService.updateTelemetryState(conversationDataEnabled, clineApiKey)
})
// Sync VSCode settings with extension state
// Only called when the setting is explicitly changed, not on initial load
const syncConversationDataSetting = async () => {
const config = vscode.workspace.getConfiguration("cline")
const configValue = config.get<boolean>("conversationData")
if (configValue !== undefined) {
// Convert boolean to "enabled"/"disabled" string
const stringValue: ConversationDataSetting = configValue ? "enabled" : "disabled"
// Update the global state
await context.globalState.update("conversationDataSetting", stringValue)
// Update telemetry service
const isEnabled = configValue && globalTelemetryEnabled
const clineApiKey = await context.secrets.get("clineApiKey")
conversationTelemetryService.updateTelemetryState(isEnabled, clineApiKey)
// Update all providers
const visibleProvider = ClineProvider.getVisibleInstance()
if (visibleProvider) {
await visibleProvider.postStateToWebview()
}
}
}
// No initial sync - we want to preserve the "unset" state for new users
// Listen for configuration changes
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration(async (e) => {
if (e.affectsConfiguration("cline.conversationData")) {
await syncConversationDataSetting()
}
}),
)
const sidebarProvider = new ClineProvider(context, outputChannel)
context.subscriptions.push(
@@ -193,6 +249,9 @@ export function activate(context: vscode.ExtensionContext) {
// This method is called when your extension is deactivated
export function deactivate() {
telemetryService.shutdown()
conversationTelemetryService.shutdown().catch((error) => {
console.error("Error shutting down conversation telemetry:", error)
})
Logger.log("Cline extension deactivated")
}
@@ -202,9 +261,9 @@ export function deactivate() {
//
// This is a workaround to reload the extension when the source code changes
// since vscode doesn't support hot reload for extensions
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
const { NODE_ENV, DEV_WORKSPACE_FOLDER } = process.env
if (IS_DEV && IS_DEV !== "false") {
if (NODE_ENV && NODE_ENV === "debug") {
assert(DEV_WORKSPACE_FOLDER, "DEV_WORKSPACE_FOLDER must be set in development")
const watcher = vscode.workspace.createFileSystemWatcher(new vscode.RelativePattern(DEV_WORKSPACE_FOLDER, "src/**/*"))
@@ -0,0 +1,293 @@
import { trace, context, SpanKind, SpanStatusCode } from "@opentelemetry/api"
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http"
import { Resource } from "@opentelemetry/resources"
import { SimpleSpanProcessor } from "@opentelemetry/sdk-trace-base"
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions"
import { Anthropic } from "@anthropic-ai/sdk"
export type TelemetryChatMessage = {
role: "user" | "assistant" | "system"
ts: number
content: Anthropic.Messages.MessageParam["content"]
}
import * as vscode from "vscode"
const IS_DEV = Boolean(process.env.NODE_ENV === "development" || process.env.NODE_ENV === "debug")
interface ConversationMetadata {
apiProvider: string
model: string
tokensIn: number
tokensOut: number
}
/**
* Service for collecting conversation data using OpenTelemetry
*/
export class ConversationTelemetryService {
private static instance: ConversationTelemetryService
private enabled: boolean = false
private distinctId: string
private clineApiKey?: string
private apiEndpoint: string = "https://api.cline.bot/v1/traces"
private tracerProvider: NodeTracerProvider | undefined
private tracer: any
private messageIndices: Map<string, number> = new Map()
private constructor(distinctId: string) {
this.distinctId = distinctId
}
public static getInstance(distinctId: string): ConversationTelemetryService {
if (!ConversationTelemetryService.instance) {
ConversationTelemetryService.instance = new ConversationTelemetryService(distinctId)
}
return ConversationTelemetryService.instance
}
public updateTelemetryState(enabled: boolean, clineApiKey?: string): void {
console.log("[ConversationTelemetry] Updating telemetry state...", { enabled, clineApiKey })
// First update the state variables
this.enabled = enabled
this.clineApiKey = clineApiKey
// Then initialize the tracer if needed
if (this.enabled && !this.tracer && IS_DEV) {
this.initializeTracer()
}
}
private initializeTracer(): void {
try {
// Create a resource that identifies our service
const resource = new Resource({
[ATTR_SERVICE_NAME]: "cline-extension",
[ATTR_SERVICE_VERSION]: "1.0.0",
})
console.log("[ConversationTelemetry] Initializing OpenTelemetry tracer...", { "this.clineApiKey": this.clineApiKey })
// Configure the OTLP exporter
const headers: Record<string, string> = {
"Content-Type": "application/json",
}
// Add API key to headers if available
if (this.clineApiKey) {
headers["Authorization"] = `Bearer ${this.clineApiKey}`
}
const exporter = new OTLPTraceExporter({
url: this.apiEndpoint,
headers,
})
// Create the span processor
const spanProcessor = new SimpleSpanProcessor(exporter as any)
// Create the trace provider with the span processor in the config
this.tracerProvider = new NodeTracerProvider({
resource,
spanProcessors: [spanProcessor as any],
})
// Register the provider
this.tracerProvider.register()
// Get a tracer
this.tracer = trace.getTracer("cline-conversation-tracer")
console.log("[ConversationTelemetry] OpenTelemetry tracer initialized successfully")
} catch (error) {
console.error("[ConversationTelemetry] Failed to initialize OpenTelemetry tracer:", error)
}
}
/**
* Captures a message in the conversation as an OpenTelemetry span
* ONLY HAPPENS IF USER IS OPTED INTO CONVERSATION TELEMETRY IN ADVANCED SETTINGS
*/
public captureMessage(taskId: string, message: TelemetryChatMessage, metadata: ConversationMetadata): void {
// Do NOT capture message if user has not explicitly opted in
if (!this.enabled || !this.tracer || !IS_DEV) {
return
}
try {
// Convert taskId to a valid trace ID (must be 32 hex chars)
const traceId = this.generateTraceIdFromTimestamp(taskId)
// Convert message timestamp to a valid span ID (must be 16 hex chars)
if (!message.ts && message.ts !== 0) {
throw new Error("Message timestamp is required")
}
const timestamp = message.ts
const spanId = this.generateSpanIdFromTimestamp(timestamp)
// Create a span context with our IDs
const spanContext = trace.setSpanContext(context.active(), {
traceId,
spanId,
isRemote: false,
traceFlags: 1, // Sampled
})
// Start a new span with the context
const span = this.tracer.startSpan(
`message.${message.role}`,
{
kind: SpanKind.CLIENT,
startTime: this.millisecondsToHrTime(timestamp), // Convert to nanoseconds
},
spanContext,
)
// Get the message index for this task
const messageIndex = this.getNextMessageIndex(taskId)
// Add attributes to the span
span.setAttribute("task.id", taskId)
span.setAttribute("user.id", this.distinctId)
span.setAttribute("message.role", message.role)
span.setAttribute("message.timestamp", timestamp)
span.setAttribute("message.index", messageIndex)
const c = message.content
// Add Braintrust-compatible attributes
span.setAttribute("gen_ai.request.model", metadata.model)
if (message.role === "user") {
span.setAttribute("gen_ai.prompt", this.extractContent(message))
} else if (message.role === "assistant") {
span.setAttribute("gen_ai.completion", this.extractContent(message))
span.setAttribute("gen_ai.usage.prompt_tokens", metadata.tokensIn)
span.setAttribute("gen_ai.usage.completion_tokens", metadata.tokensOut)
} else if (message.role === "system") {
span.setAttribute("gen_ai.system_prompt", this.extractContent(message))
}
// Add custom metadata in Braintrust format
span.setAttribute("braintrust.metadata.api_provider", metadata.apiProvider)
span.setAttribute("braintrust.metadata.ts", message.ts)
// End the span immediately since messages are discrete events
span.end(this.millisecondsToHrTime(timestamp)) // Convert to nanoseconds
console.log(`[ConversationTelemetry] Captured ${message.role} message for task ${taskId}`, { span })
} catch (error) {
console.error("[ConversationTelemetry] Error capturing message:", error)
}
}
/**
* Convert a decimal timestamp to a valid trace ID (32 hex chars)
*/
private generateTraceIdFromTimestamp(timestamp: string): string {
// Pad with zeros and convert to hex
const hex = BigInt(timestamp).toString(16).padStart(32, "0")
return hex.substring(0, 32) // Ensure it's exactly 32 chars
}
/**
* Converts milliseconds to high-resolution time format expected by OpenTelemetry
* Returns [seconds, nanoseconds]
*/
private millisecondsToHrTime(milliseconds: number): [number, number] {
return [
Math.floor(milliseconds / 1000), // seconds
(milliseconds % 1000) * 1000000, // nanoseconds (remainder in ms * 10^6)
]
}
/**
* Convert a decimal timestamp to a valid span ID (16 hex chars)
*/
private generateSpanIdFromTimestamp(timestamp: number): string {
// Pad with zeros and convert to hex
const hex = BigInt(timestamp).toString(16).padStart(16, "0")
return hex.substring(0, 16) // Ensure it's exactly 16 chars
}
/**
* Helper to extract content from different message formats
*/
private extractContent(message: TelemetryChatMessage): string {
if (typeof message.content === "string") {
return message.content
}
return message.content
.map((block) => (block.type === "text" ? block.text : null))
.filter(Boolean)
.join("\n")
}
/**
* Track message indices per task
*/
private getNextMessageIndex(taskId: string): number {
const currentIndex = this.messageIndices.get(taskId) || 0
this.messageIndices.set(taskId, currentIndex + 1)
return currentIndex
}
/**
* Sends conversation data to cleanup endpoint to remove deleted messages from telemetry
* ONLY HAPPENS IF USER IS OPTED INTO CONVERSATION TELEMETRY IN ADVANCED SETTINGS
*/
public async cleanupTask(taskId: string, conversationData: any): Promise<void> {
// Do NOT send data if user has not explicitly opted in
if (!this.enabled || !this.clineApiKey) {
return
}
try {
// Configure the headers with API key
const headers: Record<string, string> = {
"Content-Type": "application/json",
}
// Add API key to headers
headers["Authorization"] = `Bearer ${this.clineApiKey}`
// Send the data to the cleanup endpoint
const cleanupEndpoint = `${this.apiEndpoint.replace("/traces", "/traces/cleanup")}`
// Use fetch API to send the data
const response = await fetch(cleanupEndpoint, {
method: "POST",
headers,
body: JSON.stringify({
taskId: taskId,
conversationData,
userId: this.distinctId,
}),
})
if (!response.ok) {
throw new Error(`Failed to send cleanup data: ${response.status} ${response.statusText}`)
}
console.log(`[ConversationTelemetry] Cleanup data sent for task ${taskId}`)
} catch (error) {
console.error("[ConversationTelemetry] Error sending cleanup data:", error)
}
}
/**
* Shutdown the tracer provider
*/
public async shutdown(): Promise<void> {
if (this.tracerProvider) {
await this.tracerProvider.shutdown()
}
}
}
export const conversationTelemetryService = ConversationTelemetryService.getInstance(vscode.env.machineId)
+2 -1
View File
@@ -7,7 +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"
import { TelemetrySetting, ConversationDataSetting } from "./TelemetrySetting"
// webview will hold state
export interface ExtensionMessage {
@@ -98,6 +98,7 @@ export interface ExtensionState {
}
mcpMarketplaceEnabled?: boolean
telemetrySetting: TelemetrySetting
conversationDataSetting?: ConversationDataSetting
planActSeparateModelsSetting: boolean
vscMachineId: string
}
+1
View File
@@ -1 +1,2 @@
export type TelemetrySetting = "unset" | "enabled" | "disabled"
export type ConversationDataSetting = "unset" | "enabled" | "disabled"
+1 -1
View File
@@ -4,7 +4,7 @@ import { BrowserSettings } from "./BrowserSettings"
import { ChatSettings } from "./ChatSettings"
import { UserInfo } from "./UserInfo"
import { ChatContent } from "./ChatContent"
import { TelemetrySetting } from "./TelemetrySetting"
import { TelemetrySetting, ConversationDataSetting } from "./TelemetrySetting"
export interface WebviewMessage {
type:
@@ -16,7 +16,7 @@ import ApiOptions from "./ApiOptions"
import { TabButton } from "../mcp/McpView"
import { useEvent } from "react-use"
import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
const { IS_DEV } = process.env
const { NODE_ENV } = process.env
type SettingsViewProps = {
onDone: () => void
@@ -31,6 +31,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
openRouterModels,
telemetrySetting,
setTelemetrySetting,
conversationDataSetting,
chatSettings,
planActSeparateModelsSetting,
setPlanActSeparateModelsSetting,
@@ -72,7 +73,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
planActSeparateModelsSetting,
customInstructionsSetting: customInstructions,
telemetrySetting,
apiConfiguration: apiConfigurationToSubmit,
apiConfiguration: apiConfiguration,
})
if (!withoutDone) {
@@ -278,8 +279,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
for more details.
</p>
</div>
{IS_DEV && (
{(NODE_ENV === "development" || NODE_ENV === "debug") && (
<>
<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
<VSCodeButton onClick={handleResetState} style={{ marginTop: "5px", width: "auto" }}>
@@ -9,7 +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"
import { TelemetrySetting, ConversationDataSetting } from "../../../src/shared/TelemetrySetting"
interface ExtensionStateContextType extends ExtensionState {
didHydrateState: boolean
+1 -2
View File
@@ -37,8 +37,7 @@ export default defineConfig({
},
define: {
"process.env": {
NODE_ENV: JSON.stringify(process.env.IS_DEV ? "development" : "production"),
IS_DEV: JSON.stringify(process.env.IS_DEV),
NODE_ENV: process.env.NODE_ENV,
},
},
})