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.
Files changed (101) hide show
  1. package/README.md +143 -0
  2. package/dist/chunk-6A7FFDIB.js +221 -0
  3. package/dist/chunk-6A7FFDIB.js.map +1 -0
  4. package/dist/chunk-ANMR5WBH.js +659 -0
  5. package/dist/chunk-ANMR5WBH.js.map +1 -0
  6. package/dist/chunk-BSKNG4V7.js +1199 -0
  7. package/dist/chunk-BSKNG4V7.js.map +1 -0
  8. package/dist/chunk-X7C5CHBO.js +2080 -0
  9. package/dist/chunk-X7C5CHBO.js.map +1 -0
  10. package/dist/chunk-ZLNGOOH2.js +554 -0
  11. package/dist/chunk-ZLNGOOH2.js.map +1 -0
  12. package/dist/cli/index.js +2829 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/client/index.cjs +2218 -0
  15. package/dist/client/index.d.ts +197 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client.iife.js +309 -0
  19. package/dist/core/index.d.ts +78 -0
  20. package/dist/core/index.js +31 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/daemon/index.d.ts +58 -0
  23. package/dist/daemon/index.js +32 -0
  24. package/dist/daemon/index.js.map +1 -0
  25. package/dist/engine-DKloFjvs.d.ts +157 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +50 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mcp/index.d.ts +11 -0
  30. package/dist/mcp/index.js +12 -0
  31. package/dist/mcp/index.js.map +1 -0
  32. package/dist/notes-CBvN91Wf.d.ts +260 -0
  33. package/dist/projects-CY8ungMt.d.ts +99 -0
  34. package/dist/server-CN-se8td.d.ts +55 -0
  35. package/examples/test-app/index.html +299 -0
  36. package/package.json +52 -7
  37. package/packages/cli/src/index.ts +413 -0
  38. package/packages/client/package.json +28 -0
  39. package/packages/client/src/breadcrumbs.ts +30 -0
  40. package/packages/client/src/client.ts +278 -0
  41. package/packages/client/src/commands/handler.ts +301 -0
  42. package/packages/client/src/config.ts +37 -0
  43. package/packages/client/src/index.ts +50 -0
  44. package/packages/client/src/interceptors/console.ts +68 -0
  45. package/packages/client/src/interceptors/interaction.ts +119 -0
  46. package/packages/client/src/interceptors/navigation.ts +93 -0
  47. package/packages/client/src/interceptors/network.ts +165 -0
  48. package/packages/client/src/interceptors/runtime.ts +65 -0
  49. package/packages/client/src/notes/inspector.ts +943 -0
  50. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  51. package/packages/client/src/screenshot/driver.ts +14 -0
  52. package/packages/client/src/transport/websocket.ts +179 -0
  53. package/packages/client/tsconfig.json +8 -0
  54. package/packages/core/dist/index.d.ts +317 -0
  55. package/packages/core/dist/index.js +221 -0
  56. package/packages/core/package.json +23 -0
  57. package/packages/core/src/fingerprint.ts +120 -0
  58. package/packages/core/src/index.ts +9 -0
  59. package/packages/core/src/redaction.ts +139 -0
  60. package/packages/core/src/selector.ts +77 -0
  61. package/packages/core/src/types/commands.ts +101 -0
  62. package/packages/core/src/types/events.ts +108 -0
  63. package/packages/core/src/types/incidents.ts +54 -0
  64. package/packages/core/src/types/notes.ts +106 -0
  65. package/packages/core/src/types/probes.ts +44 -0
  66. package/packages/core/src/types/projects.ts +20 -0
  67. package/packages/core/tsconfig.json +8 -0
  68. package/packages/daemon/src/config.ts +29 -0
  69. package/packages/daemon/src/incidents/engine.ts +211 -0
  70. package/packages/daemon/src/index.ts +18 -0
  71. package/packages/daemon/src/notes/engine.ts +92 -0
  72. package/packages/daemon/src/notes/verification.ts +191 -0
  73. package/packages/daemon/src/server/daemon.ts +112 -0
  74. package/packages/daemon/src/server/http.ts +165 -0
  75. package/packages/daemon/src/server/ws.ts +175 -0
  76. package/packages/daemon/src/session/manager.ts +126 -0
  77. package/packages/daemon/src/storage/db.ts +733 -0
  78. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  79. package/packages/daemon/src/verification/engine.ts +264 -0
  80. package/packages/mcp/src/handlers.ts +398 -0
  81. package/packages/mcp/src/index.ts +3 -0
  82. package/packages/mcp/src/server.ts +86 -0
  83. package/packages/mcp/src/tools.ts +236 -0
  84. package/src/index.ts +4 -0
  85. package/test/client/interceptors.test.ts +69 -0
  86. package/test/core/fingerprint.test.ts +53 -0
  87. package/test/core/notes.test.ts +66 -0
  88. package/test/core/redaction.test.ts +48 -0
  89. package/test/daemon/incident-engine.test.ts +98 -0
  90. package/test/daemon/notes-storage.test.ts +130 -0
  91. package/test/daemon/notes-verification.test.ts +135 -0
  92. package/test/daemon/storage.test.ts +123 -0
  93. package/test/daemon/verification-engine.test.ts +88 -0
  94. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  95. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  96. package/test/mcp/handlers.test.ts +116 -0
  97. package/test/mcp/notes.test.ts +107 -0
  98. package/tsconfig.base.json +17 -0
  99. package/tsconfig.json +26 -0
  100. package/tsup.config.ts +54 -0
  101. 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
+ }