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,278 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Breadcrumb,
|
|
3
|
+
ClientEventMessage,
|
|
4
|
+
ConsoleEvent,
|
|
5
|
+
ElementSummary,
|
|
6
|
+
NavigationEvent,
|
|
7
|
+
NetworkEvent,
|
|
8
|
+
RuntimeErrorEvent,
|
|
9
|
+
} from '../../core/src/index.js';
|
|
10
|
+
import { BreadcrumbBuffer } from './breadcrumbs.js';
|
|
11
|
+
import { ClientCommandHandler } from './commands/handler.js';
|
|
12
|
+
import type { BrowserDiagClientOptions } from './config.js';
|
|
13
|
+
import { DEFAULT_OPTIONS } from './config.js';
|
|
14
|
+
import { setupConsoleInterceptors } from './interceptors/console.js';
|
|
15
|
+
import { setupInteractionInterceptors } from './interceptors/interaction.js';
|
|
16
|
+
import { setupNavigationInterceptors } from './interceptors/navigation.js';
|
|
17
|
+
import { setupNetworkInterceptors } from './interceptors/network.js';
|
|
18
|
+
import { setupRuntimeInterceptors } from './interceptors/runtime.js';
|
|
19
|
+
import { BrowserScriptScreenshotDriver } from './screenshot/browser-script-driver.js';
|
|
20
|
+
import type { ScreenshotDriver } from './screenshot/driver.js';
|
|
21
|
+
import { WebSocketTransport } from './transport/websocket.js';
|
|
22
|
+
import { NoteInspector } from './notes/inspector.js';
|
|
23
|
+
|
|
24
|
+
export class BrowserTrackClient {
|
|
25
|
+
public options: Required<Omit<BrowserDiagClientOptions, 'notes'>> & { notes: Required<NonNullable<BrowserDiagClientOptions['notes']>> };
|
|
26
|
+
private breadcrumbs: BreadcrumbBuffer;
|
|
27
|
+
private transport: WebSocketTransport;
|
|
28
|
+
private screenshotDriver: ScreenshotDriver;
|
|
29
|
+
private commandHandler: ClientCommandHandler;
|
|
30
|
+
private inspector?: NoteInspector;
|
|
31
|
+
private lastElement: ElementSummary | undefined;
|
|
32
|
+
private currentRoute = '/';
|
|
33
|
+
private cleanups: (() => void)[] = [];
|
|
34
|
+
private isInitialized = false;
|
|
35
|
+
|
|
36
|
+
constructor(options: BrowserDiagClientOptions = {}) {
|
|
37
|
+
this.options = {
|
|
38
|
+
...DEFAULT_OPTIONS,
|
|
39
|
+
...options,
|
|
40
|
+
notes: {
|
|
41
|
+
...DEFAULT_OPTIONS.notes,
|
|
42
|
+
...(options.notes || {}),
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
this.breadcrumbs = new BreadcrumbBuffer(this.options.maxBreadcrumbs);
|
|
46
|
+
this.transport = new WebSocketTransport({
|
|
47
|
+
url: this.options.daemonUrl,
|
|
48
|
+
projectId: this.options.projectId,
|
|
49
|
+
debug: this.options.debug,
|
|
50
|
+
});
|
|
51
|
+
this.screenshotDriver = new BrowserScriptScreenshotDriver();
|
|
52
|
+
this.commandHandler = new ClientCommandHandler(this.screenshotDriver);
|
|
53
|
+
this.transport.setCommandHandler(this.commandHandler);
|
|
54
|
+
|
|
55
|
+
if (this.options.notes.enabled) {
|
|
56
|
+
this.inspector = new NoteInspector(this.transport, this.screenshotDriver, {
|
|
57
|
+
shortcut: this.options.notes.shortcut,
|
|
58
|
+
maskSelectors: this.options.notes.maskSelectors,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public init(): this {
|
|
64
|
+
if (this.isInitialized || typeof window === 'undefined') return this;
|
|
65
|
+
this.isInitialized = true;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
this.currentRoute = window.location.pathname + window.location.search;
|
|
69
|
+
} catch {
|
|
70
|
+
this.currentRoute = '/';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// 1. Connect WebSocket
|
|
74
|
+
this.transport.connect();
|
|
75
|
+
|
|
76
|
+
// 2. Setup Runtime error interceptor
|
|
77
|
+
if (this.options.captureErrors) {
|
|
78
|
+
const cleanupRuntime = setupRuntimeInterceptors(async (errEvent) => {
|
|
79
|
+
await this.handleRuntimeError(errEvent);
|
|
80
|
+
});
|
|
81
|
+
this.cleanups.push(cleanupRuntime);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 3. Setup Console interceptor
|
|
85
|
+
if (this.options.captureConsole) {
|
|
86
|
+
const cleanupConsole = setupConsoleInterceptors(async (consoleEvent) => {
|
|
87
|
+
await this.handleConsole(consoleEvent);
|
|
88
|
+
});
|
|
89
|
+
this.cleanups.push(cleanupConsole);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. Setup Network interceptor
|
|
93
|
+
if (this.options.captureNetwork) {
|
|
94
|
+
const cleanupNetwork = setupNetworkInterceptors((netEvent) => {
|
|
95
|
+
this.handleNetwork(netEvent);
|
|
96
|
+
});
|
|
97
|
+
this.cleanups.push(cleanupNetwork);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 5. Setup Navigation interceptor
|
|
101
|
+
if (this.options.captureNavigation) {
|
|
102
|
+
const cleanupNav = setupNavigationInterceptors((navEvent) => {
|
|
103
|
+
this.handleNavigation(navEvent);
|
|
104
|
+
});
|
|
105
|
+
this.cleanups.push(cleanupNav);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// 6. Setup Interaction interceptor
|
|
109
|
+
if (this.options.captureInteractions) {
|
|
110
|
+
const cleanupInteraction = setupInteractionInterceptors((breadcrumb, elSummary) => {
|
|
111
|
+
if (elSummary) {
|
|
112
|
+
this.lastElement = elSummary;
|
|
113
|
+
}
|
|
114
|
+
this.breadcrumbs.add(breadcrumb);
|
|
115
|
+
});
|
|
116
|
+
this.cleanups.push(cleanupInteraction);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 7. Setup Visual Note Inspector
|
|
120
|
+
if (this.inspector) {
|
|
121
|
+
this.inspector.init();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
public openNoteEditor(el?: HTMLElement, noteType: 'element' | 'page' = 'element'): void {
|
|
128
|
+
if (this.inspector) {
|
|
129
|
+
this.inspector.openNoteEditor(el, noteType);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public startRegionSelection(): void {
|
|
134
|
+
if (this.inspector) {
|
|
135
|
+
this.inspector.setMode('region');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
public startElementSelection(): void {
|
|
140
|
+
if (this.inspector) {
|
|
141
|
+
this.inspector.setMode('element');
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
public setInspectMode(mode: 'element' | 'region' | 'page' | 'idle'): void {
|
|
146
|
+
if (this.inspector) {
|
|
147
|
+
this.inspector.setMode(mode);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
public async createNote(targetEl: HTMLElement, message: string, noteType: 'element' | 'page' = 'element'): Promise<void> {
|
|
152
|
+
if (this.inspector) {
|
|
153
|
+
await this.inspector.saveVisualNote(targetEl, message, noteType);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async handleRuntimeError(errorEvent: RuntimeErrorEvent): Promise<void> {
|
|
158
|
+
// Add to breadcrumbs
|
|
159
|
+
this.breadcrumbs.add({
|
|
160
|
+
type: 'error',
|
|
161
|
+
category: 'runtime',
|
|
162
|
+
message: `${errorEvent.errorType}: ${errorEvent.message}`,
|
|
163
|
+
timestamp: errorEvent.timestamp,
|
|
164
|
+
level: 'error',
|
|
165
|
+
data: {
|
|
166
|
+
filename: errorEvent.filename,
|
|
167
|
+
lineno: errorEvent.lineno,
|
|
168
|
+
colno: errorEvent.colno,
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
let screenshotDataUrl: string | undefined;
|
|
173
|
+
if (this.options.onErrorScreenshot && this.lastElement?.selector) {
|
|
174
|
+
try {
|
|
175
|
+
const snap = await this.screenshotDriver.captureSelector(this.lastElement.selector);
|
|
176
|
+
if (snap.ok) {
|
|
177
|
+
screenshotDataUrl = snap.dataUrl;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
// Defensive
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
this.sendEvent('runtime_error', errorEvent, screenshotDataUrl);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private async handleConsole(consoleEvent: ConsoleEvent): Promise<void> {
|
|
188
|
+
this.breadcrumbs.add({
|
|
189
|
+
type: 'console',
|
|
190
|
+
category: 'console',
|
|
191
|
+
message: `console.${consoleEvent.level}: ${consoleEvent.message}`,
|
|
192
|
+
timestamp: consoleEvent.timestamp,
|
|
193
|
+
level: consoleEvent.level === 'error' ? 'error' : consoleEvent.level === 'warn' ? 'warn' : 'info',
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
if (consoleEvent.level === 'error' || consoleEvent.level === 'warn') {
|
|
197
|
+
this.sendEvent('console', consoleEvent);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private handleNetwork(netEvent: NetworkEvent): void {
|
|
202
|
+
const isError = (netEvent.status && netEvent.status >= 400) || !!netEvent.error;
|
|
203
|
+
const msg = `${netEvent.method} ${netEvent.url} -> ${netEvent.status || 'ERR'} (${netEvent.durationMs}ms)`;
|
|
204
|
+
|
|
205
|
+
this.breadcrumbs.add({
|
|
206
|
+
type: netEvent.id?.startsWith('xhr') ? 'xhr' : 'fetch',
|
|
207
|
+
category: 'network',
|
|
208
|
+
message: msg,
|
|
209
|
+
timestamp: netEvent.timestamp,
|
|
210
|
+
level: isError ? 'error' : 'info',
|
|
211
|
+
data: {
|
|
212
|
+
status: netEvent.status,
|
|
213
|
+
durationMs: netEvent.durationMs,
|
|
214
|
+
error: netEvent.error,
|
|
215
|
+
aborted: netEvent.aborted,
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.sendEvent('fetch', netEvent);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private handleNavigation(navEvent: NavigationEvent): void {
|
|
223
|
+
this.currentRoute = navEvent.to;
|
|
224
|
+
this.breadcrumbs.add({
|
|
225
|
+
type: 'navigation',
|
|
226
|
+
category: 'navigation',
|
|
227
|
+
message: `navigate ${navEvent.to} (${navEvent.type})`,
|
|
228
|
+
timestamp: navEvent.timestamp,
|
|
229
|
+
level: 'info',
|
|
230
|
+
data: { from: navEvent.from, to: navEvent.to, type: navEvent.type },
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
this.sendEvent('navigation', navEvent);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private sendEvent(eventType: any, payload: any, screenshotDataUrl?: string): void {
|
|
237
|
+
if (typeof window === 'undefined') return;
|
|
238
|
+
|
|
239
|
+
const message: ClientEventMessage & { screenshot?: string } = {
|
|
240
|
+
type: 'event',
|
|
241
|
+
sessionId: this.transport.getSessionId() || '',
|
|
242
|
+
eventType,
|
|
243
|
+
payload,
|
|
244
|
+
breadcrumbs: this.breadcrumbs.getRecent(),
|
|
245
|
+
lastElement: this.lastElement,
|
|
246
|
+
route: this.currentRoute,
|
|
247
|
+
url: window.location.href,
|
|
248
|
+
title: document.title,
|
|
249
|
+
timestamp: Date.now(),
|
|
250
|
+
screenshot: screenshotDataUrl,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
this.transport.send(message);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
public getBreadcrumbs(): Breadcrumb[] {
|
|
257
|
+
return this.breadcrumbs.getRecent();
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
public getLastElement(): ElementSummary | undefined {
|
|
261
|
+
return this.lastElement;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
public destroy(): void {
|
|
265
|
+
for (const cleanup of this.cleanups) {
|
|
266
|
+
try {
|
|
267
|
+
cleanup();
|
|
268
|
+
} catch {}
|
|
269
|
+
}
|
|
270
|
+
this.cleanups = [];
|
|
271
|
+
this.transport.destroy();
|
|
272
|
+
if (this.inspector) {
|
|
273
|
+
this.inspector.destroy();
|
|
274
|
+
}
|
|
275
|
+
this.breadcrumbs.clear();
|
|
276
|
+
this.isInitialized = false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClientCommand,
|
|
3
|
+
CommandResponse,
|
|
4
|
+
PageStateResult,
|
|
5
|
+
QueryElementResult,
|
|
6
|
+
CaptureElementResult,
|
|
7
|
+
} from '../../../core/src/index.js';
|
|
8
|
+
import { truncate } from '../../../core/src/index.js';
|
|
9
|
+
import { extractElementSummary } from '../interceptors/interaction.js';
|
|
10
|
+
import type { ScreenshotDriver } from '../screenshot/driver.js';
|
|
11
|
+
|
|
12
|
+
export class ClientCommandHandler {
|
|
13
|
+
private screenshotDriver: ScreenshotDriver;
|
|
14
|
+
|
|
15
|
+
constructor(screenshotDriver: ScreenshotDriver) {
|
|
16
|
+
this.screenshotDriver = screenshotDriver;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
public async executeCommand(command: ClientCommand): Promise<CommandResponse> {
|
|
20
|
+
try {
|
|
21
|
+
switch (command.type) {
|
|
22
|
+
case 'reload':
|
|
23
|
+
return this.handleReload(command);
|
|
24
|
+
|
|
25
|
+
case 'navigate':
|
|
26
|
+
return this.handleNavigate(command);
|
|
27
|
+
|
|
28
|
+
case 'get_page_state':
|
|
29
|
+
return this.handleGetPageState(command);
|
|
30
|
+
|
|
31
|
+
case 'query_element':
|
|
32
|
+
return this.handleQueryElement(command);
|
|
33
|
+
|
|
34
|
+
case 'capture_element':
|
|
35
|
+
return await this.handleCaptureElement(command);
|
|
36
|
+
|
|
37
|
+
case 'check_overflow':
|
|
38
|
+
return this.handleCheckOverflow(command);
|
|
39
|
+
|
|
40
|
+
case 'get_element_rect':
|
|
41
|
+
return this.handleGetElementRect(command);
|
|
42
|
+
|
|
43
|
+
case 'get_element_style':
|
|
44
|
+
return this.handleGetElementStyle(command);
|
|
45
|
+
|
|
46
|
+
default:
|
|
47
|
+
return {
|
|
48
|
+
id: command.id,
|
|
49
|
+
ok: false,
|
|
50
|
+
error: `Unsupported command type: ${(command as any).type}`,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
} catch (err: any) {
|
|
54
|
+
return {
|
|
55
|
+
id: command.id,
|
|
56
|
+
ok: false,
|
|
57
|
+
error: err?.message || 'Command execution failed',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private handleReload(command: ClientCommand): CommandResponse {
|
|
63
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
64
|
+
setTimeout(() => {
|
|
65
|
+
window.location.reload();
|
|
66
|
+
}, 50);
|
|
67
|
+
return { id: command.id, ok: true, result: { message: 'Reload initiated' } };
|
|
68
|
+
}
|
|
69
|
+
return { id: command.id, ok: false, error: 'No browser window available' };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private handleNavigate(command: ClientCommand<{ url: string }>): CommandResponse {
|
|
73
|
+
const targetUrl = command.params?.url;
|
|
74
|
+
if (!targetUrl) {
|
|
75
|
+
return { id: command.id, ok: false, error: 'Missing target url parameter' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (typeof window !== 'undefined' && window.location) {
|
|
79
|
+
setTimeout(() => {
|
|
80
|
+
window.location.href = targetUrl;
|
|
81
|
+
}, 50);
|
|
82
|
+
return { id: command.id, ok: true, result: { message: `Navigating to ${targetUrl}` } };
|
|
83
|
+
}
|
|
84
|
+
return { id: command.id, ok: false, error: 'No browser window available' };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
private handleGetPageState(command: ClientCommand): CommandResponse<PageStateResult> {
|
|
88
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
89
|
+
return { id: command.id, ok: false, error: 'No browser document available' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const activeEl = document.activeElement && document.activeElement !== document.body ? extractElementSummary(document.activeElement) : undefined;
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
id: command.id,
|
|
96
|
+
ok: true,
|
|
97
|
+
result: {
|
|
98
|
+
url: window.location.href,
|
|
99
|
+
route: window.location.pathname + window.location.search + window.location.hash,
|
|
100
|
+
title: document.title,
|
|
101
|
+
readyState: document.readyState,
|
|
102
|
+
activeElement: activeEl,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private handleQueryElement(command: ClientCommand<{ selector: string }>): CommandResponse<QueryElementResult> {
|
|
108
|
+
if (typeof document === 'undefined') {
|
|
109
|
+
return { id: command.id, ok: false, error: 'No browser document available' };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const selector = command.params?.selector;
|
|
113
|
+
if (!selector) {
|
|
114
|
+
return { id: command.id, ok: false, error: 'Missing selector parameter' };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const element = document.querySelector(selector);
|
|
118
|
+
if (!element) {
|
|
119
|
+
return {
|
|
120
|
+
id: command.id,
|
|
121
|
+
ok: true,
|
|
122
|
+
result: { exists: false },
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const summary = extractElementSummary(element);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
id: command.id,
|
|
130
|
+
ok: true,
|
|
131
|
+
result: {
|
|
132
|
+
exists: true,
|
|
133
|
+
visible: summary.visible,
|
|
134
|
+
tag: summary.tag,
|
|
135
|
+
id: summary.id,
|
|
136
|
+
classes: summary.classes,
|
|
137
|
+
boundingRect: summary.boundingRect,
|
|
138
|
+
innerText: summary.innerText,
|
|
139
|
+
outerHTML: summary.outerHTML,
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
private async handleCaptureElement(command: ClientCommand<{ selector?: string }>): Promise<CommandResponse<CaptureElementResult>> {
|
|
145
|
+
const selector = command.params?.selector;
|
|
146
|
+
let targetEl: Element | null = null;
|
|
147
|
+
|
|
148
|
+
if (selector) {
|
|
149
|
+
targetEl = document.querySelector(selector);
|
|
150
|
+
if (!targetEl) {
|
|
151
|
+
return {
|
|
152
|
+
id: command.id,
|
|
153
|
+
ok: false,
|
|
154
|
+
reason: `ELEMENT_NOT_FOUND: ${selector}`,
|
|
155
|
+
error: `Element not found for selector: ${selector}`,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
targetEl = document.body || document.documentElement;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const captureRes = await this.screenshotDriver.captureElement(targetEl);
|
|
163
|
+
if (!captureRes.ok) {
|
|
164
|
+
return {
|
|
165
|
+
id: command.id,
|
|
166
|
+
ok: false,
|
|
167
|
+
reason: captureRes.reason || 'CAPTURE_FAILED',
|
|
168
|
+
error: captureRes.reason || 'Failed to capture element screenshot',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
id: command.id,
|
|
174
|
+
ok: true,
|
|
175
|
+
result: {
|
|
176
|
+
dataUrl: captureRes.dataUrl,
|
|
177
|
+
format: captureRes.format,
|
|
178
|
+
width: captureRes.width,
|
|
179
|
+
height: captureRes.height,
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private handleCheckOverflow(command: ClientCommand<{ selector: string }>): CommandResponse {
|
|
185
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
186
|
+
return { id: command.id, ok: false, error: 'No browser window available' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const selector = command.params?.selector;
|
|
190
|
+
if (!selector) {
|
|
191
|
+
return { id: command.id, ok: false, error: 'Missing selector parameter' };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const el = document.querySelector(selector);
|
|
195
|
+
if (!el) {
|
|
196
|
+
return { id: command.id, ok: false, error: `Element not found: ${selector}` };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const rect = el.getBoundingClientRect();
|
|
200
|
+
const vw = window.innerWidth;
|
|
201
|
+
const vh = window.innerHeight;
|
|
202
|
+
|
|
203
|
+
const overflowRightPx = Math.max(0, Math.round(rect.right - vw));
|
|
204
|
+
const overflowBottomPx = Math.max(0, Math.round(rect.bottom - vh));
|
|
205
|
+
const isViewportOverflow = overflowRightPx > 0 || rect.left < 0;
|
|
206
|
+
|
|
207
|
+
let parentOverflow = false;
|
|
208
|
+
if (el.parentElement) {
|
|
209
|
+
parentOverflow = el.parentElement.scrollWidth > el.parentElement.clientWidth;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return {
|
|
213
|
+
id: command.id,
|
|
214
|
+
ok: true,
|
|
215
|
+
result: {
|
|
216
|
+
selector,
|
|
217
|
+
overflow: isViewportOverflow || parentOverflow,
|
|
218
|
+
viewportWidth: vw,
|
|
219
|
+
viewportHeight: vh,
|
|
220
|
+
rect: {
|
|
221
|
+
x: Math.round(rect.x),
|
|
222
|
+
y: Math.round(rect.y),
|
|
223
|
+
width: Math.round(rect.width),
|
|
224
|
+
height: Math.round(rect.height),
|
|
225
|
+
top: Math.round(rect.top),
|
|
226
|
+
left: Math.round(rect.left),
|
|
227
|
+
bottom: Math.round(rect.bottom),
|
|
228
|
+
right: Math.round(rect.right),
|
|
229
|
+
},
|
|
230
|
+
overflowRightPx,
|
|
231
|
+
overflowBottomPx,
|
|
232
|
+
parentOverflow,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
private handleGetElementRect(command: ClientCommand<{ selector: string }>): CommandResponse {
|
|
238
|
+
if (typeof document === 'undefined') {
|
|
239
|
+
return { id: command.id, ok: false, error: 'No browser document available' };
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const selector = command.params?.selector;
|
|
243
|
+
if (!selector) {
|
|
244
|
+
return { id: command.id, ok: false, error: 'Missing selector parameter' };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const el = document.querySelector(selector);
|
|
248
|
+
if (!el) {
|
|
249
|
+
return { id: command.id, ok: false, error: `Element not found: ${selector}` };
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const rect = el.getBoundingClientRect();
|
|
253
|
+
return {
|
|
254
|
+
id: command.id,
|
|
255
|
+
ok: true,
|
|
256
|
+
result: {
|
|
257
|
+
x: Math.round(rect.x),
|
|
258
|
+
y: Math.round(rect.y),
|
|
259
|
+
width: Math.round(rect.width),
|
|
260
|
+
height: Math.round(rect.height),
|
|
261
|
+
top: Math.round(rect.top),
|
|
262
|
+
left: Math.round(rect.left),
|
|
263
|
+
bottom: Math.round(rect.bottom),
|
|
264
|
+
right: Math.round(rect.right),
|
|
265
|
+
},
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
private handleGetElementStyle(command: ClientCommand<{ selector: string; properties?: string[] }>): CommandResponse {
|
|
270
|
+
if (typeof window === 'undefined' || typeof document === 'undefined') {
|
|
271
|
+
return { id: command.id, ok: false, error: 'No browser window available' };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const selector = command.params?.selector;
|
|
275
|
+
if (!selector) {
|
|
276
|
+
return { id: command.id, ok: false, error: 'Missing selector parameter' };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
const el = document.querySelector(selector);
|
|
280
|
+
if (!el) {
|
|
281
|
+
return { id: command.id, ok: false, error: `Element not found: ${selector}` };
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const computed = window.getComputedStyle(el);
|
|
285
|
+
const properties = command.params?.properties || ['overflow', 'overflowX', 'overflowY', 'width', 'maxWidth', 'display', 'position'];
|
|
286
|
+
const styles: Record<string, string> = {};
|
|
287
|
+
|
|
288
|
+
for (const prop of properties) {
|
|
289
|
+
styles[prop] = computed.getPropertyValue(prop) || (computed as any)[prop] || '';
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
id: command.id,
|
|
294
|
+
ok: true,
|
|
295
|
+
result: {
|
|
296
|
+
selector,
|
|
297
|
+
styles,
|
|
298
|
+
},
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface BrowserDiagClientOptions {
|
|
2
|
+
daemonUrl?: string;
|
|
3
|
+
projectId?: string;
|
|
4
|
+
maxBreadcrumbs?: number;
|
|
5
|
+
captureErrors?: boolean;
|
|
6
|
+
captureConsole?: boolean;
|
|
7
|
+
captureNetwork?: boolean;
|
|
8
|
+
captureNavigation?: boolean;
|
|
9
|
+
captureInteractions?: boolean;
|
|
10
|
+
onErrorScreenshot?: boolean;
|
|
11
|
+
notes?: {
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
shortcut?: string;
|
|
14
|
+
showBadges?: boolean;
|
|
15
|
+
maskSelectors?: string[];
|
|
16
|
+
};
|
|
17
|
+
debug?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_OPTIONS: Required<Omit<BrowserDiagClientOptions, 'notes'>> & { notes: Required<NonNullable<BrowserDiagClientOptions['notes']>> } = {
|
|
21
|
+
daemonUrl: 'ws://127.0.0.1:7331',
|
|
22
|
+
projectId: '',
|
|
23
|
+
maxBreadcrumbs: 50,
|
|
24
|
+
captureErrors: true,
|
|
25
|
+
captureConsole: true,
|
|
26
|
+
captureNetwork: true,
|
|
27
|
+
captureNavigation: true,
|
|
28
|
+
captureInteractions: true,
|
|
29
|
+
onErrorScreenshot: true,
|
|
30
|
+
notes: {
|
|
31
|
+
enabled: true,
|
|
32
|
+
shortcut: 'Alt+Click',
|
|
33
|
+
showBadges: true,
|
|
34
|
+
maskSelectors: ['input[type="password"]', '[data-sensitive]'],
|
|
35
|
+
},
|
|
36
|
+
debug: false,
|
|
37
|
+
};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { BrowserTrackClient } from './client.js';
|
|
2
|
+
import type { BrowserDiagClientOptions } from './config.js';
|
|
3
|
+
|
|
4
|
+
export * from './client.js';
|
|
5
|
+
export * from './config.js';
|
|
6
|
+
export * from './breadcrumbs.js';
|
|
7
|
+
export * from './screenshot/driver.js';
|
|
8
|
+
export * from './screenshot/browser-script-driver.js';
|
|
9
|
+
export * from './notes/inspector.js';
|
|
10
|
+
|
|
11
|
+
let defaultClient: BrowserTrackClient | null = null;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Initializes the BrowserTrack client.
|
|
15
|
+
*/
|
|
16
|
+
export function init(options: BrowserDiagClientOptions = {}): BrowserTrackClient {
|
|
17
|
+
if (defaultClient) {
|
|
18
|
+
return defaultClient;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
defaultClient = new BrowserTrackClient(options);
|
|
22
|
+
defaultClient.init();
|
|
23
|
+
return defaultClient;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getClient(): BrowserTrackClient | null {
|
|
27
|
+
return defaultClient;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Auto-initialize when injected via <script src="...">
|
|
31
|
+
if (typeof window !== 'undefined') {
|
|
32
|
+
(window as any).__BROWSERTRACK__ = {
|
|
33
|
+
init,
|
|
34
|
+
getClient,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
// Auto-start by default in browser if not explicitly disabled
|
|
38
|
+
try {
|
|
39
|
+
const currentScript = document.currentScript;
|
|
40
|
+
const autoInit = currentScript?.getAttribute('data-auto-init') !== 'false';
|
|
41
|
+
const daemonUrl = currentScript?.getAttribute('data-daemon-url') || undefined;
|
|
42
|
+
const projectId = currentScript?.getAttribute('data-project-id') || undefined;
|
|
43
|
+
|
|
44
|
+
if (autoInit) {
|
|
45
|
+
init({ daemonUrl, projectId });
|
|
46
|
+
}
|
|
47
|
+
} catch {
|
|
48
|
+
// If auto-start fails, don't crash
|
|
49
|
+
}
|
|
50
|
+
}
|