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,86 @@
|
|
|
1
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
2
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
3
|
+
import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { getDaemonConfig } from '../../daemon/src/config.js';
|
|
5
|
+
import { StorageDB } from '../../daemon/src/storage/db.js';
|
|
6
|
+
import { ScreenshotStore } from '../../daemon/src/storage/screenshot-store.js';
|
|
7
|
+
import { SessionManager } from '../../daemon/src/session/manager.js';
|
|
8
|
+
import { NotesEngine } from '../../daemon/src/notes/engine.js';
|
|
9
|
+
import { NoteVerificationEngine } from '../../daemon/src/notes/verification.js';
|
|
10
|
+
import { VerificationEngine } from '../../daemon/src/verification/engine.js';
|
|
11
|
+
import type { McpContext } from './handlers.js';
|
|
12
|
+
import { handleToolCall } from './handlers.js';
|
|
13
|
+
import { TOOLS } from './tools.js';
|
|
14
|
+
|
|
15
|
+
export interface McpServerOptions {
|
|
16
|
+
dbPath?: string;
|
|
17
|
+
context?: Partial<McpContext>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createMcpServer(options: McpServerOptions = {}) {
|
|
21
|
+
const config = getDaemonConfig({ dbPath: options.dbPath });
|
|
22
|
+
const db = options.context?.db || new StorageDB(config.dbPath);
|
|
23
|
+
const screenshotStore = new ScreenshotStore(config.screenshotsDir);
|
|
24
|
+
const sessionManager = options.context?.sessionManager || new SessionManager(db);
|
|
25
|
+
const verificationEngine = options.context?.verificationEngine || new VerificationEngine(db, sessionManager, screenshotStore);
|
|
26
|
+
const notesEngine = new NotesEngine(db, screenshotStore);
|
|
27
|
+
const noteVerificationEngine = options.context?.noteVerificationEngine || new NoteVerificationEngine(db, sessionManager, notesEngine);
|
|
28
|
+
|
|
29
|
+
const ctx: McpContext = {
|
|
30
|
+
db,
|
|
31
|
+
sessionManager,
|
|
32
|
+
verificationEngine,
|
|
33
|
+
noteVerificationEngine,
|
|
34
|
+
daemonUrl: `http://${config.host}:${config.port}`,
|
|
35
|
+
...options.context,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const server = new Server(
|
|
39
|
+
{
|
|
40
|
+
name: 'browsertrack-mcp',
|
|
41
|
+
version: '0.1.0',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
capabilities: {
|
|
45
|
+
tools: {},
|
|
46
|
+
},
|
|
47
|
+
}
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
51
|
+
return { tools: TOOLS };
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
55
|
+
const { name, arguments: args } = request.params;
|
|
56
|
+
try {
|
|
57
|
+
const result = await handleToolCall(name, args || {}, ctx);
|
|
58
|
+
return {
|
|
59
|
+
content: [
|
|
60
|
+
{
|
|
61
|
+
type: 'text',
|
|
62
|
+
text: JSON.stringify(result, null, 2),
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
} catch (err: any) {
|
|
67
|
+
return {
|
|
68
|
+
isError: true,
|
|
69
|
+
content: [
|
|
70
|
+
{
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: `Error executing tool ${name}: ${err?.message || String(err)}`,
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
server,
|
|
81
|
+
async startStdio() {
|
|
82
|
+
const transport = new StdioServerTransport();
|
|
83
|
+
await server.connect(transport);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import type { Tool } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
|
|
3
|
+
export const TOOLS: Tool[] = [
|
|
4
|
+
{
|
|
5
|
+
name: 'list_projects',
|
|
6
|
+
description: 'List all registered projects tracked by BrowserTrack/BrowserDiag',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {},
|
|
10
|
+
},
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'list_sessions',
|
|
14
|
+
description: 'List active and recent browser sessions connected to the local development daemon',
|
|
15
|
+
inputSchema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
projectId: { type: 'string', description: 'Filter sessions by project ID or name' },
|
|
19
|
+
activeOnly: { type: 'boolean', description: 'Show only currently active WebSocket sessions (default: true)' },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'list_incidents',
|
|
25
|
+
description: 'List recorded browser runtime errors, unhandled rejections, and console errors grouped into incidents',
|
|
26
|
+
inputSchema: {
|
|
27
|
+
type: 'object',
|
|
28
|
+
properties: {
|
|
29
|
+
projectId: { type: 'string', description: 'Filter by project ID or name' },
|
|
30
|
+
status: {
|
|
31
|
+
type: 'string',
|
|
32
|
+
enum: ['OPEN', 'FIX_ATTEMPTED', 'VERIFYING', 'VERIFIED', 'FAILED', 'INCONCLUSIVE'],
|
|
33
|
+
description: 'Filter by incident status',
|
|
34
|
+
},
|
|
35
|
+
severity: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
enum: ['error', 'warn', 'fatal'],
|
|
38
|
+
description: 'Filter by severity',
|
|
39
|
+
},
|
|
40
|
+
limit: { type: 'number', description: 'Maximum number of incidents to return (default: 20)' },
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'get_incident',
|
|
46
|
+
description: 'Retrieve compact and high-signal debugging context for a specific error incident (stack trace, breadcrumbs, network failures, last interacted element, error screenshot)',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
incidentId: { type: 'string', description: 'The unique ID of the incident (e.g. inc_42)' },
|
|
51
|
+
},
|
|
52
|
+
required: ['incidentId'],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'get_console',
|
|
57
|
+
description: 'Get recent console logs, warnings, and errors from a browser session',
|
|
58
|
+
inputSchema: {
|
|
59
|
+
type: 'object',
|
|
60
|
+
properties: {
|
|
61
|
+
sessionId: { type: 'string', description: 'Browser session ID (optional, defaults to active session)' },
|
|
62
|
+
limit: { type: 'number', description: 'Number of console logs to retrieve (default: 30)' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: 'get_network_failures',
|
|
68
|
+
description: 'Get recent failed HTTP network requests (4xx, 5xx, network errors, timeouts, aborts)',
|
|
69
|
+
inputSchema: {
|
|
70
|
+
type: 'object',
|
|
71
|
+
properties: {
|
|
72
|
+
sessionId: { type: 'string', description: 'Browser session ID (optional)' },
|
|
73
|
+
limit: { type: 'number', description: 'Maximum number of failed requests to return (default: 20)' },
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'get_breadcrumbs',
|
|
79
|
+
description: 'Get the sequence of recent user interactions, navigations, console logs, and network events prior to an error',
|
|
80
|
+
inputSchema: {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
sessionId: { type: 'string', description: 'Browser session ID (optional)' },
|
|
84
|
+
incidentId: { type: 'string', description: 'Incident ID to fetch breadcrumbs from (optional)' },
|
|
85
|
+
limit: { type: 'number', description: 'Maximum number of breadcrumbs (default: 50)' },
|
|
86
|
+
},
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'get_page_state',
|
|
91
|
+
description: 'Query the live page state (URL, route, document title, readyState, active element) from the connected browser session',
|
|
92
|
+
inputSchema: {
|
|
93
|
+
type: 'object',
|
|
94
|
+
properties: {
|
|
95
|
+
sessionId: { type: 'string', description: 'Target session ID (optional, defaults to active)' },
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'capture_element',
|
|
101
|
+
description: 'Capture a screenshot of a specific DOM element or the entire visible page in the active browser tab',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
properties: {
|
|
105
|
+
selector: { type: 'string', description: 'CSS selector of the element to capture (e.g. [data-testid="user-card"])' },
|
|
106
|
+
sessionId: { type: 'string', description: 'Browser session ID (optional)' },
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
name: 'verify_incident',
|
|
112
|
+
description: 'Trigger closed-loop verification of a bug fix: reloads the browser, checks if the incident reoccurs, evaluates optional verification probes, and records before/after screenshots',
|
|
113
|
+
inputSchema: {
|
|
114
|
+
type: 'object',
|
|
115
|
+
properties: {
|
|
116
|
+
incidentId: { type: 'string', description: 'The ID of the incident to verify' },
|
|
117
|
+
route: { type: 'string', description: 'Optional route to navigate to for verification (defaults to incident route)' },
|
|
118
|
+
targetSelector: { type: 'string', description: 'Optional element selector to inspect and capture after-fix screenshot' },
|
|
119
|
+
expect: {
|
|
120
|
+
type: 'array',
|
|
121
|
+
description: 'Optional verification probes to evaluate after reload',
|
|
122
|
+
items: {
|
|
123
|
+
type: 'object',
|
|
124
|
+
properties: {
|
|
125
|
+
type: {
|
|
126
|
+
type: 'string',
|
|
127
|
+
enum: ['element_exists', 'element_visible', 'text_contains', 'route_is', 'no_incident'],
|
|
128
|
+
},
|
|
129
|
+
selector: { type: 'string' },
|
|
130
|
+
text: { type: 'string' },
|
|
131
|
+
route: { type: 'string' },
|
|
132
|
+
},
|
|
133
|
+
required: ['type'],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
observationWindowMs: { type: 'number', description: 'Observation window in milliseconds (default: 2000)' },
|
|
137
|
+
},
|
|
138
|
+
required: ['incidentId'],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: 'get_verification',
|
|
143
|
+
description: 'Retrieve the latest verification result and before/after screenshot artifacts for an incident',
|
|
144
|
+
inputSchema: {
|
|
145
|
+
type: 'object',
|
|
146
|
+
properties: {
|
|
147
|
+
incidentId: { type: 'string', description: 'The ID of the incident' },
|
|
148
|
+
},
|
|
149
|
+
required: ['incidentId'],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
name: 'list_notes',
|
|
154
|
+
description: 'List visual development notes / screen annotations left on elements, regions, or pages during development',
|
|
155
|
+
inputSchema: {
|
|
156
|
+
type: 'object',
|
|
157
|
+
properties: {
|
|
158
|
+
projectId: { type: 'string', description: 'Filter notes by project ID or name' },
|
|
159
|
+
status: {
|
|
160
|
+
type: 'string',
|
|
161
|
+
enum: ['OPEN', 'IN_PROGRESS', 'VERIFYING', 'RESOLVED', 'FAILED', 'INCONCLUSIVE'],
|
|
162
|
+
description: 'Filter by note status (default: OPEN)',
|
|
163
|
+
},
|
|
164
|
+
limit: { type: 'number', description: 'Maximum number of notes to return (default: 20)' },
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
name: 'get_note',
|
|
170
|
+
description: 'Retrieve full debugging context for a visual note (message, route, viewport dimensions, target element selector, DOM context, screenshot file path, project path)',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
properties: {
|
|
174
|
+
noteId: { type: 'string', description: 'The unique ID of the visual note (e.g. note_42)' },
|
|
175
|
+
},
|
|
176
|
+
required: ['noteId'],
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'resolve_note',
|
|
181
|
+
description: 'Mark a visual note as RESOLVED once the layout or styling issue has been fixed',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
type: 'object',
|
|
184
|
+
properties: {
|
|
185
|
+
noteId: { type: 'string', description: 'The ID of the visual note to resolve' },
|
|
186
|
+
},
|
|
187
|
+
required: ['noteId'],
|
|
188
|
+
},
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'reopen_note',
|
|
192
|
+
description: 'Reopen a previously resolved visual note',
|
|
193
|
+
inputSchema: {
|
|
194
|
+
type: 'object',
|
|
195
|
+
properties: {
|
|
196
|
+
noteId: { type: 'string', description: 'The ID of the visual note to reopen' },
|
|
197
|
+
},
|
|
198
|
+
required: ['noteId'],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
name: 'verify_note',
|
|
203
|
+
description: 'Run closed-loop layout & visual verification for a note: checks route, element existence/visibility, evaluates viewport overflow probes, captures after-screenshot, and computes geometry diff',
|
|
204
|
+
inputSchema: {
|
|
205
|
+
type: 'object',
|
|
206
|
+
properties: {
|
|
207
|
+
noteId: { type: 'string', description: 'The ID of the visual note to verify' },
|
|
208
|
+
observationWindowMs: { type: 'number', description: 'Observation window in milliseconds (default: 1000)' },
|
|
209
|
+
},
|
|
210
|
+
required: ['noteId'],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: 'get_note_verification',
|
|
215
|
+
description: 'Retrieve the latest verification result, before/after screenshot references, and layout geometry diff for a visual note',
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: 'object',
|
|
218
|
+
properties: {
|
|
219
|
+
noteId: { type: 'string', description: 'The ID of the visual note' },
|
|
220
|
+
},
|
|
221
|
+
required: ['noteId'],
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
name: 'capture_note_context',
|
|
226
|
+
description: 'Inspect live DOM context, bounding box, overflow, and styles for a target element in the active browser tab',
|
|
227
|
+
inputSchema: {
|
|
228
|
+
type: 'object',
|
|
229
|
+
properties: {
|
|
230
|
+
selector: { type: 'string', description: 'CSS selector of the target element' },
|
|
231
|
+
sessionId: { type: 'string', description: 'Browser session ID (optional)' },
|
|
232
|
+
},
|
|
233
|
+
required: ['selector'],
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
];
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export * from '../packages/core/src/index.js';
|
|
2
|
+
export { init as initClient, getClient, BrowserTrackClient } from '../packages/client/src/index.js';
|
|
3
|
+
export { createDaemon, BrowserTrackDaemon } from '../packages/daemon/src/index.js';
|
|
4
|
+
export { createMcpServer } from '../packages/mcp/src/index.js';
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { BreadcrumbBuffer } from '../../packages/client/src/breadcrumbs.js';
|
|
3
|
+
import { setupConsoleInterceptors } from '../../packages/client/src/interceptors/console.js';
|
|
4
|
+
import { getSemanticSelector } from '../../packages/core/src/selector.js';
|
|
5
|
+
|
|
6
|
+
describe('Client Interceptors & Utilities', () => {
|
|
7
|
+
it('should patch console without breaking original console calls', () => {
|
|
8
|
+
const originalError = console.error;
|
|
9
|
+
const captured: any[] = [];
|
|
10
|
+
|
|
11
|
+
const cleanup = setupConsoleInterceptors((event) => {
|
|
12
|
+
captured.push(event);
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
console.error('Test error message', { code: 500 });
|
|
16
|
+
|
|
17
|
+
expect(captured.length).toBe(1);
|
|
18
|
+
expect(captured[0].level).toBe('error');
|
|
19
|
+
expect(captured[0].message).toContain('Test error message');
|
|
20
|
+
|
|
21
|
+
cleanup();
|
|
22
|
+
|
|
23
|
+
// Verify restore
|
|
24
|
+
expect(console.error).toBe(originalError);
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('should maintain bounded ring buffer capacity in BreadcrumbBuffer', () => {
|
|
28
|
+
const buffer = new BreadcrumbBuffer(5);
|
|
29
|
+
|
|
30
|
+
for (let i = 1; i <= 10; i++) {
|
|
31
|
+
buffer.add({
|
|
32
|
+
type: 'click',
|
|
33
|
+
message: `click ${i}`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const recent = buffer.getRecent();
|
|
38
|
+
expect(recent.length).toBe(5);
|
|
39
|
+
expect(recent[0].message).toBe('click 6');
|
|
40
|
+
expect(recent[4].message).toBe('click 10');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should produce semantic selector with data-testid priority', () => {
|
|
44
|
+
const fakeElement = {
|
|
45
|
+
tagName: 'BUTTON',
|
|
46
|
+
getAttribute: (attr: string) => {
|
|
47
|
+
if (attr === 'data-testid') return 'submit-btn';
|
|
48
|
+
return null;
|
|
49
|
+
},
|
|
50
|
+
id: 'btn-1',
|
|
51
|
+
classList: ['primary-btn'],
|
|
52
|
+
} as any;
|
|
53
|
+
|
|
54
|
+
const selector = getSemanticSelector(fakeElement);
|
|
55
|
+
expect(selector).toBe('[data-testid="submit-btn"]');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('should produce semantic selector with ID if no testid', () => {
|
|
59
|
+
const fakeElement = {
|
|
60
|
+
tagName: 'DIV',
|
|
61
|
+
getAttribute: () => null,
|
|
62
|
+
id: 'main-container',
|
|
63
|
+
classList: ['app-body'],
|
|
64
|
+
} as any;
|
|
65
|
+
|
|
66
|
+
const selector = getSemanticSelector(fakeElement);
|
|
67
|
+
expect(selector).toBe('#main-container');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { computeFingerprint, normalizeErrorMessage, normalizeSourceFile } from '../../packages/core/src/fingerprint.js';
|
|
3
|
+
|
|
4
|
+
describe('Fingerprint Engine', () => {
|
|
5
|
+
it('should normalize error messages with UUIDs, hex values, and timestamps', () => {
|
|
6
|
+
const raw = 'Failed at 0x7ffee4b2a8 with user c9bf9e57-1685-4c89-bafb-ff5af830be8a on 2026-08-31T22:30:00Z';
|
|
7
|
+
const normalized = normalizeErrorMessage(raw);
|
|
8
|
+
expect(normalized).toBe('Failed at 0x<HEX> with user <UUID> on <TIMESTAMP>');
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should normalize source file paths by stripping query parameters and origins', () => {
|
|
12
|
+
expect(normalizeSourceFile('http://localhost:5173/src/App.tsx?t=1719283')).toBe('/src/App.tsx');
|
|
13
|
+
expect(normalizeSourceFile('https://example.com/assets/main.js#line40')).toBe('/assets/main.js');
|
|
14
|
+
expect(normalizeSourceFile('C:\\Users\\dev\\project\\src\\main.ts')).toBe('C:/Users/dev/project/src/main.ts');
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should produce deterministic identical fingerprints for duplicate errors', () => {
|
|
18
|
+
const fp1 = computeFingerprint({
|
|
19
|
+
type: 'TypeError',
|
|
20
|
+
message: 'Cannot read properties of undefined (reading "id")',
|
|
21
|
+
sourceFile: 'http://localhost:5173/src/components/User.tsx?t=100',
|
|
22
|
+
line: 42,
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const fp2 = computeFingerprint({
|
|
26
|
+
type: 'TypeError',
|
|
27
|
+
message: 'Cannot read properties of undefined (reading "id")',
|
|
28
|
+
sourceFile: 'http://localhost:5173/src/components/User.tsx?t=999',
|
|
29
|
+
line: 42,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(fp1).toBe(fp2);
|
|
33
|
+
expect(fp1.startsWith('fp_')).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should produce distinct fingerprints for errors at different lines or files', () => {
|
|
37
|
+
const fp1 = computeFingerprint({
|
|
38
|
+
type: 'TypeError',
|
|
39
|
+
message: 'Cannot read properties of undefined',
|
|
40
|
+
sourceFile: '/src/App.tsx',
|
|
41
|
+
line: 10,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const fp2 = computeFingerprint({
|
|
45
|
+
type: 'TypeError',
|
|
46
|
+
message: 'Cannot read properties of undefined',
|
|
47
|
+
sourceFile: '/src/App.tsx',
|
|
48
|
+
line: 25,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(fp1).not.toBe(fp2);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import type { VisualNote } from '../../packages/core/src/index.js';
|
|
3
|
+
import { getSemanticSelector, truncate } from '../../packages/core/src/index.js';
|
|
4
|
+
|
|
5
|
+
describe('Core Visual Notes Types & Helpers', () => {
|
|
6
|
+
it('constructs a valid VisualNote object', () => {
|
|
7
|
+
const note: VisualNote = {
|
|
8
|
+
id: 'note_12345',
|
|
9
|
+
projectId: 'proj_test',
|
|
10
|
+
sessionId: 'sess_test',
|
|
11
|
+
type: 'element',
|
|
12
|
+
message: "Mobile'da buraya bak, sağa taşıyor",
|
|
13
|
+
route: '/dashboard',
|
|
14
|
+
url: 'http://localhost:3000/dashboard',
|
|
15
|
+
viewport: {
|
|
16
|
+
width: 390,
|
|
17
|
+
height: 844,
|
|
18
|
+
devicePixelRatio: 3,
|
|
19
|
+
},
|
|
20
|
+
scroll: {
|
|
21
|
+
scrollX: 0,
|
|
22
|
+
scrollY: 120,
|
|
23
|
+
},
|
|
24
|
+
target: {
|
|
25
|
+
selector: '[data-testid="stats-card"]',
|
|
26
|
+
boundingRect: {
|
|
27
|
+
x: 20,
|
|
28
|
+
y: 100,
|
|
29
|
+
width: 420,
|
|
30
|
+
height: 180,
|
|
31
|
+
top: 100,
|
|
32
|
+
left: 20,
|
|
33
|
+
bottom: 280,
|
|
34
|
+
right: 440,
|
|
35
|
+
},
|
|
36
|
+
visible: true,
|
|
37
|
+
confidence: 'high',
|
|
38
|
+
},
|
|
39
|
+
elementContext: {
|
|
40
|
+
selector: '[data-testid="stats-card"]',
|
|
41
|
+
tag: 'section',
|
|
42
|
+
attributes: {
|
|
43
|
+
class: 'stats-card responsive',
|
|
44
|
+
'data-testid': 'stats-card',
|
|
45
|
+
},
|
|
46
|
+
outerHTML: '<section class="stats-card responsive" data-testid="stats-card"><div>142K</div></section>',
|
|
47
|
+
},
|
|
48
|
+
status: 'OPEN',
|
|
49
|
+
createdAt: new Date().toISOString(),
|
|
50
|
+
updatedAt: new Date().toISOString(),
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
expect(note.id).toBe('note_12345');
|
|
54
|
+
expect(note.viewport.width).toBe(390);
|
|
55
|
+
expect(note.target?.boundingRect.right).toBe(440);
|
|
56
|
+
expect(note.target?.boundingRect.right).toBeGreaterThan(note.viewport.width);
|
|
57
|
+
expect(note.status).toBe('OPEN');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('truncates large outerHTML properly', () => {
|
|
61
|
+
const longHtml = '<div>' + 'a'.repeat(20000) + '</div>';
|
|
62
|
+
const truncated = truncate(longHtml, 10240);
|
|
63
|
+
expect(truncated.length).toBeLessThanOrEqual(10243);
|
|
64
|
+
expect(truncated.endsWith('...')).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { redactHeaders, redactSensitiveData, redactUrl, REDACTED_PLACEHOLDER } from '../../packages/core/src/redaction.js';
|
|
3
|
+
|
|
4
|
+
describe('Redaction & Security', () => {
|
|
5
|
+
it('should redact sensitive query parameters in URLs', () => {
|
|
6
|
+
const raw = 'http://localhost:3000/api/users?token=supersecret123&page=1&apiKey=xyz987';
|
|
7
|
+
const redacted = redactUrl(raw);
|
|
8
|
+
|
|
9
|
+
expect(redacted).toContain(`token=${REDACTED_PLACEHOLDER}`);
|
|
10
|
+
expect(redacted).toContain(`apiKey=${REDACTED_PLACEHOLDER}`);
|
|
11
|
+
expect(redacted).toContain('page=1');
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('should redact sensitive headers', () => {
|
|
15
|
+
const headers = {
|
|
16
|
+
Authorization: 'Bearer eyJhbGciOi...',
|
|
17
|
+
Cookie: 'session_id=12345; auth_token=abc',
|
|
18
|
+
'Content-Type': 'application/json',
|
|
19
|
+
'X-Api-Key': 'secret-key-1',
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const redacted = redactHeaders(headers);
|
|
23
|
+
expect(redacted['Authorization']).toBe(REDACTED_PLACEHOLDER);
|
|
24
|
+
expect(redacted['Cookie']).toBe(REDACTED_PLACEHOLDER);
|
|
25
|
+
expect(redacted['X-Api-Key']).toBe(REDACTED_PLACEHOLDER);
|
|
26
|
+
expect(redacted['Content-Type']).toBe('application/json');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should deeply redact sensitive keys in nested payloads', () => {
|
|
30
|
+
const payload = {
|
|
31
|
+
user: {
|
|
32
|
+
id: 42,
|
|
33
|
+
name: 'Alice',
|
|
34
|
+
password: 'PlainTextPassword!',
|
|
35
|
+
auth: {
|
|
36
|
+
accessToken: 'secret_token_val',
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
redirectUrl: 'http://example.com/callback?code=secret_oauth_code',
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const redacted = redactSensitiveData(payload);
|
|
43
|
+
expect(redacted.user.name).toBe('Alice');
|
|
44
|
+
expect(redacted.user.password).toBe(REDACTED_PLACEHOLDER);
|
|
45
|
+
expect(redacted.user.auth.accessToken).toBe(REDACTED_PLACEHOLDER);
|
|
46
|
+
expect(redacted.redirectUrl).toContain(REDACTED_PLACEHOLDER);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import { IncidentEngine } from '../../packages/daemon/src/incidents/engine.js';
|
|
6
|
+
import { StorageDB } from '../../packages/daemon/src/storage/db.js';
|
|
7
|
+
import { ScreenshotStore } from '../../packages/daemon/src/storage/screenshot-store.js';
|
|
8
|
+
|
|
9
|
+
describe('IncidentEngine', () => {
|
|
10
|
+
let tempDir: string;
|
|
11
|
+
let db: StorageDB;
|
|
12
|
+
let screenshotStore: ScreenshotStore;
|
|
13
|
+
let engine: IncidentEngine;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'bt_inc_test_'));
|
|
17
|
+
const dbPath = path.join(tempDir, 'test.db');
|
|
18
|
+
const screenshotsDir = path.join(tempDir, 'screenshots');
|
|
19
|
+
|
|
20
|
+
db = new StorageDB(dbPath);
|
|
21
|
+
screenshotStore = new ScreenshotStore(screenshotsDir);
|
|
22
|
+
engine = new IncidentEngine(db, screenshotStore);
|
|
23
|
+
|
|
24
|
+
// Setup project and session
|
|
25
|
+
db.upsertProject({ id: 'proj_app', name: 'app', origin: 'http://localhost:5173' });
|
|
26
|
+
db.upsertSession({
|
|
27
|
+
id: 'sess_100',
|
|
28
|
+
projectId: 'proj_app',
|
|
29
|
+
origin: 'http://localhost:5173',
|
|
30
|
+
url: 'http://localhost:5173/dashboard',
|
|
31
|
+
title: 'Dashboard',
|
|
32
|
+
userAgent: 'Chrome/120',
|
|
33
|
+
connectedAt: new Date().toISOString(),
|
|
34
|
+
lastSeenAt: new Date().toISOString(),
|
|
35
|
+
active: true,
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
db.close();
|
|
41
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should create a new incident from a runtime error event', () => {
|
|
45
|
+
const incident = engine.processClientEvent({
|
|
46
|
+
type: 'event',
|
|
47
|
+
sessionId: 'sess_100',
|
|
48
|
+
eventType: 'runtime_error',
|
|
49
|
+
payload: {
|
|
50
|
+
errorType: 'TypeError',
|
|
51
|
+
message: "Cannot read properties of null (reading 'userName')",
|
|
52
|
+
filename: 'http://localhost:5173/src/User.tsx?t=12345',
|
|
53
|
+
lineno: 28,
|
|
54
|
+
colno: 5,
|
|
55
|
+
timestamp: Date.now(),
|
|
56
|
+
},
|
|
57
|
+
route: '/dashboard',
|
|
58
|
+
url: 'http://localhost:5173/dashboard',
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(incident).not.toBeNull();
|
|
63
|
+
expect(incident?.id.startsWith('inc_')).toBe(true);
|
|
64
|
+
expect(incident?.occurrences).toBe(1);
|
|
65
|
+
expect(incident?.status).toBe('OPEN');
|
|
66
|
+
expect(incident?.source.file).toBe('/src/User.tsx');
|
|
67
|
+
expect(incident?.source.line).toBe(28);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should deduplicate recurring errors and increment occurrences', () => {
|
|
71
|
+
const eventPayload = {
|
|
72
|
+
type: 'event' as const,
|
|
73
|
+
sessionId: 'sess_100',
|
|
74
|
+
eventType: 'runtime_error' as const,
|
|
75
|
+
payload: {
|
|
76
|
+
errorType: 'ReferenceError',
|
|
77
|
+
message: 'AuthService is not defined',
|
|
78
|
+
filename: '/src/services/auth.ts',
|
|
79
|
+
lineno: 14,
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
},
|
|
82
|
+
route: '/login',
|
|
83
|
+
url: 'http://localhost:5173/login',
|
|
84
|
+
timestamp: Date.now(),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const first = engine.processClientEvent(eventPayload);
|
|
88
|
+
const second = engine.processClientEvent(eventPayload);
|
|
89
|
+
const third = engine.processClientEvent(eventPayload);
|
|
90
|
+
|
|
91
|
+
expect(first?.id).toBe(second?.id);
|
|
92
|
+
expect(first?.id).toBe(third?.id);
|
|
93
|
+
expect(third?.occurrences).toBe(3);
|
|
94
|
+
|
|
95
|
+
const fromDb = db.getIncident(first!.id);
|
|
96
|
+
expect(fromDb?.occurrences).toBe(3);
|
|
97
|
+
});
|
|
98
|
+
});
|