browsertrack 0.2.1 → 0.2.3
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/AGENTS.md +9 -6
- package/README.md +2 -0
- package/dist/{chunk-464D4U2U.js → chunk-5NR5K3ER.js} +299 -44
- package/dist/chunk-5NR5K3ER.js.map +1 -0
- package/dist/{chunk-3HOXPTM2.js → chunk-G5CIZSQM.js} +808 -53
- package/dist/chunk-G5CIZSQM.js.map +1 -0
- package/dist/{chunk-6VA7GBAO.js → chunk-ONPW7AYL.js} +144 -12
- package/dist/chunk-ONPW7AYL.js.map +1 -0
- package/dist/{chunk-INXDWPJW.js → chunk-PG4JJDCV.js} +353 -119
- package/dist/chunk-PG4JJDCV.js.map +1 -0
- package/dist/cli/index.js +1058 -546
- package/dist/cli/index.js.map +1 -1
- package/dist/client/index.cjs +449 -128
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +2 -2
- package/dist/client.iife.js +121 -15
- package/dist/core/index.d.ts +32 -2
- package/dist/core/index.js +11 -1
- package/dist/daemon/index.d.ts +2 -2
- package/dist/daemon/index.js +6 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +16 -7
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +7 -4
- package/dist/{server-DiVmTrIR.d.ts → server-BRG-RQQP.d.ts} +10 -1
- package/docs/cli.md +4 -1
- package/docs/getting-started.md +60 -6
- package/docs/mcp-reference.md +42 -0
- package/package.json +1 -1
- package/packages/cli/src/index.ts +247 -151
- package/packages/client/src/interceptors/navigation.ts +38 -26
- package/packages/client/src/interceptors/network.ts +22 -17
- package/packages/client/src/notes/inspector.ts +289 -65
- package/packages/client/src/source/resolver.ts +9 -3
- package/packages/client/src/transport/websocket.ts +23 -18
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/safety.ts +86 -0
- package/packages/core/src/selector.ts +110 -15
- package/packages/daemon/src/server/daemon.ts +7 -1
- package/packages/daemon/src/server/http.ts +125 -5
- package/packages/daemon/src/server/ws.ts +33 -29
- package/packages/daemon/src/storage/db.ts +57 -35
- package/packages/mcp/src/handlers.ts +121 -45
- package/packages/mcp/src/server.ts +221 -3
- package/test/core/safety.test.ts +106 -0
- package/test/core/selector.test.ts +133 -0
- package/test/daemon/storage.test.ts +36 -0
- package/test/e2e/daemon-mcp-e2e.test.ts +10 -0
- package/test/mcp/auto-start.test.ts +87 -0
- package/dist/chunk-3HOXPTM2.js.map +0 -1
- package/dist/chunk-464D4U2U.js.map +0 -1
- package/dist/chunk-6VA7GBAO.js.map +0 -1
- package/dist/chunk-7OCOQGDN.js +0 -635
- package/dist/chunk-7OCOQGDN.js.map +0 -1
- package/dist/chunk-INXDWPJW.js.map +0 -1
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computeFingerprint,
|
|
3
|
+
extractSourceFromStack,
|
|
4
|
+
normalizeSourceFile,
|
|
5
|
+
redactSensitiveData,
|
|
6
|
+
safeJsonParse,
|
|
7
|
+
safeJsonStringify
|
|
8
|
+
} from "./chunk-ONPW7AYL.js";
|
|
9
|
+
|
|
1
10
|
// packages/daemon/src/config.ts
|
|
2
11
|
import os from "os";
|
|
3
12
|
import path from "path";
|
|
@@ -29,6 +38,7 @@ var StorageDB = class {
|
|
|
29
38
|
this.db = new Database(dbPath);
|
|
30
39
|
this.db.pragma("journal_mode = WAL");
|
|
31
40
|
this.db.pragma("synchronous = NORMAL");
|
|
41
|
+
this.db.pragma("busy_timeout = 5000");
|
|
32
42
|
this.initTables();
|
|
33
43
|
}
|
|
34
44
|
initTables() {
|
|
@@ -160,18 +170,36 @@ var StorageDB = class {
|
|
|
160
170
|
CREATE INDEX IF NOT EXISTS idx_incidents_project ON incidents(project_id, status);
|
|
161
171
|
CREATE INDEX IF NOT EXISTS idx_incidents_fp ON incidents(fingerprint);
|
|
162
172
|
CREATE INDEX IF NOT EXISTS idx_notes_project ON notes(project_id, status);
|
|
163
|
-
CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);
|
|
164
173
|
`);
|
|
165
174
|
try {
|
|
166
|
-
this.db.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
const columns = this.db.prepare("PRAGMA table_info(notes)").all().map(
|
|
176
|
+
(c) => c.name
|
|
177
|
+
);
|
|
178
|
+
if (!columns.includes("scenario_id")) {
|
|
179
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
|
|
180
|
+
}
|
|
181
|
+
if (!columns.includes("step_number")) {
|
|
182
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
|
|
183
|
+
}
|
|
184
|
+
if (!columns.includes("scenario_title")) {
|
|
185
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
|
|
186
|
+
}
|
|
171
187
|
} catch {
|
|
188
|
+
try {
|
|
189
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN scenario_id TEXT;");
|
|
190
|
+
} catch {
|
|
191
|
+
}
|
|
192
|
+
try {
|
|
193
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN step_number INTEGER;");
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
this.db.exec("ALTER TABLE notes ADD COLUMN scenario_title TEXT;");
|
|
198
|
+
} catch {
|
|
199
|
+
}
|
|
172
200
|
}
|
|
173
201
|
try {
|
|
174
|
-
this.db.exec("
|
|
202
|
+
this.db.exec("CREATE INDEX IF NOT EXISTS idx_notes_scenario ON notes(scenario_id, step_number);");
|
|
175
203
|
} catch {
|
|
176
204
|
}
|
|
177
205
|
}
|
|
@@ -336,7 +364,7 @@ var StorageDB = class {
|
|
|
336
364
|
id: r.id,
|
|
337
365
|
sessionId: r.session_id,
|
|
338
366
|
eventType: r.event_type,
|
|
339
|
-
payload:
|
|
367
|
+
payload: safeJsonParse(r.payload, {}),
|
|
340
368
|
timestamp: r.timestamp,
|
|
341
369
|
route: r.route,
|
|
342
370
|
url: r.url
|
|
@@ -397,9 +425,9 @@ var StorageDB = class {
|
|
|
397
425
|
incident.occurrences,
|
|
398
426
|
incident.status,
|
|
399
427
|
incident.stack || null,
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
incident.lastElement ?
|
|
428
|
+
safeJsonStringify(incident.breadcrumbs || []),
|
|
429
|
+
safeJsonStringify(incident.networkFailures || []),
|
|
430
|
+
incident.lastElement ? safeJsonStringify(incident.lastElement) : null,
|
|
403
431
|
incident.screenshots?.error || null
|
|
404
432
|
);
|
|
405
433
|
}
|
|
@@ -413,8 +441,8 @@ var StorageDB = class {
|
|
|
413
441
|
update.lastSeen,
|
|
414
442
|
update.occurrences,
|
|
415
443
|
update.route,
|
|
416
|
-
|
|
417
|
-
update.lastElement ?
|
|
444
|
+
safeJsonStringify(update.breadcrumbs || []),
|
|
445
|
+
update.lastElement ? safeJsonStringify(update.lastElement) : null,
|
|
418
446
|
update.stack || null,
|
|
419
447
|
incidentId
|
|
420
448
|
);
|
|
@@ -434,8 +462,8 @@ var StorageDB = class {
|
|
|
434
462
|
occurrence.route,
|
|
435
463
|
occurrence.url,
|
|
436
464
|
occurrence.stack || null,
|
|
437
|
-
|
|
438
|
-
occurrence.lastElement ?
|
|
465
|
+
safeJsonStringify(occurrence.breadcrumbs || []),
|
|
466
|
+
occurrence.lastElement ? safeJsonStringify(occurrence.lastElement) : null
|
|
439
467
|
);
|
|
440
468
|
}
|
|
441
469
|
// --- VERIFICATIONS ---
|
|
@@ -447,7 +475,7 @@ var StorageDB = class {
|
|
|
447
475
|
v.id,
|
|
448
476
|
v.incidentId,
|
|
449
477
|
v.status,
|
|
450
|
-
|
|
478
|
+
safeJsonStringify(v.checks),
|
|
451
479
|
v.beforeScreenshot || null,
|
|
452
480
|
v.afterScreenshot || null,
|
|
453
481
|
v.message || null,
|
|
@@ -460,7 +488,7 @@ var StorageDB = class {
|
|
|
460
488
|
return {
|
|
461
489
|
incidentId: row.incident_id,
|
|
462
490
|
status: row.status,
|
|
463
|
-
checks:
|
|
491
|
+
checks: safeJsonParse(row.checks, []),
|
|
464
492
|
screenshots: {
|
|
465
493
|
before: row.before_screenshot || void 0,
|
|
466
494
|
after: row.after_screenshot || void 0
|
|
@@ -485,11 +513,11 @@ var StorageDB = class {
|
|
|
485
513
|
note.message,
|
|
486
514
|
note.route,
|
|
487
515
|
note.url,
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
note.target ?
|
|
491
|
-
note.elementContext ?
|
|
492
|
-
note.region ?
|
|
516
|
+
safeJsonStringify(note.viewport),
|
|
517
|
+
safeJsonStringify(note.scroll),
|
|
518
|
+
note.target ? safeJsonStringify(note.target) : null,
|
|
519
|
+
note.elementContext ? safeJsonStringify(note.elementContext) : null,
|
|
520
|
+
note.region ? safeJsonStringify(note.region) : null,
|
|
493
521
|
note.screenshots?.original || null,
|
|
494
522
|
note.incidentId || null,
|
|
495
523
|
note.scenarioId || null,
|
|
@@ -630,8 +658,8 @@ var StorageDB = class {
|
|
|
630
658
|
`nver_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
|
|
631
659
|
v.noteId,
|
|
632
660
|
v.status,
|
|
633
|
-
|
|
634
|
-
v.geometryDiff ?
|
|
661
|
+
safeJsonStringify(v.checks),
|
|
662
|
+
v.geometryDiff ? safeJsonStringify(v.geometryDiff) : null,
|
|
635
663
|
v.screenshots?.before || null,
|
|
636
664
|
v.screenshots?.after || null,
|
|
637
665
|
v.message || null,
|
|
@@ -644,8 +672,8 @@ var StorageDB = class {
|
|
|
644
672
|
return {
|
|
645
673
|
noteId: row.note_id,
|
|
646
674
|
status: row.status,
|
|
647
|
-
checks:
|
|
648
|
-
geometryDiff:
|
|
675
|
+
checks: safeJsonParse(row.checks, []),
|
|
676
|
+
geometryDiff: safeJsonParse(row.geometry_diff, void 0),
|
|
649
677
|
screenshots: {
|
|
650
678
|
before: row.before_screenshot || void 0,
|
|
651
679
|
after: row.after_screenshot || void 0
|
|
@@ -686,9 +714,9 @@ var StorageDB = class {
|
|
|
686
714
|
occurrences: row.occurrences,
|
|
687
715
|
status: row.status,
|
|
688
716
|
stack: row.stack || void 0,
|
|
689
|
-
breadcrumbs:
|
|
690
|
-
networkFailures:
|
|
691
|
-
lastElement:
|
|
717
|
+
breadcrumbs: safeJsonParse(row.breadcrumbs, []),
|
|
718
|
+
networkFailures: safeJsonParse(row.network_failures, []),
|
|
719
|
+
lastElement: safeJsonParse(row.last_element, void 0),
|
|
692
720
|
screenshots: row.screenshot_path ? {
|
|
693
721
|
error: row.screenshot_path
|
|
694
722
|
} : void 0
|
|
@@ -703,11 +731,11 @@ var StorageDB = class {
|
|
|
703
731
|
message: row.message,
|
|
704
732
|
route: row.route,
|
|
705
733
|
url: row.url,
|
|
706
|
-
viewport:
|
|
707
|
-
scroll:
|
|
708
|
-
target:
|
|
709
|
-
elementContext:
|
|
710
|
-
region:
|
|
734
|
+
viewport: safeJsonParse(row.viewport_json, { width: 0, height: 0, devicePixelRatio: 1 }),
|
|
735
|
+
scroll: safeJsonParse(row.scroll_json, { scrollX: 0, scrollY: 0 }),
|
|
736
|
+
target: safeJsonParse(row.target_json, void 0),
|
|
737
|
+
elementContext: safeJsonParse(row.element_context_json, void 0),
|
|
738
|
+
region: safeJsonParse(row.region_json, void 0),
|
|
711
739
|
status: row.status,
|
|
712
740
|
incidentId: row.incident_id || void 0,
|
|
713
741
|
scenarioId: row.scenario_id || void 0,
|
|
@@ -884,8 +912,167 @@ var SessionManager = class {
|
|
|
884
912
|
}
|
|
885
913
|
};
|
|
886
914
|
|
|
887
|
-
// packages/daemon/src/
|
|
915
|
+
// packages/daemon/src/incidents/engine.ts
|
|
888
916
|
import crypto from "crypto";
|
|
917
|
+
var IncidentEngine = class {
|
|
918
|
+
db;
|
|
919
|
+
screenshotStore;
|
|
920
|
+
constructor(db, screenshotStore) {
|
|
921
|
+
this.db = db;
|
|
922
|
+
this.screenshotStore = screenshotStore;
|
|
923
|
+
}
|
|
924
|
+
processClientEvent(message) {
|
|
925
|
+
const session = this.db.getSession(message.sessionId);
|
|
926
|
+
const projectId = session?.projectId || "default";
|
|
927
|
+
const sanitizedBreadcrumbs = (message.breadcrumbs || []).map((b) => redactSensitiveData(b));
|
|
928
|
+
const sanitizedLastElement = message.lastElement ? redactSensitiveData(message.lastElement) : void 0;
|
|
929
|
+
const networkFailures = sanitizedBreadcrumbs.filter((b) => (b.type === "fetch" || b.type === "xhr") && b.level === "error" && b.data).map((b) => ({
|
|
930
|
+
url: b.message.split(" ")[1] || "",
|
|
931
|
+
method: b.message.split(" ")[0] || "GET",
|
|
932
|
+
status: b.data?.status,
|
|
933
|
+
durationMs: b.data?.durationMs || 0,
|
|
934
|
+
error: b.data?.error,
|
|
935
|
+
aborted: b.data?.aborted,
|
|
936
|
+
timestamp: b.timestamp
|
|
937
|
+
}));
|
|
938
|
+
if (message.eventType === "runtime_error" || message.eventType === "unhandled_rejection") {
|
|
939
|
+
const payload = message.payload;
|
|
940
|
+
return this.handleErrorEvent({
|
|
941
|
+
projectId,
|
|
942
|
+
sessionId: message.sessionId,
|
|
943
|
+
type: payload.errorType || "runtime_exception",
|
|
944
|
+
severity: "error",
|
|
945
|
+
message: payload.message || "Unknown runtime error",
|
|
946
|
+
stack: payload.stack,
|
|
947
|
+
sourceFile: payload.filename,
|
|
948
|
+
line: payload.lineno,
|
|
949
|
+
column: payload.colno,
|
|
950
|
+
route: message.route || "/",
|
|
951
|
+
url: message.url,
|
|
952
|
+
breadcrumbs: sanitizedBreadcrumbs,
|
|
953
|
+
networkFailures,
|
|
954
|
+
lastElement: sanitizedLastElement,
|
|
955
|
+
screenshotDataUrl: message.screenshot
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
if (message.eventType === "console") {
|
|
959
|
+
const payload = message.payload;
|
|
960
|
+
if (payload.level === "error") {
|
|
961
|
+
return this.handleErrorEvent({
|
|
962
|
+
projectId,
|
|
963
|
+
sessionId: message.sessionId,
|
|
964
|
+
type: "console_error",
|
|
965
|
+
severity: "error",
|
|
966
|
+
message: payload.message,
|
|
967
|
+
stack: payload.stack,
|
|
968
|
+
route: message.route || "/",
|
|
969
|
+
url: message.url,
|
|
970
|
+
breadcrumbs: sanitizedBreadcrumbs,
|
|
971
|
+
networkFailures,
|
|
972
|
+
lastElement: sanitizedLastElement,
|
|
973
|
+
screenshotDataUrl: message.screenshot
|
|
974
|
+
});
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
return null;
|
|
978
|
+
}
|
|
979
|
+
handleErrorEvent(input) {
|
|
980
|
+
const extracted = (!input.sourceFile || !input.line) && input.stack ? extractSourceFromStack(input.stack) : null;
|
|
981
|
+
const rawSource = input.sourceFile || extracted?.file || "unknown_source";
|
|
982
|
+
const sourceFile = normalizeSourceFile(rawSource);
|
|
983
|
+
const line = input.line || extracted?.line || 0;
|
|
984
|
+
const column = input.column || extracted?.column;
|
|
985
|
+
const fingerprint = computeFingerprint({
|
|
986
|
+
type: input.type,
|
|
987
|
+
message: input.message,
|
|
988
|
+
sourceFile,
|
|
989
|
+
line,
|
|
990
|
+
column,
|
|
991
|
+
stack: input.stack
|
|
992
|
+
});
|
|
993
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
994
|
+
const existing = this.db.findIncidentByFingerprint(fingerprint);
|
|
995
|
+
if (existing) {
|
|
996
|
+
const updatedOccurrences = existing.occurrences + 1;
|
|
997
|
+
this.db.updateIncidentOccurrence(existing.id, {
|
|
998
|
+
sessionId: input.sessionId,
|
|
999
|
+
lastSeen: now,
|
|
1000
|
+
occurrences: updatedOccurrences,
|
|
1001
|
+
route: input.route,
|
|
1002
|
+
breadcrumbs: input.breadcrumbs,
|
|
1003
|
+
lastElement: input.lastElement,
|
|
1004
|
+
stack: input.stack
|
|
1005
|
+
});
|
|
1006
|
+
const occurrenceId = `occ_${crypto.randomUUID().slice(0, 8)}`;
|
|
1007
|
+
this.db.insertIncidentOccurrence({
|
|
1008
|
+
id: occurrenceId,
|
|
1009
|
+
incidentId: existing.id,
|
|
1010
|
+
sessionId: input.sessionId,
|
|
1011
|
+
timestamp: now,
|
|
1012
|
+
route: input.route,
|
|
1013
|
+
url: input.url,
|
|
1014
|
+
stack: input.stack,
|
|
1015
|
+
breadcrumbs: input.breadcrumbs,
|
|
1016
|
+
lastElement: input.lastElement
|
|
1017
|
+
});
|
|
1018
|
+
return {
|
|
1019
|
+
...existing,
|
|
1020
|
+
occurrences: updatedOccurrences,
|
|
1021
|
+
lastSeen: now,
|
|
1022
|
+
breadcrumbs: input.breadcrumbs,
|
|
1023
|
+
lastElement: input.lastElement || existing.lastElement
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
const incidentId = `inc_${crypto.randomUUID().slice(0, 8)}`;
|
|
1027
|
+
let screenshotPath;
|
|
1028
|
+
if (input.screenshotDataUrl) {
|
|
1029
|
+
const saved = this.screenshotStore.saveScreenshot(input.projectId, incidentId, "error", input.screenshotDataUrl);
|
|
1030
|
+
if (saved) {
|
|
1031
|
+
screenshotPath = saved.filePath;
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
const newIncident = {
|
|
1035
|
+
id: incidentId,
|
|
1036
|
+
projectId: input.projectId,
|
|
1037
|
+
sessionId: input.sessionId,
|
|
1038
|
+
type: input.type,
|
|
1039
|
+
severity: input.severity,
|
|
1040
|
+
message: input.message,
|
|
1041
|
+
source: {
|
|
1042
|
+
file: sourceFile,
|
|
1043
|
+
line,
|
|
1044
|
+
column
|
|
1045
|
+
},
|
|
1046
|
+
fingerprint,
|
|
1047
|
+
route: input.route,
|
|
1048
|
+
firstSeen: now,
|
|
1049
|
+
lastSeen: now,
|
|
1050
|
+
occurrences: 1,
|
|
1051
|
+
status: "OPEN",
|
|
1052
|
+
stack: input.stack,
|
|
1053
|
+
breadcrumbs: input.breadcrumbs,
|
|
1054
|
+
networkFailures: input.networkFailures,
|
|
1055
|
+
lastElement: input.lastElement,
|
|
1056
|
+
screenshots: screenshotPath ? { error: screenshotPath } : void 0
|
|
1057
|
+
};
|
|
1058
|
+
this.db.insertIncident(newIncident);
|
|
1059
|
+
this.db.insertIncidentOccurrence({
|
|
1060
|
+
id: `occ_${crypto.randomUUID().slice(0, 8)}`,
|
|
1061
|
+
incidentId,
|
|
1062
|
+
sessionId: input.sessionId,
|
|
1063
|
+
timestamp: now,
|
|
1064
|
+
route: input.route,
|
|
1065
|
+
url: input.url,
|
|
1066
|
+
stack: input.stack,
|
|
1067
|
+
breadcrumbs: input.breadcrumbs,
|
|
1068
|
+
lastElement: input.lastElement
|
|
1069
|
+
});
|
|
1070
|
+
return newIncident;
|
|
1071
|
+
}
|
|
1072
|
+
};
|
|
1073
|
+
|
|
1074
|
+
// packages/daemon/src/notes/engine.ts
|
|
1075
|
+
import crypto2 from "crypto";
|
|
889
1076
|
import path4 from "path";
|
|
890
1077
|
import fs3 from "fs";
|
|
891
1078
|
var NotesEngine = class {
|
|
@@ -898,7 +1085,7 @@ var NotesEngine = class {
|
|
|
898
1085
|
createNoteFromClient(payload) {
|
|
899
1086
|
const session = this.db.getSession(payload.sessionId);
|
|
900
1087
|
const projectId = session?.projectId || "default";
|
|
901
|
-
const noteId = `note_${
|
|
1088
|
+
const noteId = `note_${crypto2.randomUUID().slice(0, 8)}`;
|
|
902
1089
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
903
1090
|
let screenshotPath;
|
|
904
1091
|
if (payload.screenshot) {
|
|
@@ -954,7 +1141,7 @@ var NotesEngine = class {
|
|
|
954
1141
|
};
|
|
955
1142
|
|
|
956
1143
|
// packages/daemon/src/notes/verification.ts
|
|
957
|
-
import
|
|
1144
|
+
import crypto3 from "crypto";
|
|
958
1145
|
var NoteVerificationEngine = class {
|
|
959
1146
|
db;
|
|
960
1147
|
sessionManager;
|
|
@@ -992,14 +1179,14 @@ var NoteVerificationEngine = class {
|
|
|
992
1179
|
}
|
|
993
1180
|
this.db.updateNoteStatus(noteId, "VERIFYING");
|
|
994
1181
|
const pageStateCmd = await this.sessionManager.sendCommand(session.id, {
|
|
995
|
-
id: `cmd_${
|
|
1182
|
+
id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
|
|
996
1183
|
type: "get_page_state"
|
|
997
1184
|
});
|
|
998
1185
|
const currentRoute = pageStateCmd.result?.route || "/";
|
|
999
1186
|
const isRouteMatch = currentRoute === note.route || currentRoute.startsWith(note.route);
|
|
1000
1187
|
if (!isRouteMatch && note.route) {
|
|
1001
1188
|
await this.sessionManager.sendCommand(session.id, {
|
|
1002
|
-
id: `cmd_${
|
|
1189
|
+
id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
|
|
1003
1190
|
type: "navigate",
|
|
1004
1191
|
params: { url: note.route }
|
|
1005
1192
|
});
|
|
@@ -1018,7 +1205,7 @@ var NoteVerificationEngine = class {
|
|
|
1018
1205
|
let overflowResult;
|
|
1019
1206
|
if (note.type === "element" && targetSelector) {
|
|
1020
1207
|
const queryCmd = await this.sessionManager.sendCommand(session.id, {
|
|
1021
|
-
id: `cmd_${
|
|
1208
|
+
id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
|
|
1022
1209
|
type: "query_element",
|
|
1023
1210
|
params: { selector: targetSelector }
|
|
1024
1211
|
});
|
|
@@ -1036,7 +1223,7 @@ var NoteVerificationEngine = class {
|
|
|
1036
1223
|
details: elementVisible ? `Element ${targetSelector} is visible` : `Element ${targetSelector} is hidden`
|
|
1037
1224
|
});
|
|
1038
1225
|
const overflowCmd = await this.sessionManager.sendCommand(session.id, {
|
|
1039
|
-
id: `cmd_${
|
|
1226
|
+
id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
|
|
1040
1227
|
type: "check_overflow",
|
|
1041
1228
|
params: { selector: targetSelector }
|
|
1042
1229
|
});
|
|
@@ -1053,7 +1240,7 @@ var NoteVerificationEngine = class {
|
|
|
1053
1240
|
}
|
|
1054
1241
|
let afterScreenshotPath;
|
|
1055
1242
|
const captureCmd = await this.sessionManager.sendCommand(session.id, {
|
|
1056
|
-
id: `cmd_${
|
|
1243
|
+
id: `cmd_${crypto3.randomUUID().slice(0, 8)}`,
|
|
1057
1244
|
type: "capture_element",
|
|
1058
1245
|
params: { selector: targetSelector }
|
|
1059
1246
|
});
|
|
@@ -1101,7 +1288,7 @@ var NoteVerificationEngine = class {
|
|
|
1101
1288
|
};
|
|
1102
1289
|
|
|
1103
1290
|
// packages/daemon/src/verification/engine.ts
|
|
1104
|
-
import
|
|
1291
|
+
import crypto4 from "crypto";
|
|
1105
1292
|
var VerificationEngine = class {
|
|
1106
1293
|
db;
|
|
1107
1294
|
sessionManager;
|
|
@@ -1143,13 +1330,13 @@ var VerificationEngine = class {
|
|
|
1143
1330
|
const observationMs = recipe?.observationWindowMs || 2e3;
|
|
1144
1331
|
if (recipe?.route && recipe.route !== incident.route) {
|
|
1145
1332
|
await this.sessionManager.sendCommand(session.id, {
|
|
1146
|
-
id: `cmd_${
|
|
1333
|
+
id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1147
1334
|
type: "navigate",
|
|
1148
1335
|
params: { url: recipe.route }
|
|
1149
1336
|
});
|
|
1150
1337
|
} else {
|
|
1151
1338
|
await this.sessionManager.sendCommand(session.id, {
|
|
1152
|
-
id: `cmd_${
|
|
1339
|
+
id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1153
1340
|
type: "reload",
|
|
1154
1341
|
params: { force: true }
|
|
1155
1342
|
});
|
|
@@ -1172,7 +1359,7 @@ var VerificationEngine = class {
|
|
|
1172
1359
|
const targetSelector = recipe?.targetSelector || incident.lastElement?.selector;
|
|
1173
1360
|
if (targetSelector) {
|
|
1174
1361
|
const captureCmd = await this.sessionManager.sendCommand(session.id, {
|
|
1175
|
-
id: `cmd_${
|
|
1362
|
+
id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1176
1363
|
type: "capture_element",
|
|
1177
1364
|
params: { selector: targetSelector }
|
|
1178
1365
|
});
|
|
@@ -1220,7 +1407,7 @@ var VerificationEngine = class {
|
|
|
1220
1407
|
return { type: probe.type, passed: false, details: "Missing selector in probe" };
|
|
1221
1408
|
}
|
|
1222
1409
|
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
1223
|
-
id: `cmd_${
|
|
1410
|
+
id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1224
1411
|
type: "query_element",
|
|
1225
1412
|
params: { selector: probe.selector }
|
|
1226
1413
|
});
|
|
@@ -1236,7 +1423,7 @@ var VerificationEngine = class {
|
|
|
1236
1423
|
return { type: probe.type, passed: false, details: "Missing selector in probe" };
|
|
1237
1424
|
}
|
|
1238
1425
|
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
1239
|
-
id: `cmd_${
|
|
1426
|
+
id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1240
1427
|
type: "query_element",
|
|
1241
1428
|
params: { selector: probe.selector }
|
|
1242
1429
|
});
|
|
@@ -1252,7 +1439,7 @@ var VerificationEngine = class {
|
|
|
1252
1439
|
return { type: probe.type, passed: false, details: "Missing selector or text in probe" };
|
|
1253
1440
|
}
|
|
1254
1441
|
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
1255
|
-
id: `cmd_${
|
|
1442
|
+
id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1256
1443
|
type: "query_element",
|
|
1257
1444
|
params: { selector: probe.selector }
|
|
1258
1445
|
});
|
|
@@ -1269,7 +1456,7 @@ var VerificationEngine = class {
|
|
|
1269
1456
|
return { type: probe.type, passed: false, details: "Missing route in probe" };
|
|
1270
1457
|
}
|
|
1271
1458
|
const res = await this.sessionManager.sendCommand(sessionId, {
|
|
1272
|
-
id: `cmd_${
|
|
1459
|
+
id: `cmd_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1273
1460
|
type: "get_page_state"
|
|
1274
1461
|
});
|
|
1275
1462
|
const currentRoute = res.result?.route || "";
|
|
@@ -1298,7 +1485,7 @@ var VerificationEngine = class {
|
|
|
1298
1485
|
recordVerification(result, incident) {
|
|
1299
1486
|
this.db.updateIncidentStatus(incident.id, result.status);
|
|
1300
1487
|
this.db.insertVerification({
|
|
1301
|
-
id: `ver_${
|
|
1488
|
+
id: `ver_${crypto4.randomUUID().slice(0, 8)}`,
|
|
1302
1489
|
incidentId: incident.id,
|
|
1303
1490
|
status: result.status,
|
|
1304
1491
|
checks: result.checks,
|
|
@@ -1310,13 +1497,581 @@ var VerificationEngine = class {
|
|
|
1310
1497
|
}
|
|
1311
1498
|
};
|
|
1312
1499
|
|
|
1500
|
+
// packages/daemon/src/server/http.ts
|
|
1501
|
+
import fs4 from "fs";
|
|
1502
|
+
import path5 from "path";
|
|
1503
|
+
import { fileURLToPath } from "url";
|
|
1504
|
+
function readJsonBody(req, res, maxSizeBytes = 10 * 1024 * 1024) {
|
|
1505
|
+
return new Promise((resolve, reject) => {
|
|
1506
|
+
let body = "";
|
|
1507
|
+
let isTooLarge = false;
|
|
1508
|
+
req.on("data", (chunk) => {
|
|
1509
|
+
if (isTooLarge) return;
|
|
1510
|
+
body += chunk;
|
|
1511
|
+
if (body.length > maxSizeBytes) {
|
|
1512
|
+
isTooLarge = true;
|
|
1513
|
+
if (!res.headersSent) {
|
|
1514
|
+
res.writeHead(413, { "Content-Type": "application/json" });
|
|
1515
|
+
res.end(JSON.stringify({ ok: false, error: "Payload too large" }));
|
|
1516
|
+
}
|
|
1517
|
+
req.destroy();
|
|
1518
|
+
reject(new Error("Payload too large"));
|
|
1519
|
+
}
|
|
1520
|
+
});
|
|
1521
|
+
req.on("end", () => {
|
|
1522
|
+
if (isTooLarge) return;
|
|
1523
|
+
try {
|
|
1524
|
+
const parsed = body ? JSON.parse(body) : {};
|
|
1525
|
+
resolve(parsed);
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
if (!res.headersSent) {
|
|
1528
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1529
|
+
res.end(JSON.stringify({ ok: false, error: "Malformed JSON payload" }));
|
|
1530
|
+
}
|
|
1531
|
+
reject(err);
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
req.on("error", (err) => {
|
|
1535
|
+
reject(err);
|
|
1536
|
+
});
|
|
1537
|
+
});
|
|
1538
|
+
}
|
|
1539
|
+
function createHttpHandler(db, sessionManager, baseScreenshotsDir, verificationEngine, noteVerificationEngine) {
|
|
1540
|
+
return async (req, res) => {
|
|
1541
|
+
try {
|
|
1542
|
+
const parsedUrl = new URL(req.url || "/", `http://${req.headers.host || "127.0.0.1"}`);
|
|
1543
|
+
const pathname = parsedUrl.pathname;
|
|
1544
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
1545
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
1546
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
1547
|
+
if (req.method === "OPTIONS") {
|
|
1548
|
+
res.writeHead(204);
|
|
1549
|
+
res.end();
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
if (pathname === "/health" || pathname === "/") {
|
|
1553
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1554
|
+
res.end(
|
|
1555
|
+
JSON.stringify({
|
|
1556
|
+
status: "ok",
|
|
1557
|
+
name: "browsertrack",
|
|
1558
|
+
version: "0.1.0",
|
|
1559
|
+
activeSessions: sessionManager.getActiveCount(),
|
|
1560
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1561
|
+
})
|
|
1562
|
+
);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
if (pathname === "/client.js" || pathname === "/browserdiag.js") {
|
|
1566
|
+
let scriptContent = "";
|
|
1567
|
+
try {
|
|
1568
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
1569
|
+
const __dirname = path5.dirname(__filename);
|
|
1570
|
+
const candidatePaths = [
|
|
1571
|
+
path5.resolve(__dirname, "../client.iife.js"),
|
|
1572
|
+
path5.resolve(__dirname, "../../dist/client.iife.js"),
|
|
1573
|
+
path5.resolve(__dirname, "../../../dist/client.iife.js")
|
|
1574
|
+
];
|
|
1575
|
+
for (const p of candidatePaths) {
|
|
1576
|
+
if (fs4.existsSync(p)) {
|
|
1577
|
+
scriptContent = fs4.readFileSync(p, "utf-8");
|
|
1578
|
+
break;
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
} catch {
|
|
1582
|
+
}
|
|
1583
|
+
if (!scriptContent) {
|
|
1584
|
+
scriptContent = `console.warn("[BrowserTrack] Standalone client bundle not built yet. Run 'npm run build'.");`;
|
|
1585
|
+
}
|
|
1586
|
+
res.writeHead(200, {
|
|
1587
|
+
"Content-Type": "application/javascript; charset=utf-8",
|
|
1588
|
+
"Cache-Control": "no-cache"
|
|
1589
|
+
});
|
|
1590
|
+
res.end(scriptContent);
|
|
1591
|
+
return;
|
|
1592
|
+
}
|
|
1593
|
+
if (pathname === "/api/projects") {
|
|
1594
|
+
const projects = db.listProjects();
|
|
1595
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1596
|
+
res.end(JSON.stringify({ ok: true, projects }));
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
if (pathname === "/api/sessions") {
|
|
1600
|
+
const projectId = parsedUrl.searchParams.get("project") || void 0;
|
|
1601
|
+
const activeOnly = parsedUrl.searchParams.get("active") === "true";
|
|
1602
|
+
const sessions = db.listSessions(projectId, activeOnly);
|
|
1603
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1604
|
+
res.end(JSON.stringify({ ok: true, sessions }));
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (pathname === "/api/incidents") {
|
|
1608
|
+
const projectId = parsedUrl.searchParams.get("project") || void 0;
|
|
1609
|
+
const status = parsedUrl.searchParams.get("status") || void 0;
|
|
1610
|
+
const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
|
|
1611
|
+
const incidents = db.listIncidents({ projectId, status, limit });
|
|
1612
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1613
|
+
res.end(JSON.stringify({ ok: true, incidents }));
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
if (pathname.startsWith("/api/incidents/")) {
|
|
1617
|
+
const incidentId = pathname.replace("/api/incidents/", "");
|
|
1618
|
+
const incident = db.getIncident(incidentId);
|
|
1619
|
+
if (!incident) {
|
|
1620
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1621
|
+
res.end(JSON.stringify({ ok: false, error: "Incident not found" }));
|
|
1622
|
+
return;
|
|
1623
|
+
}
|
|
1624
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1625
|
+
res.end(JSON.stringify({ ok: true, incident }));
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
if (pathname === "/api/notes") {
|
|
1629
|
+
const projectId = parsedUrl.searchParams.get("project") || void 0;
|
|
1630
|
+
const status = parsedUrl.searchParams.get("status") || void 0;
|
|
1631
|
+
const limit = parseInt(parsedUrl.searchParams.get("limit") || "50", 10);
|
|
1632
|
+
const notes = db.listNotes({ projectId, status, limit });
|
|
1633
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1634
|
+
res.end(JSON.stringify({ ok: true, notes }));
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
if (pathname.startsWith("/api/notes/")) {
|
|
1638
|
+
const noteId = pathname.replace("/api/notes/", "");
|
|
1639
|
+
const note = db.getNote(noteId);
|
|
1640
|
+
if (!note) {
|
|
1641
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1642
|
+
res.end(JSON.stringify({ ok: false, error: "Note not found" }));
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1645
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1646
|
+
res.end(JSON.stringify({ ok: true, note }));
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
if (pathname.startsWith("/screenshots/")) {
|
|
1650
|
+
const relativePath = pathname.replace("/screenshots/", "");
|
|
1651
|
+
const filePath = path5.join(baseScreenshotsDir, relativePath);
|
|
1652
|
+
if (fs4.existsSync(filePath) && fs4.statSync(filePath).isFile()) {
|
|
1653
|
+
const ext = path5.extname(filePath).toLowerCase();
|
|
1654
|
+
const mimeTypes = {
|
|
1655
|
+
".webp": "image/webp",
|
|
1656
|
+
".png": "image/png",
|
|
1657
|
+
".jpg": "image/jpeg",
|
|
1658
|
+
".jpeg": "image/jpeg"
|
|
1659
|
+
};
|
|
1660
|
+
res.writeHead(200, { "Content-Type": mimeTypes[ext] || "application/octet-stream" });
|
|
1661
|
+
fs4.createReadStream(filePath).pipe(res);
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1665
|
+
res.end(JSON.stringify({ ok: false, error: "Screenshot not found" }));
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
if (pathname === "/api/command" && req.method === "POST") {
|
|
1669
|
+
try {
|
|
1670
|
+
const data = await readJsonBody(req, res);
|
|
1671
|
+
const sessionId = data.sessionId || sessionManager.getAnyActiveSession()?.id;
|
|
1672
|
+
if (!sessionId) {
|
|
1673
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1674
|
+
res.end(JSON.stringify({ ok: false, error: "No active browser session connected" }));
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const cmdRes = await sessionManager.sendCommand(sessionId, data.command, data.timeoutMs || 5e3);
|
|
1678
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1679
|
+
res.end(JSON.stringify(cmdRes));
|
|
1680
|
+
} catch (err) {
|
|
1681
|
+
if (!res.headersSent) {
|
|
1682
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1683
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
if (pathname === "/api/verify/incident" && req.method === "POST") {
|
|
1689
|
+
try {
|
|
1690
|
+
if (!verificationEngine) {
|
|
1691
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1692
|
+
res.end(JSON.stringify({ ok: false, error: "Verification engine not attached to daemon" }));
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
const data = await readJsonBody(req, res);
|
|
1696
|
+
const result = await verificationEngine.verifyIncident(data.incidentId, data.options);
|
|
1697
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1698
|
+
res.end(JSON.stringify({ ok: true, result }));
|
|
1699
|
+
} catch (err) {
|
|
1700
|
+
if (!res.headersSent) {
|
|
1701
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1702
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
return;
|
|
1706
|
+
}
|
|
1707
|
+
if (pathname === "/api/verify/note" && req.method === "POST") {
|
|
1708
|
+
try {
|
|
1709
|
+
if (!noteVerificationEngine) {
|
|
1710
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1711
|
+
res.end(JSON.stringify({ ok: false, error: "Note verification engine not attached to daemon" }));
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
const data = await readJsonBody(req, res);
|
|
1715
|
+
const result = await noteVerificationEngine.verifyNote(data.noteId, data.options);
|
|
1716
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1717
|
+
res.end(JSON.stringify({ ok: true, result }));
|
|
1718
|
+
} catch (err) {
|
|
1719
|
+
if (!res.headersSent) {
|
|
1720
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1721
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || String(err) }));
|
|
1722
|
+
}
|
|
1723
|
+
}
|
|
1724
|
+
return;
|
|
1725
|
+
}
|
|
1726
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1727
|
+
res.end(JSON.stringify({ ok: false, error: "Not found" }));
|
|
1728
|
+
} catch (err) {
|
|
1729
|
+
if (!res.headersSent) {
|
|
1730
|
+
try {
|
|
1731
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1732
|
+
res.end(JSON.stringify({ ok: false, error: err?.message || "Internal Server Error" }));
|
|
1733
|
+
} catch {
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
}
|
|
1737
|
+
};
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
// packages/daemon/src/server/ws.ts
|
|
1741
|
+
import crypto5 from "crypto";
|
|
1742
|
+
function safeWsSend(ws, payload) {
|
|
1743
|
+
if (ws.readyState !== 1) return false;
|
|
1744
|
+
try {
|
|
1745
|
+
const raw = typeof payload === "string" ? payload : safeJsonStringify(payload);
|
|
1746
|
+
ws.send(raw);
|
|
1747
|
+
return true;
|
|
1748
|
+
} catch {
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
function setupWebSocketServer(wss, db, sessionManager, incidentEngine, notesEngine, maxEventsPerSession = 1e3, verbose = false) {
|
|
1753
|
+
wss.on("connection", (ws) => {
|
|
1754
|
+
let currentSessionId = null;
|
|
1755
|
+
ws.on("message", (raw) => {
|
|
1756
|
+
try {
|
|
1757
|
+
const text = typeof raw === "string" ? raw : raw.toString("utf-8");
|
|
1758
|
+
const data = JSON.parse(text);
|
|
1759
|
+
if (data.type === "hello") {
|
|
1760
|
+
const hello = data;
|
|
1761
|
+
const sessionId = `sess_${crypto5.randomUUID().slice(0, 8)}`;
|
|
1762
|
+
currentSessionId = sessionId;
|
|
1763
|
+
let project = hello.projectId ? db.getProject(hello.projectId) : null;
|
|
1764
|
+
if (!project && hello.origin) {
|
|
1765
|
+
project = db.getProjectByOrigin(hello.origin);
|
|
1766
|
+
}
|
|
1767
|
+
if (!project) {
|
|
1768
|
+
let projName = "default";
|
|
1769
|
+
try {
|
|
1770
|
+
const url = new URL(hello.origin || "http://localhost");
|
|
1771
|
+
projName = url.port ? `app-${url.port}` : url.hostname;
|
|
1772
|
+
} catch {
|
|
1773
|
+
projName = "app";
|
|
1774
|
+
}
|
|
1775
|
+
project = db.upsertProject({
|
|
1776
|
+
id: `proj_${crypto5.randomUUID().slice(0, 8)}`,
|
|
1777
|
+
name: projName,
|
|
1778
|
+
origin: hello.origin || "http://localhost"
|
|
1779
|
+
});
|
|
1780
|
+
}
|
|
1781
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1782
|
+
db.upsertSession({
|
|
1783
|
+
id: sessionId,
|
|
1784
|
+
projectId: project.id,
|
|
1785
|
+
origin: hello.origin || "",
|
|
1786
|
+
url: hello.url || "",
|
|
1787
|
+
title: hello.title || "",
|
|
1788
|
+
userAgent: hello.userAgent || "",
|
|
1789
|
+
connectedAt: now,
|
|
1790
|
+
lastSeenAt: now,
|
|
1791
|
+
active: true
|
|
1792
|
+
});
|
|
1793
|
+
sessionManager.registerSocket(sessionId, ws, hello.origin || "", project.id);
|
|
1794
|
+
safeWsSend(ws, {
|
|
1795
|
+
type: "hello_ack",
|
|
1796
|
+
sessionId,
|
|
1797
|
+
projectId: project.id,
|
|
1798
|
+
projectName: project.name
|
|
1799
|
+
});
|
|
1800
|
+
const existingNotes = db.listNotes({ projectId: project.id, limit: 100 });
|
|
1801
|
+
safeWsSend(ws, {
|
|
1802
|
+
type: "notes_sync",
|
|
1803
|
+
notes: existingNotes
|
|
1804
|
+
});
|
|
1805
|
+
if (verbose) {
|
|
1806
|
+
console.log(`[BrowserTrack] New session connected: ${sessionId} (${project.name} @ ${hello.origin})`);
|
|
1807
|
+
}
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
if (data.type === "event") {
|
|
1811
|
+
const eventMsg = data;
|
|
1812
|
+
const sessionId = eventMsg.sessionId || currentSessionId;
|
|
1813
|
+
if (!sessionId) return;
|
|
1814
|
+
const eventId = `evt_${crypto5.randomUUID().slice(0, 8)}`;
|
|
1815
|
+
db.insertEvent({
|
|
1816
|
+
id: eventId,
|
|
1817
|
+
sessionId,
|
|
1818
|
+
eventType: eventMsg.eventType,
|
|
1819
|
+
payload: eventMsg.payload,
|
|
1820
|
+
timestamp: eventMsg.timestamp || Date.now(),
|
|
1821
|
+
route: eventMsg.route,
|
|
1822
|
+
url: eventMsg.url
|
|
1823
|
+
});
|
|
1824
|
+
db.pruneSessionEvents(sessionId, maxEventsPerSession);
|
|
1825
|
+
const incident = incidentEngine.processClientEvent(eventMsg);
|
|
1826
|
+
if (incident && verbose) {
|
|
1827
|
+
console.log(
|
|
1828
|
+
`[BrowserTrack] Incident recorded: ${incident.id} (${incident.type}: ${incident.message}) [${incident.occurrences}x]`
|
|
1829
|
+
);
|
|
1830
|
+
}
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
if (data.type === "create_note" && notesEngine) {
|
|
1834
|
+
const note = notesEngine.createNoteFromClient({
|
|
1835
|
+
sessionId: data.sessionId || currentSessionId || "",
|
|
1836
|
+
noteType: data.noteType,
|
|
1837
|
+
message: data.message,
|
|
1838
|
+
route: data.route,
|
|
1839
|
+
url: data.url,
|
|
1840
|
+
viewport: data.viewport,
|
|
1841
|
+
scroll: data.scroll,
|
|
1842
|
+
target: data.target,
|
|
1843
|
+
elementContext: data.elementContext,
|
|
1844
|
+
region: data.region,
|
|
1845
|
+
screenshot: data.screenshot,
|
|
1846
|
+
incidentId: data.incidentId,
|
|
1847
|
+
scenarioId: data.scenarioId,
|
|
1848
|
+
stepNumber: data.stepNumber,
|
|
1849
|
+
scenarioTitle: data.scenarioTitle
|
|
1850
|
+
});
|
|
1851
|
+
if (verbose) {
|
|
1852
|
+
console.log(
|
|
1853
|
+
`[BrowserTrack] Visual note created: ${note.id} on ${note.route} ("${note.message}")${note.scenarioId ? ` [Scenario: ${note.scenarioTitle || note.scenarioId} Step ${note.stepNumber}]` : ""}`
|
|
1854
|
+
);
|
|
1855
|
+
}
|
|
1856
|
+
safeWsSend(ws, {
|
|
1857
|
+
type: "note_created_ack",
|
|
1858
|
+
noteId: note.id,
|
|
1859
|
+
status: note.status,
|
|
1860
|
+
scenarioId: note.scenarioId,
|
|
1861
|
+
stepNumber: note.stepNumber
|
|
1862
|
+
});
|
|
1863
|
+
const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
|
|
1864
|
+
sessionManager.broadcastToProject(note.projectId, {
|
|
1865
|
+
type: "notes_sync",
|
|
1866
|
+
notes: allNotes
|
|
1867
|
+
});
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
if (data.type === "resolve_note" && data.noteId) {
|
|
1871
|
+
const note = db.getNote(data.noteId);
|
|
1872
|
+
if (note) {
|
|
1873
|
+
db.updateNoteStatus(data.noteId, "RESOLVED");
|
|
1874
|
+
const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
|
|
1875
|
+
sessionManager.broadcastToProject(note.projectId, {
|
|
1876
|
+
type: "notes_sync",
|
|
1877
|
+
notes: allNotes
|
|
1878
|
+
});
|
|
1879
|
+
}
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (data.type === "reopen_note" && data.noteId) {
|
|
1883
|
+
const note = db.getNote(data.noteId);
|
|
1884
|
+
if (note) {
|
|
1885
|
+
db.updateNoteStatus(data.noteId, "OPEN");
|
|
1886
|
+
const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
|
|
1887
|
+
sessionManager.broadcastToProject(note.projectId, {
|
|
1888
|
+
type: "notes_sync",
|
|
1889
|
+
notes: allNotes
|
|
1890
|
+
});
|
|
1891
|
+
}
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
if (data.type === "delete_note" && data.noteId) {
|
|
1895
|
+
const note = db.getNote(data.noteId);
|
|
1896
|
+
if (note) {
|
|
1897
|
+
db.deleteNote(data.noteId);
|
|
1898
|
+
const allNotes = db.listNotes({ projectId: note.projectId, limit: 100 });
|
|
1899
|
+
sessionManager.broadcastToProject(note.projectId, {
|
|
1900
|
+
type: "notes_sync",
|
|
1901
|
+
notes: allNotes
|
|
1902
|
+
});
|
|
1903
|
+
}
|
|
1904
|
+
return;
|
|
1905
|
+
}
|
|
1906
|
+
if (data.type === "delete_scenario" && data.scenarioId) {
|
|
1907
|
+
const scenario = db.getScenario(data.scenarioId);
|
|
1908
|
+
if (scenario) {
|
|
1909
|
+
db.deleteScenario(data.scenarioId);
|
|
1910
|
+
const allNotes = db.listNotes({ projectId: scenario.projectId, limit: 100 });
|
|
1911
|
+
sessionManager.broadcastToProject(scenario.projectId, {
|
|
1912
|
+
type: "notes_sync",
|
|
1913
|
+
notes: allNotes
|
|
1914
|
+
});
|
|
1915
|
+
}
|
|
1916
|
+
return;
|
|
1917
|
+
}
|
|
1918
|
+
if (data.type === "get_notes") {
|
|
1919
|
+
const session = currentSessionId ? db.getSession(currentSessionId) : null;
|
|
1920
|
+
const projectId = data.projectId || session?.projectId;
|
|
1921
|
+
if (projectId) {
|
|
1922
|
+
const allNotes = db.listNotes({ projectId, limit: 100 });
|
|
1923
|
+
safeWsSend(ws, {
|
|
1924
|
+
type: "notes_sync",
|
|
1925
|
+
notes: allNotes
|
|
1926
|
+
});
|
|
1927
|
+
}
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
if (data.type === "command_response") {
|
|
1931
|
+
const res = data.response;
|
|
1932
|
+
const sessionId = data.sessionId || currentSessionId;
|
|
1933
|
+
if (sessionId && res) {
|
|
1934
|
+
sessionManager.handleCommandResponse(sessionId, res);
|
|
1935
|
+
}
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
} catch (err) {
|
|
1939
|
+
if (verbose) {
|
|
1940
|
+
console.error("[BrowserTrack] WebSocket message error:", err?.message);
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
});
|
|
1944
|
+
ws.on("close", () => {
|
|
1945
|
+
if (currentSessionId) {
|
|
1946
|
+
sessionManager.unregisterSocket(currentSessionId);
|
|
1947
|
+
if (verbose) {
|
|
1948
|
+
console.log(`[BrowserTrack] Session disconnected: ${currentSessionId}`);
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
});
|
|
1952
|
+
ws.on("error", () => {
|
|
1953
|
+
if (currentSessionId) {
|
|
1954
|
+
sessionManager.unregisterSocket(currentSessionId);
|
|
1955
|
+
}
|
|
1956
|
+
});
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// packages/daemon/src/server/daemon.ts
|
|
1961
|
+
import http from "http";
|
|
1962
|
+
import { WebSocketServer } from "ws";
|
|
1963
|
+
var BrowserTrackDaemon = class {
|
|
1964
|
+
config;
|
|
1965
|
+
db;
|
|
1966
|
+
screenshotStore;
|
|
1967
|
+
sessionManager;
|
|
1968
|
+
incidentEngine;
|
|
1969
|
+
notesEngine;
|
|
1970
|
+
verificationEngine;
|
|
1971
|
+
noteVerificationEngine;
|
|
1972
|
+
httpServer = null;
|
|
1973
|
+
wss = null;
|
|
1974
|
+
isRunning = false;
|
|
1975
|
+
constructor(config = {}) {
|
|
1976
|
+
this.config = getDaemonConfig(config);
|
|
1977
|
+
this.db = new StorageDB(this.config.dbPath);
|
|
1978
|
+
this.screenshotStore = new ScreenshotStore(this.config.screenshotsDir);
|
|
1979
|
+
this.sessionManager = new SessionManager(this.db);
|
|
1980
|
+
this.incidentEngine = new IncidentEngine(this.db, this.screenshotStore);
|
|
1981
|
+
this.notesEngine = new NotesEngine(this.db, this.screenshotStore);
|
|
1982
|
+
this.verificationEngine = new VerificationEngine(this.db, this.sessionManager, this.screenshotStore);
|
|
1983
|
+
this.noteVerificationEngine = new NoteVerificationEngine(this.db, this.sessionManager, this.notesEngine);
|
|
1984
|
+
}
|
|
1985
|
+
async start() {
|
|
1986
|
+
if (this.isRunning) return;
|
|
1987
|
+
const httpHandler = createHttpHandler(
|
|
1988
|
+
this.db,
|
|
1989
|
+
this.sessionManager,
|
|
1990
|
+
this.config.screenshotsDir,
|
|
1991
|
+
this.verificationEngine,
|
|
1992
|
+
this.noteVerificationEngine
|
|
1993
|
+
);
|
|
1994
|
+
this.httpServer = http.createServer(httpHandler);
|
|
1995
|
+
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
1996
|
+
setupWebSocketServer(
|
|
1997
|
+
this.wss,
|
|
1998
|
+
this.db,
|
|
1999
|
+
this.sessionManager,
|
|
2000
|
+
this.incidentEngine,
|
|
2001
|
+
this.notesEngine,
|
|
2002
|
+
this.config.maxEventsPerSession,
|
|
2003
|
+
this.config.verbose
|
|
2004
|
+
);
|
|
2005
|
+
await new Promise((resolve, reject) => {
|
|
2006
|
+
this.httpServer.listen(this.config.port, this.config.host, () => {
|
|
2007
|
+
this.isRunning = true;
|
|
2008
|
+
if (this.config.verbose) {
|
|
2009
|
+
console.log(`[BrowserTrack] Daemon running at http://${this.config.host}:${this.config.port}`);
|
|
2010
|
+
}
|
|
2011
|
+
resolve();
|
|
2012
|
+
});
|
|
2013
|
+
this.httpServer.once("error", (err) => {
|
|
2014
|
+
reject(err);
|
|
2015
|
+
});
|
|
2016
|
+
});
|
|
2017
|
+
}
|
|
2018
|
+
async stop() {
|
|
2019
|
+
if (!this.isRunning) return;
|
|
2020
|
+
if (this.wss) {
|
|
2021
|
+
for (const client of this.wss.clients) {
|
|
2022
|
+
try {
|
|
2023
|
+
client.terminate();
|
|
2024
|
+
} catch {
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
await new Promise((resolve) => {
|
|
2028
|
+
this.wss.close(() => resolve());
|
|
2029
|
+
});
|
|
2030
|
+
this.wss = null;
|
|
2031
|
+
}
|
|
2032
|
+
if (this.httpServer) {
|
|
2033
|
+
if (typeof this.httpServer.closeAllConnections === "function") {
|
|
2034
|
+
this.httpServer.closeAllConnections();
|
|
2035
|
+
}
|
|
2036
|
+
await new Promise((resolve) => {
|
|
2037
|
+
this.httpServer.close(() => resolve());
|
|
2038
|
+
});
|
|
2039
|
+
this.httpServer = null;
|
|
2040
|
+
}
|
|
2041
|
+
try {
|
|
2042
|
+
this.db.close();
|
|
2043
|
+
} catch {
|
|
2044
|
+
}
|
|
2045
|
+
this.isRunning = false;
|
|
2046
|
+
}
|
|
2047
|
+
getStatus() {
|
|
2048
|
+
return {
|
|
2049
|
+
isRunning: this.isRunning,
|
|
2050
|
+
host: this.config.host,
|
|
2051
|
+
port: this.config.port,
|
|
2052
|
+
activeSessions: this.sessionManager.getActiveCount(),
|
|
2053
|
+
dbPath: this.config.dbPath
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
};
|
|
2057
|
+
|
|
2058
|
+
// packages/daemon/src/index.ts
|
|
2059
|
+
function createDaemon(config = {}) {
|
|
2060
|
+
return new BrowserTrackDaemon(config);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
1313
2063
|
export {
|
|
1314
2064
|
getDaemonConfig,
|
|
1315
2065
|
StorageDB,
|
|
1316
2066
|
ScreenshotStore,
|
|
1317
2067
|
SessionManager,
|
|
2068
|
+
IncidentEngine,
|
|
1318
2069
|
NotesEngine,
|
|
1319
2070
|
NoteVerificationEngine,
|
|
1320
|
-
VerificationEngine
|
|
2071
|
+
VerificationEngine,
|
|
2072
|
+
createHttpHandler,
|
|
2073
|
+
setupWebSocketServer,
|
|
2074
|
+
BrowserTrackDaemon,
|
|
2075
|
+
createDaemon
|
|
1321
2076
|
};
|
|
1322
|
-
//# sourceMappingURL=chunk-
|
|
2077
|
+
//# sourceMappingURL=chunk-G5CIZSQM.js.map
|