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,68 @@
|
|
|
1
|
+
import type { ConsoleEvent, ConsoleLevel } from '../../../core/src/index.js';
|
|
2
|
+
|
|
3
|
+
export type ConsoleCallback = (event: ConsoleEvent) => void;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Patches console methods without modifying original behavior.
|
|
7
|
+
*/
|
|
8
|
+
export function setupConsoleInterceptors(onConsole: ConsoleCallback): () => void {
|
|
9
|
+
if (typeof console === 'undefined') return () => {};
|
|
10
|
+
|
|
11
|
+
const originalError = console.error;
|
|
12
|
+
const originalWarn = console.warn;
|
|
13
|
+
|
|
14
|
+
function formatArgs(args: any[]): string {
|
|
15
|
+
return args
|
|
16
|
+
.map((arg) => {
|
|
17
|
+
if (arg instanceof Error) {
|
|
18
|
+
return arg.stack || `${arg.name}: ${arg.message}`;
|
|
19
|
+
}
|
|
20
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
21
|
+
try {
|
|
22
|
+
return JSON.stringify(arg);
|
|
23
|
+
} catch {
|
|
24
|
+
return String(arg);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return String(arg);
|
|
28
|
+
})
|
|
29
|
+
.join(' ');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
console.error = function (...args: any[]) {
|
|
33
|
+
try {
|
|
34
|
+
const message = formatArgs(args);
|
|
35
|
+
const stack = new Error().stack;
|
|
36
|
+
onConsole({
|
|
37
|
+
level: 'error',
|
|
38
|
+
message,
|
|
39
|
+
stack,
|
|
40
|
+
timestamp: Date.now(),
|
|
41
|
+
});
|
|
42
|
+
} catch {
|
|
43
|
+
// Defensive
|
|
44
|
+
}
|
|
45
|
+
return originalError.apply(console, args);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
console.warn = function (...args: any[]) {
|
|
49
|
+
try {
|
|
50
|
+
const message = formatArgs(args);
|
|
51
|
+
const stack = new Error().stack;
|
|
52
|
+
onConsole({
|
|
53
|
+
level: 'warn',
|
|
54
|
+
message,
|
|
55
|
+
stack,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
// Defensive
|
|
60
|
+
}
|
|
61
|
+
return originalWarn.apply(console, args);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return () => {
|
|
65
|
+
console.error = originalError;
|
|
66
|
+
console.warn = originalWarn;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Breadcrumb, ElementSummary } from '../../../core/src/index.js';
|
|
2
|
+
import { getSemanticSelector, truncate } from '../../../core/src/index.js';
|
|
3
|
+
|
|
4
|
+
export type InteractionCallback = (breadcrumb: Breadcrumb, elementSummary?: ElementSummary) => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Extracts a concise ElementSummary from a DOM element.
|
|
8
|
+
* Never collects sensitive user input values.
|
|
9
|
+
*/
|
|
10
|
+
export function extractElementSummary(el: HTMLElement | Element): ElementSummary {
|
|
11
|
+
const selector = getSemanticSelector(el);
|
|
12
|
+
const tag = el.tagName.toLowerCase();
|
|
13
|
+
const id = el.id || undefined;
|
|
14
|
+
const classes = Array.from(el.classList || []);
|
|
15
|
+
|
|
16
|
+
let boundingRect: ElementSummary['boundingRect'] = undefined;
|
|
17
|
+
let visible = true;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const rect = el.getBoundingClientRect();
|
|
21
|
+
boundingRect = {
|
|
22
|
+
x: Math.round(rect.x),
|
|
23
|
+
y: Math.round(rect.y),
|
|
24
|
+
width: Math.round(rect.width),
|
|
25
|
+
height: Math.round(rect.height),
|
|
26
|
+
top: Math.round(rect.top),
|
|
27
|
+
left: Math.round(rect.left),
|
|
28
|
+
bottom: Math.round(rect.bottom),
|
|
29
|
+
right: Math.round(rect.right),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const style = window.getComputedStyle(el);
|
|
33
|
+
visible = style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && rect.width > 0 && rect.height > 0;
|
|
34
|
+
} catch {
|
|
35
|
+
// Defensive
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Safe innerText (truncate to 80 chars, ignore password/sensitive elements)
|
|
39
|
+
let innerText: string | undefined;
|
|
40
|
+
if (tag !== 'input' && tag !== 'textarea' && tag !== 'select') {
|
|
41
|
+
innerText = truncate(el.textContent?.trim(), 80);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Safe outerHTML (truncate to 200 chars)
|
|
45
|
+
const outerHTML = truncate(el.outerHTML?.trim(), 200);
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
selector,
|
|
49
|
+
tag,
|
|
50
|
+
id,
|
|
51
|
+
classes,
|
|
52
|
+
boundingRect,
|
|
53
|
+
visible,
|
|
54
|
+
innerText,
|
|
55
|
+
outerHTML,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Sets up global click and submit event tracking.
|
|
61
|
+
*/
|
|
62
|
+
export function setupInteractionInterceptors(onInteraction: InteractionCallback): () => void {
|
|
63
|
+
if (typeof document === 'undefined') return () => {};
|
|
64
|
+
|
|
65
|
+
const onClick = (event: MouseEvent) => {
|
|
66
|
+
try {
|
|
67
|
+
const target = event.target as HTMLElement | null;
|
|
68
|
+
if (!target || !(target instanceof Element)) return;
|
|
69
|
+
|
|
70
|
+
const summary = extractElementSummary(target);
|
|
71
|
+
const message = `click ${summary.selector}${summary.innerText ? ` ("${summary.innerText}")` : ''}`;
|
|
72
|
+
|
|
73
|
+
onInteraction(
|
|
74
|
+
{
|
|
75
|
+
type: 'click',
|
|
76
|
+
category: 'ui',
|
|
77
|
+
message,
|
|
78
|
+
timestamp: Date.now(),
|
|
79
|
+
element: summary,
|
|
80
|
+
},
|
|
81
|
+
summary
|
|
82
|
+
);
|
|
83
|
+
} catch {
|
|
84
|
+
// Defensive
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const onSubmit = (event: SubmitEvent) => {
|
|
89
|
+
try {
|
|
90
|
+
const target = event.target as HTMLFormElement | null;
|
|
91
|
+
if (!target || !(target instanceof Element)) return;
|
|
92
|
+
|
|
93
|
+
const summary = extractElementSummary(target);
|
|
94
|
+
const formAction = target.getAttribute('action') || '';
|
|
95
|
+
const message = `submit form ${summary.selector}${formAction ? ` (action: ${formAction})` : ''}`;
|
|
96
|
+
|
|
97
|
+
onInteraction(
|
|
98
|
+
{
|
|
99
|
+
type: 'submit',
|
|
100
|
+
category: 'ui',
|
|
101
|
+
message,
|
|
102
|
+
timestamp: Date.now(),
|
|
103
|
+
element: summary,
|
|
104
|
+
},
|
|
105
|
+
summary
|
|
106
|
+
);
|
|
107
|
+
} catch {
|
|
108
|
+
// Defensive
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
document.addEventListener('click', onClick, { capture: true, passive: true });
|
|
113
|
+
document.addEventListener('submit', onSubmit, { capture: true, passive: true });
|
|
114
|
+
|
|
115
|
+
return () => {
|
|
116
|
+
document.removeEventListener('click', onClick, { capture: true });
|
|
117
|
+
document.removeEventListener('submit', onSubmit, { capture: true });
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { NavigationEvent } from '../../../core/src/index.js';
|
|
2
|
+
import { redactUrl } from '../../../core/src/index.js';
|
|
3
|
+
|
|
4
|
+
export type NavigationCallback = (event: NavigationEvent) => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Tracks browser SPA routing and history navigation.
|
|
8
|
+
*/
|
|
9
|
+
export function setupNavigationInterceptors(onNavigation: NavigationCallback): () => void {
|
|
10
|
+
if (typeof window === 'undefined' || typeof history === 'undefined') return () => {};
|
|
11
|
+
|
|
12
|
+
let currentUrl = redactUrl(window.location.href);
|
|
13
|
+
|
|
14
|
+
// Initial navigation
|
|
15
|
+
onNavigation({
|
|
16
|
+
to: currentUrl,
|
|
17
|
+
type: 'initial',
|
|
18
|
+
timestamp: Date.now(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const originalPushState = history.pushState;
|
|
22
|
+
const originalReplaceState = history.replaceState;
|
|
23
|
+
|
|
24
|
+
history.pushState = function (data: any, unused: string, url?: string | URL | null) {
|
|
25
|
+
const from = currentUrl;
|
|
26
|
+
const result = originalPushState.apply(history, [data, unused, url]);
|
|
27
|
+
try {
|
|
28
|
+
const to = redactUrl(window.location.href);
|
|
29
|
+
currentUrl = to;
|
|
30
|
+
onNavigation({
|
|
31
|
+
from,
|
|
32
|
+
to,
|
|
33
|
+
type: 'pushState',
|
|
34
|
+
timestamp: Date.now(),
|
|
35
|
+
});
|
|
36
|
+
} catch {
|
|
37
|
+
// Defensive
|
|
38
|
+
}
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
history.replaceState = function (data: any, unused: string, url?: string | URL | null) {
|
|
43
|
+
const from = currentUrl;
|
|
44
|
+
const result = originalReplaceState.apply(history, [data, unused, url]);
|
|
45
|
+
try {
|
|
46
|
+
const to = redactUrl(window.location.href);
|
|
47
|
+
currentUrl = to;
|
|
48
|
+
onNavigation({
|
|
49
|
+
from,
|
|
50
|
+
to,
|
|
51
|
+
type: 'replaceState',
|
|
52
|
+
timestamp: Date.now(),
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
// Defensive
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const onPopState = () => {
|
|
61
|
+
const from = currentUrl;
|
|
62
|
+
const to = redactUrl(window.location.href);
|
|
63
|
+
currentUrl = to;
|
|
64
|
+
onNavigation({
|
|
65
|
+
from,
|
|
66
|
+
to,
|
|
67
|
+
type: 'popstate',
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
});
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const onHashChange = () => {
|
|
73
|
+
const from = currentUrl;
|
|
74
|
+
const to = redactUrl(window.location.href);
|
|
75
|
+
currentUrl = to;
|
|
76
|
+
onNavigation({
|
|
77
|
+
from,
|
|
78
|
+
to,
|
|
79
|
+
type: 'hashchange',
|
|
80
|
+
timestamp: Date.now(),
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
window.addEventListener('popstate', onPopState);
|
|
85
|
+
window.addEventListener('hashchange', onHashChange);
|
|
86
|
+
|
|
87
|
+
return () => {
|
|
88
|
+
history.pushState = originalPushState;
|
|
89
|
+
history.replaceState = originalReplaceState;
|
|
90
|
+
window.removeEventListener('popstate', onPopState);
|
|
91
|
+
window.removeEventListener('hashchange', onHashChange);
|
|
92
|
+
};
|
|
93
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import type { NetworkEvent } from '../../../core/src/index.js';
|
|
2
|
+
import { redactUrl } from '../../../core/src/index.js';
|
|
3
|
+
|
|
4
|
+
export type NetworkCallback = (event: NetworkEvent) => void;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Patches window.fetch and XMLHttpRequest to track network requests and failures.
|
|
8
|
+
*/
|
|
9
|
+
export function setupNetworkInterceptors(onNetwork: NetworkCallback): () => void {
|
|
10
|
+
const cleanups: (() => void)[] = [];
|
|
11
|
+
|
|
12
|
+
// 1. Patch window.fetch
|
|
13
|
+
if (typeof window !== 'undefined' && typeof window.fetch === 'function') {
|
|
14
|
+
const originalFetch = window.fetch;
|
|
15
|
+
|
|
16
|
+
window.fetch = async function (input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
17
|
+
const startTime = Date.now();
|
|
18
|
+
let urlStr = '';
|
|
19
|
+
let method = 'GET';
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (typeof input === 'string') {
|
|
23
|
+
urlStr = input;
|
|
24
|
+
} else if (input instanceof URL) {
|
|
25
|
+
urlStr = input.toString();
|
|
26
|
+
} else if (input && typeof input === 'object' && 'url' in input) {
|
|
27
|
+
urlStr = input.url;
|
|
28
|
+
method = input.method || 'GET';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (init && init.method) {
|
|
32
|
+
method = init.method.toUpperCase();
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
urlStr = 'unknown_url';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const safeUrl = redactUrl(urlStr);
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
const response = await originalFetch.apply(window, [input as any, init]);
|
|
42
|
+
const durationMs = Date.now() - startTime;
|
|
43
|
+
|
|
44
|
+
onNetwork({
|
|
45
|
+
url: safeUrl,
|
|
46
|
+
method,
|
|
47
|
+
status: response.status,
|
|
48
|
+
statusText: response.statusText,
|
|
49
|
+
durationMs,
|
|
50
|
+
timestamp: startTime,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return response;
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
const durationMs = Date.now() - startTime;
|
|
56
|
+
const isAbort = err?.name === 'AbortError';
|
|
57
|
+
|
|
58
|
+
onNetwork({
|
|
59
|
+
url: safeUrl,
|
|
60
|
+
method,
|
|
61
|
+
durationMs,
|
|
62
|
+
error: err?.message || 'Network request failed',
|
|
63
|
+
aborted: isAbort,
|
|
64
|
+
timestamp: startTime,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
throw err;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
cleanups.push(() => {
|
|
72
|
+
window.fetch = originalFetch;
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 2. Patch XMLHttpRequest
|
|
77
|
+
if (typeof window !== 'undefined' && typeof window.XMLHttpRequest === 'function') {
|
|
78
|
+
const originalOpen = XMLHttpRequest.prototype.open;
|
|
79
|
+
const originalSend = XMLHttpRequest.prototype.send;
|
|
80
|
+
const originalAbort = XMLHttpRequest.prototype.abort;
|
|
81
|
+
|
|
82
|
+
const xhrMap = new WeakMap<XMLHttpRequest, { url: string; method: string; startTime: number; aborted?: boolean }>();
|
|
83
|
+
|
|
84
|
+
XMLHttpRequest.prototype.open = function (this: XMLHttpRequest, ...args: any[]) {
|
|
85
|
+
try {
|
|
86
|
+
const method = String(args[0] || 'GET').toUpperCase();
|
|
87
|
+
const rawUrl = String(args[1] || '');
|
|
88
|
+
xhrMap.set(this, {
|
|
89
|
+
url: redactUrl(rawUrl),
|
|
90
|
+
method,
|
|
91
|
+
startTime: Date.now(),
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
// Defensive
|
|
95
|
+
}
|
|
96
|
+
return originalOpen.apply(this, args as any);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
XMLHttpRequest.prototype.abort = function (this: XMLHttpRequest) {
|
|
100
|
+
const meta = xhrMap.get(this);
|
|
101
|
+
if (meta) {
|
|
102
|
+
meta.aborted = true;
|
|
103
|
+
}
|
|
104
|
+
return originalAbort.apply(this);
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
XMLHttpRequest.prototype.send = function (this: XMLHttpRequest, body?: any) {
|
|
108
|
+
const meta = xhrMap.get(this) || { url: 'unknown_url', method: 'GET', startTime: Date.now() };
|
|
109
|
+
|
|
110
|
+
const onComplete = () => {
|
|
111
|
+
try {
|
|
112
|
+
const durationMs = Date.now() - meta.startTime;
|
|
113
|
+
onNetwork({
|
|
114
|
+
url: meta.url,
|
|
115
|
+
method: meta.method,
|
|
116
|
+
status: this.status,
|
|
117
|
+
statusText: this.statusText,
|
|
118
|
+
durationMs,
|
|
119
|
+
aborted: meta.aborted,
|
|
120
|
+
timestamp: meta.startTime,
|
|
121
|
+
});
|
|
122
|
+
} catch {
|
|
123
|
+
// Defensive
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const onError = () => {
|
|
128
|
+
try {
|
|
129
|
+
const durationMs = Date.now() - meta.startTime;
|
|
130
|
+
onNetwork({
|
|
131
|
+
url: meta.url,
|
|
132
|
+
method: meta.method,
|
|
133
|
+
status: this.status || 0,
|
|
134
|
+
durationMs,
|
|
135
|
+
error: 'XHR Network Error',
|
|
136
|
+
aborted: meta.aborted,
|
|
137
|
+
timestamp: meta.startTime,
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
// Defensive
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
this.addEventListener('load', onComplete, { once: true });
|
|
145
|
+
this.addEventListener('error', onError, { once: true });
|
|
146
|
+
this.addEventListener('timeout', onError, { once: true });
|
|
147
|
+
|
|
148
|
+
return originalSend.apply(this, [body]);
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
cleanups.push(() => {
|
|
152
|
+
XMLHttpRequest.prototype.open = originalOpen;
|
|
153
|
+
XMLHttpRequest.prototype.send = originalSend;
|
|
154
|
+
XMLHttpRequest.prototype.abort = originalAbort;
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return () => {
|
|
159
|
+
for (const cleanup of cleanups) {
|
|
160
|
+
try {
|
|
161
|
+
cleanup();
|
|
162
|
+
} catch {}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { RuntimeErrorEvent } from '@browserdiag/core';
|
|
2
|
+
|
|
3
|
+
export type RuntimeErrorCallback = (event: RuntimeErrorEvent, error?: any) => void;
|
|
4
|
+
|
|
5
|
+
export function setupRuntimeInterceptors(onRuntimeError: RuntimeErrorCallback): () => void {
|
|
6
|
+
if (typeof window === 'undefined') return () => {};
|
|
7
|
+
|
|
8
|
+
const errorHandler = (event: ErrorEvent) => {
|
|
9
|
+
try {
|
|
10
|
+
const errorEvent: RuntimeErrorEvent = {
|
|
11
|
+
message: event.message || (event.error && event.error.message) || 'Script error',
|
|
12
|
+
stack: (event.error && event.error.stack) || undefined,
|
|
13
|
+
filename: event.filename || undefined,
|
|
14
|
+
lineno: event.lineno || undefined,
|
|
15
|
+
colno: event.colno || undefined,
|
|
16
|
+
errorType: (event.error && event.error.name) || 'Error',
|
|
17
|
+
timestamp: Date.now(),
|
|
18
|
+
};
|
|
19
|
+
onRuntimeError(errorEvent, event.error);
|
|
20
|
+
} catch {
|
|
21
|
+
// Defensive: never let diagnostic handler crash host app
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const unhandledRejectionHandler = (event: PromiseRejectionEvent) => {
|
|
26
|
+
try {
|
|
27
|
+
const reason = event.reason;
|
|
28
|
+
let message = 'Unhandled Promise Rejection';
|
|
29
|
+
let stack: string | undefined;
|
|
30
|
+
let errorType = 'UnhandledRejection';
|
|
31
|
+
|
|
32
|
+
if (reason instanceof Error) {
|
|
33
|
+
message = reason.message;
|
|
34
|
+
stack = reason.stack;
|
|
35
|
+
errorType = reason.name || 'UnhandledRejection';
|
|
36
|
+
} else if (typeof reason === 'string') {
|
|
37
|
+
message = reason;
|
|
38
|
+
} else if (reason && typeof reason === 'object') {
|
|
39
|
+
try {
|
|
40
|
+
message = JSON.stringify(reason);
|
|
41
|
+
} catch {
|
|
42
|
+
message = String(reason);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const errorEvent: RuntimeErrorEvent = {
|
|
47
|
+
message,
|
|
48
|
+
stack,
|
|
49
|
+
errorType,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
};
|
|
52
|
+
onRuntimeError(errorEvent, reason);
|
|
53
|
+
} catch {
|
|
54
|
+
// Defensive
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
window.addEventListener('error', errorHandler);
|
|
59
|
+
window.addEventListener('unhandledrejection', unhandledRejectionHandler);
|
|
60
|
+
|
|
61
|
+
return () => {
|
|
62
|
+
window.removeEventListener('error', errorHandler);
|
|
63
|
+
window.removeEventListener('unhandledrejection', unhandledRejectionHandler);
|
|
64
|
+
};
|
|
65
|
+
}
|