Skip to content

Commit 11811ec

Browse files
committed
fix(webview): serialize webviewDidLaunch telemetry init, polish locale copy
1 parent ded1b6d commit 11811ec

10 files changed

Lines changed: 88 additions & 14 deletions

File tree

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

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ vi.mock("../../../api/providers/fetchers/lmstudio", () => ({
1111
getLMStudioModels: vi.fn(),
1212
}))
1313

14+
vi.mock("../../../integrations/theme/getTheme", () => ({
15+
getTheme: vi.fn().mockResolvedValue({}),
16+
}))
17+
1418
vi.mock("../../../integrations/openai-codex/oauth", () => ({
1519
openAiCodexOAuthManager: {
1620
getAccessToken: vi.fn(),
@@ -1533,4 +1537,64 @@ describe("webviewMessageHandler - telemetrySetting", () => {
15331537
const calls = vi.mocked(TelemetryService.instance.updateTelemetryState).mock.calls
15341538
expect(calls.at(-1)).toEqual([true])
15351539
})
1540+
1541+
// CodeRabbit follow-up on the finding #12 fix: webviewDidLaunch's telemetry init read state
1542+
// via an async provider.getStateToPostToWebview().then(...) continuation, outside
1543+
// telemetrySettingQueue -- so it could resolve after a concurrent "telemetrySetting" message
1544+
// and clobber that message's queued (correct) update with a stale value. webviewDidLaunch now
1545+
// reads getGlobalState synchronously and is routed through the same queue.
1546+
it("does not let webviewDidLaunch's telemetry init race and clobber a concurrent telemetrySetting message", async () => {
1547+
const { TelemetryService } = await import("@roo-code/telemetry")
1548+
vi.mocked(TelemetryService.hasInstance).mockReturnValue(true)
1549+
vi.mocked(vscode.env).isTelemetryEnabled = true
1550+
1551+
// webviewDidLaunch starts out "unset" (disclosed opt-out default -- opted in).
1552+
let storedSetting: string | undefined = "unset"
1553+
vi.mocked(mockClineProvider.contextProxy.getValue).mockImplementation(() => storedSetting)
1554+
vi.mocked(mockClineProvider.contextProxy.setValue).mockImplementation(async (_key, value) => {
1555+
storedSetting = value as string
1556+
})
1557+
1558+
vi.mocked(mockClineProvider.customModesManager.getCustomModes).mockResolvedValue([])
1559+
;(mockClineProvider as any).getMcpHub = vi.fn().mockReturnValue(undefined)
1560+
;(mockClineProvider as any).providerSettingsManager = {
1561+
listConfig: vi.fn().mockResolvedValue(undefined),
1562+
}
1563+
// Only exercised by the pre-fix code path (provider.getStateToPostToWebview().then(...));
1564+
// snapshots storedSetting at call time (mirroring the real ClineProvider building its
1565+
// state object synchronously before any internal awaits) and resolves slowly, so a
1566+
// concurrent write that lands during that delay is a genuinely stale read by the time
1567+
// the .then() callback runs -- reproducing the pre-fix race.
1568+
;(mockClineProvider as any).getStateToPostToWebview = vi.fn().mockImplementation(async () => {
1569+
const snapshot = storedSetting
1570+
await new Promise((resolve) => setTimeout(resolve, 10))
1571+
return { telemetrySetting: snapshot }
1572+
})
1573+
1574+
// webviewDidLaunch fires first (e.g. webview reload) -- its telemetry init is now queued
1575+
// behind telemetrySettingQueue rather than resolving independently.
1576+
const launch = webviewMessageHandler(mockClineProvider, { type: "webviewDidLaunch" } as any)
1577+
1578+
// A concurrent "telemetrySetting" message turns telemetry off, arriving just after
1579+
// webviewDidLaunch has taken its (pre-fix) state snapshot but well before the slow
1580+
// getStateToPostToWebview() mock resolves -- reproducing the window where that snapshot
1581+
// is stale by the time its .then() callback actually runs.
1582+
const disable = new Promise((resolve) => setTimeout(resolve, 2)).then(() =>
1583+
webviewMessageHandler(mockClineProvider, { type: "telemetrySetting", text: "disabled" }),
1584+
)
1585+
1586+
await Promise.all([launch, disable])
1587+
1588+
// webviewDidLaunch's telemetry init is fire-and-forget from the handler's own point of
1589+
// view (the "webviewDidLaunch" case doesn't await it), so waiting on `launch` alone isn't
1590+
// enough to observe its trailing continuation -- give the slow getStateToPostToWebview()
1591+
// mock time to resolve too.
1592+
await new Promise((resolve) => setTimeout(resolve, 50))
1593+
1594+
// The user's explicit "disabled" choice must be the final state -- webviewDidLaunch's
1595+
// queued re-application of the (by-then-stale) "unset"/opted-in state must not run after
1596+
// and override it.
1597+
const calls = vi.mocked(TelemetryService.instance.updateTelemetryState).mock.calls
1598+
expect(calls.at(-1)).toEqual([false])
1599+
})
15361600
})

src/core/webview/webviewMessageHandler.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -643,12 +643,22 @@ export const webviewMessageHandler = async (
643643
// vscode.env.isTelemetryEnabled is ANDed in (matching extension.ts's
644644
// onDidChangeTelemetryEnabled listener) so a webview reload can't re-enable
645645
// telemetry while VS Code's global toggle is off.
646-
provider.getStateToPostToWebview().then((state) => {
647-
const { telemetrySetting } = state
648-
TelemetryService.instance.updateTelemetryState(
649-
isTelemetryOptedIn(telemetrySetting) && vscode.env.isTelemetryEnabled,
650-
)
651-
})
646+
//
647+
// Read the setting synchronously via getGlobalState (same as the "telemetrySetting"
648+
// handler below) rather than awaiting provider.getStateToPostToWebview() -- that
649+
// async gap let this continuation resolve after a concurrent "telemetrySetting"
650+
// message's queued update and clobber it with a stale value, the same interleaving
651+
// class of bug telemetrySettingQueue exists to prevent. Routing through the queue
652+
// here too means webviewDidLaunch can't race a concurrent telemetrySetting message
653+
// either.
654+
telemetrySettingQueue = telemetrySettingQueue
655+
.catch(() => undefined)
656+
.then(async () => {
657+
const telemetrySetting = getGlobalState("telemetrySetting") || "unset"
658+
TelemetryService.instance.updateTelemetryState(
659+
isTelemetryOptedIn(telemetrySetting) && vscode.env.isTelemetryEnabled,
660+
)
661+
})
652662

653663
provider.isViewLaunched = true
654664
break

webview-ui/src/i18n/locales/fr/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/it/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/ja/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/ko/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/tr/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/vi/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/zh-CN/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/zh-TW/settings.json

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)