browsertrack 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +143 -0
- package/dist/chunk-6A7FFDIB.js +221 -0
- package/dist/chunk-6A7FFDIB.js.map +1 -0
- package/dist/chunk-ANMR5WBH.js +659 -0
- package/dist/chunk-ANMR5WBH.js.map +1 -0
- package/dist/chunk-BSKNG4V7.js +1199 -0
- package/dist/chunk-BSKNG4V7.js.map +1 -0
- package/dist/chunk-X7C5CHBO.js +2080 -0
- package/dist/chunk-X7C5CHBO.js.map +1 -0
- package/dist/chunk-ZLNGOOH2.js +554 -0
- package/dist/chunk-ZLNGOOH2.js.map +1 -0
- package/dist/cli/index.js +2829 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/client/index.cjs +2218 -0
- package/dist/client/index.d.ts +197 -0
- package/dist/client/index.js +20 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client.iife.js +309 -0
- package/dist/core/index.d.ts +78 -0
- package/dist/core/index.js +31 -0
- package/dist/core/index.js.map +1 -0
- package/dist/daemon/index.d.ts +58 -0
- package/dist/daemon/index.js +32 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/engine-DKloFjvs.d.ts +157 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +11 -0
- package/dist/mcp/index.js +12 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/notes-CBvN91Wf.d.ts +260 -0
- package/dist/projects-CY8ungMt.d.ts +99 -0
- package/dist/server-CN-se8td.d.ts +55 -0
- package/examples/test-app/index.html +299 -0
- package/package.json +52 -7
- package/packages/cli/src/index.ts +413 -0
- package/packages/client/package.json +28 -0
- package/packages/client/src/breadcrumbs.ts +30 -0
- package/packages/client/src/client.ts +278 -0
- package/packages/client/src/commands/handler.ts +301 -0
- package/packages/client/src/config.ts +37 -0
- package/packages/client/src/index.ts +50 -0
- package/packages/client/src/interceptors/console.ts +68 -0
- package/packages/client/src/interceptors/interaction.ts +119 -0
- package/packages/client/src/interceptors/navigation.ts +93 -0
- package/packages/client/src/interceptors/network.ts +165 -0
- package/packages/client/src/interceptors/runtime.ts +65 -0
- package/packages/client/src/notes/inspector.ts +943 -0
- package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
- package/packages/client/src/screenshot/driver.ts +14 -0
- package/packages/client/src/transport/websocket.ts +179 -0
- package/packages/client/tsconfig.json +8 -0
- package/packages/core/dist/index.d.ts +317 -0
- package/packages/core/dist/index.js +221 -0
- package/packages/core/package.json +23 -0
- package/packages/core/src/fingerprint.ts +120 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/redaction.ts +139 -0
- package/packages/core/src/selector.ts +77 -0
- package/packages/core/src/types/commands.ts +101 -0
- package/packages/core/src/types/events.ts +108 -0
- package/packages/core/src/types/incidents.ts +54 -0
- package/packages/core/src/types/notes.ts +106 -0
- package/packages/core/src/types/probes.ts +44 -0
- package/packages/core/src/types/projects.ts +20 -0
- package/packages/core/tsconfig.json +8 -0
- package/packages/daemon/src/config.ts +29 -0
- package/packages/daemon/src/incidents/engine.ts +211 -0
- package/packages/daemon/src/index.ts +18 -0
- package/packages/daemon/src/notes/engine.ts +92 -0
- package/packages/daemon/src/notes/verification.ts +191 -0
- package/packages/daemon/src/server/daemon.ts +112 -0
- package/packages/daemon/src/server/http.ts +165 -0
- package/packages/daemon/src/server/ws.ts +175 -0
- package/packages/daemon/src/session/manager.ts +126 -0
- package/packages/daemon/src/storage/db.ts +733 -0
- package/packages/daemon/src/storage/screenshot-store.ts +49 -0
- package/packages/daemon/src/verification/engine.ts +264 -0
- package/packages/mcp/src/handlers.ts +398 -0
- package/packages/mcp/src/index.ts +3 -0
- package/packages/mcp/src/server.ts +86 -0
- package/packages/mcp/src/tools.ts +236 -0
- package/src/index.ts +4 -0
- package/test/client/interceptors.test.ts +69 -0
- package/test/core/fingerprint.test.ts +53 -0
- package/test/core/notes.test.ts +66 -0
- package/test/core/redaction.test.ts +48 -0
- package/test/daemon/incident-engine.test.ts +98 -0
- package/test/daemon/notes-storage.test.ts +130 -0
- package/test/daemon/notes-verification.test.ts +135 -0
- package/test/daemon/storage.test.ts +123 -0
- package/test/daemon/verification-engine.test.ts +88 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
- package/test/e2e/visual-notes-e2e.test.ts +205 -0
- package/test/mcp/handlers.test.ts +116 -0
- package/test/mcp/notes.test.ts +107 -0
- package/tsconfig.base.json +17 -0
- package/tsconfig.json +26 -0
- package/tsup.config.ts +54 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { snapdom } from '@zumer/snapdom';
|
|
2
|
+
import type { ScreenshotDriver, ScreenshotResult } from './driver.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Modern DOM snapshot driver using @zumer/snapdom with graceful fallbacks.
|
|
6
|
+
* Converts DOM subtrees and elements into WebP / PNG data URLs.
|
|
7
|
+
*/
|
|
8
|
+
export class BrowserScriptScreenshotDriver implements ScreenshotDriver {
|
|
9
|
+
public name = 'BrowserScriptScreenshotDriver';
|
|
10
|
+
|
|
11
|
+
public async captureSelector(selector: string): Promise<ScreenshotResult> {
|
|
12
|
+
try {
|
|
13
|
+
if (typeof document === 'undefined') {
|
|
14
|
+
return { ok: false, reason: 'NO_DOCUMENT_ENVIRONMENT' };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const el = document.querySelector(selector);
|
|
18
|
+
if (!el) {
|
|
19
|
+
return { ok: false, reason: `ELEMENT_NOT_FOUND: ${selector}` };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return await this.captureElement(el);
|
|
23
|
+
} catch (err: any) {
|
|
24
|
+
return { ok: false, reason: err?.message || 'UNKNOWN_CAPTURE_ERROR' };
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
public async captureElement(element: HTMLElement | Element): Promise<ScreenshotResult> {
|
|
29
|
+
try {
|
|
30
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
31
|
+
return { ok: false, reason: 'NO_BROWSER_ENVIRONMENT' };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const rect = element.getBoundingClientRect();
|
|
35
|
+
const width = Math.max(Math.round(rect.width), 10);
|
|
36
|
+
const height = Math.max(Math.round(rect.height), 10);
|
|
37
|
+
|
|
38
|
+
// Attempt capture with snapdom
|
|
39
|
+
const capturePromise = (async () => {
|
|
40
|
+
try {
|
|
41
|
+
// Prefer WebP export if supported, otherwise fallback to PNG
|
|
42
|
+
let imgElement: HTMLImageElement | undefined = undefined;
|
|
43
|
+
let format: 'webp' | 'png' = 'webp';
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
if (typeof snapdom === 'function') {
|
|
47
|
+
const captureRes = await snapdom(element as any, { scale: 1, embedFonts: false });
|
|
48
|
+
if (captureRes && typeof captureRes.toWebp === 'function') {
|
|
49
|
+
imgElement = await captureRes.toWebp();
|
|
50
|
+
} else if (captureRes && typeof captureRes.toPng === 'function') {
|
|
51
|
+
imgElement = await captureRes.toPng();
|
|
52
|
+
format = 'png';
|
|
53
|
+
} else if (captureRes?.url) {
|
|
54
|
+
return {
|
|
55
|
+
ok: true,
|
|
56
|
+
dataUrl: captureRes.url,
|
|
57
|
+
format: 'webp' as const,
|
|
58
|
+
width,
|
|
59
|
+
height,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
} else if ((snapdom as any)?.toWebp) {
|
|
63
|
+
imgElement = await (snapdom as any).toWebp(element, {
|
|
64
|
+
scale: 1,
|
|
65
|
+
embedFonts: false,
|
|
66
|
+
});
|
|
67
|
+
} else if ((snapdom as any)?.toPng) {
|
|
68
|
+
imgElement = await (snapdom as any).toPng(element, {
|
|
69
|
+
scale: 1,
|
|
70
|
+
embedFonts: false,
|
|
71
|
+
});
|
|
72
|
+
format = 'png';
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// Fall through to fallback
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (imgElement && imgElement.src) {
|
|
79
|
+
return {
|
|
80
|
+
ok: true,
|
|
81
|
+
dataUrl: imgElement.src,
|
|
82
|
+
format: imgElement.src.startsWith('data:image/webp') ? ('webp' as const) : format,
|
|
83
|
+
width: imgElement.naturalWidth || width,
|
|
84
|
+
height: imgElement.naturalHeight || height,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
} catch (snapErr: any) {
|
|
88
|
+
const errMsg = String(snapErr?.message || snapErr);
|
|
89
|
+
if (errMsg.includes('CORS') || errMsg.includes('SecurityError') || errMsg.includes('tainted')) {
|
|
90
|
+
return { ok: false, reason: 'CROSS_ORIGIN_RESOURCE' };
|
|
91
|
+
}
|
|
92
|
+
// Fallback to minimal canvas capture below
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Fallback: minimal inline SVG ForeignObject capture
|
|
96
|
+
return await this.captureWithSvgFallback(element, width, height);
|
|
97
|
+
})();
|
|
98
|
+
|
|
99
|
+
// 3 second timeout protection
|
|
100
|
+
const timeoutPromise = new Promise<ScreenshotResult>((resolve) => {
|
|
101
|
+
setTimeout(() => resolve({ ok: false, reason: 'CAPTURE_TIMEOUT' }), 3000);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
return await Promise.race([capturePromise, timeoutPromise]);
|
|
105
|
+
} catch (err: any) {
|
|
106
|
+
return { ok: false, reason: err?.message || 'CAPTURE_EXCEPTION' };
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private async captureWithSvgFallback(
|
|
111
|
+
element: HTMLElement | Element,
|
|
112
|
+
width: number,
|
|
113
|
+
height: number
|
|
114
|
+
): Promise<ScreenshotResult> {
|
|
115
|
+
try {
|
|
116
|
+
const clone = element.cloneNode(true) as HTMLElement;
|
|
117
|
+
const computed = window.getComputedStyle(element);
|
|
118
|
+
let cssText = '';
|
|
119
|
+
for (let i = 0; i < computed.length; i++) {
|
|
120
|
+
const prop = computed[i];
|
|
121
|
+
cssText += `${prop}:${computed.getPropertyValue(prop)};`;
|
|
122
|
+
}
|
|
123
|
+
clone.style.cssText = cssText;
|
|
124
|
+
clone.style.margin = '0';
|
|
125
|
+
clone.style.position = 'static';
|
|
126
|
+
|
|
127
|
+
const svg = `
|
|
128
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">
|
|
129
|
+
<foreignObject width="100%" height="100%">
|
|
130
|
+
<div xmlns="http://www.w3.org/1999/xhtml" style="width:${width}px;height:${height}px;">
|
|
131
|
+
${clone.outerHTML}
|
|
132
|
+
</div>
|
|
133
|
+
</foreignObject>
|
|
134
|
+
</svg>
|
|
135
|
+
`.trim();
|
|
136
|
+
|
|
137
|
+
const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
|
|
138
|
+
const url = URL.createObjectURL(svgBlob);
|
|
139
|
+
|
|
140
|
+
return await new Promise<ScreenshotResult>((resolve) => {
|
|
141
|
+
const img = new Image();
|
|
142
|
+
img.onload = () => {
|
|
143
|
+
try {
|
|
144
|
+
const canvas = document.createElement('canvas');
|
|
145
|
+
canvas.width = width;
|
|
146
|
+
canvas.height = height;
|
|
147
|
+
const ctx = canvas.getContext('2d');
|
|
148
|
+
if (!ctx) {
|
|
149
|
+
URL.revokeObjectURL(url);
|
|
150
|
+
resolve({ ok: false, reason: 'CANVAS_CONTEXT_UNAVAILABLE' });
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
ctx.drawImage(img, 0, 0);
|
|
155
|
+
URL.revokeObjectURL(url);
|
|
156
|
+
|
|
157
|
+
let dataUrl: string;
|
|
158
|
+
try {
|
|
159
|
+
dataUrl = canvas.toDataURL('image/webp', 0.85);
|
|
160
|
+
} catch {
|
|
161
|
+
dataUrl = canvas.toDataURL('image/png');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
resolve({
|
|
165
|
+
ok: true,
|
|
166
|
+
dataUrl,
|
|
167
|
+
format: dataUrl.startsWith('data:image/webp') ? 'webp' : 'png',
|
|
168
|
+
width,
|
|
169
|
+
height,
|
|
170
|
+
});
|
|
171
|
+
} catch (e: any) {
|
|
172
|
+
URL.revokeObjectURL(url);
|
|
173
|
+
if (e?.name === 'SecurityError' || String(e).includes('tainted')) {
|
|
174
|
+
resolve({ ok: false, reason: 'CROSS_ORIGIN_RESOURCE' });
|
|
175
|
+
} else {
|
|
176
|
+
resolve({ ok: false, reason: e?.message || 'CANVAS_EXPORT_FAILED' });
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
img.onerror = () => {
|
|
182
|
+
URL.revokeObjectURL(url);
|
|
183
|
+
resolve({ ok: false, reason: 'IMAGE_LOAD_FAILED' });
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
img.src = url;
|
|
187
|
+
});
|
|
188
|
+
} catch (e: any) {
|
|
189
|
+
return { ok: false, reason: e?.message || 'FALLBACK_CAPTURE_FAILED' };
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface ScreenshotResult {
|
|
2
|
+
ok: boolean;
|
|
3
|
+
dataUrl?: string;
|
|
4
|
+
format?: 'webp' | 'png' | 'jpeg';
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
reason?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ScreenshotDriver {
|
|
11
|
+
name: string;
|
|
12
|
+
captureElement(element: HTMLElement | Element): Promise<ScreenshotResult>;
|
|
13
|
+
captureSelector(selector: string): Promise<ScreenshotResult>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { HelloMessage, ClientCommand, CommandResponse } from '../../../core/src/index.js';
|
|
2
|
+
import type { ClientCommandHandler } from '../commands/handler.js';
|
|
3
|
+
|
|
4
|
+
export interface TransportOptions {
|
|
5
|
+
url: string;
|
|
6
|
+
projectId?: string;
|
|
7
|
+
debug?: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class WebSocketTransport {
|
|
11
|
+
private wsUrl: string;
|
|
12
|
+
private projectId?: string;
|
|
13
|
+
private debug: boolean;
|
|
14
|
+
private ws: WebSocket | null = null;
|
|
15
|
+
private sessionId: string | null = null;
|
|
16
|
+
private queue: string[] = [];
|
|
17
|
+
private maxQueueSize = 50;
|
|
18
|
+
private reconnectAttempts = 0;
|
|
19
|
+
private maxReconnectDelay = 10000;
|
|
20
|
+
private reconnectTimer: any = null;
|
|
21
|
+
private commandHandler?: ClientCommandHandler;
|
|
22
|
+
private isDestroyed = false;
|
|
23
|
+
|
|
24
|
+
constructor(options: TransportOptions) {
|
|
25
|
+
this.wsUrl = options.url || 'ws://127.0.0.1:7331';
|
|
26
|
+
this.projectId = options.projectId;
|
|
27
|
+
this.debug = !!options.debug;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public setCommandHandler(handler: ClientCommandHandler): void {
|
|
31
|
+
this.commandHandler = handler;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
public getSessionId(): string | null {
|
|
35
|
+
return this.sessionId;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public isConnected(): boolean {
|
|
39
|
+
return this.ws !== null && this.ws.readyState === (typeof WebSocket !== 'undefined' ? WebSocket.OPEN : 1);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public connect(): void {
|
|
43
|
+
if (this.isDestroyed || typeof WebSocket === 'undefined') return;
|
|
44
|
+
if (this.ws && (this.ws.readyState === WebSocket.CONNECTING || this.ws.readyState === WebSocket.OPEN)) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
this.ws = new WebSocket(this.wsUrl);
|
|
50
|
+
|
|
51
|
+
this.ws.onopen = () => {
|
|
52
|
+
this.reconnectAttempts = 0;
|
|
53
|
+
this.sendHello();
|
|
54
|
+
this.flushQueue();
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
this.ws.onmessage = async (event: MessageEvent) => {
|
|
58
|
+
try {
|
|
59
|
+
const raw = typeof event.data === 'string' ? event.data : '';
|
|
60
|
+
if (!raw) return;
|
|
61
|
+
const msg = JSON.parse(raw);
|
|
62
|
+
|
|
63
|
+
if (msg.type === 'hello_ack') {
|
|
64
|
+
this.sessionId = msg.sessionId;
|
|
65
|
+
if (this.debug) {
|
|
66
|
+
console.log('[BrowserTrack] Connected, session ID:', this.sessionId);
|
|
67
|
+
}
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (msg.type === 'command' && this.commandHandler) {
|
|
72
|
+
const command: ClientCommand = msg.command;
|
|
73
|
+
const res: CommandResponse = await this.commandHandler.executeCommand(command);
|
|
74
|
+
this.send({
|
|
75
|
+
type: 'command_response',
|
|
76
|
+
sessionId: this.sessionId,
|
|
77
|
+
response: res,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Defensive
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
this.ws.onerror = () => {
|
|
86
|
+
// Silent fail if daemon is offline
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
this.ws.onclose = () => {
|
|
90
|
+
this.ws = null;
|
|
91
|
+
this.scheduleReconnect();
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
this.scheduleReconnect();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
public send(payload: any): void {
|
|
99
|
+
const raw = JSON.stringify(payload);
|
|
100
|
+
if (this.isConnected()) {
|
|
101
|
+
try {
|
|
102
|
+
this.ws!.send(raw);
|
|
103
|
+
} catch {
|
|
104
|
+
this.enqueue(raw);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
this.enqueue(raw);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private enqueue(raw: string): void {
|
|
112
|
+
if (this.queue.length >= this.maxQueueSize) {
|
|
113
|
+
this.queue.shift();
|
|
114
|
+
}
|
|
115
|
+
this.queue.push(raw);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private flushQueue(): void {
|
|
119
|
+
if (!this.isConnected()) return;
|
|
120
|
+
while (this.queue.length > 0) {
|
|
121
|
+
const item = this.queue.shift();
|
|
122
|
+
if (item) {
|
|
123
|
+
try {
|
|
124
|
+
this.ws!.send(item);
|
|
125
|
+
} catch {
|
|
126
|
+
this.queue.unshift(item);
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
private sendHello(): void {
|
|
134
|
+
if (typeof window === 'undefined') return;
|
|
135
|
+
|
|
136
|
+
const hello: HelloMessage = {
|
|
137
|
+
type: 'hello',
|
|
138
|
+
origin: window.location.origin,
|
|
139
|
+
url: window.location.href,
|
|
140
|
+
title: document.title,
|
|
141
|
+
userAgent: navigator.userAgent,
|
|
142
|
+
timestamp: Date.now(),
|
|
143
|
+
projectId: this.projectId,
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
try {
|
|
147
|
+
this.ws!.send(JSON.stringify(hello));
|
|
148
|
+
} catch {
|
|
149
|
+
// Defensive
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
private scheduleReconnect(): void {
|
|
154
|
+
if (this.isDestroyed || this.reconnectTimer) return;
|
|
155
|
+
|
|
156
|
+
this.reconnectAttempts++;
|
|
157
|
+
const delay = Math.min(1000 * Math.pow(1.5, this.reconnectAttempts), this.maxReconnectDelay);
|
|
158
|
+
|
|
159
|
+
this.reconnectTimer = setTimeout(() => {
|
|
160
|
+
this.reconnectTimer = null;
|
|
161
|
+
this.connect();
|
|
162
|
+
}, delay);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public destroy(): void {
|
|
166
|
+
this.isDestroyed = true;
|
|
167
|
+
if (this.reconnectTimer) {
|
|
168
|
+
clearTimeout(this.reconnectTimer);
|
|
169
|
+
this.reconnectTimer = null;
|
|
170
|
+
}
|
|
171
|
+
if (this.ws) {
|
|
172
|
+
try {
|
|
173
|
+
this.ws.close();
|
|
174
|
+
} catch {}
|
|
175
|
+
this.ws = null;
|
|
176
|
+
}
|
|
177
|
+
this.queue = [];
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
type EventType = 'runtime_error' | 'unhandled_rejection' | 'console' | 'fetch' | 'xhr' | 'navigation' | 'interaction';
|
|
2
|
+
type ConsoleLevel = 'error' | 'warn' | 'info' | 'log' | 'debug';
|
|
3
|
+
interface ElementSummary {
|
|
4
|
+
selector: string;
|
|
5
|
+
tag: string;
|
|
6
|
+
id?: string;
|
|
7
|
+
classes?: string[];
|
|
8
|
+
attributes?: Record<string, string>;
|
|
9
|
+
outerHTML?: string;
|
|
10
|
+
innerText?: string;
|
|
11
|
+
boundingRect?: {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
width: number;
|
|
15
|
+
height: number;
|
|
16
|
+
top: number;
|
|
17
|
+
left: number;
|
|
18
|
+
bottom: number;
|
|
19
|
+
right: number;
|
|
20
|
+
};
|
|
21
|
+
visible: boolean;
|
|
22
|
+
}
|
|
23
|
+
interface Breadcrumb {
|
|
24
|
+
id?: string;
|
|
25
|
+
type: 'navigation' | 'click' | 'submit' | 'fetch' | 'xhr' | 'console' | 'error';
|
|
26
|
+
category?: string;
|
|
27
|
+
message: string;
|
|
28
|
+
timestamp: number;
|
|
29
|
+
level?: 'info' | 'warn' | 'error';
|
|
30
|
+
data?: Record<string, any>;
|
|
31
|
+
element?: ElementSummary;
|
|
32
|
+
}
|
|
33
|
+
interface NetworkEvent {
|
|
34
|
+
id?: string;
|
|
35
|
+
url: string;
|
|
36
|
+
method: string;
|
|
37
|
+
status?: number;
|
|
38
|
+
statusText?: string;
|
|
39
|
+
durationMs: number;
|
|
40
|
+
error?: string;
|
|
41
|
+
aborted?: boolean;
|
|
42
|
+
timestamp: number;
|
|
43
|
+
requestHeaders?: Record<string, string>;
|
|
44
|
+
}
|
|
45
|
+
interface RuntimeErrorEvent {
|
|
46
|
+
message: string;
|
|
47
|
+
stack?: string;
|
|
48
|
+
filename?: string;
|
|
49
|
+
lineno?: number;
|
|
50
|
+
colno?: number;
|
|
51
|
+
errorType: string;
|
|
52
|
+
timestamp: number;
|
|
53
|
+
}
|
|
54
|
+
interface ConsoleEvent {
|
|
55
|
+
level: ConsoleLevel;
|
|
56
|
+
message: string;
|
|
57
|
+
args?: any[];
|
|
58
|
+
stack?: string;
|
|
59
|
+
timestamp: number;
|
|
60
|
+
}
|
|
61
|
+
interface NavigationEvent {
|
|
62
|
+
from?: string;
|
|
63
|
+
to: string;
|
|
64
|
+
type: 'pushState' | 'replaceState' | 'popstate' | 'hashchange' | 'initial';
|
|
65
|
+
timestamp: number;
|
|
66
|
+
}
|
|
67
|
+
interface HelloMessage {
|
|
68
|
+
type: 'hello';
|
|
69
|
+
origin: string;
|
|
70
|
+
url: string;
|
|
71
|
+
title: string;
|
|
72
|
+
userAgent: string;
|
|
73
|
+
timestamp: number;
|
|
74
|
+
projectId?: string;
|
|
75
|
+
}
|
|
76
|
+
interface ClientEventMessage {
|
|
77
|
+
type: 'event';
|
|
78
|
+
sessionId: string;
|
|
79
|
+
eventType: EventType;
|
|
80
|
+
payload: RuntimeErrorEvent | ConsoleEvent | NetworkEvent | NavigationEvent | Breadcrumb;
|
|
81
|
+
breadcrumbs?: Breadcrumb[];
|
|
82
|
+
lastElement?: ElementSummary;
|
|
83
|
+
route?: string;
|
|
84
|
+
url: string;
|
|
85
|
+
title?: string;
|
|
86
|
+
timestamp: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
type IncidentStatus = 'OPEN' | 'FIX_ATTEMPTED' | 'VERIFYING' | 'VERIFIED' | 'FAILED' | 'INCONCLUSIVE';
|
|
90
|
+
type IncidentSeverity = 'error' | 'warn' | 'fatal';
|
|
91
|
+
interface IncidentSource {
|
|
92
|
+
file: string;
|
|
93
|
+
line: number;
|
|
94
|
+
column?: number;
|
|
95
|
+
}
|
|
96
|
+
interface Incident {
|
|
97
|
+
id: string;
|
|
98
|
+
projectId: string;
|
|
99
|
+
sessionId: string;
|
|
100
|
+
type: string;
|
|
101
|
+
severity: IncidentSeverity;
|
|
102
|
+
message: string;
|
|
103
|
+
source: IncidentSource;
|
|
104
|
+
fingerprint: string;
|
|
105
|
+
route: string;
|
|
106
|
+
firstSeen: string;
|
|
107
|
+
lastSeen: string;
|
|
108
|
+
occurrences: number;
|
|
109
|
+
status: IncidentStatus;
|
|
110
|
+
stack?: string;
|
|
111
|
+
breadcrumbs: Breadcrumb[];
|
|
112
|
+
networkFailures: NetworkEvent[];
|
|
113
|
+
lastElement?: ElementSummary;
|
|
114
|
+
screenshots?: {
|
|
115
|
+
error?: string;
|
|
116
|
+
before?: string;
|
|
117
|
+
after?: string;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
interface IncidentOccurrence {
|
|
121
|
+
id: string;
|
|
122
|
+
incidentId: string;
|
|
123
|
+
sessionId: string;
|
|
124
|
+
timestamp: string;
|
|
125
|
+
route: string;
|
|
126
|
+
url: string;
|
|
127
|
+
stack?: string;
|
|
128
|
+
breadcrumbs: Breadcrumb[];
|
|
129
|
+
lastElement?: ElementSummary;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
type CommandType = 'reload' | 'navigate' | 'get_page_state' | 'query_element' | 'capture_element';
|
|
133
|
+
interface ClientCommand<T = any> {
|
|
134
|
+
id: string;
|
|
135
|
+
type: CommandType;
|
|
136
|
+
params?: T;
|
|
137
|
+
}
|
|
138
|
+
interface ReloadParams {
|
|
139
|
+
force?: boolean;
|
|
140
|
+
}
|
|
141
|
+
interface NavigateParams {
|
|
142
|
+
url: string;
|
|
143
|
+
}
|
|
144
|
+
interface QueryElementParams {
|
|
145
|
+
selector: string;
|
|
146
|
+
}
|
|
147
|
+
interface CaptureElementParams {
|
|
148
|
+
selector?: string;
|
|
149
|
+
}
|
|
150
|
+
interface QueryElementResult {
|
|
151
|
+
exists: boolean;
|
|
152
|
+
visible?: boolean;
|
|
153
|
+
tag?: string;
|
|
154
|
+
id?: string;
|
|
155
|
+
classes?: string[];
|
|
156
|
+
boundingRect?: {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
width: number;
|
|
160
|
+
height: number;
|
|
161
|
+
top: number;
|
|
162
|
+
left: number;
|
|
163
|
+
bottom: number;
|
|
164
|
+
right: number;
|
|
165
|
+
};
|
|
166
|
+
innerText?: string;
|
|
167
|
+
outerHTML?: string;
|
|
168
|
+
}
|
|
169
|
+
interface PageStateResult {
|
|
170
|
+
url: string;
|
|
171
|
+
route: string;
|
|
172
|
+
title: string;
|
|
173
|
+
readyState: string;
|
|
174
|
+
activeElement?: ElementSummary;
|
|
175
|
+
}
|
|
176
|
+
interface CaptureElementResult {
|
|
177
|
+
dataUrl?: string;
|
|
178
|
+
format?: string;
|
|
179
|
+
width?: number;
|
|
180
|
+
height?: number;
|
|
181
|
+
}
|
|
182
|
+
interface CommandResponse<T = any> {
|
|
183
|
+
id: string;
|
|
184
|
+
ok: boolean;
|
|
185
|
+
result?: T;
|
|
186
|
+
error?: string;
|
|
187
|
+
reason?: string;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
type ProbeType = 'element_exists' | 'element_visible' | 'text_contains' | 'route_is' | 'network_request_succeeded' | 'no_incident';
|
|
191
|
+
interface VerificationProbe {
|
|
192
|
+
type: ProbeType;
|
|
193
|
+
selector?: string;
|
|
194
|
+
text?: string;
|
|
195
|
+
route?: string;
|
|
196
|
+
fingerprint?: string;
|
|
197
|
+
urlPattern?: string;
|
|
198
|
+
timeoutMs?: number;
|
|
199
|
+
}
|
|
200
|
+
interface VerificationRecipe {
|
|
201
|
+
route?: string;
|
|
202
|
+
targetSelector?: string;
|
|
203
|
+
expect?: VerificationProbe[];
|
|
204
|
+
observationWindowMs?: number;
|
|
205
|
+
}
|
|
206
|
+
interface ProbeResult {
|
|
207
|
+
type: ProbeType;
|
|
208
|
+
passed: boolean;
|
|
209
|
+
details?: string;
|
|
210
|
+
}
|
|
211
|
+
interface VerificationResult {
|
|
212
|
+
incidentId: string;
|
|
213
|
+
status: IncidentStatus;
|
|
214
|
+
checks: ProbeResult[];
|
|
215
|
+
screenshots?: {
|
|
216
|
+
before?: string;
|
|
217
|
+
after?: string;
|
|
218
|
+
};
|
|
219
|
+
timestamp: string;
|
|
220
|
+
message?: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
interface Project {
|
|
224
|
+
id: string;
|
|
225
|
+
name: string;
|
|
226
|
+
origin: string;
|
|
227
|
+
path?: string;
|
|
228
|
+
createdAt: string;
|
|
229
|
+
updatedAt: string;
|
|
230
|
+
}
|
|
231
|
+
interface Session {
|
|
232
|
+
id: string;
|
|
233
|
+
projectId: string;
|
|
234
|
+
origin: string;
|
|
235
|
+
url: string;
|
|
236
|
+
title: string;
|
|
237
|
+
userAgent: string;
|
|
238
|
+
connectedAt: string;
|
|
239
|
+
lastSeenAt: string;
|
|
240
|
+
active: boolean;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Error fingerprinting and normalization utilities
|
|
245
|
+
*/
|
|
246
|
+
interface FingerprintInput {
|
|
247
|
+
type: string;
|
|
248
|
+
message: string;
|
|
249
|
+
sourceFile?: string;
|
|
250
|
+
line?: number;
|
|
251
|
+
column?: number;
|
|
252
|
+
stack?: string;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Normalizes error messages by stripping dynamic content like UUIDs, timestamps,
|
|
256
|
+
* hex addresses, and file timestamps.
|
|
257
|
+
*/
|
|
258
|
+
declare function normalizeErrorMessage(message: string): string;
|
|
259
|
+
/**
|
|
260
|
+
* Normalizes source filenames by removing origin and cache-busting query strings.
|
|
261
|
+
*/
|
|
262
|
+
declare function normalizeSourceFile(filename?: string): string;
|
|
263
|
+
/**
|
|
264
|
+
* Extracts source location from stack if not explicitly provided
|
|
265
|
+
*/
|
|
266
|
+
declare function extractSourceFromStack(stack?: string): {
|
|
267
|
+
file: string;
|
|
268
|
+
line: number;
|
|
269
|
+
column?: number;
|
|
270
|
+
} | null;
|
|
271
|
+
/**
|
|
272
|
+
* Computes a deterministic fingerprint for an error
|
|
273
|
+
*/
|
|
274
|
+
declare function computeFingerprint(input: FingerprintInput): string;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Security and Redaction Utilities
|
|
278
|
+
*/
|
|
279
|
+
declare const SENSITIVE_KEY_PATTERNS: RegExp[];
|
|
280
|
+
declare const SENSITIVE_QUERY_PARAMS: string[];
|
|
281
|
+
declare const REDACTED_PLACEHOLDER = "[REDACTED]";
|
|
282
|
+
/**
|
|
283
|
+
* Checks if a property name matches any sensitive pattern
|
|
284
|
+
*/
|
|
285
|
+
declare function isSensitiveKey(key: string): boolean;
|
|
286
|
+
/**
|
|
287
|
+
* Redacts sensitive query parameters from a URL string
|
|
288
|
+
*/
|
|
289
|
+
declare function redactUrl(rawUrl: string): string;
|
|
290
|
+
/**
|
|
291
|
+
* Redacts sensitive HTTP headers
|
|
292
|
+
*/
|
|
293
|
+
declare function redactHeaders(headers: Record<string, string>): Record<string, string>;
|
|
294
|
+
/**
|
|
295
|
+
* Deeply redacts sensitive keys from an object or array
|
|
296
|
+
*/
|
|
297
|
+
declare function redactSensitiveData<T = any>(data: T, maxDepth?: number, currentDepth?: number): T;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Semantic element selector generation and inspection
|
|
301
|
+
*/
|
|
302
|
+
interface SelectorOptions {
|
|
303
|
+
maxClasses?: number;
|
|
304
|
+
maxOuterHTMLLength?: number;
|
|
305
|
+
maxInnerTextLength?: number;
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Builds a clean, human-readable semantic selector for a DOM element.
|
|
309
|
+
* Prioritizes data-testid/data-test, id, semantic tag + specific class, or concise parent path.
|
|
310
|
+
*/
|
|
311
|
+
declare function getSemanticSelector(element: HTMLElement | Element, options?: SelectorOptions): string;
|
|
312
|
+
/**
|
|
313
|
+
* Truncates string to specified max length with ellipsis
|
|
314
|
+
*/
|
|
315
|
+
declare function truncate(str: string | undefined | null, maxLength?: number): string;
|
|
316
|
+
|
|
317
|
+
export { type Breadcrumb, type CaptureElementParams, type CaptureElementResult, type ClientCommand, type ClientEventMessage, type CommandResponse, type CommandType, type ConsoleEvent, type ConsoleLevel, type ElementSummary, type EventType, type FingerprintInput, type HelloMessage, type Incident, type IncidentOccurrence, type IncidentSeverity, type IncidentSource, type IncidentStatus, type NavigateParams, type NavigationEvent, type NetworkEvent, type PageStateResult, type ProbeResult, type ProbeType, type Project, type QueryElementParams, type QueryElementResult, REDACTED_PLACEHOLDER, type ReloadParams, type RuntimeErrorEvent, SENSITIVE_KEY_PATTERNS, SENSITIVE_QUERY_PARAMS, type SelectorOptions, type Session, type VerificationProbe, type VerificationRecipe, type VerificationResult, computeFingerprint, extractSourceFromStack, getSemanticSelector, isSensitiveKey, normalizeErrorMessage, normalizeSourceFile, redactHeaders, redactSensitiveData, redactUrl, truncate };
|