mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: implement enhanced logging feature for troubleshooting
- Add enhancedLoggingEnabled toggle in Settings → Info (About) section - Create logEnhancedError and logEnhanced utility functions - Add sanitization for sensitive data in logs - Integrate enhanced logging into API error handling - Add yellow warning banner when enhanced logging is enabled - Add comprehensive test coverage for logging functionality - Export disposeEnhancedLogging for proper cleanup Implements #8098
This commit is contained in:
parent
a255c95bd0
commit
27558b87d2
11 changed files with 682 additions and 0 deletions
|
|
@ -136,6 +136,8 @@ export const globalSettingsSchema = z.object({
|
|||
|
||||
telemetrySetting: telemetrySettingsSchema.optional(),
|
||||
|
||||
enhancedLoggingEnabled: z.boolean().optional(),
|
||||
|
||||
mcpEnabled: z.boolean().optional(),
|
||||
enableMcpServerCreation: z.boolean().optional(),
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"
|
|||
// utils
|
||||
import { calculateApiCostAnthropic } from "../../shared/cost"
|
||||
import { getWorkspacePath } from "../../utils/path"
|
||||
import { logEnhancedError } from "../../utils/enhancedLogging"
|
||||
|
||||
// prompts
|
||||
import { formatResponse } from "../prompts/responses"
|
||||
|
|
@ -2182,6 +2183,19 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// Cline instance to finish aborting (error is thrown here when
|
||||
// any function in the for loop throws due to this.abort).
|
||||
if (!this.abandoned) {
|
||||
// Log enhanced error details if enabled
|
||||
const provider = this.providerRef.deref()
|
||||
if (provider && !this.abort) {
|
||||
logEnhancedError(provider.context, error, {
|
||||
operation: "API Stream Processing",
|
||||
provider: this.apiConfiguration.apiProvider,
|
||||
model: this.api.getModel().id,
|
||||
taskId: this.taskId,
|
||||
streamPosition: "during streaming",
|
||||
assistantMessageLength: assistantMessage.length,
|
||||
})
|
||||
}
|
||||
|
||||
// If the stream failed, there's various states the task
|
||||
// could be in (i.e. could have streamed some tools the user
|
||||
// may have executed), so we just resort to replicating a
|
||||
|
|
@ -2688,6 +2702,20 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
this.isWaitingForFirstChunk = false
|
||||
const isContextWindowExceededError = checkContextWindowExceededError(error)
|
||||
|
||||
// Log enhanced error details if enabled
|
||||
const provider = this.providerRef.deref()
|
||||
if (provider) {
|
||||
logEnhancedError(provider.context, error, {
|
||||
operation: "API Request (First Chunk)",
|
||||
provider: this.apiConfiguration.apiProvider,
|
||||
model: this.api.getModel().id,
|
||||
taskId: this.taskId,
|
||||
retryAttempt,
|
||||
isContextWindowError: isContextWindowExceededError,
|
||||
contextTokens: this.getTokenUsage().contextTokens,
|
||||
})
|
||||
}
|
||||
|
||||
// If it's a context window error and we haven't exceeded max retries for this error type
|
||||
if (isContextWindowExceededError && retryAttempt < MAX_CONTEXT_WINDOW_RETRIES) {
|
||||
console.warn(
|
||||
|
|
|
|||
|
|
@ -2286,6 +2286,12 @@ export const webviewMessageHandler = async (
|
|||
await provider.postStateToWebview()
|
||||
break
|
||||
}
|
||||
case "enhancedLoggingEnabled": {
|
||||
const enabled = message.bool ?? false
|
||||
await updateGlobalState("enhancedLoggingEnabled", enabled)
|
||||
await provider.postStateToWebview()
|
||||
break
|
||||
}
|
||||
case "cloudButtonClicked": {
|
||||
// Navigate to the cloud tab.
|
||||
provider.postMessageToWebview({ type: "action", action: "cloudButtonClicked" })
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ export interface WebviewMessage {
|
|||
| "allowedMaxCost"
|
||||
| "alwaysAllowSubtasks"
|
||||
| "alwaysAllowUpdateTodoList"
|
||||
| "enhancedLoggingEnabled"
|
||||
| "autoCondenseContext"
|
||||
| "autoCondenseContextPercent"
|
||||
| "condensingApiConfigId"
|
||||
|
|
|
|||
306
src/utils/__tests__/enhancedLogging.spec.ts
Normal file
306
src/utils/__tests__/enhancedLogging.spec.ts
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
|
||||
import * as vscode from "vscode"
|
||||
import { logEnhancedError, logEnhanced, disposeEnhancedLogging } from "../enhancedLogging"
|
||||
|
||||
// Mock the Package module
|
||||
vi.mock("../shared/package", () => ({
|
||||
Package: {
|
||||
outputChannel: "Roo-Code",
|
||||
},
|
||||
}))
|
||||
|
||||
// Mock vscode module
|
||||
vi.mock("vscode", () => ({
|
||||
window: {
|
||||
createOutputChannel: vi.fn(),
|
||||
},
|
||||
OutputChannel: vi.fn(),
|
||||
}))
|
||||
|
||||
describe("enhancedLogging", () => {
|
||||
let mockContext: vscode.ExtensionContext
|
||||
let mockOutputChannel: any
|
||||
let mockGlobalState: any
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset all mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create mock output channel
|
||||
mockOutputChannel = {
|
||||
appendLine: vi.fn(),
|
||||
append: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
show: vi.fn(),
|
||||
hide: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
replace: vi.fn(),
|
||||
name: "Roo-Code",
|
||||
}
|
||||
|
||||
// Mock window.createOutputChannel to return our mock
|
||||
vi.mocked(vscode.window.createOutputChannel).mockReturnValue(mockOutputChannel as any)
|
||||
|
||||
// Create mock global state
|
||||
mockGlobalState = {
|
||||
get: vi.fn(),
|
||||
update: vi.fn(),
|
||||
keys: vi.fn().mockReturnValue([]),
|
||||
setKeysForSync: vi.fn(),
|
||||
}
|
||||
|
||||
// Create mock context
|
||||
mockContext = {
|
||||
globalState: mockGlobalState as any,
|
||||
subscriptions: [],
|
||||
workspaceState: {} as any,
|
||||
extensionUri: {} as any,
|
||||
extensionPath: "",
|
||||
asAbsolutePath: vi.fn(),
|
||||
storagePath: "",
|
||||
globalStoragePath: "",
|
||||
logPath: "",
|
||||
extensionMode: 3,
|
||||
extension: {} as any,
|
||||
globalStorageUri: {} as any,
|
||||
logUri: {} as any,
|
||||
storageUri: {} as any,
|
||||
secrets: {} as any,
|
||||
environmentVariableCollection: {} as any,
|
||||
languageModelAccessInformation: {} as any,
|
||||
}
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
// Dispose the output channel to reset module state
|
||||
disposeEnhancedLogging()
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
describe("logEnhancedError", () => {
|
||||
it("should not log when enhanced logging is disabled", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return false
|
||||
return defaultValue
|
||||
})
|
||||
const error = new Error("Test error")
|
||||
|
||||
// Act
|
||||
logEnhancedError(mockContext, error)
|
||||
|
||||
// Assert
|
||||
expect(vscode.window.createOutputChannel).not.toHaveBeenCalled()
|
||||
expect(mockOutputChannel.appendLine).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should log error details when enhanced logging is enabled", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const error = new Error("Test error")
|
||||
error.stack = "Error: Test error\n at test.js:1:1"
|
||||
|
||||
// Act
|
||||
logEnhancedError(mockContext, error, {
|
||||
operation: "Test Operation",
|
||||
provider: "test-provider",
|
||||
})
|
||||
|
||||
// Assert
|
||||
expect(vscode.window.createOutputChannel).toHaveBeenCalledWith("Roo-Code")
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
expect.stringContaining("[ENHANCED LOGGING] Error occurred at"),
|
||||
)
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Operation: Test Operation")
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Provider: test-provider")
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Error Type: Error")
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Message: Test error")
|
||||
expect(mockOutputChannel.show).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it("should sanitize sensitive information", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const error = new Error("API key failed")
|
||||
|
||||
// Act
|
||||
logEnhancedError(mockContext, error, {
|
||||
request: {
|
||||
headers: {
|
||||
authorization: "Bearer sk-abcdef123456",
|
||||
apikey: "key-123456789",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Assert
|
||||
// Check that sensitive data is redacted in the output
|
||||
const calls = mockOutputChannel.appendLine.mock.calls
|
||||
|
||||
// The function should have been called multiple times
|
||||
expect(calls.length).toBeGreaterThan(0)
|
||||
|
||||
const allOutput = calls.map((call: any) => call[0]).join("\n")
|
||||
expect(allOutput).toContain("[REDACTED")
|
||||
expect(allOutput).not.toContain("sk-abcdef123456")
|
||||
expect(allOutput).not.toContain("key-123456789")
|
||||
})
|
||||
|
||||
it("should handle non-Error objects", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const error = { code: "NETWORK_ERROR", details: "Connection failed" }
|
||||
|
||||
// Act
|
||||
logEnhancedError(mockContext, error)
|
||||
|
||||
// Assert
|
||||
const calls = mockOutputChannel.appendLine.mock.calls
|
||||
const allOutput = calls.map((call: any) => call[0]).join("\n")
|
||||
expect(allOutput).toContain("NETWORK_ERROR")
|
||||
expect(allOutput).toContain("Connection failed")
|
||||
})
|
||||
|
||||
it("should handle string errors", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const error = "Simple error string"
|
||||
|
||||
// Act
|
||||
logEnhancedError(mockContext, error)
|
||||
|
||||
// Assert
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Error Message: Simple error string"),
|
||||
)
|
||||
})
|
||||
|
||||
it("should include timestamp in logs", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const error = new Error("Test error")
|
||||
const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z")
|
||||
|
||||
// Act
|
||||
logEnhancedError(mockContext, error)
|
||||
|
||||
// Assert
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
expect.stringContaining("2024-01-01T12:00:00.000Z"),
|
||||
)
|
||||
|
||||
dateSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("logEnhanced", () => {
|
||||
it("should not log when enhanced logging is disabled", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return false
|
||||
return defaultValue
|
||||
})
|
||||
|
||||
// Act
|
||||
logEnhanced(mockContext, "Test message")
|
||||
|
||||
// Assert
|
||||
expect(vscode.window.createOutputChannel).not.toHaveBeenCalled()
|
||||
expect(mockOutputChannel.appendLine).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should log message with INFO level by default", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z")
|
||||
|
||||
// Act
|
||||
logEnhanced(mockContext, "Test message")
|
||||
|
||||
// Assert
|
||||
expect(vscode.window.createOutputChannel).toHaveBeenCalledWith("Roo-Code")
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("[2024-01-01T12:00:00.000Z] [INFO] Test message")
|
||||
|
||||
dateSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should log message with specified level", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z")
|
||||
|
||||
// Act
|
||||
logEnhanced(mockContext, "Error occurred", "ERROR")
|
||||
|
||||
// Assert
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[2024-01-01T12:00:00.000Z] [ERROR] Error occurred",
|
||||
)
|
||||
|
||||
dateSpy.mockRestore()
|
||||
})
|
||||
|
||||
it("should support different log levels", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
const dateSpy = vi.spyOn(Date.prototype, "toISOString").mockReturnValue("2024-01-01T12:00:00.000Z")
|
||||
|
||||
// Act
|
||||
logEnhanced(mockContext, "Debug info", "DEBUG")
|
||||
logEnhanced(mockContext, "Warning message", "WARNING")
|
||||
|
||||
// Assert
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("[2024-01-01T12:00:00.000Z] [DEBUG] Debug info")
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
|
||||
"[2024-01-01T12:00:00.000Z] [WARNING] Warning message",
|
||||
)
|
||||
|
||||
dateSpy.mockRestore()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Output channel management", () => {
|
||||
it("should reuse the same output channel for multiple logs", () => {
|
||||
// Arrange
|
||||
vi.mocked(mockGlobalState.get).mockImplementation((key: string, defaultValue?: any) => {
|
||||
if (key === "enhancedLoggingEnabled") return true
|
||||
return defaultValue
|
||||
})
|
||||
|
||||
// Act
|
||||
logEnhanced(mockContext, "First message")
|
||||
logEnhanced(mockContext, "Second message")
|
||||
logEnhancedError(mockContext, new Error("Test error"))
|
||||
|
||||
// Assert
|
||||
expect(vscode.window.createOutputChannel).toHaveBeenCalledTimes(1)
|
||||
expect(mockOutputChannel.appendLine).toHaveBeenCalled()
|
||||
// logEnhanced doesn't call show(), only logEnhancedError does
|
||||
expect(mockOutputChannel.show).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
})
|
||||
250
src/utils/enhancedLogging.ts
Normal file
250
src/utils/enhancedLogging.ts
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
import * as vscode from "vscode"
|
||||
import { Package } from "../shared/package"
|
||||
|
||||
let outputChannel: vscode.OutputChannel | undefined
|
||||
|
||||
/**
|
||||
* Gets or creates the output channel for enhanced logging
|
||||
*/
|
||||
function getOutputChannel(): vscode.OutputChannel {
|
||||
if (!outputChannel) {
|
||||
outputChannel = vscode.window.createOutputChannel(Package.outputChannel)
|
||||
}
|
||||
return outputChannel
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs detailed error information to the VS Code Output channel when enhanced logging is enabled
|
||||
*
|
||||
* @param context - The vscode.ExtensionContext to access global state
|
||||
* @param error - The error object or error information to log
|
||||
* @param additionalContext - Optional additional context information
|
||||
*/
|
||||
export function logEnhancedError(
|
||||
context: vscode.ExtensionContext,
|
||||
error: unknown,
|
||||
additionalContext?: {
|
||||
operation?: string
|
||||
provider?: string
|
||||
model?: string
|
||||
request?: any
|
||||
response?: any
|
||||
[key: string]: any
|
||||
},
|
||||
): void {
|
||||
// Check if enhanced logging is enabled
|
||||
const enhancedLoggingEnabled = context.globalState.get<boolean>("enhancedLoggingEnabled", false)
|
||||
|
||||
if (!enhancedLoggingEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = getOutputChannel()
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
// Start logging the error
|
||||
channel.appendLine("")
|
||||
channel.appendLine("=".repeat(80))
|
||||
channel.appendLine(`[ENHANCED LOGGING] Error occurred at ${timestamp}`)
|
||||
channel.appendLine("=".repeat(80))
|
||||
|
||||
// Log the operation context if provided
|
||||
if (additionalContext?.operation) {
|
||||
channel.appendLine(`Operation: ${additionalContext.operation}`)
|
||||
}
|
||||
|
||||
if (additionalContext?.provider) {
|
||||
channel.appendLine(`Provider: ${additionalContext.provider}`)
|
||||
}
|
||||
|
||||
if (additionalContext?.model) {
|
||||
channel.appendLine(`Model: ${additionalContext.model}`)
|
||||
}
|
||||
|
||||
// Log the error details
|
||||
channel.appendLine("")
|
||||
channel.appendLine("Error Details:")
|
||||
channel.appendLine("-".repeat(40))
|
||||
|
||||
if (error instanceof Error) {
|
||||
channel.appendLine(`Error Type: ${error.constructor.name}`)
|
||||
channel.appendLine(`Message: ${error.message}`)
|
||||
|
||||
if (error.stack) {
|
||||
channel.appendLine("")
|
||||
channel.appendLine("Stack Trace:")
|
||||
channel.appendLine(error.stack)
|
||||
}
|
||||
|
||||
// Log any additional error properties
|
||||
const errorObj = error as any
|
||||
const standardProps = ["name", "message", "stack"]
|
||||
const additionalProps = Object.keys(errorObj).filter((key) => !standardProps.includes(key))
|
||||
|
||||
if (additionalProps.length > 0) {
|
||||
channel.appendLine("")
|
||||
channel.appendLine("Additional Error Properties:")
|
||||
for (const prop of additionalProps) {
|
||||
try {
|
||||
const value = errorObj[prop]
|
||||
if (value !== undefined && value !== null) {
|
||||
const displayValue = typeof value === "object" ? JSON.stringify(value, null, 2) : String(value)
|
||||
channel.appendLine(` ${prop}: ${displayValue}`)
|
||||
}
|
||||
} catch (e) {
|
||||
channel.appendLine(` ${prop}: [Unable to serialize]`)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (typeof error === "string") {
|
||||
channel.appendLine(`Error Message: ${error}`)
|
||||
} else if (error !== null && error !== undefined) {
|
||||
try {
|
||||
channel.appendLine(`Error Object: ${JSON.stringify(error, null, 2)}`)
|
||||
} catch (e) {
|
||||
channel.appendLine(`Error: ${String(error)}`)
|
||||
}
|
||||
} else {
|
||||
channel.appendLine("Unknown error occurred (null or undefined)")
|
||||
}
|
||||
|
||||
// Log request details if provided
|
||||
if (additionalContext?.request) {
|
||||
channel.appendLine("")
|
||||
channel.appendLine("Request Details:")
|
||||
channel.appendLine("-".repeat(40))
|
||||
try {
|
||||
// Sanitize sensitive information from request
|
||||
const sanitizedRequest = sanitizeForLogging(additionalContext.request)
|
||||
channel.appendLine(JSON.stringify(sanitizedRequest, null, 2))
|
||||
} catch (e) {
|
||||
channel.appendLine("[Unable to serialize request]")
|
||||
}
|
||||
}
|
||||
|
||||
// Log response details if provided
|
||||
if (additionalContext?.response) {
|
||||
channel.appendLine("")
|
||||
channel.appendLine("Response Details:")
|
||||
channel.appendLine("-".repeat(40))
|
||||
try {
|
||||
// Sanitize sensitive information from response
|
||||
const sanitizedResponse = sanitizeForLogging(additionalContext.response)
|
||||
channel.appendLine(JSON.stringify(sanitizedResponse, null, 2))
|
||||
} catch (e) {
|
||||
channel.appendLine("[Unable to serialize response]")
|
||||
}
|
||||
}
|
||||
|
||||
// Log any other additional context
|
||||
const excludedKeys = ["operation", "provider", "model", "request", "response"]
|
||||
const otherContext = Object.keys(additionalContext || {})
|
||||
.filter((key) => !excludedKeys.includes(key))
|
||||
.reduce((acc, key) => {
|
||||
acc[key] = additionalContext![key]
|
||||
return acc
|
||||
}, {} as any)
|
||||
|
||||
if (Object.keys(otherContext).length > 0) {
|
||||
channel.appendLine("")
|
||||
channel.appendLine("Additional Context:")
|
||||
channel.appendLine("-".repeat(40))
|
||||
try {
|
||||
channel.appendLine(JSON.stringify(otherContext, null, 2))
|
||||
} catch (e) {
|
||||
channel.appendLine("[Unable to serialize additional context]")
|
||||
}
|
||||
}
|
||||
|
||||
channel.appendLine("")
|
||||
channel.appendLine("=".repeat(80))
|
||||
channel.appendLine("")
|
||||
|
||||
// Show the output channel to the user
|
||||
channel.show(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes an object for logging by removing or masking sensitive information
|
||||
*/
|
||||
function sanitizeForLogging(obj: any): any {
|
||||
if (obj === null || obj === undefined) {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (typeof obj !== "object") {
|
||||
return obj
|
||||
}
|
||||
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map((item) => sanitizeForLogging(item))
|
||||
}
|
||||
|
||||
const sanitized: any = {}
|
||||
const sensitiveKeys = [
|
||||
"apikey",
|
||||
"api_key",
|
||||
"apiKey",
|
||||
"password",
|
||||
"passwd",
|
||||
"pwd",
|
||||
"secret",
|
||||
"token",
|
||||
"auth",
|
||||
"authorization",
|
||||
"bearer",
|
||||
"credential",
|
||||
"credentials",
|
||||
]
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const lowerKey = key.toLowerCase()
|
||||
|
||||
// Check if this key contains sensitive information
|
||||
if (sensitiveKeys.some((sensitive) => lowerKey.includes(sensitive))) {
|
||||
// Mask the value but show its type and partial info
|
||||
if (typeof value === "string" && value.length > 0) {
|
||||
sanitized[key] = `[REDACTED: ${value.slice(0, 4)}...${value.slice(-4)}]`
|
||||
} else {
|
||||
sanitized[key] = "[REDACTED]"
|
||||
}
|
||||
} else if (typeof value === "object" && value !== null) {
|
||||
// Recursively sanitize nested objects
|
||||
sanitized[key] = sanitizeForLogging(value)
|
||||
} else {
|
||||
sanitized[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return sanitized
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a simple enhanced message (not necessarily an error)
|
||||
*/
|
||||
export function logEnhanced(
|
||||
context: vscode.ExtensionContext,
|
||||
message: string,
|
||||
level: "INFO" | "WARNING" | "ERROR" | "DEBUG" = "INFO",
|
||||
): void {
|
||||
const enhancedLoggingEnabled = context.globalState.get<boolean>("enhancedLoggingEnabled", false)
|
||||
|
||||
if (!enhancedLoggingEnabled) {
|
||||
return
|
||||
}
|
||||
|
||||
const channel = getOutputChannel()
|
||||
const timestamp = new Date().toISOString()
|
||||
|
||||
channel.appendLine(`[${timestamp}] [${level}] ${message}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Disposes the output channel if it exists
|
||||
*/
|
||||
export function disposeEnhancedLogging(): void {
|
||||
if (outputChannel) {
|
||||
outputChannel.dispose()
|
||||
outputChannel = undefined
|
||||
}
|
||||
}
|
||||
|
|
@ -43,6 +43,7 @@ import { useAutoApprovalToggles } from "@src/hooks/useAutoApprovalToggles"
|
|||
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
|
||||
|
||||
import TelemetryBanner from "../common/TelemetryBanner"
|
||||
import EnhancedLoggingBanner from "../common/EnhancedLoggingBanner"
|
||||
import VersionIndicator from "../common/VersionIndicator"
|
||||
import { useTaskSearch } from "../history/useTaskSearch"
|
||||
import HistoryPreview from "../history/HistoryPreview"
|
||||
|
|
@ -117,6 +118,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
alwaysAllowUpdateTodoList,
|
||||
customModes,
|
||||
telemetrySetting,
|
||||
enhancedLoggingEnabled,
|
||||
hasSystemPromptOverride,
|
||||
historyPreviewCollapsed, // Added historyPreviewCollapsed
|
||||
soundEnabled,
|
||||
|
|
@ -1777,6 +1779,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
|
|||
data-testid="chat-view"
|
||||
className={isHidden ? "hidden" : "fixed top-0 left-0 right-0 bottom-0 flex flex-col overflow-hidden"}>
|
||||
{telemetrySetting === "unset" && <TelemetryBanner />}
|
||||
{enhancedLoggingEnabled && <EnhancedLoggingBanner />}
|
||||
{(showAnnouncement || showAnnouncementModal) && (
|
||||
<Announcement
|
||||
hideAnnouncement={() => {
|
||||
|
|
|
|||
53
webview-ui/src/components/common/EnhancedLoggingBanner.tsx
Normal file
53
webview-ui/src/components/common/EnhancedLoggingBanner.tsx
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
import { memo } from "react"
|
||||
import { VSCodeLink } from "@vscode/webview-ui-toolkit/react"
|
||||
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
import { useAppTranslation } from "@src/i18n/TranslationContext"
|
||||
|
||||
const EnhancedLoggingBanner = () => {
|
||||
const { t } = useAppTranslation()
|
||||
const { enhancedLoggingEnabled, setEnhancedLoggingEnabled } = useExtensionState()
|
||||
|
||||
const handleDisable = () => {
|
||||
setEnhancedLoggingEnabled(false)
|
||||
vscode.postMessage({ type: "enhancedLoggingEnabled", bool: false })
|
||||
}
|
||||
|
||||
const handleOpenSettings = () => {
|
||||
window.postMessage({
|
||||
type: "action",
|
||||
action: "settingsButtonClicked",
|
||||
values: { section: "about" },
|
||||
})
|
||||
}
|
||||
|
||||
if (!enhancedLoggingEnabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative px-4 py-2.5 pr-10 bg-yellow-500/20 border-b border-yellow-500/30 text-sm leading-normal text-vscode-foreground">
|
||||
{/* Close button (X) */}
|
||||
<button
|
||||
onClick={handleDisable}
|
||||
className="absolute top-1.5 right-2 bg-transparent border-none text-vscode-foreground cursor-pointer text-2xl p-1 opacity-70 hover:opacity-100 transition-opacity duration-200 leading-none"
|
||||
aria-label="Disable enhanced logging">
|
||||
×
|
||||
</button>
|
||||
|
||||
<div className="mb-0.5 font-bold flex items-center gap-2">
|
||||
<span className="text-yellow-600">⚠️</span>
|
||||
{t("settings:about.enhancedLogging.bannerTitle")}
|
||||
</div>
|
||||
<div>
|
||||
{t("settings:about.enhancedLogging.bannerMessage")}{" "}
|
||||
<VSCodeLink href="#" onClick={handleOpenSettings}>
|
||||
{t("settings:about.enhancedLogging.bannerSettings")}
|
||||
</VSCodeLink>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(EnhancedLoggingBanner)
|
||||
|
|
@ -11,6 +11,7 @@ import { Package } from "@roo/package"
|
|||
import { vscode } from "@/utils/vscode"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui"
|
||||
import { useExtensionState } from "@/context/ExtensionStateContext"
|
||||
|
||||
import { SectionHeader } from "./SectionHeader"
|
||||
import { Section } from "./Section"
|
||||
|
|
@ -22,6 +23,7 @@ type AboutProps = HTMLAttributes<HTMLDivElement> & {
|
|||
|
||||
export const About = ({ telemetrySetting, setTelemetrySetting, className, ...props }: AboutProps) => {
|
||||
const { t } = useAppTranslation()
|
||||
const { enhancedLoggingEnabled, setEnhancedLoggingEnabled } = useExtensionState()
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2", className)} {...props}>
|
||||
|
|
@ -57,6 +59,21 @@ export const About = ({ telemetrySetting, setTelemetrySetting, className, ...pro
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<VSCodeCheckbox
|
||||
checked={enhancedLoggingEnabled || false}
|
||||
onChange={(e: any) => {
|
||||
const checked = e.target.checked === true
|
||||
setEnhancedLoggingEnabled(checked)
|
||||
vscode.postMessage({ type: "enhancedLoggingEnabled", bool: checked })
|
||||
}}>
|
||||
{t("settings:footer.enhancedLogging.label")}
|
||||
</VSCodeCheckbox>
|
||||
<p className="text-vscode-descriptionForeground text-sm mt-0">
|
||||
{t("settings:footer.enhancedLogging.description")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Trans
|
||||
i18nKey="settings:footer.feedback"
|
||||
|
|
|
|||
|
|
@ -155,6 +155,8 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setMaxDiagnosticMessages: (value: number) => void
|
||||
includeTaskHistoryInEnhance?: boolean
|
||||
setIncludeTaskHistoryInEnhance: (value: boolean) => void
|
||||
enhancedLoggingEnabled?: boolean
|
||||
setEnhancedLoggingEnabled: (value: boolean) => void
|
||||
}
|
||||
|
||||
export const ExtensionStateContext = createContext<ExtensionStateContextType | undefined>(undefined)
|
||||
|
|
@ -280,6 +282,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
global: {},
|
||||
})
|
||||
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
|
||||
const [enhancedLoggingEnabled, setEnhancedLoggingEnabled] = useState(false)
|
||||
|
||||
const setListApiConfigMeta = useCallback(
|
||||
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
|
||||
|
|
@ -317,6 +320,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
if ((newState as any).includeTaskHistoryInEnhance !== undefined) {
|
||||
setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance)
|
||||
}
|
||||
// Update enhancedLoggingEnabled if present in state message
|
||||
if ((newState as any).enhancedLoggingEnabled !== undefined) {
|
||||
setEnhancedLoggingEnabled((newState as any).enhancedLoggingEnabled)
|
||||
}
|
||||
// Handle marketplace data if present in state message
|
||||
if (newState.marketplaceItems !== undefined) {
|
||||
setMarketplaceItems(newState.marketplaceItems)
|
||||
|
|
@ -538,6 +545,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
},
|
||||
includeTaskHistoryInEnhance,
|
||||
setIncludeTaskHistoryInEnhance,
|
||||
enhancedLoggingEnabled,
|
||||
setEnhancedLoggingEnabled,
|
||||
}
|
||||
|
||||
return <ExtensionStateContext.Provider value={contextValue}>{children}</ExtensionStateContext.Provider>
|
||||
|
|
|
|||
|
|
@ -801,6 +801,13 @@
|
|||
"import": "Import",
|
||||
"export": "Export",
|
||||
"reset": "Reset"
|
||||
},
|
||||
"enhancedLogging": {
|
||||
"label": "Enhanced logging",
|
||||
"description": "When enabled, writes detailed failure logs to the VS Code Output panel for troubleshooting. Logs remain local and are not uploaded.",
|
||||
"bannerTitle": "Enhanced Logging Enabled",
|
||||
"bannerMessage": "Detailed failure logs are being written to the VS Code Output panel. This may include sensitive information.",
|
||||
"bannerSettings": "Disable in settings"
|
||||
}
|
||||
},
|
||||
"thinkingBudget": {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue