browsertrack 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. package/README.md +143 -0
  2. package/dist/chunk-6A7FFDIB.js +221 -0
  3. package/dist/chunk-6A7FFDIB.js.map +1 -0
  4. package/dist/chunk-ANMR5WBH.js +659 -0
  5. package/dist/chunk-ANMR5WBH.js.map +1 -0
  6. package/dist/chunk-BSKNG4V7.js +1199 -0
  7. package/dist/chunk-BSKNG4V7.js.map +1 -0
  8. package/dist/chunk-X7C5CHBO.js +2080 -0
  9. package/dist/chunk-X7C5CHBO.js.map +1 -0
  10. package/dist/chunk-ZLNGOOH2.js +554 -0
  11. package/dist/chunk-ZLNGOOH2.js.map +1 -0
  12. package/dist/cli/index.js +2829 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/client/index.cjs +2218 -0
  15. package/dist/client/index.d.ts +197 -0
  16. package/dist/client/index.js +20 -0
  17. package/dist/client/index.js.map +1 -0
  18. package/dist/client.iife.js +309 -0
  19. package/dist/core/index.d.ts +78 -0
  20. package/dist/core/index.js +31 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/daemon/index.d.ts +58 -0
  23. package/dist/daemon/index.js +32 -0
  24. package/dist/daemon/index.js.map +1 -0
  25. package/dist/engine-DKloFjvs.d.ts +157 -0
  26. package/dist/index.d.ts +10 -0
  27. package/dist/index.js +50 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mcp/index.d.ts +11 -0
  30. package/dist/mcp/index.js +12 -0
  31. package/dist/mcp/index.js.map +1 -0
  32. package/dist/notes-CBvN91Wf.d.ts +260 -0
  33. package/dist/projects-CY8ungMt.d.ts +99 -0
  34. package/dist/server-CN-se8td.d.ts +55 -0
  35. package/examples/test-app/index.html +299 -0
  36. package/package.json +52 -7
  37. package/packages/cli/src/index.ts +413 -0
  38. package/packages/client/package.json +28 -0
  39. package/packages/client/src/breadcrumbs.ts +30 -0
  40. package/packages/client/src/client.ts +278 -0
  41. package/packages/client/src/commands/handler.ts +301 -0
  42. package/packages/client/src/config.ts +37 -0
  43. package/packages/client/src/index.ts +50 -0
  44. package/packages/client/src/interceptors/console.ts +68 -0
  45. package/packages/client/src/interceptors/interaction.ts +119 -0
  46. package/packages/client/src/interceptors/navigation.ts +93 -0
  47. package/packages/client/src/interceptors/network.ts +165 -0
  48. package/packages/client/src/interceptors/runtime.ts +65 -0
  49. package/packages/client/src/notes/inspector.ts +943 -0
  50. package/packages/client/src/screenshot/browser-script-driver.ts +192 -0
  51. package/packages/client/src/screenshot/driver.ts +14 -0
  52. package/packages/client/src/transport/websocket.ts +179 -0
  53. package/packages/client/tsconfig.json +8 -0
  54. package/packages/core/dist/index.d.ts +317 -0
  55. package/packages/core/dist/index.js +221 -0
  56. package/packages/core/package.json +23 -0
  57. package/packages/core/src/fingerprint.ts +120 -0
  58. package/packages/core/src/index.ts +9 -0
  59. package/packages/core/src/redaction.ts +139 -0
  60. package/packages/core/src/selector.ts +77 -0
  61. package/packages/core/src/types/commands.ts +101 -0
  62. package/packages/core/src/types/events.ts +108 -0
  63. package/packages/core/src/types/incidents.ts +54 -0
  64. package/packages/core/src/types/notes.ts +106 -0
  65. package/packages/core/src/types/probes.ts +44 -0
  66. package/packages/core/src/types/projects.ts +20 -0
  67. package/packages/core/tsconfig.json +8 -0
  68. package/packages/daemon/src/config.ts +29 -0
  69. package/packages/daemon/src/incidents/engine.ts +211 -0
  70. package/packages/daemon/src/index.ts +18 -0
  71. package/packages/daemon/src/notes/engine.ts +92 -0
  72. package/packages/daemon/src/notes/verification.ts +191 -0
  73. package/packages/daemon/src/server/daemon.ts +112 -0
  74. package/packages/daemon/src/server/http.ts +165 -0
  75. package/packages/daemon/src/server/ws.ts +175 -0
  76. package/packages/daemon/src/session/manager.ts +126 -0
  77. package/packages/daemon/src/storage/db.ts +733 -0
  78. package/packages/daemon/src/storage/screenshot-store.ts +49 -0
  79. package/packages/daemon/src/verification/engine.ts +264 -0
  80. package/packages/mcp/src/handlers.ts +398 -0
  81. package/packages/mcp/src/index.ts +3 -0
  82. package/packages/mcp/src/server.ts +86 -0
  83. package/packages/mcp/src/tools.ts +236 -0
  84. package/src/index.ts +4 -0
  85. package/test/client/interceptors.test.ts +69 -0
  86. package/test/core/fingerprint.test.ts +53 -0
  87. package/test/core/notes.test.ts +66 -0
  88. package/test/core/redaction.test.ts +48 -0
  89. package/test/daemon/incident-engine.test.ts +98 -0
  90. package/test/daemon/notes-storage.test.ts +130 -0
  91. package/test/daemon/notes-verification.test.ts +135 -0
  92. package/test/daemon/storage.test.ts +123 -0
  93. package/test/daemon/verification-engine.test.ts +88 -0
  94. package/test/e2e/daemon-mcp-e2e.test.ts +153 -0
  95. package/test/e2e/visual-notes-e2e.test.ts +205 -0
  96. package/test/mcp/handlers.test.ts +116 -0
  97. package/test/mcp/notes.test.ts +107 -0
  98. package/tsconfig.base.json +17 -0
  99. package/tsconfig.json +26 -0
  100. package/tsup.config.ts +54 -0
  101. package/vitest.config.ts +9 -0
@@ -0,0 +1,108 @@
1
+ export type EventType =
2
+ | 'runtime_error'
3
+ | 'unhandled_rejection'
4
+ | 'console'
5
+ | 'fetch'
6
+ | 'xhr'
7
+ | 'navigation'
8
+ | 'interaction';
9
+
10
+ export type ConsoleLevel = 'error' | 'warn' | 'info' | 'log' | 'debug';
11
+
12
+ export interface ElementSummary {
13
+ selector: string;
14
+ tag: string;
15
+ id?: string;
16
+ classes?: string[];
17
+ attributes?: Record<string, string>;
18
+ outerHTML?: string;
19
+ innerText?: string;
20
+ boundingRect?: {
21
+ x: number;
22
+ y: number;
23
+ width: number;
24
+ height: number;
25
+ top: number;
26
+ left: number;
27
+ bottom: number;
28
+ right: number;
29
+ };
30
+ visible: boolean;
31
+ }
32
+
33
+ export interface Breadcrumb {
34
+ id?: string;
35
+ type: 'navigation' | 'click' | 'submit' | 'fetch' | 'xhr' | 'console' | 'error';
36
+ category?: string;
37
+ message: string;
38
+ timestamp: number;
39
+ level?: 'info' | 'warn' | 'error';
40
+ data?: Record<string, any>;
41
+ element?: ElementSummary;
42
+ }
43
+
44
+ export interface NetworkEvent {
45
+ id?: string;
46
+ url: string;
47
+ method: string;
48
+ status?: number;
49
+ statusText?: string;
50
+ durationMs: number;
51
+ error?: string;
52
+ aborted?: boolean;
53
+ timestamp: number;
54
+ requestHeaders?: Record<string, string>;
55
+ }
56
+
57
+ export interface RuntimeErrorEvent {
58
+ message: string;
59
+ stack?: string;
60
+ filename?: string;
61
+ lineno?: number;
62
+ colno?: number;
63
+ errorType: string;
64
+ timestamp: number;
65
+ }
66
+
67
+ export interface ConsoleEvent {
68
+ level: ConsoleLevel;
69
+ message: string;
70
+ args?: any[];
71
+ stack?: string;
72
+ timestamp: number;
73
+ }
74
+
75
+ export interface NavigationEvent {
76
+ from?: string;
77
+ to: string;
78
+ type: 'pushState' | 'replaceState' | 'popstate' | 'hashchange' | 'initial';
79
+ timestamp: number;
80
+ }
81
+
82
+ export interface HelloMessage {
83
+ type: 'hello';
84
+ origin: string;
85
+ url: string;
86
+ title: string;
87
+ userAgent: string;
88
+ timestamp: number;
89
+ projectId?: string;
90
+ }
91
+
92
+ export interface ClientEventMessage {
93
+ type: 'event';
94
+ sessionId: string;
95
+ eventType: EventType;
96
+ payload:
97
+ | RuntimeErrorEvent
98
+ | ConsoleEvent
99
+ | NetworkEvent
100
+ | NavigationEvent
101
+ | Breadcrumb;
102
+ breadcrumbs?: Breadcrumb[];
103
+ lastElement?: ElementSummary;
104
+ route?: string;
105
+ url: string;
106
+ title?: string;
107
+ timestamp: number;
108
+ }
@@ -0,0 +1,54 @@
1
+ import type { Breadcrumb, ElementSummary, NetworkEvent } from './events.js';
2
+
3
+ export type IncidentStatus =
4
+ | 'OPEN'
5
+ | 'FIX_ATTEMPTED'
6
+ | 'VERIFYING'
7
+ | 'VERIFIED'
8
+ | 'FAILED'
9
+ | 'INCONCLUSIVE';
10
+
11
+ export type IncidentSeverity = 'error' | 'warn' | 'fatal';
12
+
13
+ export interface IncidentSource {
14
+ file: string;
15
+ line: number;
16
+ column?: number;
17
+ }
18
+
19
+ export interface Incident {
20
+ id: string;
21
+ projectId: string;
22
+ sessionId: string;
23
+ type: string;
24
+ severity: IncidentSeverity;
25
+ message: string;
26
+ source: IncidentSource;
27
+ fingerprint: string;
28
+ route: string;
29
+ firstSeen: string;
30
+ lastSeen: string;
31
+ occurrences: number;
32
+ status: IncidentStatus;
33
+ stack?: string;
34
+ breadcrumbs: Breadcrumb[];
35
+ networkFailures: NetworkEvent[];
36
+ lastElement?: ElementSummary;
37
+ screenshots?: {
38
+ error?: string;
39
+ before?: string;
40
+ after?: string;
41
+ };
42
+ }
43
+
44
+ export interface IncidentOccurrence {
45
+ id: string;
46
+ incidentId: string;
47
+ sessionId: string;
48
+ timestamp: string;
49
+ route: string;
50
+ url: string;
51
+ stack?: string;
52
+ breadcrumbs: Breadcrumb[];
53
+ lastElement?: ElementSummary;
54
+ }
@@ -0,0 +1,106 @@
1
+ export type NoteType = 'element' | 'region' | 'page';
2
+
3
+ export type NoteStatus =
4
+ | 'OPEN'
5
+ | 'IN_PROGRESS'
6
+ | 'VERIFYING'
7
+ | 'READY_FOR_REVIEW'
8
+ | 'VERIFIED'
9
+ | 'RESOLVED'
10
+ | 'FAILED'
11
+ | 'INCONCLUSIVE';
12
+
13
+ export interface ViewportContext {
14
+ width: number;
15
+ height: number;
16
+ devicePixelRatio: number;
17
+ }
18
+
19
+ export interface ScrollContext {
20
+ scrollX: number;
21
+ scrollY: number;
22
+ }
23
+
24
+ export interface DOMRectJson {
25
+ x: number;
26
+ y: number;
27
+ width: number;
28
+ height: number;
29
+ top: number;
30
+ left: number;
31
+ bottom: number;
32
+ right: number;
33
+ }
34
+
35
+ export interface NoteTarget {
36
+ selector: string;
37
+ boundingRect: DOMRectJson;
38
+ visible: boolean;
39
+ confidence?: 'high' | 'medium' | 'low';
40
+ }
41
+
42
+ export interface ElementContext {
43
+ selector: string;
44
+ tag: string;
45
+ attributes?: Record<string, string>;
46
+ outerHTML?: string;
47
+ innerText?: string;
48
+ parent?: {
49
+ selector: string;
50
+ tag: string;
51
+ };
52
+ }
53
+
54
+ export interface RegionContext {
55
+ x: number;
56
+ y: number;
57
+ width: number;
58
+ height: number;
59
+ }
60
+
61
+ export interface VisualNote {
62
+ id: string;
63
+ projectId: string;
64
+ sessionId: string;
65
+ type: NoteType;
66
+ message: string;
67
+ route: string;
68
+ url: string;
69
+ viewport: ViewportContext;
70
+ scroll: ScrollContext;
71
+ target?: NoteTarget;
72
+ elementContext?: ElementContext;
73
+ region?: RegionContext;
74
+ status: NoteStatus;
75
+ incidentId?: string;
76
+ screenshots?: {
77
+ original?: string;
78
+ after?: string;
79
+ };
80
+ createdAt: string;
81
+ updatedAt: string;
82
+ resolvedAt?: string;
83
+ }
84
+
85
+ export interface NoteVerificationResult {
86
+ noteId: string;
87
+ status: NoteStatus;
88
+ checks: Array<{
89
+ type: string;
90
+ passed: boolean;
91
+ details?: string;
92
+ }>;
93
+ geometryDiff?: {
94
+ before?: DOMRectJson;
95
+ current?: DOMRectJson;
96
+ viewportWidth?: number;
97
+ overflowFixed?: boolean;
98
+ overflowPx?: number;
99
+ };
100
+ screenshots?: {
101
+ before?: string;
102
+ after?: string;
103
+ };
104
+ timestamp: string;
105
+ message?: string;
106
+ }
@@ -0,0 +1,44 @@
1
+ import type { IncidentStatus } from './incidents.js';
2
+
3
+ export type ProbeType =
4
+ | 'element_exists'
5
+ | 'element_visible'
6
+ | 'text_contains'
7
+ | 'route_is'
8
+ | 'network_request_succeeded'
9
+ | 'no_incident';
10
+
11
+ export interface VerificationProbe {
12
+ type: ProbeType;
13
+ selector?: string;
14
+ text?: string;
15
+ route?: string;
16
+ fingerprint?: string;
17
+ urlPattern?: string;
18
+ timeoutMs?: number;
19
+ }
20
+
21
+ export interface VerificationRecipe {
22
+ route?: string;
23
+ targetSelector?: string;
24
+ expect?: VerificationProbe[];
25
+ observationWindowMs?: number;
26
+ }
27
+
28
+ export interface ProbeResult {
29
+ type: ProbeType;
30
+ passed: boolean;
31
+ details?: string;
32
+ }
33
+
34
+ export interface VerificationResult {
35
+ incidentId: string;
36
+ status: IncidentStatus;
37
+ checks: ProbeResult[];
38
+ screenshots?: {
39
+ before?: string;
40
+ after?: string;
41
+ };
42
+ timestamp: string;
43
+ message?: string;
44
+ }
@@ -0,0 +1,20 @@
1
+ export interface Project {
2
+ id: string;
3
+ name: string;
4
+ origin: string;
5
+ path?: string;
6
+ createdAt: string;
7
+ updatedAt: string;
8
+ }
9
+
10
+ export interface Session {
11
+ id: string;
12
+ projectId: string;
13
+ origin: string;
14
+ url: string;
15
+ title: string;
16
+ userAgent: string;
17
+ connectedAt: string;
18
+ lastSeenAt: string;
19
+ active: boolean;
20
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*"]
8
+ }
@@ -0,0 +1,29 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export interface DaemonConfig {
5
+ host: string;
6
+ port: number;
7
+ dataDir: string;
8
+ dbPath: string;
9
+ screenshotsDir: string;
10
+ maxEventsPerSession: number;
11
+ verbose: boolean;
12
+ }
13
+
14
+ export function getDaemonConfig(overrides: Partial<DaemonConfig> = {}): DaemonConfig {
15
+ const homeDir = os.homedir();
16
+ const dataDir = overrides.dataDir || path.join(homeDir, '.browsertrack');
17
+ const dbPath = overrides.dbPath || path.join(dataDir, 'browsertrack.db');
18
+ const screenshotsDir = overrides.screenshotsDir || path.join(dataDir, 'projects');
19
+
20
+ return {
21
+ host: overrides.host || '127.0.0.1',
22
+ port: overrides.port || 7331,
23
+ dataDir,
24
+ dbPath,
25
+ screenshotsDir,
26
+ maxEventsPerSession: overrides.maxEventsPerSession || 1000,
27
+ verbose: !!overrides.verbose,
28
+ };
29
+ }
@@ -0,0 +1,211 @@
1
+ import crypto from 'node:crypto';
2
+ import type {
3
+ ClientEventMessage,
4
+ ConsoleEvent,
5
+ Incident,
6
+ IncidentOccurrence,
7
+ IncidentSeverity,
8
+ NetworkEvent,
9
+ RuntimeErrorEvent,
10
+ } from '../../../core/src/index.js';
11
+ import { computeFingerprint, extractSourceFromStack, normalizeSourceFile, redactSensitiveData } from '../../../core/src/index.js';
12
+ import type { StorageDB } from '../storage/db.js';
13
+ import type { ScreenshotStore } from '../storage/screenshot-store.js';
14
+
15
+ export class IncidentEngine {
16
+ private db: StorageDB;
17
+ private screenshotStore: ScreenshotStore;
18
+
19
+ constructor(db: StorageDB, screenshotStore: ScreenshotStore) {
20
+ this.db = db;
21
+ this.screenshotStore = screenshotStore;
22
+ }
23
+
24
+ public processClientEvent(message: ClientEventMessage & { screenshot?: string }): Incident | null {
25
+ const session = this.db.getSession(message.sessionId);
26
+ const projectId = session?.projectId || 'default';
27
+ const sanitizedBreadcrumbs = (message.breadcrumbs || []).map((b) => redactSensitiveData(b));
28
+ const sanitizedLastElement = message.lastElement ? redactSensitiveData(message.lastElement) : undefined;
29
+
30
+ // Filter network failures from breadcrumbs
31
+ const networkFailures: NetworkEvent[] = sanitizedBreadcrumbs
32
+ .filter((b) => (b.type === 'fetch' || b.type === 'xhr') && b.level === 'error' && b.data)
33
+ .map((b) => ({
34
+ url: b.message.split(' ')[1] || '',
35
+ method: b.message.split(' ')[0] || 'GET',
36
+ status: b.data?.status,
37
+ durationMs: b.data?.durationMs || 0,
38
+ error: b.data?.error,
39
+ aborted: b.data?.aborted,
40
+ timestamp: b.timestamp,
41
+ }));
42
+
43
+ if (message.eventType === 'runtime_error' || message.eventType === 'unhandled_rejection') {
44
+ const payload = message.payload as RuntimeErrorEvent;
45
+ return this.handleErrorEvent({
46
+ projectId,
47
+ sessionId: message.sessionId,
48
+ type: payload.errorType || 'runtime_exception',
49
+ severity: 'error',
50
+ message: payload.message || 'Unknown runtime error',
51
+ stack: payload.stack,
52
+ sourceFile: payload.filename,
53
+ line: payload.lineno,
54
+ column: payload.colno,
55
+ route: message.route || '/',
56
+ url: message.url,
57
+ breadcrumbs: sanitizedBreadcrumbs,
58
+ networkFailures,
59
+ lastElement: sanitizedLastElement,
60
+ screenshotDataUrl: message.screenshot,
61
+ });
62
+ }
63
+
64
+ if (message.eventType === 'console') {
65
+ const payload = message.payload as ConsoleEvent;
66
+ if (payload.level === 'error') {
67
+ return this.handleErrorEvent({
68
+ projectId,
69
+ sessionId: message.sessionId,
70
+ type: 'console_error',
71
+ severity: 'error',
72
+ message: payload.message,
73
+ stack: payload.stack,
74
+ route: message.route || '/',
75
+ url: message.url,
76
+ breadcrumbs: sanitizedBreadcrumbs,
77
+ networkFailures,
78
+ lastElement: sanitizedLastElement,
79
+ screenshotDataUrl: message.screenshot,
80
+ });
81
+ }
82
+ }
83
+
84
+ return null;
85
+ }
86
+
87
+ private handleErrorEvent(input: {
88
+ projectId: string;
89
+ sessionId: string;
90
+ type: string;
91
+ severity: IncidentSeverity;
92
+ message: string;
93
+ stack?: string;
94
+ sourceFile?: string;
95
+ line?: number;
96
+ column?: number;
97
+ route: string;
98
+ url: string;
99
+ breadcrumbs: any[];
100
+ networkFailures: NetworkEvent[];
101
+ lastElement?: any;
102
+ screenshotDataUrl?: string;
103
+ }): Incident {
104
+ const extracted = (!input.sourceFile || !input.line) && input.stack ? extractSourceFromStack(input.stack) : null;
105
+
106
+ const rawSource = input.sourceFile || extracted?.file || 'unknown_source';
107
+ const sourceFile = normalizeSourceFile(rawSource);
108
+ const line = input.line || extracted?.line || 0;
109
+ const column = input.column || extracted?.column;
110
+
111
+ const fingerprint = computeFingerprint({
112
+ type: input.type,
113
+ message: input.message,
114
+ sourceFile,
115
+ line,
116
+ column,
117
+ stack: input.stack,
118
+ });
119
+
120
+ const now = new Date().toISOString();
121
+ const existing = this.db.findIncidentByFingerprint(fingerprint);
122
+
123
+ if (existing) {
124
+ const updatedOccurrences = existing.occurrences + 1;
125
+ this.db.updateIncidentOccurrence(existing.id, {
126
+ sessionId: input.sessionId,
127
+ lastSeen: now,
128
+ occurrences: updatedOccurrences,
129
+ route: input.route,
130
+ breadcrumbs: input.breadcrumbs,
131
+ lastElement: input.lastElement,
132
+ stack: input.stack,
133
+ });
134
+
135
+ // Insert occurrence
136
+ const occurrenceId = `occ_${crypto.randomUUID().slice(0, 8)}`;
137
+ this.db.insertIncidentOccurrence({
138
+ id: occurrenceId,
139
+ incidentId: existing.id,
140
+ sessionId: input.sessionId,
141
+ timestamp: now,
142
+ route: input.route,
143
+ url: input.url,
144
+ stack: input.stack,
145
+ breadcrumbs: input.breadcrumbs,
146
+ lastElement: input.lastElement,
147
+ });
148
+
149
+ return {
150
+ ...existing,
151
+ occurrences: updatedOccurrences,
152
+ lastSeen: now,
153
+ breadcrumbs: input.breadcrumbs,
154
+ lastElement: input.lastElement || existing.lastElement,
155
+ };
156
+ }
157
+
158
+ // New incident
159
+ const incidentId = `inc_${crypto.randomUUID().slice(0, 8)}`;
160
+ let screenshotPath: string | undefined;
161
+
162
+ if (input.screenshotDataUrl) {
163
+ const saved = this.screenshotStore.saveScreenshot(input.projectId, incidentId, 'error', input.screenshotDataUrl);
164
+ if (saved) {
165
+ screenshotPath = saved.filePath;
166
+ }
167
+ }
168
+
169
+ const newIncident: Incident = {
170
+ id: incidentId,
171
+ projectId: input.projectId,
172
+ sessionId: input.sessionId,
173
+ type: input.type,
174
+ severity: input.severity,
175
+ message: input.message,
176
+ source: {
177
+ file: sourceFile,
178
+ line,
179
+ column,
180
+ },
181
+ fingerprint,
182
+ route: input.route,
183
+ firstSeen: now,
184
+ lastSeen: now,
185
+ occurrences: 1,
186
+ status: 'OPEN',
187
+ stack: input.stack,
188
+ breadcrumbs: input.breadcrumbs,
189
+ networkFailures: input.networkFailures,
190
+ lastElement: input.lastElement,
191
+ screenshots: screenshotPath ? { error: screenshotPath } : undefined,
192
+ };
193
+
194
+ this.db.insertIncident(newIncident);
195
+
196
+ // Initial occurrence
197
+ this.db.insertIncidentOccurrence({
198
+ id: `occ_${crypto.randomUUID().slice(0, 8)}`,
199
+ incidentId,
200
+ sessionId: input.sessionId,
201
+ timestamp: now,
202
+ route: input.route,
203
+ url: input.url,
204
+ stack: input.stack,
205
+ breadcrumbs: input.breadcrumbs,
206
+ lastElement: input.lastElement,
207
+ });
208
+
209
+ return newIncident;
210
+ }
211
+ }
@@ -0,0 +1,18 @@
1
+ export * from './config.js';
2
+ export * from './storage/db.js';
3
+ export * from './storage/screenshot-store.js';
4
+ export * from './session/manager.js';
5
+ export * from './incidents/engine.js';
6
+ export * from './notes/engine.js';
7
+ export * from './notes/verification.js';
8
+ export * from './verification/engine.js';
9
+ export * from './server/http.js';
10
+ export * from './server/ws.js';
11
+ export * from './server/daemon.js';
12
+
13
+ import { BrowserTrackDaemon } from './server/daemon.js';
14
+ import type { DaemonConfig } from './config.js';
15
+
16
+ export function createDaemon(config: Partial<DaemonConfig> = {}): BrowserTrackDaemon {
17
+ return new BrowserTrackDaemon(config);
18
+ }
@@ -0,0 +1,92 @@
1
+ import crypto from 'node:crypto';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs';
4
+ import type { VisualNote } from '../../../core/src/index.js';
5
+ import type { StorageDB } from '../storage/db.js';
6
+ import type { ScreenshotStore } from '../storage/screenshot-store.js';
7
+
8
+ export class NotesEngine {
9
+ private db: StorageDB;
10
+ private screenshotStore: ScreenshotStore;
11
+
12
+ constructor(db: StorageDB, screenshotStore: ScreenshotStore) {
13
+ this.db = db;
14
+ this.screenshotStore = screenshotStore;
15
+ }
16
+
17
+ public createNoteFromClient(payload: {
18
+ sessionId: string;
19
+ noteType?: 'element' | 'region' | 'page';
20
+ message: string;
21
+ route?: string;
22
+ url?: string;
23
+ viewport?: { width: number; height: number; devicePixelRatio: number };
24
+ scroll?: { scrollX: number; scrollY: number };
25
+ target?: any;
26
+ elementContext?: any;
27
+ region?: any;
28
+ screenshot?: string;
29
+ incidentId?: string;
30
+ }): VisualNote {
31
+ const session = this.db.getSession(payload.sessionId);
32
+ const projectId = session?.projectId || 'default';
33
+ const noteId = `note_${crypto.randomUUID().slice(0, 8)}`;
34
+ const now = new Date().toISOString();
35
+
36
+ let screenshotPath: string | undefined;
37
+ if (payload.screenshot) {
38
+ const saved = this.saveNoteScreenshot(projectId, noteId, 'original', payload.screenshot);
39
+ if (saved) {
40
+ screenshotPath = saved;
41
+ }
42
+ }
43
+
44
+ const note: VisualNote = {
45
+ id: noteId,
46
+ projectId,
47
+ sessionId: payload.sessionId,
48
+ type: payload.noteType || 'element',
49
+ message: payload.message,
50
+ route: payload.route || '/',
51
+ url: payload.url || '',
52
+ viewport: payload.viewport || { width: 1280, height: 800, devicePixelRatio: 1 },
53
+ scroll: payload.scroll || { scrollX: 0, scrollY: 0 },
54
+ target: payload.target,
55
+ elementContext: payload.elementContext,
56
+ region: payload.region,
57
+ status: 'OPEN',
58
+ incidentId: payload.incidentId,
59
+ screenshots: screenshotPath ? { original: screenshotPath } : undefined,
60
+ createdAt: now,
61
+ updatedAt: now,
62
+ };
63
+
64
+ this.db.insertNote(note);
65
+ return note;
66
+ }
67
+
68
+ public saveNoteScreenshot(projectId: string, noteId: string, name: string, dataUrl: string): string | null {
69
+ try {
70
+ if (!dataUrl || !dataUrl.startsWith('data:image/')) return null;
71
+
72
+ const match = dataUrl.match(/^data:image\/([a-zA-Z0-9+]+);base64,(.+)$/);
73
+ if (!match) return null;
74
+
75
+ let format = match[1].toLowerCase();
76
+ if (format === 'jpeg') format = 'jpg';
77
+ const base64Data = match[2];
78
+ const buffer = Buffer.from(base64Data, 'base64');
79
+
80
+ const targetDir = path.join((this.screenshotStore as any).baseDir, projectId || 'default', 'notes', noteId);
81
+ fs.mkdirSync(targetDir, { recursive: true });
82
+
83
+ const fileName = `${name}.${format}`;
84
+ const filePath = path.join(targetDir, fileName);
85
+
86
+ fs.writeFileSync(filePath, buffer);
87
+ return filePath;
88
+ } catch {
89
+ return null;
90
+ }
91
+ }
92
+ }