Skip to content

Commit 8709004

Browse files
committed
fix(telemetry): address parallel-review findings on consent PR
1 parent fd2df21 commit 8709004

5 files changed

Lines changed: 88 additions & 7 deletions

File tree

PRIVACY.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ go—and, importantly, where they don't.
3939
some jurisdictions; we use it only for product analytics and error grouping.
4040
We retain telemetry only as long as needed for product analytics and
4141
debugging. This PostHog-based telemetry does **not** collect your code or AI
42-
prompts, and you can opt out at any time through the settings.
42+
prompts. Telemetry is on by default. To turn it off, choose "Disabled" in
43+
the settings or in the notice shown on first use; closing that notice
44+
without a choice leaves telemetry on. Telemetry also follows VS Code's own
45+
global telemetry setting: if you turn that off, Zoo Code stops sending
46+
telemetry right away, even if the Zoo Code setting says "Enabled."
4347
- **Marketplace Requests**: When you browse or search the Marketplace for Model
4448
Configuration Profiles (MCPs) or Custom Modes, Zoo Code makes a secure API
4549
call to Zoo Code's backend servers to retrieve listing information. These

src/__tests__/extension.spec.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const mockTelemetryServiceInstance = {
8484
vi.mock("@roo-code/telemetry", () => ({
8585
TelemetryService: {
8686
createInstance: vi.fn().mockReturnValue(mockTelemetryServiceInstance),
87+
hasInstance: vi.fn().mockReturnValue(true),
8788
get instance() {
8889
return mockTelemetryServiceInstance
8990
},
@@ -461,5 +462,42 @@ describe("extension.ts", () => {
461462

462463
setTerminalProfileSpy.mockRestore()
463464
})
465+
466+
// Review finding: every other TelemetryService call site touched by this PR checks
467+
// hasInstance() first; deactivate()'s shutdown call didn't. Not a crash today (the mock
468+
// always resolves), but TelemetryService.instance throws for real if no instance exists,
469+
// so the guard keeps this call site consistent with the rest of the file.
470+
test("does not touch TelemetryService.instance when no instance exists", async () => {
471+
const { TelemetryService } = await import("@roo-code/telemetry")
472+
const { Terminal } = await import("../integrations/terminal/Terminal")
473+
const { TerminalRegistry } = await import("../integrations/terminal/TerminalRegistry")
474+
475+
const setTerminalProfileSpy = vi.spyOn(Terminal, "setTerminalProfile")
476+
477+
const { activate, deactivate } = await import("../extension")
478+
await activate(mockContext)
479+
480+
// Flip to false only after activate() completes, so this only exercises
481+
// deactivate()'s own guard rather than any hasInstance() check during activation.
482+
vi.mocked(TelemetryService.hasInstance).mockReturnValue(false)
483+
484+
// Model the real singleton failure mode: TelemetryService.instance throws when no
485+
// instance exists. If deactivate()'s hasInstance() guard were ever removed, this
486+
// throw would surface instead of the assertion below silently passing regardless.
487+
const instanceGetterSpy = vi.spyOn(TelemetryService, "instance", "get").mockImplementation(() => {
488+
throw new Error("TelemetryService not initialized")
489+
})
490+
491+
await expect(deactivate()).resolves.toBeUndefined()
492+
493+
expect(instanceGetterSpy).not.toHaveBeenCalled()
494+
expect(mockTelemetryServiceInstance.shutdown).not.toHaveBeenCalled()
495+
expect(setTerminalProfileSpy).toHaveBeenCalledWith(undefined)
496+
expect(TerminalRegistry.cleanup).toHaveBeenCalledTimes(1)
497+
498+
instanceGetterSpy.mockRestore()
499+
500+
setTerminalProfileSpy.mockRestore()
501+
})
464502
})
465503
})

src/core/webview/__tests__/webviewMessageHandler.spec.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1930,6 +1930,37 @@ describe("webviewMessageHandler - telemetrySetting", () => {
19301930
expect(calls.at(-1)).toEqual([false])
19311931
})
19321932

1933+
// Review finding: webviewDidLaunch's queued telemetry update wasn't awaited by the
1934+
// "webviewDidLaunch" case, so a thrown error inside it was only ever caught by a later,
1935+
// unrelated queue link's leading .catch(() => undefined) -- silently swallowed rather than
1936+
// logged. Now awaited with its own .catch that logs via provider.log.
1937+
it("logs an error via provider.log if the queued telemetry init throws on launch", async () => {
1938+
const { TelemetryService } = await import("@roo-code/telemetry")
1939+
vi.mocked(TelemetryService.hasInstance).mockReturnValue(true)
1940+
1941+
vi.mocked(mockClineProvider.contextProxy.getValue).mockImplementation((key: string) => {
1942+
if (key === "telemetrySetting") {
1943+
throw new Error("contextProxy read failed")
1944+
}
1945+
return undefined
1946+
})
1947+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([])
1948+
const providerForLaunch = mockClineProvider as unknown as {
1949+
getMcpHub: ReturnType<typeof vi.fn>
1950+
providerSettingsManager: { listConfig: ReturnType<typeof vi.fn> }
1951+
getStateToPostToWebview: ReturnType<typeof vi.fn>
1952+
}
1953+
providerForLaunch.getMcpHub = vi.fn().mockReturnValue(undefined)
1954+
providerForLaunch.providerSettingsManager = { listConfig: vi.fn().mockResolvedValue(undefined) }
1955+
providerForLaunch.getStateToPostToWebview = vi.fn().mockResolvedValue({ telemetrySetting: "unset" })
1956+
1957+
await webviewMessageHandler(mockClineProvider, { type: "webviewDidLaunch" })
1958+
1959+
expect(mockClineProvider.log).toHaveBeenCalledWith(
1960+
expect.stringContaining("Error initializing telemetry state on launch"),
1961+
)
1962+
})
1963+
19331964
// CodeRabbit finding: webviewDidLaunch's queued telemetry update called
19341965
// TelemetryService.instance directly, unlike the "telemetrySetting" case a few lines
19351966
// below which checks hasInstance() first. If webviewDidLaunch fires before the service

src/core/webview/webviewMessageHandler.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,12 @@ export const webviewMessageHandler = async (
669669
)
670670
})
671671

672+
await telemetrySettingQueue.catch((error) =>
673+
provider.log(
674+
`Error initializing telemetry state on launch: ${error instanceof Error ? error.message : String(error)}`,
675+
),
676+
)
677+
672678
provider.isViewLaunched = true
673679
break
674680
case "newTask":

src/extension.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -413,12 +413,14 @@ export async function deactivate() {
413413

414414
await McpServerManager.cleanup(extensionContext)
415415

416-
try {
417-
await TelemetryService.instance.shutdown()
418-
} catch (error) {
419-
outputChannel.appendLine(
420-
`Failed to shut down telemetry service: ${error instanceof Error ? error.message : String(error)}`,
421-
)
416+
if (TelemetryService.hasInstance()) {
417+
try {
418+
await TelemetryService.instance.shutdown()
419+
} catch (error) {
420+
outputChannel.appendLine(
421+
`Failed to shut down telemetry service: ${error instanceof Error ? error.message : String(error)}`,
422+
)
423+
}
422424
}
423425

424426
Terminal.setTerminalProfile(undefined)

0 commit comments

Comments
 (0)