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
package/README.md ADDED
@@ -0,0 +1,143 @@
1
+ # BrowserTrack 🔍
2
+
3
+ > **Local browser diagnostics shared with coding agents through MCP.**
4
+
5
+ BrowserTrack is a lightweight, framework-agnostic local development daemon and Model Context Protocol (MCP) bridge. It automatically collects browser-side runtime errors, unhandled rejections, console logs, network failures, user breadcrumbs, and DOM snapshots—making them instantly accessible to AI coding assistants (Antigravity, Cursor, Claude Code) for rapid debugging and closed-loop verification.
6
+
7
+ ---
8
+
9
+ ## ⚡ Quick Start
10
+
11
+ ### 1. Start the Local Daemon
12
+
13
+ ```bash
14
+ npm install -g browsertrack
15
+ browsertrack start
16
+ ```
17
+
18
+ This starts the local diagnostics server at `http://127.0.0.1:7331` (WebSocket at `ws://127.0.0.1:7331`).
19
+
20
+ ### 2. Connect Your Web Project
21
+
22
+ #### Option A: Zero-install script tag (Plain HTML / Vite / Next.js)
23
+
24
+ ```html
25
+ <script src="http://127.0.0.1:7331/client.js"></script>
26
+ ```
27
+
28
+ #### Option B: NPM package (React / Vue / Svelte / Astro)
29
+
30
+ ```bash
31
+ npm install -D browsertrack
32
+ ```
33
+
34
+ In your main application entry point (e.g. `main.ts` or `index.tsx`):
35
+
36
+ ```typescript
37
+ import 'browsertrack/client';
38
+ ```
39
+
40
+ ---
41
+
42
+ ## 🤖 Coding Agent (MCP) Setup
43
+
44
+ Add BrowserTrack to your MCP client configuration:
45
+
46
+ ### Cursor (`~/.cursor/mcp.json`) & Claude Code
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "browsertrack": {
51
+ "command": "npx",
52
+ "args": ["browsertrack", "mcp"]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ### Antigravity (`~/.gemini/antigravity-ide/mcp_config.json`)
59
+ ```json
60
+ {
61
+ "mcpServers": {
62
+ "browsertrack": {
63
+ "command": "browsertrack",
64
+ "args": ["mcp"]
65
+ }
66
+ }
67
+ }
68
+ ```
69
+
70
+ ---
71
+
72
+ ## 🛠️ MCP Tools Exposed to AI Agents
73
+
74
+ | MCP Tool | Description |
75
+ | :--- | :--- |
76
+ | `list_projects` | Lists registered local development projects and mapped filesystem paths. |
77
+ | `list_sessions` | Lists active browser tabs connected over WebSocket. |
78
+ | `list_incidents` | Lists grouped error incidents with occurrences, severity, and status. |
79
+ | `get_incident` | Retrieves concise, high-signal context (stack trace, breadcrumbs, network failures, last interacted element, error screenshot). |
80
+ | `get_console` | Retrieves recent console errors/warnings. |
81
+ | `get_network_failures` | Retrieves recent 4xx/5xx HTTP failures and aborted requests. |
82
+ | `get_breadcrumbs` | Returns user interaction timeline leading up to an error. |
83
+ | `get_page_state` | Queries live DOM/URL state from the active browser tab. |
84
+ | `capture_element` | Captures an on-demand screenshot of a specific element or full page. |
85
+ | `verify_incident` | Runs closed-loop verification: reloads browser, checks if error reoccurs, executes probes, captures after-screenshot, and returns verdict (`VERIFIED`, `FAILED`, `INCONCLUSIVE`). |
86
+ | `get_verification` | Returns latest verification result and before/after screenshot artifacts. |
87
+
88
+ ---
89
+
90
+ ## 💻 CLI Commands
91
+
92
+ ```bash
93
+ browsertrack start # Start local daemon
94
+ browsertrack start --verbose # Verbose logging mode
95
+ browsertrack stop # Stop background daemon
96
+ browsertrack status # Check status & connected sessions
97
+
98
+ browsertrack projects # List tracked projects
99
+ browsertrack project add myapp --origin http://localhost:5173 --path ~/projects/myapp
100
+
101
+ browsertrack errors # List open runtime errors
102
+ browsertrack errors --project myapp
103
+
104
+ browsertrack clear # Clear stored logs & incidents
105
+ browsertrack mcp # Run MCP server over stdio
106
+ ```
107
+
108
+ ---
109
+
110
+ ## 🛡️ Security & Privacy
111
+
112
+ - **Localhost Only**: Daemon strictly binds to `127.0.0.1`.
113
+ - **Sensitive Key Redaction**: `Authorization`, `Cookie`, `Set-Cookie`, passwords, API tokens, and secret parameters are automatically sanitized.
114
+ - **No Form Values**: Form inputs (`<input>`, `<textarea>`) are never inspected or collected.
115
+ - **Zero Request/Response Body**: Payload bodies are disabled by default.
116
+
117
+ ---
118
+
119
+ ## 🏗️ Architecture
120
+
121
+ ```text
122
+ Browser Client (Vanilla / React / Vue / Vite)
123
+ ↕ WebSocket (ws://127.0.0.1:7331)
124
+ Local Daemon (SQLite Persistence + Verification Engine)
125
+ ↕ MCP (stdio)
126
+ Coding Agents (Antigravity / Cursor / Claude Code)
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 🧪 Testing
132
+
133
+ Run the automated test suite:
134
+
135
+ ```bash
136
+ npm test
137
+ ```
138
+
139
+ Start the interactive fixture app for manual and end-to-end verification:
140
+
141
+ ```bash
142
+ npx serve examples/test-app
143
+ ```
@@ -0,0 +1,221 @@
1
+ // packages/core/src/fingerprint.ts
2
+ function normalizeErrorMessage(message) {
3
+ if (!message) return "unknown_error";
4
+ return message.trim().replace(/0x[0-9a-fA-F]+/g, "0x<HEX>").replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g, "<UUID>").replace(/\?[tv]=[\w.-]+/g, "").replace(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?/g, "<TIMESTAMP>").replace(/#\d+/g, "#<ID>").replace(/\s+/g, " ");
5
+ }
6
+ function normalizeSourceFile(filename) {
7
+ if (!filename) return "unknown_source";
8
+ let cleaned = filename.trim();
9
+ try {
10
+ if (cleaned.startsWith("http://") || cleaned.startsWith("https://")) {
11
+ const url = new URL(cleaned);
12
+ cleaned = url.pathname;
13
+ }
14
+ } catch {
15
+ cleaned = cleaned.replace(/^https?:\/\/[^/]+/, "");
16
+ }
17
+ cleaned = cleaned.split("?")[0].split("#")[0];
18
+ cleaned = cleaned.replace(/\\/g, "/");
19
+ return cleaned || "unknown_source";
20
+ }
21
+ function extractSourceFromStack(stack) {
22
+ if (!stack) return null;
23
+ const lines = stack.split("\n");
24
+ for (const line of lines) {
25
+ const match = line.match(/(?:at\s+(?:.*?\s+\()?)?(https?:\/\/[^\s)]+|file:\/\/[^\s)]+|\/[^\s)]+):(\d+):(\d+)\)?/);
26
+ if (match) {
27
+ return {
28
+ file: normalizeSourceFile(match[1]),
29
+ line: parseInt(match[2], 10),
30
+ column: parseInt(match[3], 10)
31
+ };
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ function djb2Hash(str) {
37
+ let hash = 5381;
38
+ for (let i = 0; i < str.length; i++) {
39
+ hash = hash * 33 ^ str.charCodeAt(i);
40
+ }
41
+ return (hash >>> 0).toString(16).padStart(8, "0");
42
+ }
43
+ function computeFingerprint(input) {
44
+ const normType = (input.type || "Error").trim().toLowerCase();
45
+ const normMsg = normalizeErrorMessage(input.message);
46
+ let sourceFile = normalizeSourceFile(input.sourceFile);
47
+ let line = input.line || 0;
48
+ if ((sourceFile === "unknown_source" || line === 0) && input.stack) {
49
+ const extracted = extractSourceFromStack(input.stack);
50
+ if (extracted) {
51
+ sourceFile = extracted.file;
52
+ line = extracted.line;
53
+ }
54
+ }
55
+ const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;
56
+ const hash = djb2Hash(rawKey);
57
+ return `fp_${hash}`;
58
+ }
59
+
60
+ // packages/core/src/redaction.ts
61
+ var SENSITIVE_KEY_PATTERNS = [
62
+ /^authorization$/i,
63
+ /^cookie$/i,
64
+ /^set-cookie$/i,
65
+ /password/i,
66
+ /token/i,
67
+ /secret/i,
68
+ /api[-_]?key/i,
69
+ /access[-_]?token/i,
70
+ /refresh[-_]?token/i,
71
+ /credentials/i,
72
+ /private[-_]?key/i,
73
+ /ssn/i,
74
+ /credit[-_]?card/i,
75
+ /cvv/i
76
+ ];
77
+ var SENSITIVE_QUERY_PARAMS = [
78
+ "token",
79
+ "auth",
80
+ "key",
81
+ "apikey",
82
+ "api_key",
83
+ "secret",
84
+ "password",
85
+ "access_token",
86
+ "refresh_token",
87
+ "code",
88
+ "signature"
89
+ ];
90
+ var REDACTED_PLACEHOLDER = "[REDACTED]";
91
+ function isSensitiveKey(key) {
92
+ if (!key) return false;
93
+ const cleaned = key.replace(/[-_]/g, "");
94
+ return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));
95
+ }
96
+ function redactUrl(rawUrl) {
97
+ if (!rawUrl) return rawUrl;
98
+ try {
99
+ const isRelative = !rawUrl.startsWith("http://") && !rawUrl.startsWith("https://") && !rawUrl.startsWith("ws://") && !rawUrl.startsWith("wss://");
100
+ const base = "http://localhost";
101
+ const parsed = new URL(rawUrl, base);
102
+ let changed = false;
103
+ for (const param of SENSITIVE_QUERY_PARAMS) {
104
+ if (parsed.searchParams.has(param)) {
105
+ parsed.searchParams.set(param, REDACTED_PLACEHOLDER);
106
+ changed = true;
107
+ }
108
+ }
109
+ for (const key of Array.from(parsed.searchParams.keys())) {
110
+ if (isSensitiveKey(key)) {
111
+ parsed.searchParams.set(key, REDACTED_PLACEHOLDER);
112
+ changed = true;
113
+ }
114
+ }
115
+ if (!changed) return rawUrl;
116
+ let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();
117
+ result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);
118
+ return result;
119
+ } catch {
120
+ let safe = rawUrl;
121
+ for (const param of SENSITIVE_QUERY_PARAMS) {
122
+ const reg = new RegExp(`([?&]${param}=)[^&#]+`, "gi");
123
+ safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);
124
+ }
125
+ return safe;
126
+ }
127
+ }
128
+ function redactHeaders(headers) {
129
+ if (!headers || typeof headers !== "object") return {};
130
+ const safe = {};
131
+ for (const [key, value] of Object.entries(headers)) {
132
+ if (isSensitiveKey(key)) {
133
+ safe[key] = REDACTED_PLACEHOLDER;
134
+ } else {
135
+ safe[key] = value;
136
+ }
137
+ }
138
+ return safe;
139
+ }
140
+ function redactSensitiveData(data, maxDepth = 6, currentDepth = 0) {
141
+ if (data === null || data === void 0) return data;
142
+ if (typeof data !== "object") return data;
143
+ if (currentDepth > maxDepth) return "[DEPTH_EXCEEDED]";
144
+ if (Array.isArray(data)) {
145
+ return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1));
146
+ }
147
+ const result = {};
148
+ for (const [key, value] of Object.entries(data)) {
149
+ if (typeof value === "object" && value !== null) {
150
+ result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);
151
+ } else if (isSensitiveKey(key)) {
152
+ result[key] = REDACTED_PLACEHOLDER;
153
+ } else if (typeof value === "string") {
154
+ if (value.startsWith("http://") || value.startsWith("https://") || value.includes("?")) {
155
+ result[key] = redactUrl(value);
156
+ } else {
157
+ result[key] = value;
158
+ }
159
+ } else {
160
+ result[key] = value;
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+
166
+ // packages/core/src/selector.ts
167
+ function getSemanticSelector(element, options = {}) {
168
+ if (!element || !element.tagName) return "unknown";
169
+ const maxClasses = options.maxClasses ?? 2;
170
+ const testId = element.getAttribute("data-testid") || element.getAttribute("data-test") || element.getAttribute("data-cy") || element.getAttribute("data-qa");
171
+ if (testId) {
172
+ return `[data-testid="${testId}"]`;
173
+ }
174
+ const id = element.id;
175
+ if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {
176
+ return `#${id}`;
177
+ }
178
+ const ariaLabel = element.getAttribute("aria-label");
179
+ if (ariaLabel && ariaLabel.length < 30) {
180
+ return `${element.tagName.toLowerCase()}[aria-label="${ariaLabel}"]`;
181
+ }
182
+ const nameAttr = element.getAttribute("name");
183
+ if (nameAttr) {
184
+ return `${element.tagName.toLowerCase()}[name="${nameAttr}"]`;
185
+ }
186
+ const tag = element.tagName.toLowerCase();
187
+ const classList = Array.from(element.classList || []).filter((c) => !c.startsWith("css-") && !c.startsWith("_") && !/^[a-z0-9]{5,}$/i.test(c)).slice(0, maxClasses);
188
+ if (classList.length > 0) {
189
+ const classStr = classList.map((c) => `.${c}`).join("");
190
+ return `${tag}${classStr}`;
191
+ }
192
+ if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {
193
+ const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });
194
+ if (parentSelector && parentSelector !== "unknown" && !parentSelector.includes(">")) {
195
+ return `${parentSelector} > ${tag}`;
196
+ }
197
+ }
198
+ return tag;
199
+ }
200
+ function truncate(str, maxLength = 200) {
201
+ if (!str) return "";
202
+ if (str.length <= maxLength) return str;
203
+ return str.slice(0, maxLength) + "...";
204
+ }
205
+
206
+ export {
207
+ normalizeErrorMessage,
208
+ normalizeSourceFile,
209
+ extractSourceFromStack,
210
+ computeFingerprint,
211
+ SENSITIVE_KEY_PATTERNS,
212
+ SENSITIVE_QUERY_PARAMS,
213
+ REDACTED_PLACEHOLDER,
214
+ isSensitiveKey,
215
+ redactUrl,
216
+ redactHeaders,
217
+ redactSensitiveData,
218
+ getSemanticSelector,
219
+ truncate
220
+ };
221
+ //# sourceMappingURL=chunk-6A7FFDIB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../packages/core/src/fingerprint.ts","../packages/core/src/redaction.ts","../packages/core/src/selector.ts"],"sourcesContent":["/**\n * Error fingerprinting and normalization utilities\n */\n\nexport interface FingerprintInput {\n type: string;\n message: string;\n sourceFile?: string;\n line?: number;\n column?: number;\n stack?: string;\n}\n\n/**\n * Normalizes error messages by stripping dynamic content like UUIDs, timestamps,\n * hex addresses, and file timestamps.\n */\nexport function normalizeErrorMessage(message: string): string {\n if (!message) return 'unknown_error';\n\n return message\n // Normalize string representation\n .trim()\n // Replace hex addresses (e.g. 0x7ffee4b2a8)\n .replace(/0x[0-9a-fA-F]+/g, '0x<HEX>')\n // Replace UUIDs\n .replace(/[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g, '<UUID>')\n // Replace timestamp query params like ?t=1719283748 or ?v=1.2.3\n .replace(/\\?[tv]=[\\w.-]+/g, '')\n // Replace ISO timestamps\n .replace(/\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}(\\.\\d+)?Z?/g, '<TIMESTAMP>')\n // Replace numbers in quotes/brackets if likely IDs (e.g., user \"12345\")\n .replace(/#\\d+/g, '#<ID>')\n // Normalize whitespaces\n .replace(/\\s+/g, ' ');\n}\n\n/**\n * Normalizes source filenames by removing origin and cache-busting query strings.\n */\nexport function normalizeSourceFile(filename?: string): string {\n if (!filename) return 'unknown_source';\n\n let cleaned = filename.trim();\n // Strip protocol and origin if present (e.g. http://localhost:5173/src/App.tsx?t=123)\n try {\n if (cleaned.startsWith('http://') || cleaned.startsWith('https://')) {\n const url = new URL(cleaned);\n cleaned = url.pathname;\n }\n } catch {\n // fallback string manipulation\n cleaned = cleaned.replace(/^https?:\\/\\/[^/]+/, '');\n }\n\n // Remove query parameters\n cleaned = cleaned.split('?')[0].split('#')[0];\n\n // Normalize windows path separators\n cleaned = cleaned.replace(/\\\\/g, '/');\n\n return cleaned || 'unknown_source';\n}\n\n/**\n * Extracts source location from stack if not explicitly provided\n */\nexport function extractSourceFromStack(stack?: string): { file: string; line: number; column?: number } | null {\n if (!stack) return null;\n\n const lines = stack.split('\\n');\n for (const line of lines) {\n // Match standard stack line: at functionName (http://localhost:5173/src/main.tsx:15:3) or at http://...:15:3\n const match = line.match(/(?:at\\s+(?:.*?\\s+\\()?)?(https?:\\/\\/[^\\s)]+|file:\\/\\/[^\\s)]+|\\/[^\\s)]+):(\\d+):(\\d+)\\)?/);\n if (match) {\n return {\n file: normalizeSourceFile(match[1]),\n line: parseInt(match[2], 10),\n column: parseInt(match[3], 10),\n };\n }\n }\n return null;\n}\n\n/**\n * Fast synchronous djb2-based hash for universal client/server runtime\n */\nfunction djb2Hash(str: string): string {\n let hash = 5381;\n for (let i = 0; i < str.length; i++) {\n hash = (hash * 33) ^ str.charCodeAt(i);\n }\n // Convert to unsigned 32-bit hex string\n return (hash >>> 0).toString(16).padStart(8, '0');\n}\n\n/**\n * Computes a deterministic fingerprint for an error\n */\nexport function computeFingerprint(input: FingerprintInput): string {\n const normType = (input.type || 'Error').trim().toLowerCase();\n const normMsg = normalizeErrorMessage(input.message);\n\n let sourceFile = normalizeSourceFile(input.sourceFile);\n let line = input.line || 0;\n\n if ((sourceFile === 'unknown_source' || line === 0) && input.stack) {\n const extracted = extractSourceFromStack(input.stack);\n if (extracted) {\n sourceFile = extracted.file;\n line = extracted.line;\n }\n }\n\n const rawKey = `${normType}::${normMsg}::${sourceFile}::${line}`;\n const hash = djb2Hash(rawKey);\n\n return `fp_${hash}`;\n}\n","/**\n * Security and Redaction Utilities\n */\n\nexport const SENSITIVE_KEY_PATTERNS = [\n /^authorization$/i,\n /^cookie$/i,\n /^set-cookie$/i,\n /password/i,\n /token/i,\n /secret/i,\n /api[-_]?key/i,\n /access[-_]?token/i,\n /refresh[-_]?token/i,\n /credentials/i,\n /private[-_]?key/i,\n /ssn/i,\n /credit[-_]?card/i,\n /cvv/i,\n];\n\nexport const SENSITIVE_QUERY_PARAMS = [\n 'token',\n 'auth',\n 'key',\n 'apikey',\n 'api_key',\n 'secret',\n 'password',\n 'access_token',\n 'refresh_token',\n 'code',\n 'signature',\n];\n\nexport const REDACTED_PLACEHOLDER = '[REDACTED]';\n\n/**\n * Checks if a property name matches any sensitive pattern\n */\nexport function isSensitiveKey(key: string): boolean {\n if (!key) return false;\n const cleaned = key.replace(/[-_]/g, '');\n return SENSITIVE_KEY_PATTERNS.some((pattern) => pattern.test(key) || pattern.test(cleaned));\n}\n\n/**\n * Redacts sensitive query parameters from a URL string\n */\nexport function redactUrl(rawUrl: string): string {\n if (!rawUrl) return rawUrl;\n\n try {\n const isRelative = !rawUrl.startsWith('http://') && !rawUrl.startsWith('https://') && !rawUrl.startsWith('ws://') && !rawUrl.startsWith('wss://');\n const base = 'http://localhost';\n const parsed = new URL(rawUrl, base);\n\n let changed = false;\n for (const param of SENSITIVE_QUERY_PARAMS) {\n if (parsed.searchParams.has(param)) {\n parsed.searchParams.set(param, REDACTED_PLACEHOLDER);\n changed = true;\n }\n }\n\n // Also check all query params against isSensitiveKey\n for (const key of Array.from(parsed.searchParams.keys())) {\n if (isSensitiveKey(key)) {\n parsed.searchParams.set(key, REDACTED_PLACEHOLDER);\n changed = true;\n }\n }\n\n if (!changed) return rawUrl;\n\n let result = isRelative ? parsed.pathname + parsed.search + parsed.hash : parsed.toString();\n // Normalize URL-encoded brackets %5BREDACTED%5D back to [REDACTED]\n result = result.replace(/%5BREDACTED%5D/g, REDACTED_PLACEHOLDER);\n return result;\n } catch {\n // If URL parsing fails, perform regex replace\n let safe = rawUrl;\n for (const param of SENSITIVE_QUERY_PARAMS) {\n const reg = new RegExp(`([?&]${param}=)[^&#]+`, 'gi');\n safe = safe.replace(reg, `$1${REDACTED_PLACEHOLDER}`);\n }\n return safe;\n }\n}\n\n/**\n * Redacts sensitive HTTP headers\n */\nexport function redactHeaders(headers: Record<string, string>): Record<string, string> {\n if (!headers || typeof headers !== 'object') return {};\n\n const safe: Record<string, string> = {};\n for (const [key, value] of Object.entries(headers)) {\n if (isSensitiveKey(key)) {\n safe[key] = REDACTED_PLACEHOLDER;\n } else {\n safe[key] = value;\n }\n }\n return safe;\n}\n\n/**\n * Deeply redacts sensitive keys from an object or array\n */\nexport function redactSensitiveData<T = any>(data: T, maxDepth = 6, currentDepth = 0): T {\n if (data === null || data === undefined) return data;\n if (typeof data !== 'object') return data;\n if (currentDepth > maxDepth) return '[DEPTH_EXCEEDED]' as any;\n\n if (Array.isArray(data)) {\n return data.map((item) => redactSensitiveData(item, maxDepth, currentDepth + 1)) as any;\n }\n\n const result: Record<string, any> = {};\n for (const [key, value] of Object.entries(data as Record<string, any>)) {\n if (typeof value === 'object' && value !== null) {\n result[key] = redactSensitiveData(value, maxDepth, currentDepth + 1);\n } else if (isSensitiveKey(key)) {\n result[key] = REDACTED_PLACEHOLDER;\n } else if (typeof value === 'string') {\n // Check if value is a URL containing secrets\n if (value.startsWith('http://') || value.startsWith('https://') || value.includes('?')) {\n result[key] = redactUrl(value);\n } else {\n result[key] = value;\n }\n } else {\n result[key] = value;\n }\n }\n\n return result as T;\n}\n","/**\n * Semantic element selector generation and inspection\n */\n\nexport interface SelectorOptions {\n maxClasses?: number;\n maxOuterHTMLLength?: number;\n maxInnerTextLength?: number;\n}\n\n/**\n * Builds a clean, human-readable semantic selector for a DOM element.\n * Prioritizes data-testid/data-test, id, semantic tag + specific class, or concise parent path.\n */\nexport function getSemanticSelector(element: HTMLElement | Element, options: SelectorOptions = {}): string {\n if (!element || !element.tagName) return 'unknown';\n\n const maxClasses = options.maxClasses ?? 2;\n\n // 1. Check for standard test attributes\n const testId =\n element.getAttribute('data-testid') ||\n element.getAttribute('data-test') ||\n element.getAttribute('data-cy') ||\n element.getAttribute('data-qa');\n\n if (testId) {\n return `[data-testid=\"${testId}\"]`;\n }\n\n // 2. Check for meaningful ID (skip dynamic UUID/numeric IDs)\n const id = element.id;\n if (id && !/^[0-9]+$/.test(id) && !/[0-9a-f]{8}-[0-9a-f]{4}/i.test(id)) {\n return `#${id}`;\n }\n\n // 3. Check for aria-label or name on buttons/inputs\n const ariaLabel = element.getAttribute('aria-label');\n if (ariaLabel && ariaLabel.length < 30) {\n return `${element.tagName.toLowerCase()}[aria-label=\"${ariaLabel}\"]`;\n }\n\n const nameAttr = element.getAttribute('name');\n if (nameAttr) {\n return `${element.tagName.toLowerCase()}[name=\"${nameAttr}\"]`;\n }\n\n // 4. Tag + curated classes\n const tag = element.tagName.toLowerCase();\n const classList = Array.from(element.classList || [])\n .filter((c) => !c.startsWith('css-') && !c.startsWith('_') && !/^[a-z0-9]{5,}$/i.test(c)) // filter hashed utility classes\n .slice(0, maxClasses);\n\n if (classList.length > 0) {\n const classStr = classList.map((c) => `.${c}`).join('');\n return `${tag}${classStr}`;\n }\n\n // 5. If generic element (div, span, p) without attributes, try parent context\n if (element.parentElement && element.parentElement !== document.body && element.parentElement !== document.documentElement) {\n const parentSelector = getSemanticSelector(element.parentElement, { maxClasses: 1 });\n if (parentSelector && parentSelector !== 'unknown' && !parentSelector.includes('>')) {\n return `${parentSelector} > ${tag}`;\n }\n }\n\n return tag;\n}\n\n/**\n * Truncates string to specified max length with ellipsis\n */\nexport function truncate(str: string | undefined | null, maxLength = 200): string {\n if (!str) return '';\n if (str.length <= maxLength) return str;\n return str.slice(0, maxLength) + '...';\n}\n"],"mappings":"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","names":[]}