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,733 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import type {
|
|
5
|
+
Breadcrumb,
|
|
6
|
+
ElementSummary,
|
|
7
|
+
Incident,
|
|
8
|
+
IncidentOccurrence,
|
|
9
|
+
IncidentSeverity,
|
|
10
|
+
IncidentStatus,
|
|
11
|
+
NetworkEvent,
|
|
12
|
+
Project,
|
|
13
|
+
Session,
|
|
14
|
+
VerificationResult,
|
|
15
|
+
VisualNote,
|
|
16
|
+
NoteStatus,
|
|
17
|
+
NoteVerificationResult,
|
|
18
|
+
} from '../../../core/src/index.js';
|
|
19
|
+
|
|
20
|
+
export class StorageDB {
|
|
21
|
+
private db: Database.Database;
|
|
22
|
+
|
|
23
|
+
constructor(dbPath: string) {
|
|
24
|
+
const dir = path.dirname(dbPath);
|
|
25
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
26
|
+
|
|
27
|
+
this.db = new Database(dbPath);
|
|
28
|
+
this.db.pragma('journal_mode = WAL');
|
|
29
|
+
this.db.pragma('synchronous = NORMAL');
|
|
30
|
+
this.initTables();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private initTables(): void {
|
|
34
|
+
this.db.exec(`
|
|
35
|
+
CREATE TABLE IF NOT EXISTS projects (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
name TEXT UNIQUE,
|
|
38
|
+
origin TEXT,
|
|
39
|
+
path TEXT,
|
|
40
|
+
created_at TEXT,
|
|
41
|
+
updated_at TEXT
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
45
|
+
id TEXT PRIMARY KEY,
|
|
46
|
+
project_id TEXT,
|
|
47
|
+
origin TEXT,
|
|
48
|
+
url TEXT,
|
|
49
|
+
title TEXT,
|
|
50
|
+
user_agent TEXT,
|
|
51
|
+
connected_at TEXT,
|
|
52
|
+
last_seen_at TEXT,
|
|
53
|
+
active INTEGER DEFAULT 1
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
session_id TEXT,
|
|
59
|
+
event_type TEXT,
|
|
60
|
+
payload TEXT,
|
|
61
|
+
timestamp INTEGER,
|
|
62
|
+
route TEXT,
|
|
63
|
+
url TEXT
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
CREATE TABLE IF NOT EXISTS incidents (
|
|
67
|
+
id TEXT PRIMARY KEY,
|
|
68
|
+
project_id TEXT,
|
|
69
|
+
session_id TEXT,
|
|
70
|
+
type TEXT,
|
|
71
|
+
severity TEXT,
|
|
72
|
+
message TEXT,
|
|
73
|
+
source_file TEXT,
|
|
74
|
+
source_line INTEGER,
|
|
75
|
+
source_col INTEGER,
|
|
76
|
+
fingerprint TEXT UNIQUE,
|
|
77
|
+
route TEXT,
|
|
78
|
+
first_seen TEXT,
|
|
79
|
+
last_seen TEXT,
|
|
80
|
+
occurrences INTEGER DEFAULT 1,
|
|
81
|
+
status TEXT DEFAULT 'OPEN',
|
|
82
|
+
stack TEXT,
|
|
83
|
+
breadcrumbs TEXT,
|
|
84
|
+
network_failures TEXT,
|
|
85
|
+
last_element TEXT,
|
|
86
|
+
screenshot_path TEXT
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
CREATE TABLE IF NOT EXISTS incident_occurrences (
|
|
90
|
+
id TEXT PRIMARY KEY,
|
|
91
|
+
incident_id TEXT,
|
|
92
|
+
session_id TEXT,
|
|
93
|
+
timestamp TEXT,
|
|
94
|
+
route TEXT,
|
|
95
|
+
url TEXT,
|
|
96
|
+
stack TEXT,
|
|
97
|
+
breadcrumbs TEXT,
|
|
98
|
+
last_element TEXT
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
CREATE TABLE IF NOT EXISTS screenshots (
|
|
102
|
+
id TEXT PRIMARY KEY,
|
|
103
|
+
incident_id TEXT,
|
|
104
|
+
file_path TEXT,
|
|
105
|
+
format TEXT,
|
|
106
|
+
width INTEGER,
|
|
107
|
+
height INTEGER,
|
|
108
|
+
created_at TEXT
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
CREATE TABLE IF NOT EXISTS verifications (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
incident_id TEXT,
|
|
114
|
+
status TEXT,
|
|
115
|
+
checks TEXT,
|
|
116
|
+
before_screenshot TEXT,
|
|
117
|
+
after_screenshot TEXT,
|
|
118
|
+
message TEXT,
|
|
119
|
+
created_at TEXT
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
CREATE TABLE IF NOT EXISTS notes (
|
|
123
|
+
id TEXT PRIMARY KEY,
|
|
124
|
+
project_id TEXT,
|
|
125
|
+
session_id TEXT,
|
|
126
|
+
type TEXT,
|
|
127
|
+
message TEXT,
|
|
128
|
+
route TEXT,
|
|
129
|
+
url TEXT,
|
|
130
|
+
viewport_json TEXT,
|
|
131
|
+
scroll_json TEXT,
|
|
132
|
+
target_json TEXT,
|
|
133
|
+
element_context_json TEXT,
|
|
134
|
+
region_json TEXT,
|
|
135
|
+
screenshot_path TEXT,
|
|
136
|
+
incident_id TEXT,
|
|
137
|
+
status TEXT DEFAULT 'OPEN',
|
|
138
|
+
created_at TEXT,
|
|
139
|
+
updated_at TEXT,
|
|
140
|
+
resolved_at TEXT
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
CREATE TABLE IF NOT EXISTS note_verifications (
|
|
144
|
+
id TEXT PRIMARY KEY,
|
|
145
|
+
note_id TEXT,
|
|
146
|
+
status TEXT,
|
|
147
|
+
checks TEXT,
|
|
148
|
+
geometry_diff TEXT,
|
|
149
|
+
before_screenshot TEXT,
|
|
150
|
+
after_screenshot TEXT,
|
|
151
|
+
message TEXT,
|
|
152
|
+
created_at TEXT
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
CREATE INDEX IF NOT EXISTS idx_events_session ON events(session_id, timestamp);
|
|
156
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_project ON incidents(project_id, status);
|
|
157
|
+
CREATE INDEX IF NOT EXISTS idx_incidents_fp ON incidents(fingerprint);
|
|
158
|
+
CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id, status);
|
|
159
|
+
`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// --- PROJECTS ---
|
|
163
|
+
public upsertProject(project: { id: string; name: string; origin: string; path?: string }): Project {
|
|
164
|
+
const now = new Date().toISOString();
|
|
165
|
+
const existing = this.db.prepare('SELECT * FROM projects WHERE name = ? OR origin = ?').get(project.name, project.origin) as any;
|
|
166
|
+
|
|
167
|
+
if (existing) {
|
|
168
|
+
this.db
|
|
169
|
+
.prepare('UPDATE projects SET name = ?, origin = ?, path = COALESCE(?, path), updated_at = ? WHERE id = ?')
|
|
170
|
+
.run(project.name, project.origin, project.path || null, now, existing.id);
|
|
171
|
+
return {
|
|
172
|
+
id: existing.id,
|
|
173
|
+
name: project.name,
|
|
174
|
+
origin: project.origin,
|
|
175
|
+
path: project.path || existing.path,
|
|
176
|
+
createdAt: existing.created_at,
|
|
177
|
+
updatedAt: now,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
this.db
|
|
182
|
+
.prepare('INSERT INTO projects (id, name, origin, path, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)')
|
|
183
|
+
.run(project.id, project.name, project.origin, project.path || null, now, now);
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
id: project.id,
|
|
187
|
+
name: project.name,
|
|
188
|
+
origin: project.origin,
|
|
189
|
+
path: project.path,
|
|
190
|
+
createdAt: now,
|
|
191
|
+
updatedAt: now,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public getProject(idOrName: string): Project | null {
|
|
196
|
+
const row = this.db.prepare('SELECT * FROM projects WHERE id = ? OR name = ?').get(idOrName, idOrName) as any;
|
|
197
|
+
if (!row) return null;
|
|
198
|
+
return {
|
|
199
|
+
id: row.id,
|
|
200
|
+
name: row.name,
|
|
201
|
+
origin: row.origin,
|
|
202
|
+
path: row.path,
|
|
203
|
+
createdAt: row.created_at,
|
|
204
|
+
updatedAt: row.updated_at,
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public getProjectByOrigin(origin: string): Project | null {
|
|
209
|
+
const row = this.db.prepare('SELECT * FROM projects WHERE origin = ?').get(origin) as any;
|
|
210
|
+
if (!row) return null;
|
|
211
|
+
return {
|
|
212
|
+
id: row.id,
|
|
213
|
+
name: row.name,
|
|
214
|
+
origin: row.origin,
|
|
215
|
+
path: row.path,
|
|
216
|
+
createdAt: row.created_at,
|
|
217
|
+
updatedAt: row.updated_at,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
public listProjects(): Project[] {
|
|
222
|
+
const rows = this.db.prepare('SELECT * FROM projects ORDER BY updated_at DESC').all() as any[];
|
|
223
|
+
return rows.map((row) => ({
|
|
224
|
+
id: row.id,
|
|
225
|
+
name: row.name,
|
|
226
|
+
origin: row.origin,
|
|
227
|
+
path: row.path,
|
|
228
|
+
createdAt: row.created_at,
|
|
229
|
+
updatedAt: row.updated_at,
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// --- SESSIONS ---
|
|
234
|
+
public upsertSession(session: Session): void {
|
|
235
|
+
const existing = this.db.prepare('SELECT id FROM sessions WHERE id = ?').get(session.id);
|
|
236
|
+
if (existing) {
|
|
237
|
+
this.db
|
|
238
|
+
.prepare('UPDATE sessions SET url = ?, title = ?, last_seen_at = ?, active = ? WHERE id = ?')
|
|
239
|
+
.run(session.url, session.title, session.lastSeenAt, session.active ? 1 : 0, session.id);
|
|
240
|
+
} else {
|
|
241
|
+
this.db
|
|
242
|
+
.prepare(
|
|
243
|
+
'INSERT INTO sessions (id, project_id, origin, url, title, user_agent, connected_at, last_seen_at, active) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
|
244
|
+
)
|
|
245
|
+
.run(
|
|
246
|
+
session.id,
|
|
247
|
+
session.projectId,
|
|
248
|
+
session.origin,
|
|
249
|
+
session.url,
|
|
250
|
+
session.title,
|
|
251
|
+
session.userAgent,
|
|
252
|
+
session.connectedAt,
|
|
253
|
+
session.lastSeenAt,
|
|
254
|
+
session.active ? 1 : 0
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
public getSession(id: string): Session | null {
|
|
260
|
+
const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id) as any;
|
|
261
|
+
if (!row) return null;
|
|
262
|
+
return {
|
|
263
|
+
id: row.id,
|
|
264
|
+
projectId: row.project_id,
|
|
265
|
+
origin: row.origin,
|
|
266
|
+
url: row.url,
|
|
267
|
+
title: row.title,
|
|
268
|
+
userAgent: row.user_agent,
|
|
269
|
+
connectedAt: row.connected_at,
|
|
270
|
+
lastSeenAt: row.last_seen_at,
|
|
271
|
+
active: row.active === 1,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public listSessions(projectId?: string, activeOnly = false): Session[] {
|
|
276
|
+
let sql = 'SELECT * FROM sessions WHERE 1=1';
|
|
277
|
+
const params: any[] = [];
|
|
278
|
+
|
|
279
|
+
if (projectId) {
|
|
280
|
+
sql += ' AND project_id = ?';
|
|
281
|
+
params.push(projectId);
|
|
282
|
+
}
|
|
283
|
+
if (activeOnly) {
|
|
284
|
+
sql += ' AND active = 1';
|
|
285
|
+
}
|
|
286
|
+
sql += ' ORDER BY last_seen_at DESC';
|
|
287
|
+
|
|
288
|
+
const rows = this.db.prepare(sql).all(...params) as any[];
|
|
289
|
+
return rows.map((row) => ({
|
|
290
|
+
id: row.id,
|
|
291
|
+
projectId: row.project_id,
|
|
292
|
+
origin: row.origin,
|
|
293
|
+
url: row.url,
|
|
294
|
+
title: row.title,
|
|
295
|
+
userAgent: row.user_agent,
|
|
296
|
+
connectedAt: row.connected_at,
|
|
297
|
+
lastSeenAt: row.last_seen_at,
|
|
298
|
+
active: row.active === 1,
|
|
299
|
+
}));
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
public deactivateSession(id: string): void {
|
|
303
|
+
this.db.prepare('UPDATE sessions SET active = 0, last_seen_at = ? WHERE id = ?').run(new Date().toISOString(), id);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// --- EVENTS & RETENTION ---
|
|
307
|
+
public insertEvent(event: {
|
|
308
|
+
id: string;
|
|
309
|
+
sessionId: string;
|
|
310
|
+
eventType: string;
|
|
311
|
+
payload: any;
|
|
312
|
+
timestamp: number;
|
|
313
|
+
route?: string;
|
|
314
|
+
url?: string;
|
|
315
|
+
}): void {
|
|
316
|
+
this.db
|
|
317
|
+
.prepare('INSERT INTO events (id, session_id, event_type, payload, timestamp, route, url) VALUES (?, ?, ?, ?, ?, ?, ?)')
|
|
318
|
+
.run(
|
|
319
|
+
event.id,
|
|
320
|
+
event.sessionId,
|
|
321
|
+
event.eventType,
|
|
322
|
+
JSON.stringify(event.payload),
|
|
323
|
+
event.timestamp,
|
|
324
|
+
event.route || '',
|
|
325
|
+
event.url || ''
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
public pruneSessionEvents(sessionId: string, maxEvents = 1000): void {
|
|
330
|
+
this.db
|
|
331
|
+
.prepare(
|
|
332
|
+
`DELETE FROM events
|
|
333
|
+
WHERE session_id = ? AND id NOT IN (
|
|
334
|
+
SELECT id FROM events WHERE session_id = ? ORDER BY timestamp DESC LIMIT ?
|
|
335
|
+
)`
|
|
336
|
+
)
|
|
337
|
+
.run(sessionId, sessionId, maxEvents);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
public getEvents(options: { sessionId?: string; eventType?: string; limit?: number }): any[] {
|
|
341
|
+
let sql = 'SELECT * FROM events WHERE 1=1';
|
|
342
|
+
const params: any[] = [];
|
|
343
|
+
|
|
344
|
+
if (options.sessionId) {
|
|
345
|
+
sql += ' AND session_id = ?';
|
|
346
|
+
params.push(options.sessionId);
|
|
347
|
+
}
|
|
348
|
+
if (options.eventType) {
|
|
349
|
+
sql += ' AND event_type = ?';
|
|
350
|
+
params.push(options.eventType);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
sql += ' ORDER BY timestamp DESC LIMIT ?';
|
|
354
|
+
params.push(options.limit || 50);
|
|
355
|
+
|
|
356
|
+
const rows = this.db.prepare(sql).all(...params) as any[];
|
|
357
|
+
return rows.map((r) => ({
|
|
358
|
+
id: r.id,
|
|
359
|
+
sessionId: r.session_id,
|
|
360
|
+
eventType: r.event_type,
|
|
361
|
+
payload: JSON.parse(r.payload),
|
|
362
|
+
timestamp: r.timestamp,
|
|
363
|
+
route: r.route,
|
|
364
|
+
url: r.url,
|
|
365
|
+
}));
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// --- INCIDENTS ---
|
|
369
|
+
public findIncidentByFingerprint(fingerprint: string): Incident | null {
|
|
370
|
+
const row = this.db.prepare('SELECT * FROM incidents WHERE fingerprint = ?').get(fingerprint) as any;
|
|
371
|
+
if (!row) return null;
|
|
372
|
+
return this.mapIncidentRow(row);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
public getIncident(id: string): Incident | null {
|
|
376
|
+
const row = this.db.prepare('SELECT * FROM incidents WHERE id = ?').get(id) as any;
|
|
377
|
+
if (!row) return null;
|
|
378
|
+
return this.mapIncidentRow(row);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
public listIncidents(options: { projectId?: string; status?: IncidentStatus; severity?: IncidentSeverity; limit?: number } = {}): Incident[] {
|
|
382
|
+
let sql = 'SELECT * FROM incidents WHERE 1=1';
|
|
383
|
+
const params: any[] = [];
|
|
384
|
+
|
|
385
|
+
if (options.projectId) {
|
|
386
|
+
sql += ' AND project_id = ?';
|
|
387
|
+
params.push(options.projectId);
|
|
388
|
+
}
|
|
389
|
+
if (options.status) {
|
|
390
|
+
sql += ' AND status = ?';
|
|
391
|
+
params.push(options.status);
|
|
392
|
+
}
|
|
393
|
+
if (options.severity) {
|
|
394
|
+
sql += ' AND severity = ?';
|
|
395
|
+
params.push(options.severity);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
sql += ' ORDER BY last_seen DESC LIMIT ?';
|
|
399
|
+
params.push(options.limit || 50);
|
|
400
|
+
|
|
401
|
+
const rows = this.db.prepare(sql).all(...params) as any[];
|
|
402
|
+
return rows.map((r) => this.mapIncidentRow(r));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
public insertIncident(incident: Incident): void {
|
|
406
|
+
this.db
|
|
407
|
+
.prepare(
|
|
408
|
+
`INSERT INTO incidents (
|
|
409
|
+
id, project_id, session_id, type, severity, message, source_file, source_line, source_col,
|
|
410
|
+
fingerprint, route, first_seen, last_seen, occurrences, status, stack, breadcrumbs,
|
|
411
|
+
network_failures, last_element, screenshot_path
|
|
412
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
413
|
+
)
|
|
414
|
+
.run(
|
|
415
|
+
incident.id,
|
|
416
|
+
incident.projectId,
|
|
417
|
+
incident.sessionId,
|
|
418
|
+
incident.type,
|
|
419
|
+
incident.severity,
|
|
420
|
+
incident.message,
|
|
421
|
+
incident.source.file,
|
|
422
|
+
incident.source.line,
|
|
423
|
+
incident.source.column || null,
|
|
424
|
+
incident.fingerprint,
|
|
425
|
+
incident.route,
|
|
426
|
+
incident.firstSeen,
|
|
427
|
+
incident.lastSeen,
|
|
428
|
+
incident.occurrences,
|
|
429
|
+
incident.status,
|
|
430
|
+
incident.stack || null,
|
|
431
|
+
JSON.stringify(incident.breadcrumbs || []),
|
|
432
|
+
JSON.stringify(incident.networkFailures || []),
|
|
433
|
+
incident.lastElement ? JSON.stringify(incident.lastElement) : null,
|
|
434
|
+
incident.screenshots?.error || null
|
|
435
|
+
);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
public updateIncidentOccurrence(
|
|
439
|
+
incidentId: string,
|
|
440
|
+
update: {
|
|
441
|
+
sessionId: string;
|
|
442
|
+
lastSeen: string;
|
|
443
|
+
occurrences: number;
|
|
444
|
+
route: string;
|
|
445
|
+
breadcrumbs: Breadcrumb[];
|
|
446
|
+
lastElement?: ElementSummary;
|
|
447
|
+
stack?: string;
|
|
448
|
+
}
|
|
449
|
+
): void {
|
|
450
|
+
this.db
|
|
451
|
+
.prepare(
|
|
452
|
+
`UPDATE incidents
|
|
453
|
+
SET session_id = ?, last_seen = ?, occurrences = ?, route = ?, breadcrumbs = ?, last_element = COALESCE(?, last_element), stack = COALESCE(?, stack)
|
|
454
|
+
WHERE id = ?`
|
|
455
|
+
)
|
|
456
|
+
.run(
|
|
457
|
+
update.sessionId,
|
|
458
|
+
update.lastSeen,
|
|
459
|
+
update.occurrences,
|
|
460
|
+
update.route,
|
|
461
|
+
JSON.stringify(update.breadcrumbs || []),
|
|
462
|
+
update.lastElement ? JSON.stringify(update.lastElement) : null,
|
|
463
|
+
update.stack || null,
|
|
464
|
+
incidentId
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
public updateIncidentStatus(id: string, status: IncidentStatus): void {
|
|
469
|
+
this.db.prepare('UPDATE incidents SET status = ? WHERE id = ?').run(status, id);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
public insertIncidentOccurrence(occurrence: IncidentOccurrence): void {
|
|
473
|
+
this.db
|
|
474
|
+
.prepare(
|
|
475
|
+
`INSERT INTO incident_occurrences (id, incident_id, session_id, timestamp, route, url, stack, breadcrumbs, last_element)
|
|
476
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
477
|
+
)
|
|
478
|
+
.run(
|
|
479
|
+
occurrence.id,
|
|
480
|
+
occurrence.incidentId,
|
|
481
|
+
occurrence.sessionId,
|
|
482
|
+
occurrence.timestamp,
|
|
483
|
+
occurrence.route,
|
|
484
|
+
occurrence.url,
|
|
485
|
+
occurrence.stack || null,
|
|
486
|
+
JSON.stringify(occurrence.breadcrumbs || []),
|
|
487
|
+
occurrence.lastElement ? JSON.stringify(occurrence.lastElement) : null
|
|
488
|
+
);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// --- VERIFICATIONS ---
|
|
492
|
+
public insertVerification(v: {
|
|
493
|
+
id: string;
|
|
494
|
+
incidentId: string;
|
|
495
|
+
status: IncidentStatus;
|
|
496
|
+
checks: any[];
|
|
497
|
+
beforeScreenshot?: string;
|
|
498
|
+
afterScreenshot?: string;
|
|
499
|
+
message?: string;
|
|
500
|
+
createdAt: string;
|
|
501
|
+
}): void {
|
|
502
|
+
this.db
|
|
503
|
+
.prepare(
|
|
504
|
+
`INSERT INTO verifications (id, incident_id, status, checks, before_screenshot, after_screenshot, message, created_at)
|
|
505
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
506
|
+
)
|
|
507
|
+
.run(
|
|
508
|
+
v.id,
|
|
509
|
+
v.incidentId,
|
|
510
|
+
v.status,
|
|
511
|
+
JSON.stringify(v.checks),
|
|
512
|
+
v.beforeScreenshot || null,
|
|
513
|
+
v.afterScreenshot || null,
|
|
514
|
+
v.message || null,
|
|
515
|
+
v.createdAt
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
public getLatestVerification(incidentId: string): VerificationResult | null {
|
|
520
|
+
const row = this.db.prepare('SELECT * FROM verifications WHERE incident_id = ? ORDER BY created_at DESC LIMIT 1').get(incidentId) as any;
|
|
521
|
+
if (!row) return null;
|
|
522
|
+
return {
|
|
523
|
+
incidentId: row.incident_id,
|
|
524
|
+
status: row.status as IncidentStatus,
|
|
525
|
+
checks: JSON.parse(row.checks || '[]'),
|
|
526
|
+
screenshots: {
|
|
527
|
+
before: row.before_screenshot || undefined,
|
|
528
|
+
after: row.after_screenshot || undefined,
|
|
529
|
+
},
|
|
530
|
+
timestamp: row.created_at,
|
|
531
|
+
message: row.message || undefined,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// --- VISUAL NOTES ---
|
|
536
|
+
public insertNote(note: VisualNote): void {
|
|
537
|
+
this.db
|
|
538
|
+
.prepare(
|
|
539
|
+
`INSERT INTO notes (
|
|
540
|
+
id, project_id, session_id, type, message, route, url,
|
|
541
|
+
viewport_json, scroll_json, target_json, element_context_json, region_json,
|
|
542
|
+
screenshot_path, incident_id, status, created_at, updated_at, resolved_at
|
|
543
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
544
|
+
)
|
|
545
|
+
.run(
|
|
546
|
+
note.id,
|
|
547
|
+
note.projectId,
|
|
548
|
+
note.sessionId,
|
|
549
|
+
note.type,
|
|
550
|
+
note.message,
|
|
551
|
+
note.route,
|
|
552
|
+
note.url,
|
|
553
|
+
JSON.stringify(note.viewport),
|
|
554
|
+
JSON.stringify(note.scroll),
|
|
555
|
+
note.target ? JSON.stringify(note.target) : null,
|
|
556
|
+
note.elementContext ? JSON.stringify(note.elementContext) : null,
|
|
557
|
+
note.region ? JSON.stringify(note.region) : null,
|
|
558
|
+
note.screenshots?.original || null,
|
|
559
|
+
note.incidentId || null,
|
|
560
|
+
note.status,
|
|
561
|
+
note.createdAt,
|
|
562
|
+
note.updatedAt,
|
|
563
|
+
note.resolvedAt || null
|
|
564
|
+
);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
public getNote(id: string): VisualNote | null {
|
|
568
|
+
const row = this.db.prepare('SELECT * FROM notes WHERE id = ?').get(id) as any;
|
|
569
|
+
if (!row) return null;
|
|
570
|
+
return this.mapNoteRow(row);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
public listNotes(options: { projectId?: string; status?: NoteStatus; limit?: number } = {}): VisualNote[] {
|
|
574
|
+
let sql = 'SELECT * FROM notes WHERE 1=1';
|
|
575
|
+
const params: any[] = [];
|
|
576
|
+
|
|
577
|
+
if (options.projectId) {
|
|
578
|
+
sql += ' AND project_id = ?';
|
|
579
|
+
params.push(options.projectId);
|
|
580
|
+
}
|
|
581
|
+
if (options.status) {
|
|
582
|
+
sql += ' AND status = ?';
|
|
583
|
+
params.push(options.status);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
sql += ' ORDER BY created_at DESC LIMIT ?';
|
|
587
|
+
params.push(options.limit || 50);
|
|
588
|
+
|
|
589
|
+
const rows = this.db.prepare(sql).all(...params) as any[];
|
|
590
|
+
return rows.map((r) => this.mapNoteRow(r));
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
public updateNoteStatus(id: string, status: NoteStatus): void {
|
|
594
|
+
const now = new Date().toISOString();
|
|
595
|
+
const resolvedAt = status === 'RESOLVED' ? now : null;
|
|
596
|
+
this.db
|
|
597
|
+
.prepare('UPDATE notes SET status = ?, updated_at = ?, resolved_at = COALESCE(?, resolved_at) WHERE id = ?')
|
|
598
|
+
.run(status, now, resolvedAt, id);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
public updateNote(id: string, updates: Partial<VisualNote>): void {
|
|
602
|
+
const now = new Date().toISOString();
|
|
603
|
+
const current = this.getNote(id);
|
|
604
|
+
if (!current) return;
|
|
605
|
+
|
|
606
|
+
this.db
|
|
607
|
+
.prepare(
|
|
608
|
+
`UPDATE notes
|
|
609
|
+
SET message = COALESCE(?, message), status = COALESCE(?, status),
|
|
610
|
+
screenshot_path = COALESCE(?, screenshot_path), updated_at = ?
|
|
611
|
+
WHERE id = ?`
|
|
612
|
+
)
|
|
613
|
+
.run(
|
|
614
|
+
updates.message || null,
|
|
615
|
+
updates.status || null,
|
|
616
|
+
updates.screenshots?.original || updates.screenshots?.after || null,
|
|
617
|
+
now,
|
|
618
|
+
id
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
public insertNoteVerification(v: NoteVerificationResult): void {
|
|
623
|
+
this.db
|
|
624
|
+
.prepare(
|
|
625
|
+
`INSERT INTO note_verifications (
|
|
626
|
+
id, note_id, status, checks, geometry_diff, before_screenshot, after_screenshot, message, created_at
|
|
627
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
628
|
+
)
|
|
629
|
+
.run(
|
|
630
|
+
`nver_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
631
|
+
v.noteId,
|
|
632
|
+
v.status,
|
|
633
|
+
JSON.stringify(v.checks),
|
|
634
|
+
v.geometryDiff ? JSON.stringify(v.geometryDiff) : null,
|
|
635
|
+
v.screenshots?.before || null,
|
|
636
|
+
v.screenshots?.after || null,
|
|
637
|
+
v.message || null,
|
|
638
|
+
v.timestamp
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
public getLatestNoteVerification(noteId: string): NoteVerificationResult | null {
|
|
643
|
+
const row = this.db.prepare('SELECT * FROM note_verifications WHERE note_id = ? ORDER BY created_at DESC LIMIT 1').get(noteId) as any;
|
|
644
|
+
if (!row) return null;
|
|
645
|
+
return {
|
|
646
|
+
noteId: row.note_id,
|
|
647
|
+
status: row.status as NoteStatus,
|
|
648
|
+
checks: JSON.parse(row.checks || '[]'),
|
|
649
|
+
geometryDiff: row.geometry_diff ? JSON.parse(row.geometry_diff) : undefined,
|
|
650
|
+
screenshots: {
|
|
651
|
+
before: row.before_screenshot || undefined,
|
|
652
|
+
after: row.after_screenshot || undefined,
|
|
653
|
+
},
|
|
654
|
+
timestamp: row.created_at,
|
|
655
|
+
message: row.message || undefined,
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
public clearAll(): void {
|
|
660
|
+
this.db.exec(`
|
|
661
|
+
DELETE FROM events;
|
|
662
|
+
DELETE FROM incident_occurrences;
|
|
663
|
+
DELETE FROM incidents;
|
|
664
|
+
DELETE FROM screenshots;
|
|
665
|
+
DELETE FROM verifications;
|
|
666
|
+
DELETE FROM notes;
|
|
667
|
+
DELETE FROM note_verifications;
|
|
668
|
+
DELETE FROM sessions;
|
|
669
|
+
`);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
private mapIncidentRow(row: any): Incident {
|
|
673
|
+
return {
|
|
674
|
+
id: row.id,
|
|
675
|
+
projectId: row.project_id,
|
|
676
|
+
sessionId: row.session_id,
|
|
677
|
+
type: row.type,
|
|
678
|
+
severity: row.severity as IncidentSeverity,
|
|
679
|
+
message: row.message,
|
|
680
|
+
source: {
|
|
681
|
+
file: row.source_file,
|
|
682
|
+
line: row.source_line,
|
|
683
|
+
column: row.source_col || undefined,
|
|
684
|
+
},
|
|
685
|
+
fingerprint: row.fingerprint,
|
|
686
|
+
route: row.route,
|
|
687
|
+
firstSeen: row.first_seen,
|
|
688
|
+
lastSeen: row.last_seen,
|
|
689
|
+
occurrences: row.occurrences,
|
|
690
|
+
status: row.status as IncidentStatus,
|
|
691
|
+
stack: row.stack || undefined,
|
|
692
|
+
breadcrumbs: JSON.parse(row.breadcrumbs || '[]'),
|
|
693
|
+
networkFailures: JSON.parse(row.network_failures || '[]'),
|
|
694
|
+
lastElement: row.last_element ? JSON.parse(row.last_element) : undefined,
|
|
695
|
+
screenshots: row.screenshot_path
|
|
696
|
+
? {
|
|
697
|
+
error: row.screenshot_path,
|
|
698
|
+
}
|
|
699
|
+
: undefined,
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
private mapNoteRow(row: any): VisualNote {
|
|
704
|
+
return {
|
|
705
|
+
id: row.id,
|
|
706
|
+
projectId: row.project_id,
|
|
707
|
+
sessionId: row.session_id,
|
|
708
|
+
type: row.type,
|
|
709
|
+
message: row.message,
|
|
710
|
+
route: row.route,
|
|
711
|
+
url: row.url,
|
|
712
|
+
viewport: JSON.parse(row.viewport_json || '{}'),
|
|
713
|
+
scroll: JSON.parse(row.scroll_json || '{}'),
|
|
714
|
+
target: row.target_json ? JSON.parse(row.target_json) : undefined,
|
|
715
|
+
elementContext: row.element_context_json ? JSON.parse(row.element_context_json) : undefined,
|
|
716
|
+
region: row.region_json ? JSON.parse(row.region_json) : undefined,
|
|
717
|
+
status: row.status as NoteStatus,
|
|
718
|
+
incidentId: row.incident_id || undefined,
|
|
719
|
+
screenshots: row.screenshot_path
|
|
720
|
+
? {
|
|
721
|
+
original: row.screenshot_path,
|
|
722
|
+
}
|
|
723
|
+
: undefined,
|
|
724
|
+
createdAt: row.created_at,
|
|
725
|
+
updatedAt: row.updated_at,
|
|
726
|
+
resolvedAt: row.resolved_at || undefined,
|
|
727
|
+
};
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
public close(): void {
|
|
731
|
+
this.db.close();
|
|
732
|
+
}
|
|
733
|
+
}
|