mirror of
https://github.com/MoonshotAI/kimi-code.git
synced 2026-08-26 00:54:36 +00:00
feat(web): capture front-end errors in the troubleshooting log export
The KAP debug panel already exported REST + WS traffic as JSONL, but a broken page is usually explained by a JS error the network log doesn't show. Fold window error/unhandledrejection and console.error/warn into the trace buffer (opt-in, install-once, behavior-preserving) as a 'client' source, surface them in the panel with an 'app errors' filter + APP badge, so 'export jsonl' yields a complete troubleshooting log.
This commit is contained in:
parent
4b0eae5ab3
commit
481bfa0dfc
4 changed files with 108 additions and 4 deletions
|
|
@ -19,7 +19,7 @@ const open = ref(false);
|
|||
// ---------------------------------------------------------------------------
|
||||
// Filters
|
||||
// ---------------------------------------------------------------------------
|
||||
const sourceFilter = ref<'all' | 'rest' | 'ws'>('all');
|
||||
const sourceFilter = ref<'all' | 'rest' | 'ws' | 'client'>('all');
|
||||
const textFilter = ref('');
|
||||
const sessionFilter = ref<string>('');
|
||||
const errorsOnly = ref(false);
|
||||
|
|
@ -142,10 +142,17 @@ function exportJsonl(): void {
|
|||
|
||||
function badgeClass(e: TraceEntry): string {
|
||||
if (isError(e)) return 'b-err';
|
||||
if (e.source === 'client') return 'b-err';
|
||||
if (e.source === 'rest') return 'b-rest';
|
||||
if (e.kind === 'ws:lifecycle') return 'b-life';
|
||||
return e.kind === 'ws:out' ? 'b-out' : 'b-in';
|
||||
}
|
||||
|
||||
function badgeLabel(e: TraceEntry): string {
|
||||
if (e.source === 'rest') return 'REST';
|
||||
if (e.source === 'client') return 'APP';
|
||||
return 'WS';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -170,9 +177,10 @@ function badgeClass(e: TraceEntry): string {
|
|||
|
||||
<div class="kap-filters">
|
||||
<select v-model="sourceFilter" aria-label="Source filter">
|
||||
<option value="all">rest + ws</option>
|
||||
<option value="all">rest + ws + app</option>
|
||||
<option value="rest">rest</option>
|
||||
<option value="ws">ws</option>
|
||||
<option value="client">app errors</option>
|
||||
</select>
|
||||
<select v-model="sessionFilter" aria-label="Session filter">
|
||||
<option value="">all sessions</option>
|
||||
|
|
@ -194,7 +202,7 @@ function badgeClass(e: TraceEntry): string {
|
|||
<div v-for="e in filtered" :key="e.id" class="kap-row-wrap">
|
||||
<button type="button" class="kap-row" :class="{ expanded: expandedId === e.id }" @click="toggleDetail(e.id)">
|
||||
<span class="kap-ts">{{ fmtTime(e.ts) }}</span>
|
||||
<span class="kap-badge" :class="badgeClass(e)">{{ e.source === 'rest' ? 'REST' : 'WS' }}</span>
|
||||
<span class="kap-badge" :class="badgeClass(e)">{{ badgeLabel(e) }}</span>
|
||||
<span class="kap-label">{{ e.label }}</span>
|
||||
</button>
|
||||
<div v-if="expandedId === e.id" class="kap-detail">
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
import { ref, shallowRef } from 'vue';
|
||||
|
||||
export type TraceSource = 'rest' | 'ws';
|
||||
export type TraceSource = 'rest' | 'ws' | 'client';
|
||||
|
||||
export interface TraceEntry {
|
||||
id: number;
|
||||
|
|
@ -298,6 +298,77 @@ export function traceWsIn(frame: unknown): void {
|
|||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Client-side error capture — so the exported troubleshooting log includes the
|
||||
// front-end errors (uncaught exceptions, rejected promises, console.error/warn)
|
||||
// that explain a broken page, not just network traffic. Install-once, opt-in
|
||||
// (only when tracing is enabled), and never alters runtime behavior: the
|
||||
// original console methods and default error handling still run.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function traceClientLog(level: 'error' | 'warn', label: string, detail?: unknown): void {
|
||||
if (!isTraceEnabled()) return;
|
||||
push({
|
||||
source: 'client',
|
||||
kind: `client:${level}`,
|
||||
label: `${level === 'error' ? '✕' : '⚠'} ${label}`,
|
||||
detail: detailOf(detail),
|
||||
});
|
||||
}
|
||||
|
||||
let clientCaptureInstalled = false;
|
||||
|
||||
/** Wire up window error + console.error/warn capture into the trace buffer. */
|
||||
export function installClientErrorCapture(): void {
|
||||
if (clientCaptureInstalled || !isTraceEnabled()) return;
|
||||
clientCaptureInstalled = true;
|
||||
|
||||
try {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('error', (e: ErrorEvent) => {
|
||||
traceClientLog('error', e.message || 'window error', {
|
||||
source: e.filename,
|
||||
line: e.lineno,
|
||||
col: e.colno,
|
||||
stack: e.error instanceof Error ? e.error.stack : undefined,
|
||||
});
|
||||
});
|
||||
window.addEventListener('unhandledrejection', (e: PromiseRejectionEvent) => {
|
||||
const reason = e.reason;
|
||||
traceClientLog('error', 'unhandled promise rejection', {
|
||||
reason: reason instanceof Error ? `${reason.name}: ${reason.message}` : reason,
|
||||
stack: reason instanceof Error ? reason.stack : undefined,
|
||||
});
|
||||
});
|
||||
}
|
||||
} catch {
|
||||
// window unavailable
|
||||
}
|
||||
|
||||
for (const level of ['error', 'warn'] as const) {
|
||||
const original = console[level];
|
||||
if (typeof original !== 'function') continue;
|
||||
console[level] = (...args: unknown[]): void => {
|
||||
try {
|
||||
traceClientLog(level, args.map(stringifyArg).join(' '), args.length > 1 ? args : args[0]);
|
||||
} catch {
|
||||
// never let tracing break logging
|
||||
}
|
||||
original.apply(console, args);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function stringifyArg(a: unknown): string {
|
||||
if (typeof a === 'string') return a;
|
||||
if (a instanceof Error) return `${a.name}: ${a.message}`;
|
||||
try {
|
||||
return JSON.stringify(a);
|
||||
} catch {
|
||||
return String(a);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Export
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,7 +1,13 @@
|
|||
import { createApp } from 'vue';
|
||||
import App from './App.vue';
|
||||
import i18n from './i18n';
|
||||
import { installClientErrorCapture } from './debug/trace';
|
||||
import '@fontsource-variable/jetbrains-mono/wght.css';
|
||||
import './style.css';
|
||||
|
||||
// Opt-in (only with ?debug=1 / the debug flag): fold front-end errors and
|
||||
// console.error/warn into the trace buffer so the panel's "export jsonl" gives
|
||||
// a complete troubleshooting log, not just network traffic.
|
||||
installClientErrorCapture();
|
||||
|
||||
createApp(App).use(i18n).mount('#app');
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { DaemonHttpClient } from '../src/api/daemon/http';
|
|||
import { DaemonEventSocket, type DaemonEventSocketHandlers } from '../src/api/daemon/ws';
|
||||
import {
|
||||
clearTrace,
|
||||
installClientErrorCapture,
|
||||
sanitizeForTrace,
|
||||
traceEntries,
|
||||
traceToJsonl,
|
||||
|
|
@ -42,6 +43,24 @@ afterEach(() => {
|
|||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe('client-side error capture', () => {
|
||||
it('folds console.error into the trace so the export includes app errors', () => {
|
||||
const original = console.error;
|
||||
installClientErrorCapture();
|
||||
try {
|
||||
console.error('render failed', new Error('boom'));
|
||||
} finally {
|
||||
console.error = original; // undo the install-once wrap for other tests
|
||||
}
|
||||
const entry = traceEntries().find((e) => e.kind === 'client:error');
|
||||
expect(entry).toBeDefined();
|
||||
expect(entry!.source).toBe('client');
|
||||
expect(entry!.label).toContain('render failed');
|
||||
// The exported JSONL carries the client entry alongside network traffic.
|
||||
expect(traceToJsonl().includes('"client:error"')).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('REST tracing via DaemonHttpClient', () => {
|
||||
it('records request + response with envelope code, status, duration and requestId', async () => {
|
||||
vi.stubGlobal('fetch', vi.fn(async () => okEnvelope({ id: 'ses_1' })));
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue