browser-debug-mcp-bridge 1.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 (31) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +101 -0
  3. package/apps/mcp-server/dist/db/connection.js +42 -0
  4. package/apps/mcp-server/dist/db/connection.js.map +1 -0
  5. package/apps/mcp-server/dist/db/error-fingerprints.js +21 -0
  6. package/apps/mcp-server/dist/db/error-fingerprints.js.map +1 -0
  7. package/apps/mcp-server/dist/db/events-repository.js +109 -0
  8. package/apps/mcp-server/dist/db/events-repository.js.map +1 -0
  9. package/apps/mcp-server/dist/db/index.js +4 -0
  10. package/apps/mcp-server/dist/db/index.js.map +1 -0
  11. package/apps/mcp-server/dist/db/migrations.js +101 -0
  12. package/apps/mcp-server/dist/db/migrations.js.map +1 -0
  13. package/apps/mcp-server/dist/db/schema.js +157 -0
  14. package/apps/mcp-server/dist/db/schema.js.map +1 -0
  15. package/apps/mcp-server/dist/main.js +384 -0
  16. package/apps/mcp-server/dist/main.js.map +1 -0
  17. package/apps/mcp-server/dist/mcp/server.js +1619 -0
  18. package/apps/mcp-server/dist/mcp/server.js.map +1 -0
  19. package/apps/mcp-server/dist/mcp-bridge.js +55 -0
  20. package/apps/mcp-server/dist/mcp-bridge.js.map +1 -0
  21. package/apps/mcp-server/dist/retention.js +841 -0
  22. package/apps/mcp-server/dist/retention.js.map +1 -0
  23. package/apps/mcp-server/dist/websocket/index.js +3 -0
  24. package/apps/mcp-server/dist/websocket/index.js.map +1 -0
  25. package/apps/mcp-server/dist/websocket/messages.js +150 -0
  26. package/apps/mcp-server/dist/websocket/messages.js.map +1 -0
  27. package/apps/mcp-server/dist/websocket/websocket-server.js +302 -0
  28. package/apps/mcp-server/dist/websocket/websocket-server.js.map +1 -0
  29. package/apps/mcp-server/package.json +28 -0
  30. package/package.json +88 -0
  31. package/scripts/mcp-start.cjs +229 -0
package/LICENSE ADDED
@@ -0,0 +1,15 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
10
+ REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
11
+ FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
12
+ INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
13
+ LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
14
+ OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
15
+ PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,101 @@
1
+ # Browser Debug MCP Bridge
2
+
3
+ Chrome Extension + local Node.js MCP server that captures browser debugging telemetry from a real user session and exposes it through MCP tools.
4
+
5
+ ## Why this project exists
6
+
7
+ - Debug with real browser context (logged-in sessions, feature flags, extensions)
8
+ - Store lightweight telemetry continuously, request heavy DOM data on demand
9
+ - Keep privacy-first defaults with safe mode, allowlists, and redaction
10
+
11
+ ## Prerequisites
12
+
13
+ - Node.js 20+
14
+ - pnpm 9+
15
+ - Chrome (for the extension)
16
+
17
+ ## Quick start
18
+
19
+ ```bash
20
+ pnpm install
21
+ pnpm nx serve mcp-server
22
+ pnpm nx build chrome-extension --watch
23
+ ```
24
+
25
+ Run unified ingest + MCP stdio runtime (for external MCP clients):
26
+
27
+ ```bash
28
+ pnpm install
29
+ node scripts/mcp-start.cjs
30
+ ```
31
+
32
+ Quick npm MCP launch (marketplace-style, after publish):
33
+
34
+ ```bash
35
+ npx -y browser-debug-mcp-bridge
36
+ ```
37
+
38
+ GitHub fallback launch (if npm package is not available yet):
39
+
40
+ ```bash
41
+ npx -y --package=github:RobertoM80/browser-debug-mcp-bridge browser-debug-mcp-bridge
42
+ ```
43
+
44
+ Optional one-step setup scripts:
45
+
46
+ ```bash
47
+ # Windows (PowerShell)
48
+ ./install.ps1
49
+
50
+ # macOS/Linux
51
+ bash ./install.sh
52
+ ```
53
+
54
+ Useful workspace commands:
55
+
56
+ ```bash
57
+ pnpm typecheck
58
+ pnpm test
59
+ pnpm nx run-many -t lint
60
+ pnpm nx run-many -t build
61
+ ```
62
+
63
+ Enable local pre-commit checks (typecheck + lint + test before each commit):
64
+
65
+ ```bash
66
+ pnpm hooks:install
67
+ ```
68
+
69
+ ## Load the extension
70
+
71
+ 1. Build the extension: `pnpm nx build chrome-extension`
72
+ 2. Open Chrome -> `chrome://extensions`
73
+ 3. Enable Developer mode
74
+ 4. Click **Load unpacked**
75
+ 5. Select `dist/apps/chrome-extension`
76
+
77
+ ## Main docs
78
+
79
+ - Project spec: `PROJECT_INFOS.md`
80
+ - Full beginner setup guide: `HOW_TO_USE_BROWSER_DEBUG_MCP_BRIDGE.md`
81
+ - MCP tools reference: `docs/MCP_TOOLS.md`
82
+ - MCP client setup (Codex/Claude/Cursor/Windsurf): `docs/MCP_CLIENT_SETUP.md`
83
+ - GitHub Actions explained: `docs/GITHUB_ACTIONS.md`
84
+ - Security and privacy controls: `SECURITY.md`
85
+ - Troubleshooting guide: `docs/TROUBLESHOOTING.md`
86
+ - Architecture overview: `docs/ARCHITECTURE.md`
87
+ - Architecture decisions: `docs/ARCHITECTURE_DECISIONS.md`
88
+
89
+ ## Repository layout
90
+
91
+ ```text
92
+ apps/
93
+ mcp-server/ Fastify + WebSocket ingest + MCP server
94
+ chrome-extension/ MV3 extension (background/content/injected)
95
+ viewer/ Optional UI
96
+ libs/
97
+ shared/ Shared schemas/types/utils
98
+ redaction/ Privacy redaction engine
99
+ selectors/ Robust selector generation
100
+ mcp-contracts/ MCP tool contracts and schemas
101
+ ```
@@ -0,0 +1,42 @@
1
+ import Database from 'better-sqlite3';
2
+ import { join } from 'path';
3
+ import { dirname } from 'path';
4
+ import { mkdirSync } from 'fs';
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
+ export function createConnection(dbPath) {
11
+ const path = dbPath || getDatabasePath();
12
+ if (path !== ':memory:') {
13
+ mkdirSync(dirname(path), { recursive: true });
14
+ }
15
+ const db = new Database(path);
16
+ db.pragma('journal_mode = WAL');
17
+ db.pragma('foreign_keys = ON');
18
+ return {
19
+ db,
20
+ isConnected: true
21
+ };
22
+ }
23
+ export function getConnection() {
24
+ if (!connection) {
25
+ connection = createConnection();
26
+ }
27
+ return connection;
28
+ }
29
+ export function closeConnection() {
30
+ if (connection) {
31
+ connection.db.close();
32
+ connection = null;
33
+ }
34
+ }
35
+ export function isConnected() {
36
+ return connection !== null && connection.isConnected;
37
+ }
38
+ export function resetConnection() {
39
+ closeConnection();
40
+ connection = null;
41
+ }
42
+ //# sourceMappingURL=connection.js.map
@@ -0,0 +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"}
@@ -0,0 +1,21 @@
1
+ import { createHash } from 'node:crypto';
2
+ function normalizeText(value) {
3
+ return value.replace(/\s+/g, ' ').trim().toLowerCase();
4
+ }
5
+ export function createErrorFingerprint(message, stack) {
6
+ const normalizedMessage = normalizeText(message || 'unknown error');
7
+ const normalizedStack = normalizeText(stack || '');
8
+ const source = `${normalizedMessage}\n${normalizedStack}`;
9
+ const digest = createHash('sha256').update(source).digest('hex').slice(0, 16);
10
+ return `fp-${digest}`;
11
+ }
12
+ export function resolveErrorFingerprint(data) {
13
+ const provided = typeof data.fingerprint === 'string' ? data.fingerprint.trim() : '';
14
+ if (provided) {
15
+ return provided;
16
+ }
17
+ const message = typeof data.message === 'string' ? data.message : 'Unknown error';
18
+ const stack = typeof data.stack === 'string' ? data.stack : undefined;
19
+ return createErrorFingerprint(message, stack);
20
+ }
21
+ //# sourceMappingURL=error-fingerprints.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-fingerprints.js","sourceRoot":"","sources":["../../src/db/error-fingerprints.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,SAAS,aAAa,CAAC,KAAa;IAClC,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AACzD,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,OAAe,EAAE,KAAc;IACpE,MAAM,iBAAiB,GAAG,aAAa,CAAC,OAAO,IAAI,eAAe,CAAC,CAAC;IACpE,MAAM,eAAe,GAAG,aAAa,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC;IACnD,MAAM,MAAM,GAAG,GAAG,iBAAiB,KAAK,eAAe,EAAE,CAAC;IAC1D,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IAC9E,OAAO,MAAM,MAAM,EAAE,CAAC;AACxB,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,IAA6B;IACnE,MAAM,QAAQ,GAAG,OAAO,IAAI,CAAC,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;IACrF,IAAI,QAAQ,EAAE,CAAC;QACb,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,OAAO,GAAG,OAAO,IAAI,CAAC,OAAO,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,CAAC;IAClF,MAAM,KAAK,GAAG,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC;IAEtE,OAAO,sBAAsB,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;AAChD,CAAC"}
@@ -0,0 +1,109 @@
1
+ import { resolveErrorFingerprint } from './error-fingerprints';
2
+ import { getDatabasePath } from './connection';
3
+ import { writeSnapshot } from '../retention';
4
+ export class EventsRepository {
5
+ db;
6
+ constructor(db) {
7
+ this.db = db;
8
+ }
9
+ insertEventsBatch(messages) {
10
+ if (messages.length === 0) {
11
+ return;
12
+ }
13
+ const insert = this.db.prepare(`
14
+ INSERT INTO events (event_id, session_id, ts, type, payload_json)
15
+ VALUES (?, ?, ?, ?, ?)
16
+ `);
17
+ const insertNetwork = this.db.prepare(`
18
+ INSERT INTO network (
19
+ request_id, session_id, ts_start, duration_ms, method, url, status, initiator, error_class, response_size_est
20
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
21
+ `);
22
+ const upsertFingerprint = this.db.prepare(`
23
+ INSERT INTO error_fingerprints (
24
+ fingerprint, session_id, count, sample_message, sample_stack, first_seen_at, last_seen_at
25
+ ) VALUES (?, ?, 1, ?, ?, ?, ?)
26
+ ON CONFLICT(fingerprint) DO UPDATE SET
27
+ count = count + 1,
28
+ last_seen_at = excluded.last_seen_at,
29
+ sample_message = COALESCE(error_fingerprints.sample_message, excluded.sample_message),
30
+ sample_stack = COALESCE(error_fingerprints.sample_stack, excluded.sample_stack)
31
+ `);
32
+ const runBatch = this.db.transaction((batch) => {
33
+ for (const message of batch) {
34
+ const eventId = `${message.sessionId}-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
35
+ const dbEventType = this.mapEventType(message.eventType);
36
+ insert.run(eventId, message.sessionId, message.timestamp ?? Date.now(), dbEventType, JSON.stringify(message.data));
37
+ if (message.eventType === 'error') {
38
+ this.upsertErrorFingerprintPrepared(upsertFingerprint, message.sessionId, message.data);
39
+ }
40
+ if (message.eventType === 'network') {
41
+ this.insertNetworkEventPrepared(insertNetwork, message.sessionId, message.data);
42
+ }
43
+ if (message.eventType === 'ui_snapshot') {
44
+ this.insertSnapshotPrepared(message.sessionId, eventId, message.data);
45
+ }
46
+ }
47
+ });
48
+ runBatch(messages);
49
+ }
50
+ createSession(message) {
51
+ const insert = this.db.prepare(`
52
+ INSERT INTO sessions (
53
+ session_id, created_at, tab_id, window_id, url_start, url_last,
54
+ user_agent, viewport_w, viewport_h, dpr, safe_mode, allowlist_hash
55
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
56
+ `);
57
+ const now = Date.now();
58
+ insert.run(message.sessionId, now, message.tabId ?? null, message.windowId ?? null, message.url, message.url, message.userAgent ?? null, message.viewport?.width ?? null, message.viewport?.height ?? null, message.dpr ?? null, message.safeMode ? 1 : 0, null);
59
+ }
60
+ endSession(message) {
61
+ const update = this.db.prepare(`
62
+ UPDATE sessions
63
+ SET ended_at = ?
64
+ WHERE session_id = ?
65
+ `);
66
+ update.run(Date.now(), message.sessionId);
67
+ }
68
+ insertEvent(message) {
69
+ this.insertEventsBatch([message]);
70
+ }
71
+ mapEventType(eventType) {
72
+ const mapping = {
73
+ 'navigation': 'nav',
74
+ 'console': 'console',
75
+ 'error': 'error',
76
+ 'network': 'network',
77
+ 'click': 'ui',
78
+ 'scroll': 'ui',
79
+ 'input': 'ui',
80
+ 'change': 'ui',
81
+ 'submit': 'ui',
82
+ 'focus': 'ui',
83
+ 'blur': 'ui',
84
+ 'keydown': 'ui',
85
+ 'ui_snapshot': 'ui',
86
+ 'custom': 'ui',
87
+ };
88
+ return mapping[eventType] || 'ui';
89
+ }
90
+ upsertErrorFingerprintPrepared(statement, sessionId, data) {
91
+ const fingerprint = resolveErrorFingerprint(data);
92
+ if (!fingerprint)
93
+ return;
94
+ const now = Date.now();
95
+ statement.run(fingerprint, sessionId, data.message ?? 'Unknown error', data.stack ?? null, now, now);
96
+ }
97
+ insertNetworkEventPrepared(statement, sessionId, data) {
98
+ const requestId = `${sessionId}-net-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
99
+ statement.run(requestId, sessionId, data.timestamp ?? Date.now(), data.duration ?? null, data.method ?? 'GET', data.url ?? '', data.status ?? null, data.initiator ?? 'other', data.errorType ?? null, data.responseSize ?? null);
100
+ }
101
+ insertSnapshotPrepared(sessionId, triggerEventId, data) {
102
+ writeSnapshot(this.db, getDatabasePath(), sessionId, data, triggerEventId);
103
+ }
104
+ sessionExists(sessionId) {
105
+ const result = this.db.prepare('SELECT 1 FROM sessions WHERE session_id = ?').get(sessionId);
106
+ return !!result;
107
+ }
108
+ }
109
+ //# sourceMappingURL=events-repository.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"events-repository.js","sourceRoot":"","sources":["../../src/db/events-repository.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export * from './connection';
2
+ export * from './schema';
3
+ export * from './migrations';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC"}
@@ -0,0 +1,101 @@
1
+ import { initializeSchema, getSchemaVersion, clearDatabase, SCHEMA_VERSION } from './schema';
2
+ const migrations = [
3
+ {
4
+ version: 1,
5
+ name: 'initial_schema',
6
+ up: initializeSchema
7
+ },
8
+ {
9
+ version: 2,
10
+ name: 'retention_and_pinning',
11
+ up: (db) => {
12
+ const hasPinnedColumn = db.prepare("PRAGMA table_info('sessions')").all().some((column) => column.name === 'pinned');
13
+ if (!hasPinnedColumn) {
14
+ db.exec(`
15
+ ALTER TABLE sessions ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0;
16
+ `);
17
+ }
18
+ db.exec(`
19
+ CREATE INDEX IF NOT EXISTS idx_sessions_pinned_created_at ON sessions(pinned, created_at);
20
+ `);
21
+ db.exec(`
22
+ CREATE TABLE IF NOT EXISTS server_settings (
23
+ id INTEGER PRIMARY KEY CHECK (id = 1),
24
+ retention_days INTEGER NOT NULL DEFAULT 30,
25
+ max_db_mb INTEGER NOT NULL DEFAULT 1024,
26
+ max_sessions INTEGER NOT NULL DEFAULT 10000,
27
+ cleanup_interval_minutes INTEGER NOT NULL DEFAULT 60,
28
+ last_cleanup_at INTEGER,
29
+ export_path_override TEXT
30
+ );
31
+ `);
32
+ db.exec(`
33
+ INSERT OR IGNORE INTO server_settings (
34
+ id,
35
+ retention_days,
36
+ max_db_mb,
37
+ max_sessions,
38
+ cleanup_interval_minutes,
39
+ last_cleanup_at,
40
+ export_path_override
41
+ ) VALUES (1, 30, 1024, 10000, 60, NULL, NULL);
42
+ `);
43
+ }
44
+ },
45
+ {
46
+ version: 3,
47
+ name: 'snapshots_storage',
48
+ up: (db) => {
49
+ db.exec(`
50
+ CREATE TABLE IF NOT EXISTS snapshots (
51
+ snapshot_id TEXT PRIMARY KEY,
52
+ session_id TEXT NOT NULL,
53
+ trigger_event_id TEXT,
54
+ ts INTEGER NOT NULL,
55
+ trigger TEXT NOT NULL,
56
+ selector TEXT,
57
+ url TEXT,
58
+ mode TEXT NOT NULL,
59
+ style_mode TEXT,
60
+ dom_json TEXT,
61
+ styles_json TEXT,
62
+ png_path TEXT,
63
+ png_mime TEXT,
64
+ png_bytes INTEGER,
65
+ dom_truncated INTEGER NOT NULL DEFAULT 0,
66
+ styles_truncated INTEGER NOT NULL DEFAULT 0,
67
+ png_truncated INTEGER NOT NULL DEFAULT 0,
68
+ created_at INTEGER NOT NULL,
69
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
70
+ FOREIGN KEY (trigger_event_id) REFERENCES events(event_id) ON DELETE SET NULL
71
+ );
72
+ `);
73
+ db.exec(`
74
+ CREATE INDEX IF NOT EXISTS idx_snapshots_session_ts ON snapshots(session_id, ts);
75
+ CREATE INDEX IF NOT EXISTS idx_snapshots_session_trigger_ts ON snapshots(session_id, trigger, ts);
76
+ CREATE INDEX IF NOT EXISTS idx_snapshots_png_path ON snapshots(png_path);
77
+ `);
78
+ },
79
+ }
80
+ ];
81
+ export function runMigrations(db) {
82
+ const currentVersion = getSchemaVersion(db) || 0;
83
+ const pendingMigrations = migrations.filter(m => m.version > currentVersion);
84
+ for (const migration of pendingMigrations) {
85
+ migration.up(db);
86
+ const insertVersion = db.prepare(`
87
+ INSERT INTO schema_version (version, applied_at)
88
+ VALUES (?, ?)
89
+ `);
90
+ insertVersion.run(migration.version, Date.now());
91
+ }
92
+ }
93
+ export function initializeDatabase(db) {
94
+ runMigrations(db);
95
+ }
96
+ export function resetDatabase(db) {
97
+ clearDatabase(db);
98
+ initializeDatabase(db);
99
+ }
100
+ export { getSchemaVersion, clearDatabase, SCHEMA_VERSION };
101
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../../src/db/migrations.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAQ7F,MAAM,UAAU,GAAgB;IAC9B;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,gBAAgB;QACtB,EAAE,EAAE,gBAAgB;KACrB;IACD;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,uBAAuB;QAC7B,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE;YACT,MAAM,eAAe,GAAI,EAAE,CAAC,OAAO,CAAC,+BAA+B,CAAC,CAAC,GAAG,EAA8B,CAAC,IAAI,CACzG,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,CACrC,CAAC;YACF,IAAI,CAAC,eAAe,EAAE,CAAC;gBACrB,EAAE,CAAC,IAAI,CAAC;;SAEP,CAAC,CAAC;YACL,CAAC;YACD,EAAE,CAAC,IAAI,CAAC;;OAEP,CAAC,CAAC;YACH,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;OAUP,CAAC,CAAC;YACH,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;OAUP,CAAC,CAAC;QACL,CAAC;KACF;IACD;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,mBAAmB;QACzB,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE;YACT,EAAE,CAAC,IAAI,CAAC;;;;;;;;;;;;;;;;;;;;;;;OAuBP,CAAC,CAAC;YACH,EAAE,CAAC,IAAI,CAAC;;;;OAIP,CAAC,CAAC;QACL,CAAC;KACF;CACF,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,EAAY;IACxC,MAAM,cAAc,GAAG,gBAAgB,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;IAEjD,MAAM,iBAAiB,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,GAAG,cAAc,CAAC,CAAC;IAE7E,KAAK,MAAM,SAAS,IAAI,iBAAiB,EAAE,CAAC;QAC1C,SAAS,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC;QAEjB,MAAM,aAAa,GAAG,EAAE,CAAC,OAAO,CAAC;;;KAGhC,CAAC,CAAC;QACH,aAAa,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,EAAE,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IACnD,CAAC;AACH,CAAC;AAED,MAAM,UAAU,kBAAkB,CAAC,EAAY;IAC7C,aAAa,CAAC,EAAE,CAAC,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAY;IACxC,aAAa,CAAC,EAAE,CAAC,CAAC;IAClB,kBAAkB,CAAC,EAAE,CAAC,CAAC;AACzB,CAAC;AAED,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,157 @@
1
+ export const SCHEMA_VERSION = 3;
2
+ export const CREATE_TABLES_SQL = `
3
+ -- Sessions table
4
+ CREATE TABLE IF NOT EXISTS sessions (
5
+ session_id TEXT PRIMARY KEY,
6
+ created_at INTEGER NOT NULL,
7
+ ended_at INTEGER,
8
+ tab_id INTEGER,
9
+ window_id INTEGER,
10
+ url_start TEXT,
11
+ url_last TEXT,
12
+ user_agent TEXT,
13
+ viewport_w INTEGER,
14
+ viewport_h INTEGER,
15
+ dpr REAL,
16
+ safe_mode INTEGER NOT NULL DEFAULT 0,
17
+ allowlist_hash TEXT,
18
+ pinned INTEGER NOT NULL DEFAULT 0
19
+ );
20
+
21
+ CREATE INDEX IF NOT EXISTS idx_sessions_created_at ON sessions(created_at);
22
+ CREATE INDEX IF NOT EXISTS idx_sessions_ended_at ON sessions(ended_at);
23
+ CREATE INDEX IF NOT EXISTS idx_sessions_pinned_created_at ON sessions(pinned, created_at);
24
+
25
+ -- Server settings table
26
+ CREATE TABLE IF NOT EXISTS server_settings (
27
+ id INTEGER PRIMARY KEY CHECK (id = 1),
28
+ retention_days INTEGER NOT NULL DEFAULT 30,
29
+ max_db_mb INTEGER NOT NULL DEFAULT 1024,
30
+ max_sessions INTEGER NOT NULL DEFAULT 10000,
31
+ cleanup_interval_minutes INTEGER NOT NULL DEFAULT 60,
32
+ last_cleanup_at INTEGER,
33
+ export_path_override TEXT
34
+ );
35
+
36
+ INSERT OR IGNORE INTO server_settings (
37
+ id,
38
+ retention_days,
39
+ max_db_mb,
40
+ max_sessions,
41
+ cleanup_interval_minutes,
42
+ last_cleanup_at,
43
+ export_path_override
44
+ ) VALUES (1, 30, 1024, 10000, 60, NULL, NULL);
45
+
46
+ -- Events table
47
+ CREATE TABLE IF NOT EXISTS events (
48
+ event_id TEXT PRIMARY KEY,
49
+ session_id TEXT NOT NULL,
50
+ ts INTEGER NOT NULL,
51
+ type TEXT NOT NULL CHECK(type IN ('console', 'error', 'network', 'nav', 'ui', 'element_ref')),
52
+ payload_json TEXT NOT NULL,
53
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
54
+ );
55
+
56
+ CREATE INDEX IF NOT EXISTS idx_events_session_id ON events(session_id);
57
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
58
+ CREATE INDEX IF NOT EXISTS idx_events_type ON events(type);
59
+ CREATE INDEX IF NOT EXISTS idx_events_session_type ON events(session_id, type);
60
+
61
+ -- Network table
62
+ CREATE TABLE IF NOT EXISTS network (
63
+ request_id TEXT PRIMARY KEY,
64
+ session_id TEXT NOT NULL,
65
+ ts_start INTEGER NOT NULL,
66
+ duration_ms INTEGER,
67
+ method TEXT NOT NULL,
68
+ url TEXT NOT NULL,
69
+ status INTEGER,
70
+ initiator TEXT CHECK(initiator IN ('fetch', 'xhr', 'img', 'script', 'other')),
71
+ error_class TEXT CHECK(error_class IN ('timeout', 'cors', 'dns', 'blocked', 'http_error', 'unknown')),
72
+ response_size_est INTEGER,
73
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
74
+ );
75
+
76
+ CREATE INDEX IF NOT EXISTS idx_network_session_id ON network(session_id);
77
+ CREATE INDEX IF NOT EXISTS idx_network_url ON network(url);
78
+ CREATE INDEX IF NOT EXISTS idx_network_ts_start ON network(ts_start);
79
+ CREATE INDEX IF NOT EXISTS idx_network_error_class ON network(error_class);
80
+ CREATE INDEX IF NOT EXISTS idx_network_session_error ON network(session_id, error_class);
81
+
82
+ -- Error fingerprints table
83
+ CREATE TABLE IF NOT EXISTS error_fingerprints (
84
+ fingerprint TEXT PRIMARY KEY,
85
+ session_id TEXT NOT NULL,
86
+ count INTEGER NOT NULL DEFAULT 1,
87
+ sample_message TEXT NOT NULL,
88
+ sample_stack TEXT,
89
+ first_seen_at INTEGER NOT NULL,
90
+ last_seen_at INTEGER NOT NULL,
91
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
92
+ );
93
+
94
+ CREATE INDEX IF NOT EXISTS idx_error_fingerprints_session_id ON error_fingerprints(session_id);
95
+ CREATE INDEX IF NOT EXISTS idx_error_fingerprints_count ON error_fingerprints(count);
96
+ CREATE INDEX IF NOT EXISTS idx_error_fingerprints_last_seen ON error_fingerprints(last_seen_at);
97
+
98
+ -- UI snapshots table
99
+ CREATE TABLE IF NOT EXISTS snapshots (
100
+ snapshot_id TEXT PRIMARY KEY,
101
+ session_id TEXT NOT NULL,
102
+ trigger_event_id TEXT,
103
+ ts INTEGER NOT NULL,
104
+ trigger TEXT NOT NULL,
105
+ selector TEXT,
106
+ url TEXT,
107
+ mode TEXT NOT NULL,
108
+ style_mode TEXT,
109
+ dom_json TEXT,
110
+ styles_json TEXT,
111
+ png_path TEXT,
112
+ png_mime TEXT,
113
+ png_bytes INTEGER,
114
+ dom_truncated INTEGER NOT NULL DEFAULT 0,
115
+ styles_truncated INTEGER NOT NULL DEFAULT 0,
116
+ png_truncated INTEGER NOT NULL DEFAULT 0,
117
+ created_at INTEGER NOT NULL,
118
+ FOREIGN KEY (session_id) REFERENCES sessions(session_id) ON DELETE CASCADE,
119
+ FOREIGN KEY (trigger_event_id) REFERENCES events(event_id) ON DELETE SET NULL
120
+ );
121
+
122
+ CREATE INDEX IF NOT EXISTS idx_snapshots_session_ts ON snapshots(session_id, ts);
123
+ CREATE INDEX IF NOT EXISTS idx_snapshots_session_trigger_ts ON snapshots(session_id, trigger, ts);
124
+ CREATE INDEX IF NOT EXISTS idx_snapshots_png_path ON snapshots(png_path);
125
+
126
+ -- Schema version tracking
127
+ CREATE TABLE IF NOT EXISTS schema_version (
128
+ version INTEGER PRIMARY KEY,
129
+ applied_at INTEGER NOT NULL
130
+ );
131
+ `;
132
+ export function initializeSchema(db) {
133
+ db.exec(CREATE_TABLES_SQL);
134
+ }
135
+ export function getSchemaVersion(db) {
136
+ const tableExists = db.prepare(`
137
+ SELECT name FROM sqlite_master
138
+ WHERE type='table' AND name='schema_version'
139
+ `).get();
140
+ if (!tableExists) {
141
+ return null;
142
+ }
143
+ const result = db.prepare('SELECT version FROM schema_version ORDER BY version DESC LIMIT 1').get();
144
+ return result?.version ?? null;
145
+ }
146
+ export function clearDatabase(db) {
147
+ db.exec(`
148
+ DELETE FROM error_fingerprints;
149
+ DELETE FROM network;
150
+ DELETE FROM snapshots;
151
+ DELETE FROM events;
152
+ DELETE FROM sessions;
153
+ DELETE FROM server_settings;
154
+ DELETE FROM schema_version;
155
+ `);
156
+ }
157
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/db/schema.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,CAAC;AAEhC,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiIhC,CAAC;AAEF,MAAM,UAAU,gBAAgB,CAAC,EAAY;IAC3C,EAAE,CAAC,IAAI,CAAC,iBAAiB,CAAC,CAAC;AAC7B,CAAC;AAED,MAAM,UAAU,gBAAgB,CAAC,EAAY;IAC3C,MAAM,WAAW,GAAG,EAAE,CAAC,OAAO,CAAC;;;GAG9B,CAAC,CAAC,GAAG,EAAE,CAAC;IAET,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,CAAC,OAAO,CAAC,kEAAkE,CAAC,CAAC,GAAG,EAAqC,CAAC;IACvI,OAAO,MAAM,EAAE,OAAO,IAAI,IAAI,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,EAAY;IACxC,EAAE,CAAC,IAAI,CAAC;;;;;;;;GAQP,CAAC,CAAC;AACL,CAAC"}