browser-debug-mcp-bridge 1.6.0 → 1.10.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 (30) hide show
  1. package/README.md +25 -0
  2. package/apps/mcp-server/dist/db/automation-repository.js +199 -0
  3. package/apps/mcp-server/dist/db/automation-repository.js.map +1 -0
  4. package/apps/mcp-server/dist/db/connection.js +1 -5
  5. package/apps/mcp-server/dist/db/connection.js.map +1 -1
  6. package/apps/mcp-server/dist/db/events-repository.js +263 -14
  7. package/apps/mcp-server/dist/db/events-repository.js.map +1 -1
  8. package/apps/mcp-server/dist/db/index.js +2 -0
  9. package/apps/mcp-server/dist/db/index.js.map +1 -1
  10. package/apps/mcp-server/dist/db/migrations.js +180 -0
  11. package/apps/mcp-server/dist/db/migrations.js.map +1 -1
  12. package/apps/mcp-server/dist/db/schema.js +93 -1
  13. package/apps/mcp-server/dist/db/schema.js.map +1 -1
  14. package/apps/mcp-server/dist/main.js +54 -4
  15. package/apps/mcp-server/dist/main.js.map +1 -1
  16. package/apps/mcp-server/dist/mcp/server.js +2860 -86
  17. package/apps/mcp-server/dist/mcp/server.js.map +1 -1
  18. package/apps/mcp-server/dist/mcp-bridge.js +46 -3
  19. package/apps/mcp-server/dist/mcp-bridge.js.map +1 -1
  20. package/apps/mcp-server/dist/retention.js +67 -4
  21. package/apps/mcp-server/dist/retention.js.map +1 -1
  22. package/apps/mcp-server/dist/runtime-paths.js +33 -0
  23. package/apps/mcp-server/dist/runtime-paths.js.map +1 -0
  24. package/apps/mcp-server/dist/websocket/messages.js +30 -0
  25. package/apps/mcp-server/dist/websocket/messages.js.map +1 -1
  26. package/apps/mcp-server/dist/websocket/websocket-server.js +18 -0
  27. package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -1
  28. package/apps/mcp-server/package.json +2 -2
  29. package/package.json +17 -6
  30. package/scripts/mcp-start.cjs +201 -11
package/README.md CHANGED
@@ -10,6 +10,8 @@ It captures telemetry from an actual browser session (console, network, navigati
10
10
  - Query recent errors, failed requests, and event timelines
11
11
  - Run targeted live capture (DOM subtree/document, styles, layout)
12
12
  - Pull live in-memory console logs with server-side filters (`url`, `tabId`, `levels`, `contains`)
13
+ - Query targeted API calls with optional sanitized request/response bodies
14
+ - Wait deterministically for the next matching API request during repro flows
13
15
  - Correlate user actions with network/runtime failures
14
16
  - Keep privacy controls enabled (safe mode, allowlist, redaction)
15
17
 
@@ -183,6 +185,7 @@ Supported tools:
183
185
  - `get_navigation_history`
184
186
  - `get_console_events`
185
187
  - `get_network_failures`
188
+ - `get_network_calls`
186
189
 
187
190
  Example: URL-only query
188
191
 
@@ -245,6 +248,17 @@ Default port is `8065`.
245
248
  - If a stale process still remains, stop it explicitly with `node scripts/mcp-start.cjs --stop`.
246
249
  - Optional: set `MCP_STARTUP_TIMEOUT_MS` (default `15000`) for slower machines.
247
250
 
251
+ ## Runtime Storage
252
+
253
+ By default, the launcher and server store local runtime state in a user-local app-data directory, not in the repo or package root.
254
+
255
+ - Windows: `%LOCALAPPDATA%\\browser-debug-mcp-bridge`
256
+ - macOS: `~/Library/Application Support/browser-debug-mcp-bridge`
257
+ - Linux: `$XDG_STATE_HOME/browser-debug-mcp-bridge` or `$XDG_DATA_HOME/browser-debug-mcp-bridge`
258
+ - Fallback: `~/.local/share/browser-debug-mcp-bridge`
259
+
260
+ This keeps SQLite data, snapshot assets, exports, and launcher lock files out of host app roots. To override it explicitly, set `DATA_DIR`.
261
+
248
262
  Useful Windows command:
249
263
 
250
264
  ```powershell
@@ -268,12 +282,23 @@ node scripts/mcp-start.cjs --stop
268
282
  pnpm typecheck
269
283
  pnpm lint
270
284
  pnpm test
285
+ pnpm test:e2e
286
+ pnpm test:e2e:head
287
+ pnpm test:e2e:smoke
288
+ pnpm test:e2e:full
271
289
  pnpm build
272
290
  pnpm docs:ci
273
291
  pnpm verify
274
292
  node scripts/mcp-start.cjs --stop
275
293
  ```
276
294
 
295
+ E2E commands run headless by default. Use `pnpm test:e2e:head` only for local headed debugging.
296
+
297
+ CI lanes:
298
+
299
+ - Pull requests and pushes to `main`: `verify` + Playwright smoke + Playwright full.
300
+ - Nightly: `verify` + Playwright full + runtime `/health` smoke check.
301
+
277
302
  Optional one-shot local setup:
278
303
 
279
304
  ```powershell
@@ -0,0 +1,199 @@
1
+ const AUTOMATION_EVENT_TYPES = new Set([
2
+ 'automation_requested',
3
+ 'automation_started',
4
+ 'automation_succeeded',
5
+ 'automation_failed',
6
+ 'automation_stopped',
7
+ ]);
8
+ export function isAutomationLifecycleEventType(eventType) {
9
+ return AUTOMATION_EVENT_TYPES.has(eventType);
10
+ }
11
+ export class AutomationRepository {
12
+ db;
13
+ constructor(db) {
14
+ this.db = db;
15
+ }
16
+ upsertLifecycleEvent(input) {
17
+ if (!isAutomationLifecycleEventType(input.eventType)) {
18
+ return;
19
+ }
20
+ const normalized = normalizeLifecycleEvent(input);
21
+ const upsertRun = this.db.prepare(`
22
+ INSERT INTO automation_runs (
23
+ run_id,
24
+ session_id,
25
+ trace_id,
26
+ action,
27
+ tab_id,
28
+ selector,
29
+ status,
30
+ started_at,
31
+ completed_at,
32
+ stop_reason,
33
+ target_summary_json,
34
+ failure_json,
35
+ redaction_json,
36
+ created_at,
37
+ updated_at
38
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
39
+ ON CONFLICT(run_id) DO UPDATE SET
40
+ trace_id = COALESCE(excluded.trace_id, automation_runs.trace_id),
41
+ action = COALESCE(excluded.action, automation_runs.action),
42
+ tab_id = COALESCE(excluded.tab_id, automation_runs.tab_id),
43
+ selector = COALESCE(excluded.selector, automation_runs.selector),
44
+ status = excluded.status,
45
+ started_at = MIN(automation_runs.started_at, excluded.started_at),
46
+ completed_at = COALESCE(excluded.completed_at, automation_runs.completed_at),
47
+ stop_reason = COALESCE(excluded.stop_reason, automation_runs.stop_reason),
48
+ target_summary_json = COALESCE(excluded.target_summary_json, automation_runs.target_summary_json),
49
+ failure_json = COALESCE(excluded.failure_json, automation_runs.failure_json),
50
+ redaction_json = COALESCE(excluded.redaction_json, automation_runs.redaction_json),
51
+ updated_at = excluded.updated_at
52
+ `);
53
+ const upsertStep = this.db.prepare(`
54
+ INSERT INTO automation_steps (
55
+ step_id,
56
+ run_id,
57
+ session_id,
58
+ step_order,
59
+ trace_id,
60
+ action,
61
+ selector,
62
+ status,
63
+ started_at,
64
+ finished_at,
65
+ duration_ms,
66
+ tab_id,
67
+ target_summary_json,
68
+ redaction_json,
69
+ failure_json,
70
+ input_metadata_json,
71
+ event_type,
72
+ event_id,
73
+ created_at,
74
+ updated_at
75
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
76
+ ON CONFLICT(run_id, step_order) DO UPDATE SET
77
+ trace_id = COALESCE(excluded.trace_id, automation_steps.trace_id),
78
+ action = COALESCE(excluded.action, automation_steps.action),
79
+ selector = COALESCE(excluded.selector, automation_steps.selector),
80
+ status = excluded.status,
81
+ started_at = COALESCE(automation_steps.started_at, excluded.started_at),
82
+ finished_at = COALESCE(excluded.finished_at, automation_steps.finished_at),
83
+ duration_ms = COALESCE(excluded.duration_ms, automation_steps.duration_ms),
84
+ tab_id = COALESCE(excluded.tab_id, automation_steps.tab_id),
85
+ target_summary_json = COALESCE(excluded.target_summary_json, automation_steps.target_summary_json),
86
+ redaction_json = COALESCE(excluded.redaction_json, automation_steps.redaction_json),
87
+ failure_json = COALESCE(excluded.failure_json, automation_steps.failure_json),
88
+ input_metadata_json = COALESCE(excluded.input_metadata_json, automation_steps.input_metadata_json),
89
+ event_type = excluded.event_type,
90
+ event_id = COALESCE(excluded.event_id, automation_steps.event_id),
91
+ updated_at = excluded.updated_at
92
+ `);
93
+ upsertRun.run(normalized.runId, input.sessionId, normalized.traceId, normalized.action, normalized.tabId, normalized.selector, normalized.status, normalized.startedAt, normalized.completedAt, normalized.stopReason, normalized.targetSummaryJson, normalized.failureJson, normalized.redactionJson, normalized.createdAt, normalized.updatedAt);
94
+ upsertStep.run(normalized.stepId, normalized.runId, input.sessionId, normalized.stepOrder, normalized.traceId, normalized.action ?? 'unknown', normalized.selector, normalized.status, normalized.startedAt, normalized.finishedAt, normalized.durationMs, normalized.tabId, normalized.targetSummaryJson, normalized.redactionJson, normalized.failureJson, normalized.inputMetadataJson, input.eventType, input.eventId ?? null, normalized.createdAt, normalized.updatedAt);
95
+ }
96
+ listRuns(sessionId) {
97
+ return this.db.prepare(`
98
+ SELECT *
99
+ FROM automation_runs
100
+ WHERE session_id = ?
101
+ ORDER BY started_at ASC, run_id ASC
102
+ `).all(sessionId);
103
+ }
104
+ listSteps(runId) {
105
+ return this.db.prepare(`
106
+ SELECT *
107
+ FROM automation_steps
108
+ WHERE run_id = ?
109
+ ORDER BY step_order ASC, created_at ASC
110
+ `).all(runId);
111
+ }
112
+ }
113
+ function normalizeLifecycleEvent(input) {
114
+ const traceId = asNonEmptyString(input.payload.traceId);
115
+ const runId = asNonEmptyString(input.payload.runId)
116
+ ?? (traceId ? `${input.sessionId}:${traceId}` : `${input.sessionId}:event:${input.eventId ?? input.timestamp}`);
117
+ const stepOrder = asInteger(input.payload.stepOrder) ?? 1;
118
+ const action = asNonEmptyString(input.payload.action);
119
+ const startedAt = asInteger(input.payload.startedAt) ?? input.timestamp;
120
+ const finishedAt = asInteger(input.payload.finishedAt);
121
+ const durationMs = asInteger(input.payload.durationMs)
122
+ ?? (finishedAt !== null ? Math.max(0, finishedAt - startedAt) : null);
123
+ const target = asRecord(input.payload.target);
124
+ const selector = asNonEmptyString(input.payload.selector)
125
+ ?? asNonEmptyString(target?.resolvedSelector)
126
+ ?? asNonEmptyString(target?.selector);
127
+ const tabId = asInteger(target?.tabId) ?? input.tabId ?? null;
128
+ const stopReason = asNonEmptyString(input.payload.stopReason)
129
+ ?? asNonEmptyString(asRecord(input.payload.failureReason)?.message);
130
+ const status = resolveStatus(input.eventType, input.payload);
131
+ const completedAt = isTerminalStatus(status) ? (finishedAt ?? input.timestamp) : null;
132
+ return {
133
+ runId,
134
+ stepId: `${runId}:${stepOrder}`,
135
+ stepOrder,
136
+ traceId,
137
+ action,
138
+ tabId,
139
+ selector,
140
+ status,
141
+ startedAt,
142
+ finishedAt: completedAt,
143
+ completedAt,
144
+ durationMs,
145
+ stopReason,
146
+ targetSummaryJson: stringifyJson(target),
147
+ failureJson: stringifyJson(asRecord(input.payload.failureReason)),
148
+ redactionJson: stringifyJson(asRecord(input.payload.redaction)),
149
+ inputMetadataJson: stringifyJson(asRecord(input.payload.input)),
150
+ createdAt: input.timestamp,
151
+ updatedAt: input.timestamp,
152
+ };
153
+ }
154
+ function resolveStatus(eventType, payload) {
155
+ const payloadStatus = asNonEmptyString(payload.status);
156
+ if (payloadStatus) {
157
+ return payloadStatus;
158
+ }
159
+ switch (eventType) {
160
+ case 'automation_requested':
161
+ return 'requested';
162
+ case 'automation_started':
163
+ return 'started';
164
+ case 'automation_succeeded':
165
+ return 'succeeded';
166
+ case 'automation_failed':
167
+ return 'failed';
168
+ case 'automation_stopped':
169
+ return 'stopped';
170
+ default:
171
+ return 'unknown';
172
+ }
173
+ }
174
+ function isTerminalStatus(status) {
175
+ return status === 'succeeded' || status === 'failed' || status === 'rejected' || status === 'stopped';
176
+ }
177
+ function asRecord(value) {
178
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
179
+ return null;
180
+ }
181
+ return value;
182
+ }
183
+ function asNonEmptyString(value) {
184
+ if (typeof value !== 'string') {
185
+ return null;
186
+ }
187
+ const trimmed = value.trim();
188
+ return trimmed.length > 0 ? trimmed : null;
189
+ }
190
+ function asInteger(value) {
191
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
192
+ return null;
193
+ }
194
+ return Math.floor(value);
195
+ }
196
+ function stringifyJson(value) {
197
+ return value ? JSON.stringify(value) : null;
198
+ }
199
+ //# sourceMappingURL=automation-repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automation-repository.js","sourceRoot":"","sources":["../../src/db/automation-repository.ts"],"names":[],"mappings":"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"}
@@ -1,12 +1,8 @@
1
1
  import Database from 'better-sqlite3';
2
- import { join } from 'path';
3
2
  import { dirname } from 'path';
4
3
  import { mkdirSync } from 'fs';
4
+ import { getDatabasePath } from '../runtime-paths.js';
5
5
  let connection = null;
6
- export function getDatabasePath() {
7
- const dataDir = process.env.DATA_DIR || join(process.cwd(), 'data');
8
- return join(dataDir, 'browser-debug.db');
9
- }
10
6
  export function createConnection(dbPath) {
11
7
  const path = dbPath || getDatabasePath();
12
8
  if (path !== ':memory:') {
@@ -1 +1 @@
1
- {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/db/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,MAAM,CAAC;AAC5B,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAO/B,IAAI,UAAU,GAA8B,IAAI,CAAC;AAEjD,MAAM,UAAU,eAAe;IAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,EAAE,MAAM,CAAC,CAAC;IACpE,OAAO,IAAI,CAAC,OAAO,EAAE,kBAAkB,CAAC,CAAC;AAC3C,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,MAAM,IAAI,GAAG,MAAM,IAAI,eAAe,EAAE,CAAC;IACzC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IAE9B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAE/B,OAAO;QACL,EAAE;QACF,WAAW,EAAE,IAAI;KAClB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,UAAU,GAAG,gBAAgB,EAAE,CAAC;IAClC,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,UAAU,EAAE,CAAC;QACf,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QACtB,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,WAAW,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,eAAe,EAAE,CAAC;IAClB,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC"}
1
+ {"version":3,"file":"connection.js","sourceRoot":"","sources":["../../src/db/connection.ts"],"names":[],"mappings":"AAAA,OAAO,QAAQ,MAAM,gBAAgB,CAAC;AACtC,OAAO,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAC/B,OAAO,EAAE,SAAS,EAAE,MAAM,IAAI,CAAC;AAC/B,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAOtD,IAAI,UAAU,GAA8B,IAAI,CAAC;AAEjD,MAAM,UAAU,gBAAgB,CAAC,MAAe;IAC9C,MAAM,IAAI,GAAG,MAAM,IAAI,eAAe,EAAE,CAAC;IACzC,IAAI,IAAI,KAAK,UAAU,EAAE,CAAC;QACxB,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAChD,CAAC;IACD,MAAM,EAAE,GAAG,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;IAE9B,EAAE,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAChC,EAAE,CAAC,MAAM,CAAC,mBAAmB,CAAC,CAAC;IAE/B,OAAO;QACL,EAAE;QACF,WAAW,EAAE,IAAI;KAClB,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,UAAU,GAAG,gBAAgB,EAAE,CAAC;IAClC,CAAC;IACD,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,UAAU,EAAE,CAAC;QACf,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,CAAC;QACtB,UAAU,GAAG,IAAI,CAAC;IACpB,CAAC;AACH,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,WAAW,CAAC;AACvD,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,eAAe,EAAE,CAAC;IAClB,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC"}
@@ -1,6 +1,33 @@
1
1
  import { resolveErrorFingerprint } from './error-fingerprints.js';
2
- import { getDatabasePath } from './connection.js';
2
+ import { getDatabasePath } from '../runtime-paths.js';
3
3
  import { writeSnapshot } from '../retention.js';
4
+ import { AutomationRepository, isAutomationLifecycleEventType } from './automation-repository.js';
5
+ const INLINE_BODY_BYTES_THRESHOLD = 16 * 1024;
6
+ const BODY_KIND_REQUEST = 'request';
7
+ const BODY_KIND_RESPONSE = 'response';
8
+ const SENSITIVE_FIELD_NAMES = new Set([
9
+ 'authorization',
10
+ 'proxy-authorization',
11
+ 'cookie',
12
+ 'set-cookie',
13
+ 'x-api-key',
14
+ 'api-key',
15
+ 'apikey',
16
+ 'x-auth-token',
17
+ 'access-token',
18
+ 'refresh-token',
19
+ 'token',
20
+ 'password',
21
+ 'secret',
22
+ 'client_secret',
23
+ ]);
24
+ const REDACTION_PATTERNS = [
25
+ { pattern: /(Authorization:\s*Bearer\s+)[\w\-\.=]+/gi, replacement: '$1[REDACTED]' },
26
+ { pattern: /eyJ[\w-]*\.eyJ[\w-]*\.[\w-]*/g, replacement: '[JWT_TOKEN]' },
27
+ { pattern: /((?:api[_-]?key|apikey)\s*[:=]\s*)[\w-]+/gi, replacement: '$1[API_KEY]' },
28
+ { pattern: /((?:access[_-]?token|refresh[_-]?token|token)\s*[:=]\s*)[^\s,;]+/gi, replacement: '$1[TOKEN]' },
29
+ { pattern: /((?:password|pwd)\s*[:=]\s*)\S+/gi, replacement: '$1[PASSWORD]' },
30
+ ];
4
31
  export class EventsRepository {
5
32
  db;
6
33
  constructor(db) {
@@ -16,8 +43,15 @@ export class EventsRepository {
16
43
  `);
17
44
  const insertNetwork = this.db.prepare(`
18
45
  INSERT INTO network (
19
- request_id, session_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class, response_size_est
20
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
46
+ request_id, session_id, trace_id, tab_id, ts_start, duration_ms, method, url, origin, status, initiator, error_class, response_size_est,
47
+ request_content_type, request_body_text, request_body_json, request_body_bytes, request_body_truncated, request_body_chunk_ref,
48
+ response_content_type, response_body_text, response_body_json, response_body_bytes, response_body_truncated, response_body_chunk_ref
49
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
50
+ `);
51
+ const insertBodyChunk = this.db.prepare(`
52
+ INSERT INTO body_chunks (
53
+ chunk_ref, session_id, request_id, trace_id, body_kind, content_type, body_text, body_bytes, truncated, created_at
54
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21
55
  `);
22
56
  const upsertFingerprint = this.db.prepare(`
23
57
  INSERT INTO error_fingerprints (
@@ -29,23 +63,37 @@ export class EventsRepository {
29
63
  sample_message = COALESCE(error_fingerprints.sample_message, excluded.sample_message),
30
64
  sample_stack = COALESCE(error_fingerprints.sample_stack, excluded.sample_stack)
31
65
  `);
66
+ const automationRepository = new AutomationRepository(this.db);
32
67
  const runBatch = this.db.transaction((batch) => {
33
68
  for (const message of batch) {
34
69
  const eventId = `${message.sessionId}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
35
70
  const dbEventType = this.mapEventType(message.eventType);
71
+ const sanitizedData = message.eventType === 'network'
72
+ ? sanitizeRecord(message.data)
73
+ : message.data;
36
74
  const eventTabId = typeof message.tabId === 'number' && Number.isFinite(message.tabId)
37
75
  ? Math.floor(message.tabId)
38
- : this.resolveTabIdFromPayload(message.data);
39
- const eventOrigin = this.resolveEventOrigin(message.data, message.origin);
40
- insert.run(eventId, message.sessionId, message.timestamp ?? Date.now(), dbEventType, JSON.stringify(message.data), eventTabId, eventOrigin);
76
+ : this.resolveTabIdFromPayload(sanitizedData);
77
+ const eventOrigin = this.resolveEventOrigin(sanitizedData, message.origin);
78
+ insert.run(eventId, message.sessionId, message.timestamp ?? Date.now(), dbEventType, JSON.stringify(sanitizedData), eventTabId, eventOrigin);
41
79
  if (message.eventType === 'error') {
42
- this.upsertErrorFingerprintPrepared(upsertFingerprint, message.sessionId, message.data);
80
+ this.upsertErrorFingerprintPrepared(upsertFingerprint, message.sessionId, sanitizedData);
43
81
  }
44
82
  if (message.eventType === 'network') {
45
- this.insertNetworkEventPrepared(insertNetwork, message.sessionId, message.data, eventOrigin);
83
+ this.insertNetworkEventPrepared(insertNetwork, insertBodyChunk, message.sessionId, sanitizedData, eventOrigin, eventTabId);
46
84
  }
47
85
  if (message.eventType === 'ui_snapshot') {
48
- this.insertSnapshotPrepared(message.sessionId, eventId, message.data);
86
+ this.insertSnapshotPrepared(message.sessionId, eventId, sanitizedData);
87
+ }
88
+ if (isAutomationLifecycleEventType(message.eventType)) {
89
+ automationRepository.upsertLifecycleEvent({
90
+ eventId,
91
+ eventType: message.eventType,
92
+ sessionId: message.sessionId,
93
+ timestamp: message.timestamp ?? Date.now(),
94
+ tabId: eventTabId,
95
+ payload: sanitizedData,
96
+ });
49
97
  }
50
98
  }
51
99
  });
@@ -64,11 +112,43 @@ export class EventsRepository {
64
112
  endSession(message) {
65
113
  const update = this.db.prepare(`
66
114
  UPDATE sessions
67
- SET ended_at = ?
115
+ SET ended_at = ?, paused_at = NULL
68
116
  WHERE session_id = ?
69
117
  `);
70
118
  update.run(Date.now(), message.sessionId);
71
119
  }
120
+ pauseSession(message) {
121
+ const update = this.db.prepare(`
122
+ UPDATE sessions
123
+ SET paused_at = COALESCE(paused_at, ?), ended_at = NULL
124
+ WHERE session_id = ? AND ended_at IS NULL
125
+ `);
126
+ const result = update.run(Date.now(), message.sessionId);
127
+ if (result.changes === 0) {
128
+ throw new Error(`Session not found or already ended: ${message.sessionId}`);
129
+ }
130
+ }
131
+ resumeSession(message) {
132
+ const update = this.db.prepare(`
133
+ UPDATE sessions
134
+ SET
135
+ paused_at = NULL,
136
+ ended_at = NULL,
137
+ url_last = COALESCE(?, url_last),
138
+ tab_id = COALESCE(?, tab_id),
139
+ window_id = COALESCE(?, window_id),
140
+ user_agent = COALESCE(?, user_agent),
141
+ viewport_w = COALESCE(?, viewport_w),
142
+ viewport_h = COALESCE(?, viewport_h),
143
+ dpr = COALESCE(?, dpr),
144
+ safe_mode = COALESCE(?, safe_mode)
145
+ WHERE session_id = ? AND ended_at IS NULL
146
+ `);
147
+ const result = update.run(message.url ?? null, message.tabId ?? null, message.windowId ?? null, message.userAgent ?? null, message.viewport?.width ?? null, message.viewport?.height ?? null, message.dpr ?? null, message.safeMode === undefined ? null : (message.safeMode ? 1 : 0), message.sessionId);
148
+ if (result.changes === 0) {
149
+ throw new Error(`Session not found or already ended: ${message.sessionId}`);
150
+ }
151
+ }
72
152
  insertEvent(message) {
73
153
  this.insertEventsBatch([message]);
74
154
  }
@@ -87,6 +167,11 @@ export class EventsRepository {
87
167
  blur: 'ui',
88
168
  keydown: 'ui',
89
169
  ui_snapshot: 'ui',
170
+ automation_requested: 'ui',
171
+ automation_started: 'ui',
172
+ automation_succeeded: 'ui',
173
+ automation_failed: 'ui',
174
+ automation_stopped: 'ui',
90
175
  custom: 'ui',
91
176
  };
92
177
  return mapping[eventType] || 'ui';
@@ -98,11 +183,94 @@ export class EventsRepository {
98
183
  const now = Date.now();
99
184
  statement.run(fingerprint, sessionId, data.message ?? 'Unknown error', data.stack ?? null, now, now);
100
185
  }
101
- insertNetworkEventPrepared(statement, sessionId, data, eventOrigin) {
102
- const requestId = `${sessionId}-net-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
103
- const url = data.url ?? '';
186
+ insertNetworkEventPrepared(networkStatement, bodyChunkStatement, sessionId, data, eventOrigin, eventTabId) {
187
+ const normalized = this.normalizeNetworkInsertInput(sessionId, data, eventOrigin, eventTabId, bodyChunkStatement);
188
+ networkStatement.run(normalized.requestId, sessionId, normalized.traceId, normalized.tabId, normalized.tsStart, normalized.durationMs, normalized.method, normalized.url, normalized.origin, normalized.status, normalized.initiator, normalized.errorClass, normalized.responseSizeEst, normalized.request.contentType, normalized.request.bodyText, normalized.request.bodyJson, normalized.request.bodyBytes, normalized.request.truncated ? 1 : 0, normalized.request.chunkRef, normalized.response.contentType, normalized.response.bodyText, normalized.response.bodyJson, normalized.response.bodyBytes, normalized.response.truncated ? 1 : 0, normalized.response.chunkRef);
189
+ }
190
+ normalizeNetworkInsertInput(sessionId, data, eventOrigin, eventTabId, bodyChunkStatement) {
191
+ const requestId = toNonEmptyString(data.requestId)
192
+ ?? `${sessionId}-net-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
193
+ const traceId = toNonEmptyString(data.traceId) ?? null;
194
+ const url = toNonEmptyString(data.url) ?? '';
104
195
  const origin = eventOrigin ?? normalizeOriginCandidate(url);
105
- statement.run(requestId, sessionId, data.timestamp ?? Date.now(), data.duration ?? null, data.method ?? 'GET', url, origin, data.status ?? null, data.initiator ?? 'other', data.errorType ?? null, data.responseSize ?? null);
196
+ const request = this.processNetworkBody({
197
+ sessionId,
198
+ requestId,
199
+ traceId,
200
+ bodyKind: BODY_KIND_REQUEST,
201
+ contentType: toNullableContentType(data.requestContentType),
202
+ bodyText: toNonEmptyString(data.requestBodyText),
203
+ bodyJson: toRecordLike(data.requestBodyJson),
204
+ bodyBytes: toNullableInteger(data.requestBodyBytes),
205
+ truncated: data.requestBodyTruncated === true,
206
+ bodyChunkStatement,
207
+ });
208
+ const response = this.processNetworkBody({
209
+ sessionId,
210
+ requestId,
211
+ traceId,
212
+ bodyKind: BODY_KIND_RESPONSE,
213
+ contentType: toNullableContentType(data.responseContentType),
214
+ bodyText: toNonEmptyString(data.responseBodyText),
215
+ bodyJson: toRecordLike(data.responseBodyJson),
216
+ bodyBytes: toNullableInteger(data.responseBodyBytes),
217
+ truncated: data.responseBodyTruncated === true,
218
+ bodyChunkStatement,
219
+ });
220
+ return {
221
+ requestId,
222
+ traceId,
223
+ tabId: eventTabId,
224
+ tsStart: toNullableInteger(data.timestamp) ?? Date.now(),
225
+ durationMs: toNullableInteger(data.duration),
226
+ method: normalizeMethod(toNonEmptyString(data.method)),
227
+ url,
228
+ origin,
229
+ status: toNullableInteger(data.status),
230
+ initiator: normalizeInitiator(toNonEmptyString(data.initiator)),
231
+ errorClass: normalizeErrorClass(toNonEmptyString(data.errorType)),
232
+ responseSizeEst: toNullableInteger(data.responseSize),
233
+ request,
234
+ response,
235
+ };
236
+ }
237
+ processNetworkBody(params) {
238
+ const redactedJson = params.bodyJson ? sanitizeRecord(params.bodyJson) : null;
239
+ const redactedText = params.bodyText ? redactString(params.bodyText) : null;
240
+ const resolvedText = redactedText
241
+ ?? (redactedJson ? JSON.stringify(redactedJson) : null);
242
+ const resolvedBytes = resolvedText ? utf8Bytes(resolvedText) : params.bodyBytes;
243
+ const resolvedJsonText = redactedJson ? JSON.stringify(redactedJson) : null;
244
+ if (!resolvedText && !resolvedJsonText && resolvedBytes === null) {
245
+ return {
246
+ contentType: params.contentType,
247
+ bodyText: null,
248
+ bodyJson: null,
249
+ bodyBytes: null,
250
+ truncated: params.truncated,
251
+ chunkRef: null,
252
+ };
253
+ }
254
+ if (resolvedText && resolvedBytes !== null && resolvedBytes > INLINE_BODY_BYTES_THRESHOLD) {
255
+ const chunkRef = `${params.requestId}:${params.bodyKind}:${Date.now()}:${Math.random().toString(36).slice(2, 8)}`;
256
+ params.bodyChunkStatement.run(chunkRef, params.sessionId, params.requestId, params.traceId, params.bodyKind, params.contentType, resolvedText, resolvedBytes, params.truncated ? 1 : 0, Date.now());
257
+ return {
258
+ contentType: params.contentType,
259
+ bodyText: null,
260
+ bodyJson: null,
261
+ bodyBytes: resolvedBytes,
262
+ truncated: params.truncated,
263
+ chunkRef,
264
+ };
265
+ }
266
+ return {
267
+ contentType: params.contentType,
268
+ bodyText: resolvedText,
269
+ bodyJson: resolvedJsonText,
270
+ bodyBytes: resolvedBytes,
271
+ truncated: params.truncated,
272
+ chunkRef: null,
273
+ };
106
274
  }
107
275
  insertSnapshotPrepared(sessionId, triggerEventId, data) {
108
276
  writeSnapshot(this.db, getDatabasePath(), sessionId, data, triggerEventId);
@@ -144,4 +312,85 @@ function normalizeOriginCandidate(value) {
144
312
  return null;
145
313
  }
146
314
  }
315
+ function toNonEmptyString(value) {
316
+ if (typeof value !== 'string') {
317
+ return null;
318
+ }
319
+ const trimmed = value.trim();
320
+ return trimmed.length > 0 ? trimmed : null;
321
+ }
322
+ function toNullableInteger(value) {
323
+ if (typeof value !== 'number' || !Number.isFinite(value)) {
324
+ return null;
325
+ }
326
+ return Math.floor(value);
327
+ }
328
+ function toNullableContentType(value) {
329
+ const normalized = toNonEmptyString(value);
330
+ if (!normalized) {
331
+ return null;
332
+ }
333
+ return normalized.toLowerCase();
334
+ }
335
+ function toRecordLike(value) {
336
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
337
+ return value;
338
+ }
339
+ return null;
340
+ }
341
+ function utf8Bytes(value) {
342
+ return Buffer.byteLength(value, 'utf-8');
343
+ }
344
+ function normalizeMethod(value) {
345
+ return value ? value.toUpperCase() : 'GET';
346
+ }
347
+ function normalizeInitiator(value) {
348
+ if (!value) {
349
+ return 'other';
350
+ }
351
+ const normalized = value.toLowerCase();
352
+ if (normalized === 'fetch' || normalized === 'xhr' || normalized === 'img' || normalized === 'script' || normalized === 'other') {
353
+ return normalized;
354
+ }
355
+ return 'other';
356
+ }
357
+ function normalizeErrorClass(value) {
358
+ if (!value) {
359
+ return null;
360
+ }
361
+ const normalized = value.toLowerCase();
362
+ if (normalized === 'timeout' || normalized === 'cors' || normalized === 'dns' || normalized === 'blocked' || normalized === 'http_error' || normalized === 'unknown') {
363
+ return normalized;
364
+ }
365
+ return 'unknown';
366
+ }
367
+ function sanitizeRecord(value) {
368
+ return sanitizeValue(value, 'root');
369
+ }
370
+ function sanitizeValue(value, key) {
371
+ if (typeof value === 'string') {
372
+ if (SENSITIVE_FIELD_NAMES.has(key.toLowerCase())) {
373
+ return '[REDACTED]';
374
+ }
375
+ return redactString(value);
376
+ }
377
+ if (Array.isArray(value)) {
378
+ return value.map((entry) => sanitizeValue(entry, key));
379
+ }
380
+ if (value && typeof value === 'object') {
381
+ const result = {};
382
+ for (const [entryKey, entryValue] of Object.entries(value)) {
383
+ result[entryKey] = sanitizeValue(entryValue, entryKey);
384
+ }
385
+ return result;
386
+ }
387
+ return value;
388
+ }
389
+ function redactString(value) {
390
+ let result = value;
391
+ for (const rule of REDACTION_PATTERNS) {
392
+ result = result.replace(rule.pattern, rule.replacement);
393
+ }
394
+ return result;
395
+ }
147
396
  //# sourceMappingURL=events-repository.js.map