Skip to content

Commit 927e210

Browse files
authored
Cloud: add auth logs to the output channel (#4270)
1 parent c4dab9e commit 927e210

File tree

7 files changed

+170
-23
lines changed

7 files changed

+170
-23
lines changed

packages/cloud/src/AuthService.ts

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,17 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
3333
private context: vscode.ExtensionContext
3434
private timer: RefreshTimer
3535
private state: AuthState = "initializing"
36+
private log: (...args: unknown[]) => void
3637

3738
private credentials: AuthCredentials | null = null
3839
private sessionToken: string | null = null
3940
private userInfo: CloudUserInfo | null = null
4041

41-
constructor(context: vscode.ExtensionContext) {
42+
constructor(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) {
4243
super()
4344

4445
this.context = context
46+
this.log = log || console.log
4547

4648
this.timer = new RefreshTimer({
4749
callback: async () => {
@@ -72,7 +74,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
7274
}
7375
}
7476
} catch (error) {
75-
console.error("[auth] Error handling credentials change:", error)
77+
this.log("[auth] Error handling credentials change:", error)
7678
}
7779
}
7880

@@ -88,7 +90,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
8890

8991
this.emit("logged-out", { previousState })
9092

91-
console.log("[auth] Transitioned to logged-out state")
93+
this.log("[auth] Transitioned to logged-out state")
9294
}
9395

9496
private transitionToInactiveSession(credentials: AuthCredentials): void {
@@ -104,7 +106,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
104106

105107
this.timer.start()
106108

107-
console.log("[auth] Transitioned to inactive-session state")
109+
this.log("[auth] Transitioned to inactive-session state")
108110
}
109111

110112
/**
@@ -115,7 +117,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
115117
*/
116118
public async initialize(): Promise<void> {
117119
if (this.state !== "initializing") {
118-
console.log("[auth] initialize() called after already initialized")
120+
this.log("[auth] initialize() called after already initialized")
119121
return
120122
}
121123

@@ -143,9 +145,9 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
143145
return authCredentialsSchema.parse(parsedJson)
144146
} catch (error) {
145147
if (error instanceof z.ZodError) {
146-
console.error("[auth] Invalid credentials format:", error.errors)
148+
this.log("[auth] Invalid credentials format:", error.errors)
147149
} else {
148-
console.error("[auth] Failed to parse stored credentials:", error)
150+
this.log("[auth] Failed to parse stored credentials:", error)
149151
}
150152
return null
151153
}
@@ -176,7 +178,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
176178
const url = `${getRooCodeApiUrl()}/extension/sign-in?${params.toString()}`
177179
await vscode.env.openExternal(vscode.Uri.parse(url))
178180
} catch (error) {
179-
console.error(`[auth] Error initiating Roo Code Cloud auth: ${error}`)
181+
this.log(`[auth] Error initiating Roo Code Cloud auth: ${error}`)
180182
throw new Error(`Failed to initiate Roo Code Cloud authentication: ${error}`)
181183
}
182184
}
@@ -201,7 +203,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
201203
const storedState = this.context.globalState.get(AUTH_STATE_KEY)
202204

203205
if (state !== storedState) {
204-
console.log("[auth] State mismatch in callback")
206+
this.log("[auth] State mismatch in callback")
205207
throw new Error("Invalid state parameter. Authentication request may have been tampered with.")
206208
}
207209

@@ -210,9 +212,9 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
210212
await this.storeCredentials(credentials)
211213

212214
vscode.window.showInformationMessage("Successfully authenticated with Roo Code Cloud")
213-
console.log("[auth] Successfully authenticated with Roo Code Cloud")
215+
this.log("[auth] Successfully authenticated with Roo Code Cloud")
214216
} catch (error) {
215-
console.log(`[auth] Error handling Roo Code Cloud callback: ${error}`)
217+
this.log(`[auth] Error handling Roo Code Cloud callback: ${error}`)
216218
const previousState = this.state
217219
this.state = "logged-out"
218220
this.emit("logged-out", { previousState })
@@ -237,14 +239,14 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
237239
try {
238240
await this.clerkLogout(oldCredentials)
239241
} catch (error) {
240-
console.error("[auth] Error calling clerkLogout:", error)
242+
this.log("[auth] Error calling clerkLogout:", error)
241243
}
242244
}
243245

244246
vscode.window.showInformationMessage("Logged out from Roo Code Cloud")
245-
console.log("[auth] Logged out from Roo Code Cloud")
247+
this.log("[auth] Logged out from Roo Code Cloud")
246248
} catch (error) {
247-
console.log(`[auth] Error logging out from Roo Code Cloud: ${error}`)
249+
this.log(`[auth] Error logging out from Roo Code Cloud: ${error}`)
248250
throw new Error(`Failed to log out from Roo Code Cloud: ${error}`)
249251
}
250252
}
@@ -281,7 +283,7 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
281283
*/
282284
private async refreshSession(): Promise<void> {
283285
if (!this.credentials) {
284-
console.log("[auth] Cannot refresh session: missing credentials")
286+
this.log("[auth] Cannot refresh session: missing credentials")
285287
this.state = "inactive-session"
286288
return
287289
}
@@ -292,12 +294,12 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
292294
this.state = "active-session"
293295

294296
if (previousState !== "active-session") {
295-
console.log("[auth] Transitioned to active-session state")
297+
this.log("[auth] Transitioned to active-session state")
296298
this.emit("active-session", { previousState })
297299
this.fetchUserInfo()
298300
}
299301
} catch (error) {
300-
console.error("[auth] Failed to refresh session", error)
302+
this.log("[auth] Failed to refresh session", error)
301303
throw error
302304
}
303305
}
@@ -446,12 +448,12 @@ export class AuthService extends EventEmitter<AuthServiceEvents> {
446448
return this._instance
447449
}
448450

449-
static async createInstance(context: vscode.ExtensionContext) {
451+
static async createInstance(context: vscode.ExtensionContext, log?: (...args: unknown[]) => void) {
450452
if (this._instance) {
451453
throw new Error("AuthService instance already created")
452454
}
453455

454-
this._instance = new AuthService(context)
456+
this._instance = new AuthService(context, log)
455457
await this._instance.initialize()
456458
return this._instance
457459
}

packages/cloud/src/CloudService.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ export class CloudService {
1818
private settingsService: SettingsService | null = null
1919
private telemetryClient: TelemetryClient | null = null
2020
private isInitialized = false
21+
private log: (...args: unknown[]) => void
2122

2223
private constructor(context: vscode.ExtensionContext, callbacks: CloudServiceCallbacks) {
2324
this.context = context
2425
this.callbacks = callbacks
26+
this.log = callbacks.log || console.log
2527
this.authListener = () => {
2628
this.callbacks.stateChanged?.()
2729
}
@@ -33,7 +35,7 @@ export class CloudService {
3335
}
3436

3537
try {
36-
this.authService = await AuthService.createInstance(this.context)
38+
this.authService = await AuthService.createInstance(this.context, this.log)
3739

3840
this.authService.on("inactive-session", this.authListener)
3941
this.authService.on("active-session", this.authListener)
@@ -49,12 +51,12 @@ export class CloudService {
4951
try {
5052
TelemetryService.instance.register(this.telemetryClient)
5153
} catch (error) {
52-
console.warn("[CloudService] Failed to register TelemetryClient:", error)
54+
this.log("[CloudService] Failed to register TelemetryClient:", error)
5355
}
5456

5557
this.isInitialized = true
5658
} catch (error) {
57-
console.error("[CloudService] Failed to initialize:", error)
59+
this.log("[CloudService] Failed to initialize:", error)
5860
throw new Error(`Failed to initialize CloudService: ${error}`)
5961
}
6062
}

packages/cloud/src/__tests__/CloudService.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,7 @@ describe("CloudService", () => {
135135
const cloudService = await CloudService.createInstance(mockContext, callbacks)
136136

137137
expect(cloudService).toBeInstanceOf(CloudService)
138-
expect(AuthService.createInstance).toHaveBeenCalledWith(mockContext)
138+
expect(AuthService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
139139
expect(SettingsService.createInstance).toHaveBeenCalledWith(mockContext, expect.any(Function))
140140
})
141141

packages/cloud/src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export interface CloudServiceCallbacks {
22
stateChanged?: () => void
3+
log?: (...args: unknown[]) => void
34
}

src/extension.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { CloudService } from "@roo-code/cloud"
1616
import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
1717

1818
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
19+
import { createOutputChannelLogger, createDualLogger } from "./utils/outputChannelLogger"
1920

2021
import { Package } from "./shared/package"
2122
import { formatLanguage } from "./shared/language"
@@ -68,9 +69,13 @@ export async function activate(context: vscode.ExtensionContext) {
6869
console.warn("Failed to register PostHogTelemetryClient:", error)
6970
}
7071

72+
// Create logger for cloud services
73+
const cloudLogger = createDualLogger(createOutputChannelLogger(outputChannel))
74+
7175
// Initialize Roo Code Cloud service.
7276
await CloudService.createInstance(context, {
7377
stateChanged: () => ClineProvider.getVisibleInstance()?.postStateToWebview(),
78+
log: cloudLogger,
7479
})
7580

7681
// Initialize i18n for internationalization support
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as vscode from "vscode"
2+
import { createOutputChannelLogger, createDualLogger } from "../outputChannelLogger"
3+
4+
// Mock VSCode output channel
5+
const mockOutputChannel = {
6+
appendLine: jest.fn(),
7+
} as unknown as vscode.OutputChannel
8+
9+
describe("outputChannelLogger", () => {
10+
beforeEach(() => {
11+
jest.clearAllMocks()
12+
// Clear console.log mock if it exists
13+
if (jest.isMockFunction(console.log)) {
14+
;(console.log as jest.Mock).mockClear()
15+
}
16+
})
17+
18+
describe("createOutputChannelLogger", () => {
19+
it("should log strings to output channel", () => {
20+
const logger = createOutputChannelLogger(mockOutputChannel)
21+
logger("test message")
22+
23+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("test message")
24+
})
25+
26+
it("should log null values", () => {
27+
const logger = createOutputChannelLogger(mockOutputChannel)
28+
logger(null)
29+
30+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("null")
31+
})
32+
33+
it("should log undefined values", () => {
34+
const logger = createOutputChannelLogger(mockOutputChannel)
35+
logger(undefined)
36+
37+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("undefined")
38+
})
39+
40+
it("should log Error objects with stack trace", () => {
41+
const logger = createOutputChannelLogger(mockOutputChannel)
42+
const error = new Error("test error")
43+
logger(error)
44+
45+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(expect.stringContaining("Error: test error"))
46+
})
47+
48+
it("should log objects as JSON", () => {
49+
const logger = createOutputChannelLogger(mockOutputChannel)
50+
const obj = { key: "value", number: 42 }
51+
logger(obj)
52+
53+
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(JSON.stringify(obj, expect.any(Function), 2))
54+
})
55+
56+
it("should handle multiple arguments", () => {
57+
const logger = createOutputChannelLogger(mockOutputChannel)
58+
logger("message", 42, { key: "value" })
59+
60+
expect(mockOutputChannel.appendLine).toHaveBeenCalledTimes(3)
61+
expect(mockOutputChannel.appendLine).toHaveBeenNthCalledWith(1, "message")
62+
expect(mockOutputChannel.appendLine).toHaveBeenNthCalledWith(2, "42")
63+
expect(mockOutputChannel.appendLine).toHaveBeenNthCalledWith(
64+
3,
65+
JSON.stringify({ key: "value" }, expect.any(Function), 2),
66+
)
67+
})
68+
})
69+
70+
describe("createDualLogger", () => {
71+
it("should log to both output channel and console", () => {
72+
const consoleSpy = jest.spyOn(console, "log").mockImplementation()
73+
const outputChannelLogger = createOutputChannelLogger(mockOutputChannel)
74+
const dualLogger = createDualLogger(outputChannelLogger)
75+
76+
dualLogger("test message", 42)
77+
78+
expect(mockOutputChannel.appendLine).toHaveBeenCalledTimes(2)
79+
expect(mockOutputChannel.appendLine).toHaveBeenNthCalledWith(1, "test message")
80+
expect(mockOutputChannel.appendLine).toHaveBeenNthCalledWith(2, "42")
81+
expect(consoleSpy).toHaveBeenCalledWith("test message", 42)
82+
83+
consoleSpy.mockRestore()
84+
})
85+
})
86+
})

src/utils/outputChannelLogger.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import * as vscode from "vscode"
2+
3+
export type LogFunction = (...args: unknown[]) => void
4+
5+
/**
6+
* Creates a logging function that writes to a VSCode output channel
7+
* Based on the outputChannelLog implementation from src/extension/api.ts
8+
*/
9+
export function createOutputChannelLogger(outputChannel: vscode.OutputChannel): LogFunction {
10+
return (...args: unknown[]) => {
11+
for (const arg of args) {
12+
if (arg === null) {
13+
outputChannel.appendLine("null")
14+
} else if (arg === undefined) {
15+
outputChannel.appendLine("undefined")
16+
} else if (typeof arg === "string") {
17+
outputChannel.appendLine(arg)
18+
} else if (arg instanceof Error) {
19+
outputChannel.appendLine(`Error: ${arg.message}\n${arg.stack || ""}`)
20+
} else {
21+
try {
22+
outputChannel.appendLine(
23+
JSON.stringify(
24+
arg,
25+
(key, value) => {
26+
if (typeof value === "bigint") return `BigInt(${value})`
27+
if (typeof value === "function") return `Function: ${value.name || "anonymous"}`
28+
if (typeof value === "symbol") return value.toString()
29+
return value
30+
},
31+
2,
32+
),
33+
)
34+
} catch (error) {
35+
outputChannel.appendLine(`[Non-serializable object: ${Object.prototype.toString.call(arg)}]`)
36+
}
37+
}
38+
}
39+
}
40+
}
41+
42+
/**
43+
* Creates a logging function that logs to both the output channel and console
44+
* Following the pattern from src/extension/api.ts
45+
*/
46+
export function createDualLogger(outputChannelLog: LogFunction): LogFunction {
47+
return (...args: unknown[]) => {
48+
outputChannelLog(...args)
49+
console.log(...args)
50+
}
51+
}

0 commit comments

Comments
 (0)