@squasher-ai/browser 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/LICENSE +21 -0
- package/README.md +12 -0
- package/dist/__tests__/client.test.d.ts +2 -0
- package/dist/__tests__/client.test.d.ts.map +1 -0
- package/dist/__tests__/client.test.js +103 -0
- package/dist/__tests__/errors.test.d.ts +2 -0
- package/dist/__tests__/errors.test.d.ts.map +1 -0
- package/dist/__tests__/errors.test.js +80 -0
- package/dist/__tests__/replay-privacy.test.d.ts +2 -0
- package/dist/__tests__/replay-privacy.test.d.ts.map +1 -0
- package/dist/__tests__/replay-privacy.test.js +124 -0
- package/dist/__tests__/replay-startup.test.d.ts +2 -0
- package/dist/__tests__/replay-startup.test.d.ts.map +1 -0
- package/dist/__tests__/replay-startup.test.js +99 -0
- package/dist/__tests__/replay.test.d.ts +2 -0
- package/dist/__tests__/replay.test.d.ts.map +1 -0
- package/dist/__tests__/replay.test.js +217 -0
- package/dist/__tests__/session.test.d.ts +2 -0
- package/dist/__tests__/session.test.d.ts.map +1 -0
- package/dist/__tests__/session.test.js +46 -0
- package/dist/__tests__/transport.test.d.ts +2 -0
- package/dist/__tests__/transport.test.d.ts.map +1 -0
- package/dist/__tests__/transport.test.js +101 -0
- package/dist/__tests__/types.test.d.ts +2 -0
- package/dist/__tests__/types.test.d.ts.map +1 -0
- package/dist/__tests__/types.test.js +139 -0
- package/dist/autocapture.d.ts +21 -0
- package/dist/autocapture.d.ts.map +1 -0
- package/dist/autocapture.js +191 -0
- package/dist/client.d.ts +46 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +215 -0
- package/dist/errors.d.ts +29 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +111 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +105 -0
- package/dist/react.d.ts +32 -0
- package/dist/react.d.ts.map +1 -0
- package/dist/react.js +42 -0
- package/dist/replay-privacy.d.ts +16 -0
- package/dist/replay-privacy.d.ts.map +1 -0
- package/dist/replay-privacy.js +64 -0
- package/dist/replay.d.ts +30 -0
- package/dist/replay.d.ts.map +1 -0
- package/dist/replay.js +158 -0
- package/dist/session.d.ts +21 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +52 -0
- package/dist/telemetry.d.ts +20 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +145 -0
- package/dist/transport.d.ts +47 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +145 -0
- package/dist/types.d.ts +234 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/vitals.d.ts +30 -0
- package/dist/vitals.d.ts.map +1 -0
- package/dist/vitals.js +92 -0
- package/package.json +44 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-capture breadcrumbs for navigation, clicks, and fetch errors.
|
|
3
|
+
*
|
|
4
|
+
* Breadcrumbs are lightweight events that provide context about what the
|
|
5
|
+
* user was doing before an error occurred. They are attached to error
|
|
6
|
+
* events to help with debugging and AI triage.
|
|
7
|
+
*
|
|
8
|
+
* Categories:
|
|
9
|
+
* - "navigation" — route changes (pushState, replaceState, popstate)
|
|
10
|
+
* - "ui.click" — user clicks on elements
|
|
11
|
+
* - "fetch" — failed fetch requests (4xx/5xx)
|
|
12
|
+
* - "console" — console.error / console.warn calls
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Install all auto-capture hooks.
|
|
16
|
+
* Returns a cleanup function to remove all hooks and restore originals.
|
|
17
|
+
*/
|
|
18
|
+
export function installAutocapture(onBreadcrumb) {
|
|
19
|
+
const cleanups = [];
|
|
20
|
+
cleanups.push(captureNavigation(onBreadcrumb));
|
|
21
|
+
cleanups.push(captureClicks(onBreadcrumb));
|
|
22
|
+
cleanups.push(captureFetch(onBreadcrumb));
|
|
23
|
+
cleanups.push(captureConsole(onBreadcrumb));
|
|
24
|
+
return () => {
|
|
25
|
+
for (const cleanup of cleanups) {
|
|
26
|
+
cleanup();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
// ─── Navigation ───────────────────────────────────────────────────────────
|
|
31
|
+
function captureNavigation(onBreadcrumb) {
|
|
32
|
+
let lastUrl = location.href;
|
|
33
|
+
const emitNavCrumb = () => {
|
|
34
|
+
const newUrl = location.href;
|
|
35
|
+
if (newUrl !== lastUrl) {
|
|
36
|
+
onBreadcrumb({
|
|
37
|
+
category: "navigation",
|
|
38
|
+
message: `${lastUrl} → ${newUrl}`,
|
|
39
|
+
data: { from: lastUrl, to: newUrl },
|
|
40
|
+
});
|
|
41
|
+
lastUrl = newUrl;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
// Patch history.pushState
|
|
45
|
+
const origPushState = history.pushState.bind(history);
|
|
46
|
+
history.pushState = function (...args) {
|
|
47
|
+
origPushState(...args);
|
|
48
|
+
emitNavCrumb();
|
|
49
|
+
};
|
|
50
|
+
// Patch history.replaceState
|
|
51
|
+
const origReplaceState = history.replaceState.bind(history);
|
|
52
|
+
history.replaceState = function (...args) {
|
|
53
|
+
origReplaceState(...args);
|
|
54
|
+
emitNavCrumb();
|
|
55
|
+
};
|
|
56
|
+
// Listen to popstate (back/forward)
|
|
57
|
+
const popstateHandler = () => emitNavCrumb();
|
|
58
|
+
window.addEventListener("popstate", popstateHandler);
|
|
59
|
+
return () => {
|
|
60
|
+
history.pushState = origPushState;
|
|
61
|
+
history.replaceState = origReplaceState;
|
|
62
|
+
window.removeEventListener("popstate", popstateHandler);
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
// ─── Clicks ─────────────────────────────────────────────────────────────
|
|
66
|
+
function captureClicks(onBreadcrumb) {
|
|
67
|
+
const handler = (event) => {
|
|
68
|
+
const target = event.target;
|
|
69
|
+
if (!target)
|
|
70
|
+
return;
|
|
71
|
+
const descriptor = describeElement(target);
|
|
72
|
+
if (!descriptor)
|
|
73
|
+
return;
|
|
74
|
+
onBreadcrumb({
|
|
75
|
+
category: "ui.click",
|
|
76
|
+
message: descriptor,
|
|
77
|
+
data: {
|
|
78
|
+
tag: target.tagName?.toLowerCase(),
|
|
79
|
+
id: target.id || undefined,
|
|
80
|
+
className: target.className || undefined,
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
};
|
|
84
|
+
// Use capture phase to catch clicks even if stopPropagation is called
|
|
85
|
+
document.addEventListener("click", handler, true);
|
|
86
|
+
return () => {
|
|
87
|
+
document.removeEventListener("click", handler, true);
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Build a human-readable descriptor for a DOM element.
|
|
92
|
+
* e.g. "button#submit-btn.primary" or "a.nav-link"
|
|
93
|
+
*/
|
|
94
|
+
function describeElement(el) {
|
|
95
|
+
const tag = el.tagName?.toLowerCase();
|
|
96
|
+
if (!tag)
|
|
97
|
+
return null;
|
|
98
|
+
let desc = tag;
|
|
99
|
+
const htmlEl = el;
|
|
100
|
+
if (htmlEl.id) {
|
|
101
|
+
desc += `#${htmlEl.id}`;
|
|
102
|
+
}
|
|
103
|
+
if (htmlEl.className && typeof htmlEl.className === "string") {
|
|
104
|
+
const classes = htmlEl.className.trim().split(/\s+/).slice(0, 2).join(".");
|
|
105
|
+
if (classes) {
|
|
106
|
+
desc += `.${classes}`;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Add text content for buttons/links (truncated)
|
|
110
|
+
const textContent = htmlEl.textContent?.trim();
|
|
111
|
+
if (textContent && (tag === "button" || tag === "a")) {
|
|
112
|
+
const truncated = textContent.length > 30 ? `${textContent.slice(0, 30)}...` : textContent;
|
|
113
|
+
desc += `[${truncated}]`;
|
|
114
|
+
}
|
|
115
|
+
return desc;
|
|
116
|
+
}
|
|
117
|
+
// ─── Fetch ──────────────────────────────────────────────────────────────
|
|
118
|
+
function captureFetch(onBreadcrumb) {
|
|
119
|
+
if (typeof window === "undefined" || !window.fetch)
|
|
120
|
+
return () => { };
|
|
121
|
+
const origFetch = window.fetch.bind(window);
|
|
122
|
+
window.fetch = async function (input, init) {
|
|
123
|
+
const method = init?.method?.toUpperCase() || "GET";
|
|
124
|
+
const url = typeof input === "string"
|
|
125
|
+
? input
|
|
126
|
+
: input instanceof URL
|
|
127
|
+
? input.href
|
|
128
|
+
: input instanceof Request
|
|
129
|
+
? input.url
|
|
130
|
+
: String(input);
|
|
131
|
+
try {
|
|
132
|
+
const response = await origFetch(input, init);
|
|
133
|
+
// Only breadcrumb on error responses (4xx/5xx)
|
|
134
|
+
if (response.status >= 400) {
|
|
135
|
+
onBreadcrumb({
|
|
136
|
+
category: "fetch",
|
|
137
|
+
message: `${method} ${url} [${response.status}]`,
|
|
138
|
+
level: response.status >= 500 ? "error" : "warning",
|
|
139
|
+
data: {
|
|
140
|
+
method,
|
|
141
|
+
url,
|
|
142
|
+
status: response.status,
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
return response;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
// Network error
|
|
150
|
+
onBreadcrumb({
|
|
151
|
+
category: "fetch",
|
|
152
|
+
message: `${method} ${url} [network error]`,
|
|
153
|
+
level: "error",
|
|
154
|
+
data: {
|
|
155
|
+
method,
|
|
156
|
+
url,
|
|
157
|
+
error: error instanceof Error ? error.message : String(error),
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
throw error;
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
return () => {
|
|
164
|
+
window.fetch = origFetch;
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
// ─── Console ────────────────────────────────────────────────────────────
|
|
168
|
+
function captureConsole(onBreadcrumb) {
|
|
169
|
+
const origError = console.error.bind(console);
|
|
170
|
+
const origWarn = console.warn.bind(console);
|
|
171
|
+
console.error = (...args) => {
|
|
172
|
+
onBreadcrumb({
|
|
173
|
+
category: "console",
|
|
174
|
+
message: args.map(String).join(" ").slice(0, 200),
|
|
175
|
+
level: "error",
|
|
176
|
+
});
|
|
177
|
+
origError(...args);
|
|
178
|
+
};
|
|
179
|
+
console.warn = (...args) => {
|
|
180
|
+
onBreadcrumb({
|
|
181
|
+
category: "console",
|
|
182
|
+
message: args.map(String).join(" ").slice(0, 200),
|
|
183
|
+
level: "warning",
|
|
184
|
+
});
|
|
185
|
+
origWarn(...args);
|
|
186
|
+
};
|
|
187
|
+
return () => {
|
|
188
|
+
console.error = origError;
|
|
189
|
+
console.warn = origWarn;
|
|
190
|
+
};
|
|
191
|
+
}
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserClient — the core SDK client for browser environments.
|
|
3
|
+
*
|
|
4
|
+
* Manages:
|
|
5
|
+
* - Configuration with sensible defaults
|
|
6
|
+
* - User context, tags, and breadcrumbs
|
|
7
|
+
* - Web Vitals collection (via vitals.ts)
|
|
8
|
+
* - Global error capture (via errors.ts)
|
|
9
|
+
* - Auto-capture breadcrumbs (via autocapture.ts)
|
|
10
|
+
* - Transport (via transport.ts)
|
|
11
|
+
*/
|
|
12
|
+
import type { Breadcrumb, BrowserConfig, BrowserErrorEvent, JsonObject, Level, UserContext } from "./types";
|
|
13
|
+
export declare class BrowserClient {
|
|
14
|
+
private config;
|
|
15
|
+
private transport;
|
|
16
|
+
private breadcrumbs;
|
|
17
|
+
private user;
|
|
18
|
+
private tags;
|
|
19
|
+
private disabled;
|
|
20
|
+
private cleanups;
|
|
21
|
+
private replayRecorder;
|
|
22
|
+
constructor(config: BrowserConfig);
|
|
23
|
+
setUser(user: UserContext | undefined): void;
|
|
24
|
+
setTag(key: string, value: string): void;
|
|
25
|
+
setTags(tags: Record<string, string>): void;
|
|
26
|
+
addBreadcrumb(crumb: Omit<Breadcrumb, "timestamp">): void;
|
|
27
|
+
captureError(error: Error, extra?: Record<string, unknown>): void;
|
|
28
|
+
captureMessage(message: string, level?: Level): void;
|
|
29
|
+
captureTelemetry(event: BrowserErrorEvent): void;
|
|
30
|
+
track(eventName: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
31
|
+
identify(distinctId: string, traits?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
32
|
+
page(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
33
|
+
screen(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
34
|
+
captureSpan(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
35
|
+
captureToolCall(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
36
|
+
captureGeneration(message: string, context?: Partial<BrowserErrorEvent>): void;
|
|
37
|
+
/**
|
|
38
|
+
* Flush remaining vitals and stop all timers.
|
|
39
|
+
* Call this before unmounting a SPA or during cleanup.
|
|
40
|
+
*/
|
|
41
|
+
close(): void;
|
|
42
|
+
private shouldSample;
|
|
43
|
+
/** Enrich an error event with current context and send it. */
|
|
44
|
+
private sendErrorEvent;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAiBH,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,KAAK,EAEL,WAAW,EACZ,MAAM,SAAS,CAAC;AAiDjB,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAAY;IAC7B,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,IAAI,CAA0B;IACtC,OAAO,CAAC,IAAI,CAA8B;IAC1C,OAAO,CAAC,QAAQ,CAAU;IAC1B,OAAO,CAAC,QAAQ,CAAyB;IACzC,OAAO,CAAC,cAAc,CAA+B;gBAEzC,MAAM,EAAE,aAAa;IAmFjC,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI;IAI5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIxC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI;IAI3C,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,IAAI;IAYzD,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI;IAIjE,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,KAAc,GAAG,IAAI;IAI5D,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAMhD,KAAK,CACH,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GACvC,IAAI;IAIP,QAAQ,CACN,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,UAAU,EACnB,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GACvC,IAAI;IAKP,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAI3F,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,UAAU,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAI7F,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAIzE,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAI7E,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,CAAC,iBAAiB,CAAM,GAAG,IAAI;IAMlF;;;OAGG;IACH,KAAK,IAAI,IAAI;IAab,OAAO,CAAC,YAAY;IAMpB,8DAA8D;IAC9D,OAAO,CAAC,cAAc;CAcvB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BrowserClient — the core SDK client for browser environments.
|
|
3
|
+
*
|
|
4
|
+
* Manages:
|
|
5
|
+
* - Configuration with sensible defaults
|
|
6
|
+
* - User context, tags, and breadcrumbs
|
|
7
|
+
* - Web Vitals collection (via vitals.ts)
|
|
8
|
+
* - Global error capture (via errors.ts)
|
|
9
|
+
* - Auto-capture breadcrumbs (via autocapture.ts)
|
|
10
|
+
* - Transport (via transport.ts)
|
|
11
|
+
*/
|
|
12
|
+
import { installAutocapture } from "./autocapture";
|
|
13
|
+
import { buildErrorEvent, buildMessageEvent, installGlobalErrorHandlers } from "./errors";
|
|
14
|
+
import { ReplayRecorder } from "./replay";
|
|
15
|
+
import { buildGenerationEvent, buildIdentifyEvent, buildPageEvent, buildScreenEvent, buildSpanEvent, buildToolCallEvent, buildTrackEvent, mergeIdentifiedUser, prepareBrowserEvent, } from "./telemetry";
|
|
16
|
+
import { Transport } from "./transport";
|
|
17
|
+
import { startVitalsCollection } from "./vitals";
|
|
18
|
+
const DEFAULT_ENDPOINT = "https://ingest.squasher.ai";
|
|
19
|
+
function resolveConfig(config) {
|
|
20
|
+
return {
|
|
21
|
+
apiKey: config.apiKey,
|
|
22
|
+
projectId: config.projectId,
|
|
23
|
+
endpoint: config.endpoint ?? DEFAULT_ENDPOINT,
|
|
24
|
+
environment: config.environment,
|
|
25
|
+
release: config.release,
|
|
26
|
+
debug: config.debug ?? false,
|
|
27
|
+
sampleRate: config.sampleRate ?? 1,
|
|
28
|
+
vitalsSampleRate: config.vitalsSampleRate ?? 1,
|
|
29
|
+
enableVitals: config.enableVitals ?? true,
|
|
30
|
+
enableErrorCapture: config.enableErrorCapture ?? true,
|
|
31
|
+
enableAutoBreadcrumbs: config.enableAutoBreadcrumbs ?? true,
|
|
32
|
+
beforeSend: config.beforeSend,
|
|
33
|
+
maxBreadcrumbs: config.maxBreadcrumbs ?? 30,
|
|
34
|
+
vitalsBufferSize: config.vitalsBufferSize ?? 10,
|
|
35
|
+
vitalsFlushIntervalMs: config.vitalsFlushIntervalMs ?? 10_000,
|
|
36
|
+
replayEnabled: config.replay?.enabled ?? false,
|
|
37
|
+
replaySampleRate: config.replay?.sampleRate ?? 1,
|
|
38
|
+
replayPrivacy: config.replay?.privacy,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
export class BrowserClient {
|
|
42
|
+
config;
|
|
43
|
+
transport;
|
|
44
|
+
breadcrumbs = [];
|
|
45
|
+
user;
|
|
46
|
+
tags = {};
|
|
47
|
+
disabled;
|
|
48
|
+
cleanups = [];
|
|
49
|
+
replayRecorder = null;
|
|
50
|
+
constructor(config) {
|
|
51
|
+
if (!config.apiKey)
|
|
52
|
+
throw new Error("Squasher: apiKey is required");
|
|
53
|
+
if (!config.projectId)
|
|
54
|
+
throw new Error("Squasher: projectId is required");
|
|
55
|
+
this.config = resolveConfig(config);
|
|
56
|
+
this.disabled = config.apiKey.length === 0 || config.projectId.length === 0;
|
|
57
|
+
if (this.disabled) {
|
|
58
|
+
if (this.config.debug) {
|
|
59
|
+
console.warn("[squasher] SDK initialized in disabled mode");
|
|
60
|
+
}
|
|
61
|
+
// Create a no-op transport
|
|
62
|
+
this.transport = new Transport({
|
|
63
|
+
endpoint: this.config.endpoint,
|
|
64
|
+
projectId: this.config.projectId,
|
|
65
|
+
apiKey: this.config.apiKey,
|
|
66
|
+
debug: this.config.debug,
|
|
67
|
+
vitalsBufferSize: this.config.vitalsBufferSize,
|
|
68
|
+
vitalsFlushIntervalMs: this.config.vitalsFlushIntervalMs,
|
|
69
|
+
});
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Initialize transport
|
|
73
|
+
this.transport = new Transport({
|
|
74
|
+
endpoint: this.config.endpoint,
|
|
75
|
+
projectId: this.config.projectId,
|
|
76
|
+
apiKey: this.config.apiKey,
|
|
77
|
+
debug: this.config.debug,
|
|
78
|
+
vitalsBufferSize: this.config.vitalsBufferSize,
|
|
79
|
+
vitalsFlushIntervalMs: this.config.vitalsFlushIntervalMs,
|
|
80
|
+
});
|
|
81
|
+
// Start Web Vitals collection
|
|
82
|
+
if (this.config.enableVitals) {
|
|
83
|
+
startVitalsCollection(this.transport, {
|
|
84
|
+
sampleRate: this.config.vitalsSampleRate,
|
|
85
|
+
environment: this.config.environment,
|
|
86
|
+
release: this.config.release,
|
|
87
|
+
debug: this.config.debug,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
// Install global error handlers
|
|
91
|
+
if (this.config.enableErrorCapture) {
|
|
92
|
+
const cleanup = installGlobalErrorHandlers((event) => {
|
|
93
|
+
this.sendErrorEvent(event);
|
|
94
|
+
});
|
|
95
|
+
this.cleanups.push(cleanup);
|
|
96
|
+
}
|
|
97
|
+
// Install auto-capture breadcrumbs
|
|
98
|
+
if (this.config.enableAutoBreadcrumbs) {
|
|
99
|
+
const cleanup = installAutocapture((crumb) => {
|
|
100
|
+
this.addBreadcrumb(crumb);
|
|
101
|
+
});
|
|
102
|
+
this.cleanups.push(cleanup);
|
|
103
|
+
}
|
|
104
|
+
if (this.config.replayEnabled) {
|
|
105
|
+
this.replayRecorder = new ReplayRecorder({
|
|
106
|
+
endpoint: this.config.endpoint,
|
|
107
|
+
projectId: this.config.projectId,
|
|
108
|
+
apiKey: this.config.apiKey,
|
|
109
|
+
debug: this.config.debug,
|
|
110
|
+
sampleRate: this.config.replaySampleRate,
|
|
111
|
+
privacy: this.config.replayPrivacy,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
if (this.config.debug) {
|
|
115
|
+
console.log("[squasher] Browser SDK initialized", {
|
|
116
|
+
projectId: this.config.projectId,
|
|
117
|
+
vitals: this.config.enableVitals,
|
|
118
|
+
errorCapture: this.config.enableErrorCapture,
|
|
119
|
+
autoBreadcrumbs: this.config.enableAutoBreadcrumbs,
|
|
120
|
+
replay: this.config.replayEnabled,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// ─── Context ──────────────────────────────────────────────────────────
|
|
125
|
+
setUser(user) {
|
|
126
|
+
this.user = user;
|
|
127
|
+
}
|
|
128
|
+
setTag(key, value) {
|
|
129
|
+
this.tags[key] = value;
|
|
130
|
+
}
|
|
131
|
+
setTags(tags) {
|
|
132
|
+
Object.assign(this.tags, tags);
|
|
133
|
+
}
|
|
134
|
+
addBreadcrumb(crumb) {
|
|
135
|
+
this.breadcrumbs.push({
|
|
136
|
+
...crumb,
|
|
137
|
+
timestamp: new Date().toISOString(),
|
|
138
|
+
});
|
|
139
|
+
if (this.breadcrumbs.length > this.config.maxBreadcrumbs) {
|
|
140
|
+
this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
// ─── Capture ──────────────────────────────────────────────────────────
|
|
144
|
+
captureError(error, extra) {
|
|
145
|
+
this.captureTelemetry(buildErrorEvent(error, extra));
|
|
146
|
+
}
|
|
147
|
+
captureMessage(message, level = "info") {
|
|
148
|
+
this.captureTelemetry(buildMessageEvent(message, level));
|
|
149
|
+
}
|
|
150
|
+
captureTelemetry(event) {
|
|
151
|
+
if (this.disabled)
|
|
152
|
+
return;
|
|
153
|
+
if (!this.shouldSample())
|
|
154
|
+
return;
|
|
155
|
+
this.sendErrorEvent(event);
|
|
156
|
+
}
|
|
157
|
+
track(eventName, properties, context = {}) {
|
|
158
|
+
this.captureTelemetry(buildTrackEvent(eventName, properties, context));
|
|
159
|
+
}
|
|
160
|
+
identify(distinctId, traits, context = {}) {
|
|
161
|
+
this.user = mergeIdentifiedUser(this.user, context.user, distinctId);
|
|
162
|
+
this.captureTelemetry(buildIdentifyEvent(distinctId, traits, context));
|
|
163
|
+
}
|
|
164
|
+
page(name, properties, context = {}) {
|
|
165
|
+
this.captureTelemetry(buildPageEvent(name, properties, context));
|
|
166
|
+
}
|
|
167
|
+
screen(name, properties, context = {}) {
|
|
168
|
+
this.captureTelemetry(buildScreenEvent(name, properties, context));
|
|
169
|
+
}
|
|
170
|
+
captureSpan(name, context = {}) {
|
|
171
|
+
this.captureTelemetry(buildSpanEvent(name, context));
|
|
172
|
+
}
|
|
173
|
+
captureToolCall(name, context = {}) {
|
|
174
|
+
this.captureTelemetry(buildToolCallEvent(name, context));
|
|
175
|
+
}
|
|
176
|
+
captureGeneration(message, context = {}) {
|
|
177
|
+
this.captureTelemetry(buildGenerationEvent(message, context));
|
|
178
|
+
}
|
|
179
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────
|
|
180
|
+
/**
|
|
181
|
+
* Flush remaining vitals and stop all timers.
|
|
182
|
+
* Call this before unmounting a SPA or during cleanup.
|
|
183
|
+
*/
|
|
184
|
+
close() {
|
|
185
|
+
this.replayRecorder?.dispose();
|
|
186
|
+
this.replayRecorder = null;
|
|
187
|
+
for (const cleanup of this.cleanups) {
|
|
188
|
+
cleanup();
|
|
189
|
+
}
|
|
190
|
+
this.cleanups = [];
|
|
191
|
+
this.transport.dispose();
|
|
192
|
+
}
|
|
193
|
+
// ─── Private ──────────────────────────────────────────────────────────
|
|
194
|
+
shouldSample() {
|
|
195
|
+
if (this.config.sampleRate >= 1)
|
|
196
|
+
return true;
|
|
197
|
+
if (this.config.sampleRate <= 0)
|
|
198
|
+
return false;
|
|
199
|
+
return Math.random() < this.config.sampleRate;
|
|
200
|
+
}
|
|
201
|
+
/** Enrich an error event with current context and send it. */
|
|
202
|
+
sendErrorEvent(event) {
|
|
203
|
+
const prepared = prepareBrowserEvent(event, {
|
|
204
|
+
beforeSend: this.config.beforeSend,
|
|
205
|
+
breadcrumbs: this.breadcrumbs,
|
|
206
|
+
environment: this.config.environment,
|
|
207
|
+
release: this.config.release,
|
|
208
|
+
tags: this.tags,
|
|
209
|
+
user: this.user,
|
|
210
|
+
});
|
|
211
|
+
if (prepared) {
|
|
212
|
+
this.transport.sendEvent(prepared);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser error capture.
|
|
3
|
+
*
|
|
4
|
+
* Installs global error handlers:
|
|
5
|
+
* - window.addEventListener("error") — sync errors
|
|
6
|
+
* - window.addEventListener("unhandledrejection") — promise rejections
|
|
7
|
+
*
|
|
8
|
+
* Parses Error.stack into StackFrame[] using the same regex pattern as
|
|
9
|
+
* @squasher-ai/node. In-app heuristic: node_modules = not in_app.
|
|
10
|
+
*/
|
|
11
|
+
import type { BrowserErrorEvent, StackFrame } from "./types";
|
|
12
|
+
/**
|
|
13
|
+
* Parse an Error.stack string into structured StackFrame[].
|
|
14
|
+
*/
|
|
15
|
+
export declare function parseStack(stack?: string): StackFrame[] | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* Build a BrowserErrorEvent from an Error object.
|
|
18
|
+
*/
|
|
19
|
+
export declare function buildErrorEvent(error: Error, extra?: Record<string, unknown>): BrowserErrorEvent;
|
|
20
|
+
/**
|
|
21
|
+
* Build a BrowserErrorEvent from a string message.
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildMessageEvent(message: string, level?: BrowserErrorEvent["level"]): BrowserErrorEvent;
|
|
24
|
+
/**
|
|
25
|
+
* Install global error handlers.
|
|
26
|
+
* Returns a cleanup function to remove the handlers.
|
|
27
|
+
*/
|
|
28
|
+
export declare function installGlobalErrorHandlers(onError: (event: BrowserErrorEvent) => void): () => void;
|
|
29
|
+
//# sourceMappingURL=errors.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAW7D;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,UAAU,EAAE,GAAG,SAAS,CAkCnE;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,iBAAiB,CAWhG;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,KAAK,GAAE,iBAAiB,CAAC,OAAO,CAAU,GACzC,iBAAiB,CAOnB;AAED;;;GAGG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAC1C,MAAM,IAAI,CAuCZ"}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Browser error capture.
|
|
3
|
+
*
|
|
4
|
+
* Installs global error handlers:
|
|
5
|
+
* - window.addEventListener("error") — sync errors
|
|
6
|
+
* - window.addEventListener("unhandledrejection") — promise rejections
|
|
7
|
+
*
|
|
8
|
+
* Parses Error.stack into StackFrame[] using the same regex pattern as
|
|
9
|
+
* @squasher-ai/node. In-app heuristic: node_modules = not in_app.
|
|
10
|
+
*/
|
|
11
|
+
const SDK_NAME = "@squasher-ai/browser";
|
|
12
|
+
const SDK_VERSION = "0.1.0";
|
|
13
|
+
/** Stack trace line regex — works for V8 (Chrome, Node) and SpiderMonkey (Firefox). */
|
|
14
|
+
const STACK_LINE_RE = /^\s*at\s+(?:(.+?)\s+\()?(.+?):(\d+):(\d+)\)?/;
|
|
15
|
+
/** Firefox stack format: "functionName@filename:line:col" */
|
|
16
|
+
const FIREFOX_STACK_RE = /^(.+?)@(.+?):(\d+):(\d+)$/;
|
|
17
|
+
/**
|
|
18
|
+
* Parse an Error.stack string into structured StackFrame[].
|
|
19
|
+
*/
|
|
20
|
+
export function parseStack(stack) {
|
|
21
|
+
if (!stack)
|
|
22
|
+
return undefined;
|
|
23
|
+
const lines = stack.split("\n").slice(1);
|
|
24
|
+
const frames = [];
|
|
25
|
+
for (const line of lines) {
|
|
26
|
+
const trimmed = line.trim();
|
|
27
|
+
// Try V8 format first
|
|
28
|
+
let match = trimmed.match(STACK_LINE_RE);
|
|
29
|
+
if (!match) {
|
|
30
|
+
// Try Firefox format
|
|
31
|
+
match = trimmed.match(FIREFOX_STACK_RE);
|
|
32
|
+
}
|
|
33
|
+
if (!match)
|
|
34
|
+
continue;
|
|
35
|
+
const [, fn, filename, lineno, colno] = match;
|
|
36
|
+
const isExternal = filename?.includes("node_modules") ||
|
|
37
|
+
filename?.includes("extensions/") ||
|
|
38
|
+
filename?.startsWith("chrome-extension://") ||
|
|
39
|
+
false;
|
|
40
|
+
frames.push({
|
|
41
|
+
function: fn || "<anonymous>",
|
|
42
|
+
filename,
|
|
43
|
+
lineno: lineno ? parseInt(lineno, 10) : undefined,
|
|
44
|
+
colno: colno ? parseInt(colno, 10) : undefined,
|
|
45
|
+
in_app: !isExternal,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
return frames.length > 0 ? frames : undefined;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build a BrowserErrorEvent from an Error object.
|
|
52
|
+
*/
|
|
53
|
+
export function buildErrorEvent(error, extra) {
|
|
54
|
+
return {
|
|
55
|
+
message: error.message || String(error),
|
|
56
|
+
type: error.name,
|
|
57
|
+
stack: error.stack,
|
|
58
|
+
frames: parseStack(error.stack),
|
|
59
|
+
level: "error",
|
|
60
|
+
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
|
61
|
+
timestamp: new Date().toISOString(),
|
|
62
|
+
extra,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Build a BrowserErrorEvent from a string message.
|
|
67
|
+
*/
|
|
68
|
+
export function buildMessageEvent(message, level = "info") {
|
|
69
|
+
return {
|
|
70
|
+
message,
|
|
71
|
+
level,
|
|
72
|
+
sdk: { name: SDK_NAME, version: SDK_VERSION },
|
|
73
|
+
timestamp: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Install global error handlers.
|
|
78
|
+
* Returns a cleanup function to remove the handlers.
|
|
79
|
+
*/
|
|
80
|
+
export function installGlobalErrorHandlers(onError) {
|
|
81
|
+
const errorHandler = (event) => {
|
|
82
|
+
// Ignore errors from browser extensions and cross-origin scripts
|
|
83
|
+
if (!event.error && !event.message)
|
|
84
|
+
return;
|
|
85
|
+
const error = event.error instanceof Error ? event.error : new Error(event.message || "Unknown error");
|
|
86
|
+
const browserEvent = buildErrorEvent(error, {
|
|
87
|
+
filename: event.filename,
|
|
88
|
+
lineno: event.lineno,
|
|
89
|
+
colno: event.colno,
|
|
90
|
+
});
|
|
91
|
+
browserEvent.level = "error";
|
|
92
|
+
onError(browserEvent);
|
|
93
|
+
};
|
|
94
|
+
const rejectionHandler = (event) => {
|
|
95
|
+
const reason = event.reason;
|
|
96
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
97
|
+
const browserEvent = buildErrorEvent(error);
|
|
98
|
+
browserEvent.level = "error";
|
|
99
|
+
browserEvent.type =
|
|
100
|
+
browserEvent.type === "Error"
|
|
101
|
+
? "UnhandledRejection"
|
|
102
|
+
: `UnhandledRejection(${browserEvent.type})`;
|
|
103
|
+
onError(browserEvent);
|
|
104
|
+
};
|
|
105
|
+
window.addEventListener("error", errorHandler);
|
|
106
|
+
window.addEventListener("unhandledrejection", rejectionHandler);
|
|
107
|
+
return () => {
|
|
108
|
+
window.removeEventListener("error", errorHandler);
|
|
109
|
+
window.removeEventListener("unhandledrejection", rejectionHandler);
|
|
110
|
+
};
|
|
111
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @squasher-ai/browser — Browser SDK for Squasher error monitoring + Web Vitals.
|
|
3
|
+
*
|
|
4
|
+
* Lightweight (<5KB gzip) browser SDK that captures:
|
|
5
|
+
* - Core Web Vitals (LCP, CLS, INP, FCP, TTFB)
|
|
6
|
+
* - JavaScript errors (window.onerror, unhandledrejection)
|
|
7
|
+
* - Navigation breadcrumbs, click breadcrumbs, fetch error breadcrumbs
|
|
8
|
+
* - Session context for error ↔ vitals correlation
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* import { init, captureError } from '@squasher-ai/browser';
|
|
12
|
+
*
|
|
13
|
+
* init({
|
|
14
|
+
* apiKey: 'sq_pk_...',
|
|
15
|
+
* projectId: 'your-project-id',
|
|
16
|
+
* environment: 'production',
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Errors are captured automatically via global handlers.
|
|
20
|
+
* // Web Vitals are collected and sent automatically.
|
|
21
|
+
* // Manual capture:
|
|
22
|
+
* captureError(new Error('Something went wrong'));
|
|
23
|
+
*/
|
|
24
|
+
export { BrowserClient } from "./client";
|
|
25
|
+
export type { AnalyticsContext, Breadcrumb, BrowserConfig, BrowserErrorEvent, DeviceType, IngestBatchPayload, IngestResponse, JsonObject, LlmContext, Level, PageContext, ReplayConfig, ReplayPrivacyConfig, SessionContext, StackFrame, TelemetryKind, TelemetryMeasurement, ToolCallContext, UserContext, VitalEvent, VitalMetricName, VitalRating, TraceContext, VisitorContext, VitalsPayload, } from "./types";
|
|
26
|
+
import { BrowserClient } from "./client";
|
|
27
|
+
import type { Breadcrumb, BrowserConfig, BrowserErrorEvent, JsonObject, Level, UserContext } from "./types";
|
|
28
|
+
/** Initialize the global Squasher browser client. Throws if called twice. */
|
|
29
|
+
export declare function init(config: BrowserConfig): BrowserClient;
|
|
30
|
+
/** Get the global client (throws if not initialized). */
|
|
31
|
+
export declare function getClient(): BrowserClient;
|
|
32
|
+
/** Capture an error using the global client. */
|
|
33
|
+
export declare function captureError(error: Error, extra?: Record<string, unknown>): void;
|
|
34
|
+
/** Capture a message using the global client. */
|
|
35
|
+
export declare function captureMessage(message: string, level?: Level): void;
|
|
36
|
+
/** Capture a generalized telemetry event using the global client. */
|
|
37
|
+
export declare function captureTelemetry(event: BrowserErrorEvent): void;
|
|
38
|
+
/** Track a product analytics event using the global client. */
|
|
39
|
+
export declare function track(eventName: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
40
|
+
/** Identify a distinct user or visitor using the global client. */
|
|
41
|
+
export declare function identify(distinctId: string, traits?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
42
|
+
/** Capture a page navigation event using the global client. */
|
|
43
|
+
export declare function page(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
44
|
+
/** Capture a screen event using the global client. */
|
|
45
|
+
export declare function screen(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
|
|
46
|
+
/** Capture an agent span using the global client. */
|
|
47
|
+
export declare function captureSpan(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
48
|
+
/** Capture a tool call using the global client. */
|
|
49
|
+
export declare function captureToolCall(name: string, context?: Partial<BrowserErrorEvent>): void;
|
|
50
|
+
/** Capture an LLM generation event using the global client. */
|
|
51
|
+
export declare function captureGeneration(message: string, context?: Partial<BrowserErrorEvent>): void;
|
|
52
|
+
/** Set user context on the global client. */
|
|
53
|
+
export declare function setUser(user: UserContext | undefined): void;
|
|
54
|
+
/** Set a single tag on the global client. */
|
|
55
|
+
export declare function setTag(key: string, value: string): void;
|
|
56
|
+
/** Set multiple tags on the global client. */
|
|
57
|
+
export declare function setTags(tags: Record<string, string>): void;
|
|
58
|
+
/** Add a breadcrumb on the global client. */
|
|
59
|
+
export declare function addBreadcrumb(crumb: Omit<Breadcrumb, "timestamp">): void;
|
|
60
|
+
/** Close the global client (stop timers, flush vitals, remove handlers). */
|
|
61
|
+
export declare function close(): void;
|
|
62
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|