Skip to content

Commit d191fe0

Browse files
JamesRobert20James Mtendamemataltasroomote
authored
feat: added log requests to backend for observability (#32)
* feat: added log requests to backend for observability * Address pr feedback v1 * minor fixes * addressed PR comments * fixed disconnect flow * fix: address observability auth follow-ups * Added image fix * fix(auth): clear stale token and user info on validation failures; emit correct telemetry status * Addressed final comments --------- Co-authored-by: James Mtendamema <jmtendamema@geologicai.com> Co-authored-by: T <taltas@users.noreply.github.com> Co-authored-by: Roomote <roomote@roocode.com>
1 parent 6735476 commit d191fe0

33 files changed

Lines changed: 1579 additions & 15 deletions

PRIVACY.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Roo Code respects your privacy and is committed to transparency about how we han
1111
- **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. If you choose Roo Code Cloud as the provider (proxy mode), prompts may transit Roo Code servers only to forward them to the upstream model and are not stored.
1212
- **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.
1313
- **Telemetry (Usage Data)**: We collect anonymous feature usage and error data to help us improve Roo Code. This telemetry is powered by PostHog and includes your VS Code machine ID, feature usage patterns, and exception reports. This telemetry does **not** collect personally identifiable information, your code, or AI prompts. You can opt out of this telemetry at any time through the settings.
14+
- **Zoo Code Observability (Authenticated Subscribers Only):** If you sign in to Zoo Code and have an active subscription, Zoo Code will send LLM usage telemetry to the Zoo Code backend (zoocode.dev). This includes task ID, AI provider name, model name, token counts (input/output/cache), and estimated cost. This data is linked to your authenticated Zoo Code account. You can stop this collection at any time by signing out via the Zoo Code badge in the chat area.
1415
- **Marketplace Requests**: When you browse or search the Marketplace for Model Configuration Profiles (MCPs) or Custom Modes, Roo Code makes a secure API call to Roo Code's backend servers to retrieve listing information. These requests send only the query parameters (e.g., extension version, search term) necessary to fulfill the request and do not include your code, prompts, or personally identifiable information.
1516

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

packages/types/src/vscode-extension-host.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,12 @@ export type ExtensionState = Pick<
372372
mdmCompliant?: boolean
373373
taskSyncEnabled: boolean
374374
openAiCodexIsAuthenticated?: boolean
375+
zooCodeIsAuthenticated?: boolean
376+
zooCodeUserName?: string
377+
zooCodeUserEmail?: string
378+
zooCodeUserImage?: string
379+
zooCodeBaseUrl?: string
380+
deviceName?: string
375381
debug?: boolean
376382

377383
/**
@@ -505,6 +511,7 @@ export interface WebviewMessage {
505511
| "rooCloudManualUrl"
506512
| "openAiCodexSignIn"
507513
| "openAiCodexSignOut"
514+
| "zooCodeSignOut"
508515
| "switchOrganization"
509516
| "condenseTaskContextRequest"
510517
| "requestIndexingStatus"

src/activate/__tests__/handleUri.spec.ts

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,22 +6,39 @@ vi.mock("vscode", () => ({
66

77
import * as vscode from "vscode"
88

9-
import { handleUri } from "../handleUri"
9+
const { mockGetVisibleInstance, mockHandleZooCodeAuthCallback, mockSetZooCodeUserInfo, mockVisibleProvider } =
10+
vi.hoisted(() => {
11+
const mockVisibleProvider = {
12+
handleOpenRouterCallback: vi.fn(),
13+
handleRequestyCallback: vi.fn(),
14+
handleZooCodeCallback: vi.fn(),
15+
} as any
1016

11-
const mockVisibleProvider = {
12-
handleOpenRouterCallback: vi.fn(),
13-
handleRequestyCallback: vi.fn(),
14-
} as any
17+
return {
18+
mockGetVisibleInstance: vi.fn(() => mockVisibleProvider),
19+
mockHandleZooCodeAuthCallback: vi.fn(),
20+
mockSetZooCodeUserInfo: vi.fn(),
21+
mockVisibleProvider,
22+
}
23+
})
1524

1625
vi.mock("../../core/webview/ClineProvider", () => ({
1726
ClineProvider: {
18-
getVisibleInstance: vi.fn(() => mockVisibleProvider),
27+
getVisibleInstance: mockGetVisibleInstance,
1928
},
2029
}))
2130

31+
vi.mock("../../services/zoo-code-auth", () => ({
32+
handleAuthCallback: mockHandleZooCodeAuthCallback,
33+
setZooCodeUserInfo: mockSetZooCodeUserInfo,
34+
}))
35+
36+
import { handleUri } from "../handleUri"
37+
2238
describe("handleUri", () => {
2339
beforeEach(() => {
2440
vi.clearAllMocks()
41+
mockGetVisibleInstance.mockReturnValue(mockVisibleProvider)
2542
})
2643

2744
it("ignores legacy cloud auth callback", async () => {
@@ -36,4 +53,67 @@ describe("handleUri", () => {
3653
"Roo Code Cloud sign-in is currently unavailable. Configure another provider to continue.",
3754
)
3855
})
56+
57+
it("stores callback user info even when no webview is visible", async () => {
58+
mockGetVisibleInstance.mockReturnValue(null)
59+
mockHandleZooCodeAuthCallback.mockResolvedValue(true)
60+
61+
await handleUri({
62+
path: "/auth-callback",
63+
query: "token=zoo_ext_test_token&name=Jane%20Doe&email=jane%40example.com&image=https%3A%2F%2Fexample.com%2Favatar.png",
64+
} as any)
65+
66+
expect(mockHandleZooCodeAuthCallback).toHaveBeenCalledWith("zoo_ext_test_token")
67+
expect(mockSetZooCodeUserInfo).toHaveBeenCalledWith({
68+
name: "Jane Doe",
69+
email: "jane@example.com",
70+
image: "https://example.com/avatar.png",
71+
})
72+
expect(mockVisibleProvider.handleZooCodeCallback).not.toHaveBeenCalled()
73+
})
74+
75+
it("refreshes the visible provider after a successful auth callback", async () => {
76+
mockHandleZooCodeAuthCallback.mockResolvedValue(true)
77+
78+
await handleUri({
79+
path: "/auth-callback",
80+
query: "token=zoo_ext_test_token",
81+
} as any)
82+
83+
// When no user info is provided, null values are passed to clear stale data
84+
expect(mockSetZooCodeUserInfo).toHaveBeenCalledWith({
85+
name: null,
86+
email: null,
87+
image: null,
88+
})
89+
expect(mockVisibleProvider.handleZooCodeCallback).toHaveBeenCalledWith("zoo_ext_test_token")
90+
})
91+
92+
it("clears stale user info fields when re-authing with missing fields", async () => {
93+
mockHandleZooCodeAuthCallback.mockResolvedValue(true)
94+
95+
// Re-auth with only name - email and image should be cleared
96+
await handleUri({
97+
path: "/auth-callback",
98+
query: "token=zoo_ext_test_token&name=John%20Doe",
99+
} as any)
100+
101+
expect(mockSetZooCodeUserInfo).toHaveBeenCalledWith({
102+
name: "John Doe",
103+
email: null,
104+
image: null,
105+
})
106+
})
107+
108+
it("does not persist user info when auth callback validation fails", async () => {
109+
mockHandleZooCodeAuthCallback.mockResolvedValue(false)
110+
111+
await handleUri({
112+
path: "/auth-callback",
113+
query: "token=zoo_ext_test_token&name=Jane%20Doe",
114+
} as any)
115+
116+
expect(mockSetZooCodeUserInfo).not.toHaveBeenCalled()
117+
expect(mockVisibleProvider.handleZooCodeCallback).not.toHaveBeenCalled()
118+
})
39119
})

src/activate/handleUri.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,24 @@ import * as vscode from "vscode"
22

33
import { getRouterUnavailableSignInMessage } from "../core/config/routerRemoval"
44
import { ClineProvider } from "../core/webview/ClineProvider"
5+
import { handleAuthCallback as handleZooCodeAuthCallback, setZooCodeUserInfo } from "../services/zoo-code-auth"
56

67
export const handleUri = async (uri: vscode.Uri) => {
78
const path = uri.path
89
const query = new URLSearchParams(uri.query.replace(/\+/g, "%2B"))
910
const visibleProvider = ClineProvider.getVisibleInstance()
1011

11-
if (!visibleProvider) {
12-
return
13-
}
14-
1512
switch (path) {
1613
case "/openrouter": {
14+
if (!visibleProvider) return
1715
const code = query.get("code")
1816
if (code) {
1917
await visibleProvider.handleOpenRouterCallback(code)
2018
}
2119
break
2220
}
2321
case "/requesty": {
22+
if (!visibleProvider) return
2423
const code = query.get("code")
2524
const baseUrl = query.get("baseUrl")
2625
if (code) {
@@ -32,6 +31,33 @@ export const handleUri = async (uri: vscode.Uri) => {
3231
vscode.window.showInformationMessage(getRouterUnavailableSignInMessage())
3332
break
3433
}
34+
case "/auth-callback": {
35+
const token = query.get("token")
36+
if (token) {
37+
// Extract user info from callback URL params
38+
// URLSearchParams.get() already decodes percent-encoded values - no need for decodeURIComponent
39+
// Use null (not undefined) for missing values to actively clear stale data
40+
const name = query.get("name") ?? null
41+
const email = query.get("email") ?? null
42+
const image = query.get("image") ?? null
43+
44+
const success = await handleZooCodeAuthCallback(token)
45+
if (success) {
46+
// Store user info after successful auth validation (regardless of webview visibility)
47+
// Always call setZooCodeUserInfo to clear stale data when fields are missing
48+
await setZooCodeUserInfo({
49+
name,
50+
email,
51+
image,
52+
})
53+
// Refresh webview state if a panel is currently open
54+
if (visibleProvider) {
55+
await visibleProvider.handleZooCodeCallback(token)
56+
}
57+
}
58+
}
59+
break
60+
}
3561
default:
3662
break
3763
}

src/core/task/Task.ts

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3076,7 +3076,10 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
30763076
total: totalCost,
30773077
}
30783078

3079-
const drainStreamInBackgroundToFindAllUsage = async (apiReqIndex: number) => {
3079+
const drainStreamInBackgroundToFindAllUsage = async (
3080+
apiReqIndex: number,
3081+
status: "completed" | "cancelled" = "completed",
3082+
) => {
30803083
const timeoutMs = DEFAULT_USAGE_COLLECTION_TIMEOUT_MS
30813084
const startTime = performance.now()
30823085
const modelId = getModelId(this.apiConfiguration)
@@ -3098,6 +3101,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
30983101
total?: number
30993102
},
31003103
messageIndex: number = apiReqIndex,
3104+
status: "completed" | "cancelled" = "completed",
31013105
) => {
31023106
if (
31033107
tokens.input > 0 ||
@@ -3155,6 +3159,27 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
31553159
cacheReadTokens: tokens.cacheRead,
31563160
cost: tokens.total ?? costResult.totalCost,
31573161
})
3162+
3163+
// Zoo Code observability telemetry
3164+
import("../../services/zoo-telemetry")
3165+
.then(async ({ sendLlmTelemetry }) => {
3166+
const mode = await this.getTaskMode().catch(() => "unknown")
3167+
return sendLlmTelemetry({
3168+
taskId: this.taskId,
3169+
provider: this.apiConfiguration?.apiProvider ?? "unknown",
3170+
model: this.apiConfiguration
3171+
? (getModelId(this.apiConfiguration) ?? "unknown")
3172+
: "unknown",
3173+
mode,
3174+
inputTokens: costResult.totalInputTokens,
3175+
outputTokens: costResult.totalOutputTokens,
3176+
cacheReadTokens: tokens.cacheRead ?? 0,
3177+
cacheWriteTokens: tokens.cacheWrite ?? 0,
3178+
totalCost: tokens.total ?? costResult.totalCost,
3179+
status,
3180+
}).catch(() => {})
3181+
})
3182+
.catch(() => {})
31583183
}
31593184
}
31603185

@@ -3208,6 +3233,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
32083233
total: bgTotalCost,
32093234
},
32103235
lastApiReqIndex,
3236+
status,
32113237
)
32123238
} else {
32133239
console.warn(
@@ -3232,13 +3258,18 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
32323258
total: bgTotalCost,
32333259
},
32343260
lastApiReqIndex,
3261+
status,
32353262
)
32363263
}
32373264
}
32383265
}
32393266

32403267
// Start the background task and handle any errors
3241-
drainStreamInBackgroundToFindAllUsage(lastApiReqIndex).catch((error) => {
3268+
// Pass "cancelled" status if the task was aborted by the user
3269+
drainStreamInBackgroundToFindAllUsage(
3270+
lastApiReqIndex,
3271+
this.abort ? "cancelled" : "completed",
3272+
).catch((error) => {
32423273
console.error("Background usage collection failed:", error)
32433274
})
32443275
} catch (error) {

src/core/webview/ClineProvider.ts

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,8 +240,9 @@ export class ClineProvider
240240

241241
// Create named listener functions so we can remove them later.
242242
const onTaskStarted = () => this.emit(RooCodeEventName.TaskStarted, instance.taskId)
243-
const onTaskCompleted = (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) =>
243+
const onTaskCompleted = (taskId: string, tokenUsage: TokenUsage, toolUsage: ToolUsage) => {
244244
this.emit(RooCodeEventName.TaskCompleted, taskId, tokenUsage, toolUsage)
245+
}
245246
const onTaskAborted = async () => {
246247
this.emit(RooCodeEventName.TaskAborted, instance.taskId)
247248

@@ -1197,7 +1198,7 @@ export class ClineProvider
11971198
"default-src 'none'",
11981199
`font-src ${webview.cspSource} data:`,
11991200
`style-src ${webview.cspSource} 'unsafe-inline' https://* http://${localServerUrl} http://0.0.0.0:${localPort}`,
1200-
`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:`,
1201+
`img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com https://avatars.githubusercontent.com https://lh3.googleusercontent.com data:`,
12011202
`media-src ${webview.cspSource}`,
12021203
`script-src 'unsafe-eval' ${webview.cspSource} https://* https://*.posthog.com http://${localServerUrl} http://0.0.0.0:${localPort} 'nonce-${nonce}'`,
12031204
`connect-src ${webview.cspSource} ${openRouterDomain} https://* https://*.posthog.com ws://${localServerUrl} ws://0.0.0.0:${localPort} http://${localServerUrl} http://0.0.0.0:${localPort}`,
@@ -1288,7 +1289,7 @@ export class ClineProvider
12881289
<meta charset="utf-8">
12891290
<meta name="viewport" content="width=device-width,initial-scale=1,shrink-to-fit=no">
12901291
<meta name="theme-color" content="#000000">
1291-
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://ph.roocode.com 'strict-dynamic'; connect-src ${webview.cspSource} ${openRouterDomain} https://api.requesty.ai https://ph.roocode.com;">
1292+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; font-src ${webview.cspSource} data:; style-src ${webview.cspSource} 'unsafe-inline'; img-src ${webview.cspSource} https://storage.googleapis.com https://img.clerk.com https://avatars.githubusercontent.com https://lh3.googleusercontent.com data:; media-src ${webview.cspSource}; script-src ${webview.cspSource} 'wasm-unsafe-eval' 'nonce-${nonce}' https://ph.roocode.com 'strict-dynamic'; connect-src ${webview.cspSource} ${openRouterDomain} https://api.requesty.ai https://ph.roocode.com;">
12921293
<link rel="stylesheet" type="text/css" href="${stylesUri}">
12931294
<link href="${codiconsUri}" rel="stylesheet" />
12941295
<script nonce="${nonce}">
@@ -1681,6 +1682,15 @@ export class ClineProvider
16811682
await this.upsertProviderProfile(currentApiConfigName, newConfiguration)
16821683
}
16831684

1685+
// Zoo Code Auth (for observability telemetry)
1686+
1687+
async handleZooCodeCallback(_token: string) {
1688+
// Auth mutation (token storage, subscription check, success toast) was already
1689+
// performed by handleAuthCallback() in handleUri.ts before this method was called.
1690+
// This method only needs to refresh the webview state to reflect the new auth status.
1691+
await this.postStateToWebview()
1692+
}
1693+
16841694
// Requesty
16851695

16861696
async handleRequestyCallback(code: string, baseUrl: string | null) {
@@ -2156,6 +2166,38 @@ export class ClineProvider
21562166
const mergedDeniedCommands = this.mergeDeniedCommands(deniedCommands)
21572167
const cwd = this.cwd
21582168
const currentTask = this.getCurrentTask()
2169+
let zooCodeState: {
2170+
zooCodeIsAuthenticated: boolean
2171+
zooCodeUserName: string | undefined
2172+
zooCodeUserEmail: string | undefined
2173+
zooCodeUserImage: string | undefined
2174+
zooCodeBaseUrl: string
2175+
deviceName: string
2176+
} = {
2177+
zooCodeIsAuthenticated: false,
2178+
zooCodeUserName: undefined,
2179+
zooCodeUserEmail: undefined,
2180+
zooCodeUserImage: undefined,
2181+
zooCodeBaseUrl: "https://www.zoocode.dev",
2182+
deviceName: os.hostname(),
2183+
}
2184+
2185+
try {
2186+
const { isZooCodeAuthenticated, getCachedZooCodeUserInfo, getZooCodeBaseUrl } = await import(
2187+
"../../services/zoo-code-auth"
2188+
)
2189+
const userInfo = getCachedZooCodeUserInfo()
2190+
zooCodeState = {
2191+
zooCodeIsAuthenticated: await isZooCodeAuthenticated(),
2192+
zooCodeUserName: userInfo.name,
2193+
zooCodeUserEmail: userInfo.email,
2194+
zooCodeUserImage: userInfo.image,
2195+
zooCodeBaseUrl: getZooCodeBaseUrl(),
2196+
deviceName: os.hostname(),
2197+
}
2198+
} catch {
2199+
// Keep the default unauthenticated state if the optional Zoo Code auth service is unavailable.
2200+
}
21592201

21602202
return {
21612203
version: this.context.extension?.packageJSON?.version ?? "",
@@ -2279,6 +2321,7 @@ export class ClineProvider
22792321
return false
22802322
}
22812323
})(),
2324+
...zooCodeState,
22822325
debug: vscode.workspace.getConfiguration(Package.name).get<boolean>("debug", false),
22832326
}
22842327
}

src/core/webview/webviewMessageHandler.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2421,6 +2421,18 @@ export const webviewMessageHandler = async (
24212421
await provider.postStateToWebview()
24222422
break
24232423
}
2424+
case "zooCodeSignOut": {
2425+
try {
2426+
const { disconnectZooCode } = await import("../../services/zoo-code-auth")
2427+
await disconnectZooCode()
2428+
await provider.postStateToWebview()
2429+
} catch (error) {
2430+
provider.log(
2431+
`Failed to sign out of Zoo Code: ${error instanceof Error ? error.message : String(error)}`,
2432+
)
2433+
}
2434+
break
2435+
}
24242436
case "switchOrganization": {
24252437
try {
24262438
const organizationId = message.organizationId ?? null

0 commit comments

Comments
 (0)