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
11 changes: 6 additions & 5 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,15 @@ go—and, importantly, where they don't.
- **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 collect feature usage and error data to help
- **Telemetry (Usage Data)**: Telemetry is **off by default**. If you explicitly
opt in through the settings, we collect feature usage and error data to help
us improve Bolt Code. This telemetry is powered by PostHog and includes your
VS Code machine ID, feature usage patterns, and exception reports. The VS Code
machine ID is a persistent identifier and may be considered personal data in
some jurisdictions; we use it only for product analytics and error grouping.
We retain telemetry only as long as needed for product analytics and debugging.
Telemetry does **not** collect your code or AI prompts, and you can opt out at
any time through the settings.
Telemetry does **not** collect your code or AI prompts, and you can opt back
out at any time through the settings.
- **Marketplace Requests**: When you browse or search the Marketplace for Model
Configuration Profiles (MCPs) or Custom Modes, Bolt Code makes a secure API
call to Bolt Code's backend servers to retrieve listing information. These
Expand All @@ -56,8 +57,8 @@ go—and, importantly, where they don't.
## **Your Choices & Control**

- You can run models locally to prevent data being sent to third-parties.
- Telemetry collection is enabled by default to help us improve Bolt Code, but
you can opt out at any time through the settings.
- Telemetry collection is disabled by default. You can opt in, and back out,
at any time through the settings.
- You can delete Bolt Code to stop all data collection.

## **Security & Updates**
Expand Down
39 changes: 23 additions & 16 deletions src/core/webview/__tests__/telemetrySettingsTracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ describe("Telemetry Settings Tracking", () => {
const newSetting = "disabled" as TelemetrySetting

// Simulate the logic from webviewMessageHandler
const isOptedIn = newSetting !== "disabled"
const wasPreviouslyOptedIn = previousSetting !== "disabled"
const isOptedIn = newSetting === "enabled"
const wasPreviouslyOptedIn = previousSetting === "enabled"

// If turning telemetry OFF, fire event BEFORE disabling
if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) {
Expand All @@ -50,20 +50,23 @@ describe("Telemetry Settings Tracking", () => {
expect(mockTelemetryService.updateTelemetryState).toHaveBeenCalledWith(false)
})

it("should fire event when going from unset to disabled", () => {
it("should not fire event when going from unset to disabled", () => {
const previousSetting = "unset" as TelemetrySetting
const newSetting = "disabled" as TelemetrySetting

const isOptedIn = newSetting !== "disabled"
const wasPreviouslyOptedIn = previousSetting !== "disabled"
const isOptedIn = newSetting === "enabled"
const wasPreviouslyOptedIn = previousSetting === "enabled"

if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) {
TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, newSetting)
}

TelemetryService.instance.updateTelemetryState(isOptedIn)

expect(mockTelemetryService.captureTelemetrySettingsChanged).toHaveBeenCalledWith("unset", "disabled")
// Telemetry is opt-in, so "unset" was never opted in; nothing was on,
// so there is no transition event to capture (and nothing to send it).
expect(mockTelemetryService.captureTelemetrySettingsChanged).not.toHaveBeenCalled()
expect(mockTelemetryService.updateTelemetryState).toHaveBeenCalledWith(false)
})
})

Expand All @@ -72,8 +75,8 @@ describe("Telemetry Settings Tracking", () => {
const previousSetting = "disabled" as TelemetrySetting
const newSetting = "enabled" as TelemetrySetting

const isOptedIn = newSetting !== "disabled"
const wasPreviouslyOptedIn = previousSetting !== "disabled"
const isOptedIn = newSetting === "enabled"
const wasPreviouslyOptedIn = previousSetting === "enabled"

// Update the telemetry state first
TelemetryService.instance.updateTelemetryState(isOptedIn)
Expand All @@ -95,8 +98,8 @@ describe("Telemetry Settings Tracking", () => {
const previousSetting = "enabled" as TelemetrySetting
const newSetting = "enabled" as TelemetrySetting

const isOptedIn = newSetting !== "disabled"
const wasPreviouslyOptedIn = previousSetting !== "disabled"
const isOptedIn = newSetting === "enabled"
const wasPreviouslyOptedIn = previousSetting === "enabled"

// Neither condition should be met
if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) {
Expand All @@ -114,14 +117,13 @@ describe("Telemetry Settings Tracking", () => {
expect(mockTelemetryService.updateTelemetryState).toHaveBeenCalledWith(true)
})

it("should fire event when going from unset to enabled (telemetry banner close)", () => {
it("should fire event AFTER enabling when going from unset to enabled (explicit opt-in)", () => {
const previousSetting = "unset" as TelemetrySetting
const newSetting = "enabled" as TelemetrySetting

const isOptedIn = newSetting !== "disabled"
const wasPreviouslyOptedIn = previousSetting !== "disabled"
const isOptedIn = newSetting === "enabled"
const wasPreviouslyOptedIn = previousSetting === "enabled"

// For unset -> enabled, both are opted in, so no event should fire
if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) {
TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, newSetting)
}
Expand All @@ -132,8 +134,13 @@ describe("Telemetry Settings Tracking", () => {
TelemetryService.instance.captureTelemetrySettingsChanged(previousSetting, newSetting)
}

// unset is treated as opted-in, so no event should fire
expect(mockTelemetryService.captureTelemetrySettingsChanged).not.toHaveBeenCalled()
// "unset" is off under opt-in semantics, so enabling is a real
// transition and the event fires after telemetry is turned on.
expect(mockTelemetryService.updateTelemetryState).toHaveBeenCalledWith(true)
expect(mockTelemetryService.captureTelemetrySettingsChanged).toHaveBeenCalledWith("unset", "enabled")
expect(mockTelemetryService.updateTelemetryState).toHaveBeenCalledBefore(
mockTelemetryService.captureTelemetrySettingsChanged,
)
})
})

Expand Down
10 changes: 6 additions & 4 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,10 +633,11 @@ export const webviewMessageHandler = async (
),
)

// Enable telemetry by default (when unset) or when explicitly enabled
// Telemetry is opt-in: only enabled when the user explicitly turned it on.
// "unset" (the default) and "disabled" both keep telemetry off.
await provider.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
const isOptedIn = telemetrySetting === "enabled"
TelemetryService.instance.updateTelemetryState(isOptedIn)
})

Expand Down Expand Up @@ -2569,8 +2570,9 @@ export const webviewMessageHandler = async (
case "telemetrySetting": {
const telemetrySetting = message.text as TelemetrySetting
const previousSetting = getGlobalState("telemetrySetting") || "unset"
const isOptedIn = telemetrySetting !== "disabled"
const wasPreviouslyOptedIn = previousSetting !== "disabled"
// Opt-in semantics: only an explicit "enabled" turns telemetry on.
const isOptedIn = telemetrySetting === "enabled"
const wasPreviouslyOptedIn = previousSetting === "enabled"

// If turning telemetry OFF, fire event BEFORE disabling
if (wasPreviouslyOptedIn && !isOptedIn && TelemetryService.hasInstance()) {
Expand Down
8 changes: 8 additions & 0 deletions webview-ui/src/__tests__/TelemetryClient.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ describe("TelemetryClient", () => {
// Assert
expect(posthog.init).not.toHaveBeenCalled()
})

it("doesn't initialize PostHog when telemetry is unset even with an API key and distinct ID", () => {
// Regression test for opt-in semantics: the default "unset" state must
// stay off even when a build ships a real key.
telemetryClient.updateTelemetryState("unset", "test-key", "test-user")

expect(posthog.init).not.toHaveBeenCalled()
})
})

/**
Expand Down
4 changes: 3 additions & 1 deletion webview-ui/src/components/common/TelemetryBanner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ const TelemetryBanner = () => {

const handleClose = () => {
setIsDismissed(true)
vscode.postMessage({ type: "telemetrySetting", text: "enabled" satisfies TelemetrySetting })
// Telemetry is opt-in; dismissing the banner records an explicit decline
// rather than silently opting the user in.
vscode.postMessage({ type: "telemetrySetting", text: "disabled" satisfies TelemetrySetting })
}

const handleOpenSettings = () => {
Expand Down
4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/ca/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/de/welcome.json

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

2 changes: 1 addition & 1 deletion webview-ui/src/i18n/locales/en/welcome.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"telemetry": {
"helpImprove": "Help Improve Bolt Code",
"helpImproveMessage": "Bolt Code collects error and usage data to help us fix bugs and improve the extension. This telemetry does not collect code, prompts or personal information. You can turn this off in <settingsLink>settings</settingsLink>."
"helpImproveMessage": "Telemetry is off by default. If you like, you can help us fix bugs and improve the extension by enabling anonymous error and usage reporting in <settingsLink>settings</settingsLink>. No code, prompts, or personal information is ever collected."
},
"importSettings": "Import Settings"
}
4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/es/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/fr/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/hi/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/id/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/it/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/ja/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/ko/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/nl/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/pl/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/pt-BR/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/ru/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/tr/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/vi/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/zh-CN/welcome.json

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

4 changes: 0 additions & 4 deletions webview-ui/src/i18n/locales/zh-TW/welcome.json

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

3 changes: 2 additions & 1 deletion webview-ui/src/utils/TelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class TelemetryClient {
public updateTelemetryState(telemetrySetting: TelemetrySetting, apiKey?: string, distinctId?: string) {
posthog.reset()

if (telemetrySetting !== "disabled" && apiKey && distinctId) {
// Opt-in semantics: only an explicit "enabled" turns telemetry on.
if (telemetrySetting === "enabled" && apiKey && distinctId) {
TelemetryClient.telemetryEnabled = true

posthog.init(apiKey, {
Expand Down
Loading