Compare commits

...
Author SHA1 Message Date
Arafatkatze 3ff09d7550 feat: add unified debug logging system for extension and webview
Add a debug logging utility that writes both extension and webview logs
to a single file (~/cline-debug.log) for easier troubleshooting of
issues that span the extension/webview boundary.

- Extension logger with direct file writing and log rotation (10MB max)
- Webview logger that intercepts console calls and sends to extension
- Unified log format with timestamp, source, level, and message
- Documentation in CLAUDE.md for usage patterns
2026-01-04 15:34:12 -08:00
3 changed files with 344 additions and 0 deletions
+42
View File
@@ -18,6 +18,48 @@ This file is the secret sauce for working effectively in this codebase. It captu
- This is a VS Code extension—check `package.json` for available scripts before trying to verify builds (e.g., `npm run compile`, not `npm run build`).
- When creating PRs, if the change is user-facing and significant enough to warrant a changelog entry, run `npm run changeset` and create a patch changeset. Never create minor or major version bumps. Skip changesets for trivial fixes, internal refactors, or minor UI tweaks that users wouldn't notice.
## Debug Logging (For Development/Debugging Only)
**IMPORTANT: These utilities are for debugging purposes only, not production logging.**
The codebase has a unified debug logging system that writes both extension and webview logs to a single file: `~/cline-debug.log`. This makes it easy to troubleshoot issues that span the extension/webview boundary.
**Files:**
- `src/utils/debugLogger.ts` - Extension-side logger with file writing
- `webview-ui/src/utils/webviewDebugLogger.ts` - Webview-side logger that intercepts console calls and sends to extension
**Extension usage:**
```typescript
import { extensionLog } from '@/utils/debugLogger'
extensionLog.info('Task started:', taskId)
extensionLog.error('Failed to load:', error)
```
**Webview usage:**
```typescript
// In your app entry point (e.g., App.tsx or index.tsx):
import { enableWebviewDebugLogging } from './utils/webviewDebugLogger'
enableWebviewDebugLogging()
// Then use console normally - it gets logged to file:
console.log('Button clicked:', buttonId)
console.error('API failed:', error)
```
**Monitoring logs in real-time:**
```bash
tail -f ~/cline-debug.log
```
**How it works:**
1. Extension logs directly write to `~/cline-debug.log` using Node.js fs
2. Webview logs intercept console calls, stringify args, and send to extension via `postMessage` with type `webview_debug_log`
3. Extension receives `webview_debug_log` messages and appends them to the same file
4. Logs are formatted with timestamp, source (EXTENSION/WEBVIEW), level, and message
5. Automatic log rotation when file exceeds 10MB
**When to add webview message handler:**
If you're integrating the webview logger, you need to handle the `webview_debug_log` message type in the extension's message handler. Look for where other webview messages are processed and add handling for this type that calls `debugLog('webview', level, ...args)`.
## gRPC/Protobuf Communication
The extension and webview communicate via gRPC-like protocol over VS Code message passing.
+184
View File
@@ -0,0 +1,184 @@
/**
* Unified Debug Logger
*
* **IMPORTANT: This is for debugging purposes only.**
*
* Provides logging utilities for both extension and webview to help troubleshoot issues.
* All logs are written to ~/cline-debug.log for easy access during development and debugging.
*
* This logger is NOT for production logging - it's specifically designed to help developers
* diagnose problems during development, testing, or when users report issues.
*
* Usage:
* import { debugLog, extensionLog } from '@/utils/debugLogger'
* extensionLog.info('Task started:', taskId)
* debugLog('extension', 'warn', 'Something unexpected happened')
*
* Monitoring logs in real-time:
* tail -f ~/cline-debug.log
*/
import * as fs from "fs"
import * as os from "os"
import * as path from "path"
// Single unified log file for both extension and webview
export const DEBUG_LOG_PATH = path.join(os.homedir(), "cline-debug.log")
// Max log file size before rotation (10MB)
const MAX_LOG_SIZE = 10 * 1024 * 1024
/**
* Log levels
*/
export type LogLevel = "debug" | "info" | "warn" | "error"
/**
* Log source (extension or webview)
*/
export type LogSource = "extension" | "webview"
/**
* Format a log entry with timestamp, source, and level
*/
function formatLogEntry(source: LogSource, level: LogLevel, args: unknown[]): string {
const timestamp = new Date().toISOString()
const levelUpper = level.toUpperCase().padEnd(5) // Align columns
const sourceLabel = `[${source.toUpperCase()}]`.padEnd(11) // Align columns
// Convert args to strings
const message = args
.map((arg) => {
if (arg === undefined) return "undefined"
if (arg === null) return "null"
if (typeof arg === "string") return arg
if (typeof arg === "number" || typeof arg === "boolean") return String(arg)
if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`
try {
return JSON.stringify(arg, null, 2)
} catch {
return String(arg)
}
})
.join(" ")
return `[${timestamp}] ${sourceLabel} [${levelUpper}] ${message}\n`
}
/**
* Check if log file needs rotation and rotate if necessary
*/
function rotateLogIfNeeded(): void {
try {
if (fs.existsSync(DEBUG_LOG_PATH)) {
const stats = fs.statSync(DEBUG_LOG_PATH)
if (stats.size > MAX_LOG_SIZE) {
const rotatedPath = `${DEBUG_LOG_PATH}.old`
// Remove old backup if it exists
if (fs.existsSync(rotatedPath)) {
fs.unlinkSync(rotatedPath)
}
// Rotate current log to backup
fs.renameSync(DEBUG_LOG_PATH, rotatedPath)
}
}
} catch (error) {
// Silently fail - don't let logging errors break the app
console.error("[debugLogger] Failed to rotate log:", error)
}
}
/**
* Write a log entry to the debug log file
*
* @param source - Where the log is coming from (extension or webview)
* @param level - Log level (debug, info, warn, error)
* @param args - Arguments to log (will be stringified)
*/
export function debugLog(source: LogSource, level: LogLevel, ...args: unknown[]): void {
try {
// Check if rotation is needed
rotateLogIfNeeded()
// Format and append log entry
const logEntry = formatLogEntry(source, level, args)
fs.appendFileSync(DEBUG_LOG_PATH, logEntry, "utf8")
} catch (error) {
// Silently fail - don't let logging errors break the app
console.error("[debugLogger] Failed to write log:", error)
}
}
/**
* Async version of debugLog for non-blocking logging
* Recommended for high-frequency logging scenarios
*/
export async function debugLogAsync(source: LogSource, level: LogLevel, ...args: unknown[]): Promise<void> {
try {
// Check if rotation is needed
rotateLogIfNeeded()
// Format and append log entry
const logEntry = formatLogEntry(source, level, args)
await fs.promises.appendFile(DEBUG_LOG_PATH, logEntry, "utf8")
} catch (error) {
// Silently fail - don't let logging errors break the app
console.error("[debugLogger] Failed to write log:", error)
}
}
/**
* Convenience wrappers for extension logging
*
* Use these for debugging extension-side code:
* - extensionLog.debug() - Verbose debugging info
* - extensionLog.info() - General informational messages
* - extensionLog.warn() - Warnings about potential issues
* - extensionLog.error() - Error conditions
*/
export const extensionLog = {
debug: (...args: unknown[]) => debugLog("extension", "debug", ...args),
info: (...args: unknown[]) => debugLog("extension", "info", ...args),
warn: (...args: unknown[]) => debugLog("extension", "warn", ...args),
error: (...args: unknown[]) => debugLog("extension", "error", ...args),
}
/**
* Convenience wrappers for webview logging
*
* Use these for debugging webview-side code:
* - webviewLog.debug() - Verbose debugging info
* - webviewLog.info() - General informational messages
* - webviewLog.warn() - Warnings about potential issues
* - webviewLog.error() - Error conditions
*/
export const webviewLog = {
debug: (...args: unknown[]) => debugLog("webview", "debug", ...args),
info: (...args: unknown[]) => debugLog("webview", "info", ...args),
warn: (...args: unknown[]) => debugLog("webview", "warn", ...args),
error: (...args: unknown[]) => debugLog("webview", "error", ...args),
}
/**
* Clear the debug log file
*
* Useful when starting a fresh debugging session
*/
export function clearDebugLog(): void {
try {
if (fs.existsSync(DEBUG_LOG_PATH)) {
fs.unlinkSync(DEBUG_LOG_PATH)
}
} catch (error) {
console.error("[debugLogger] Failed to clear log:", error)
}
}
/**
* Get the debug log file path
*
* Returns: ~/cline-debug.log
*/
export function getDebugLogPath(): string {
return DEBUG_LOG_PATH
}
+118
View File
@@ -0,0 +1,118 @@
/**
* Webview Debug Logger
*
* **IMPORTANT: This is for debugging purposes only.**
*
* Intercepts console.log/warn/error/debug calls and sends them to the extension,
* which writes them to ~/cline-debug.log (unified with extension logs).
*
* This allows developers to see webview console output in the same log file as
* extension logs, making it easier to debug issues that span both sides of the
* extension/webview boundary.
*
* Usage:
* 1. Import and enable early in your app entry point:
* import { enableWebviewDebugLogging } from './utils/webviewDebugLogger'
* enableWebviewDebugLogging()
*
* 2. Use console methods normally - they'll be intercepted and logged:
* console.log('User clicked button:', buttonId)
* console.warn('API request took longer than expected')
* console.error('Failed to load data:', error)
*
* 3. Monitor logs in real-time:
* tail -f ~/cline-debug.log
*
* Note: The extension must handle the 'webview_debug_log' message type for this to work.
*/
import { PLATFORM_CONFIG } from "../config/platform.config"
// Store original console methods
const originalConsole = {
log: console.log.bind(console),
warn: console.warn.bind(console),
error: console.error.bind(console),
debug: console.debug.bind(console),
}
// Convert any value to a string for logging
function stringify(arg: unknown): string {
if (arg === undefined) return "undefined"
if (arg === null) return "null"
if (typeof arg === "string") return arg
if (typeof arg === "number" || typeof arg === "boolean") return String(arg)
if (arg instanceof Error) return `${arg.name}: ${arg.message}\n${arg.stack}`
try {
return JSON.stringify(arg, null, 2)
} catch {
return String(arg)
}
}
// Send log to extension
function sendToExtension(level: "log" | "warn" | "error" | "debug", args: unknown[]) {
try {
PLATFORM_CONFIG.postMessage({
type: "webview_debug_log",
webview_debug_log: {
level,
args: args.map(stringify),
timestamp: Date.now(),
},
})
} catch {
// Silently fail if postMessage isn't available
}
}
// Create wrapped console method
function createWrapper(level: "log" | "warn" | "error" | "debug") {
return (...args: unknown[]) => {
// Call original console method
originalConsole[level](...args)
// Send to extension for file logging
sendToExtension(level, args)
}
}
/**
* Enable webview debug logging.
* Call this once at app startup to intercept all console calls.
*
* After enabling, all console.log/warn/error/debug calls will:
* 1. Display in the browser console as normal
* 2. Be sent to the extension and written to ~/cline-debug.log
*/
export function enableWebviewDebugLogging() {
console.log = createWrapper("log")
console.warn = createWrapper("warn")
console.error = createWrapper("error")
console.debug = createWrapper("debug")
// Log that we've enabled debug logging
console.log("[WebviewDebugLogger] Debug logging enabled. Logs written to ~/cline-debug.log")
}
/**
* Disable webview debug logging and restore original console methods.
* Useful if you want to temporarily disable logging or clean up on unmount.
*/
export function disableWebviewDebugLogging() {
console.log = originalConsole.log
console.warn = originalConsole.warn
console.error = originalConsole.error
console.debug = originalConsole.debug
}
/**
* Log directly to file without going through console.
* Useful for logging in hot paths where you don't want console output,
* or when you want to log something that shouldn't appear in the browser console.
*
* Example:
* logToFile('debug', 'Performance metric:', performanceData)
*/
export function logToFile(level: "log" | "warn" | "error" | "debug", ...args: unknown[]) {
sendToExtension(level, args)
}