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,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
+ }