Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# PostHog API Keys for telemetry
POSTHOG_API_KEY=key-goes-here
5 changes: 5 additions & 0 deletions .github/workflows/marketplace-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ jobs:
run: |
npm install -g vsce ovsx
npm run install:ci
- name: Create .env file
run: |
echo "# PostHog API Keys for telemetry" > .env
echo "POSTHOG_API_KEY=${{ secrets.POSTHOG_API_KEY }}" >> .env
- name: Package and Publish Extension
env:
VSCE_PAT: ${{ secrets.VSCE_PAT }}
Expand All @@ -43,6 +47,7 @@ jobs:
echo "$package" | grep -q "dist/extension.js" || exit 1
echo "$package" | grep -q "extension/webview-ui/build/assets/index.js" || exit 1
echo "$package" | grep -q "extension/node_modules/@vscode/codicons/dist/codicon.ttf" || exit 1
echo "$package" | grep -q ".env" || exit 1

npm run publish:marketplace
echo "Successfully published version $current_package_version to VS Code Marketplace"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ roo-cline-*.vsix
docs/_site/

# Dotenv
.env
.env.integration

#Local lint config
Expand Down
1 change: 1 addition & 0 deletions .rooignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
3 changes: 3 additions & 0 deletions .vscodeignore
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,6 @@ webview-ui/node_modules/**

# Include icons
!assets/icons/**

# Include .env file for telemetry
!.env
37 changes: 37 additions & 0 deletions PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Roo Code Privacy Policy

**Last Updated: March 7th, 2025**

Roo Code respects your privacy and is committed to transparency about how we handle your data. Below is a simple breakdown of where key pieces of data go—and, importantly, where they don’t.

### **Where Your Data Goes (And Where It Doesn’t)**

- **Code & Files**: Roo Code accesses files on your local machine when needed for AI-assisted features. When you send commands to Roo Code, relevant files may be transmitted to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not have access to this data, but AI providers may store it per their privacy policies.
- **Commands**: Any commands executed through Roo Code happen on your local environment. However, when you use AI-powered features, the relevant code and context from your commands may be transmitted to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not have access to or store this data, but AI providers may process it per their privacy policies.
- **Prompts & AI Requests**: When you use AI-powered features, your prompts and relevant project context are sent to your chosen AI model provider (e.g., OpenAI, Anthropic, OpenRouter) to generate responses. We do not store or process this data. These AI providers have their own privacy policies and may store data per their terms of service.
- **API Keys & Credentials**: If you enter an API key (e.g., to connect an AI model), it is stored locally on your device and never sent to us or any third party, except the provider you have chosen.
- **Telemetry (Usage Data)**: We only collect feature usage and error data if you explicitly opt-in. This telemetry is powered by PostHog and helps us understand feature usage to improve Roo Code. This includes your VS Code machine ID and feature usage patterns and exception reports. We do **not** collect personally identifiable information, your code, or AI prompts.

### **How We Use Your Data (If Collected)**

- If you opt-in to telemetry, we use it to understand feature usage and improve Roo Code.
- We do **not** sell or share your data.
- We do **not** train any models on your data.

### **Your Choices & Control**

- You can run models locally to prevent data being sent to third-parties.
- By default, telemetry collection is off and if you turn it on, you can opt out of telemetry at any time.
- You can delete Roo Code to stop all data collection.

### **Security & Updates**

We take reasonable measures to secure your data, but no system is 100% secure. If our privacy policy changes, we will notify you within the extension.

### **Contact Us**

For any privacy-related questions, reach out to us at [email protected].

---

By using Roo Code, you agree to this Privacy Policy.
12 changes: 12 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@
"os-name": "^6.0.0",
"p-wait-for": "^5.0.2",
"pdf-parse": "^1.1.1",
"posthog-node": "^4.7.0",
"pretty-bytes": "^6.1.1",
"puppeteer-chromium-resolver": "^23.0.0",
"puppeteer-core": "^23.4.0",
Expand Down
23 changes: 15 additions & 8 deletions src/core/Cline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ import { McpHub } from "../services/mcp/McpHub"
import crypto from "crypto"
import { insertGroups } from "./diff/insert-groups"
import { OutputBuilder } from "../integrations/terminal/OutputBuilder"
import { telemetryService } from "../services/telemetry/TelemetryService"

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
Expand Down Expand Up @@ -190,6 +191,12 @@ export class Cline {
this.enableCheckpoints = enableCheckpoints
this.checkpointStorage = checkpointStorage

if (historyItem) {
telemetryService.captureTaskRestarted(this.taskId)
} else {
telemetryService.captureTaskCreated(this.taskId)
}

// Initialize diffStrategy based on current state
this.updateDiffStrategy(
Experiments.isEnabled(experiments ?? {}, EXPERIMENT_IDS.DIFF_STRATEGY),
Expand Down Expand Up @@ -1446,6 +1453,10 @@ export class Cline {
await this.browserSession.closeBrowser()
}

if (!block.partial) {
telemetryService.captureToolUsage(this.taskId, block.name)
}

// Validate tool use before execution
const { mode, customModes } = (await this.providerRef.deref()?.getState()) ?? {}
try {
Expand Down Expand Up @@ -2897,14 +2908,6 @@ export class Cline {
false,
)

if (this.isSubTask) {
// tell the provider to remove the current subtask and resume the previous task in the stack (it might decide to run the command)
await this.providerRef
.deref()
?.finishSubTask(`new_task finished successfully! ${lastMessage?.text}`)
break
}

Comment on lines -2900 to -2907
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unrelated, but I noticed this while adding events - I don't think we want to pop the subtask until the attempt_completion command has finished.

await this.ask(
"command",
removeClosingTag("command", command),
Expand Down Expand Up @@ -2936,6 +2939,7 @@ export class Cline {
if (lastMessage && lastMessage.ask !== "command") {
// havent sent a command message yet so first send completion_result then command
await this.say("completion_result", result, undefined, false)
telemetryService.captureTaskCompleted(this.taskId)
if (this.isSubTask) {
// tell the provider to remove the current subtask and resume the previous task in the stack
await this.providerRef
Expand All @@ -2960,6 +2964,7 @@ export class Cline {
commandResult = execCommandResult
} else {
await this.say("completion_result", result, undefined, false)
telemetryService.captureTaskCompleted(this.taskId)
if (this.isSubTask) {
// tell the provider to remove the current subtask and resume the previous task in the stack
await this.providerRef
Expand Down Expand Up @@ -3125,6 +3130,7 @@ export class Cline {
userContent.push({ type: "text", text: environmentDetails })

await this.addToApiConversationHistory({ role: "user", content: userContent })
telemetryService.captureConversationMessage(this.taskId, "user")

// 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")
Expand Down Expand Up @@ -3326,6 +3332,7 @@ export class Cline {
role: "assistant",
content: [{ type: "text", text: assistantMessage }],
})
telemetryService.captureConversationMessage(this.taskId, "assistant")

// 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
Expand Down
69 changes: 64 additions & 5 deletions src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ import { Cline, ClineOptions } from "../Cline"
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
Expand Down Expand Up @@ -82,6 +84,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
this.outputChannel.appendLine("ClineProvider instantiated")
this.contextProxy = new ContextProxy(context)
ClineProvider.activeInstances.add(this)

// Register this provider with the telemetry service to enable it to add properties like mode and provider
telemetryService.setProvider(this)

this.workspaceTracker = new WorkspaceTracker(this)
this.configManager = new ConfigManager(this.context)
this.customModesManager = new CustomModesManager(this.context, async () => {
Expand Down Expand Up @@ -620,8 +626,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
`font-src ${webview.cspSource}`,
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
`img-src ${webview.cspSource} data:`,
`script-src 'unsafe-eval' https://* http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
`script-src 'unsafe-eval' https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
`connect-src https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
]

return /*html*/ `
Expand Down Expand Up @@ -710,7 +716,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}';">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource}; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} data:; script-src 'nonce-${nonce}' https://us-assets.i.posthog.com; connect-src https://us.i.posthog.com https://us-assets.i.posthog.com;">
<link rel="stylesheet" type="text/css" href="${stylesUri}">
<link href="${codiconsUri}" rel="stylesheet" />
<title>Roo Code</title>
Expand Down Expand Up @@ -925,6 +931,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)
})

this.isViewLaunched = true
break
case "newTask":
Expand Down Expand Up @@ -1776,6 +1789,15 @@ export class ClineProvider implements vscode.WebviewViewProvider {
})
}
break

case "telemetrySetting": {
const telemetrySetting = message.text as TelemetrySetting
await this.updateGlobalState("telemetrySetting", telemetrySetting)
const isOptedIn = telemetrySetting === "enabled"
telemetryService.updateTelemetryState(isOptedIn)
await this.postStateToWebview()
break
}
}
},
null,
Expand Down Expand Up @@ -1835,6 +1857,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
* @param newMode The mode to switch to
*/
public async handleModeSwitch(newMode: Mode) {
// Capture mode switch telemetry event
const currentTaskId = this.getCurrentCline()?.taskId
if (currentTaskId) {
telemetryService.captureModeSwitch(currentTaskId, newMode)
}

await this.updateGlobalState("mode", newMode)

// Load the saved API config for the new mode if it exists
Expand Down Expand Up @@ -2172,7 +2200,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
experiments,
maxOpenTabsContext,
browserToolEnabled,
telemetrySetting,
} = await this.getState()
const telemetryKey = process.env.POSTHOG_API_KEY
const machineId = vscode.env.machineId

const allowedCommands = vscode.workspace.getConfiguration("roo-cline").get<string[]>("allowedCommands") || []

Expand Down Expand Up @@ -2200,7 +2231,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
diffEnabled: diffEnabled ?? true,
enableCheckpoints: enableCheckpoints ?? true,
checkpointStorage: checkpointStorage ?? "task",
shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
shouldShowAnnouncement:
telemetrySetting !== "unset" && lastShownAnnouncementId !== this.latestAnnouncementId,
allowedCommands,
soundVolume: soundVolume ?? 0.5,
browserViewportSize: browserViewportSize ?? "900x600",
Expand All @@ -2225,8 +2257,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
experiments: experiments ?? experimentDefault,
mcpServers: this.mcpHub?.getAllServers() ?? [],
maxOpenTabsContext: maxOpenTabsContext ?? 20,
cwd: cwd,
cwd,
browserToolEnabled: browserToolEnabled ?? true,
telemetrySetting,
telemetryKey,
machineId,
}
}

Expand Down Expand Up @@ -2405,6 +2440,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
browserToolEnabled: stateValues.browserToolEnabled ?? true,
telemetrySetting: stateValues.telemetrySetting || "unset",
}
}

Expand Down Expand Up @@ -2483,4 +2519,27 @@ export class ClineProvider implements vscode.WebviewViewProvider {
public getMcpHub(): McpHub | undefined {
return this.mcpHub
}

/**
* Returns properties to be included in every telemetry event
* This method is called by the telemetry service to get context information
* like the current mode, API provider, etc.
*/
public async getTelemetryProperties(): Promise<Record<string, any>> {
const { mode, apiConfiguration } = await this.getState()

const properties: Record<string, any> = {}

// Add current mode
if (mode) {
properties.mode = mode
}

// Add API provider
if (apiConfiguration?.apiProvider) {
properties.apiProvider = apiConfiguration.apiProvider
}

return properties
}
}
7 changes: 7 additions & 0 deletions src/core/webview/__tests__/ClineProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,12 @@ describe("ClineProvider", () => {
})

expect(mockWebviewView.webview.html).toContain("<!DOCTYPE html>")

// Verify Content Security Policy contains the necessary PostHog domains
expect(mockWebviewView.webview.html).toContain("connect-src https://us.i.posthog.com")
expect(mockWebviewView.webview.html).toContain("https://us-assets.i.posthog.com")
expect(mockWebviewView.webview.html).toContain("script-src 'nonce-")
expect(mockWebviewView.webview.html).toContain("https://us-assets.i.posthog.com")
})

test("postMessageToWebview sends message to webview", async () => {
Expand Down Expand Up @@ -438,6 +444,7 @@ describe("ClineProvider", () => {
experiments: experimentDefault,
maxOpenTabsContext: 20,
browserToolEnabled: true,
telemetrySetting: "unset",
}

const message: ExtensionMessage = {
Expand Down
Loading
Loading