@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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -0
  3. package/dist/__tests__/client.test.d.ts +2 -0
  4. package/dist/__tests__/client.test.d.ts.map +1 -0
  5. package/dist/__tests__/client.test.js +103 -0
  6. package/dist/__tests__/errors.test.d.ts +2 -0
  7. package/dist/__tests__/errors.test.d.ts.map +1 -0
  8. package/dist/__tests__/errors.test.js +80 -0
  9. package/dist/__tests__/replay-privacy.test.d.ts +2 -0
  10. package/dist/__tests__/replay-privacy.test.d.ts.map +1 -0
  11. package/dist/__tests__/replay-privacy.test.js +124 -0
  12. package/dist/__tests__/replay-startup.test.d.ts +2 -0
  13. package/dist/__tests__/replay-startup.test.d.ts.map +1 -0
  14. package/dist/__tests__/replay-startup.test.js +99 -0
  15. package/dist/__tests__/replay.test.d.ts +2 -0
  16. package/dist/__tests__/replay.test.d.ts.map +1 -0
  17. package/dist/__tests__/replay.test.js +217 -0
  18. package/dist/__tests__/session.test.d.ts +2 -0
  19. package/dist/__tests__/session.test.d.ts.map +1 -0
  20. package/dist/__tests__/session.test.js +46 -0
  21. package/dist/__tests__/transport.test.d.ts +2 -0
  22. package/dist/__tests__/transport.test.d.ts.map +1 -0
  23. package/dist/__tests__/transport.test.js +101 -0
  24. package/dist/__tests__/types.test.d.ts +2 -0
  25. package/dist/__tests__/types.test.d.ts.map +1 -0
  26. package/dist/__tests__/types.test.js +139 -0
  27. package/dist/autocapture.d.ts +21 -0
  28. package/dist/autocapture.d.ts.map +1 -0
  29. package/dist/autocapture.js +191 -0
  30. package/dist/client.d.ts +46 -0
  31. package/dist/client.d.ts.map +1 -0
  32. package/dist/client.js +215 -0
  33. package/dist/errors.d.ts +29 -0
  34. package/dist/errors.d.ts.map +1 -0
  35. package/dist/errors.js +111 -0
  36. package/dist/index.d.ts +62 -0
  37. package/dist/index.d.ts.map +1 -0
  38. package/dist/index.js +105 -0
  39. package/dist/react.d.ts +32 -0
  40. package/dist/react.d.ts.map +1 -0
  41. package/dist/react.js +42 -0
  42. package/dist/replay-privacy.d.ts +16 -0
  43. package/dist/replay-privacy.d.ts.map +1 -0
  44. package/dist/replay-privacy.js +64 -0
  45. package/dist/replay.d.ts +30 -0
  46. package/dist/replay.d.ts.map +1 -0
  47. package/dist/replay.js +158 -0
  48. package/dist/session.d.ts +21 -0
  49. package/dist/session.d.ts.map +1 -0
  50. package/dist/session.js +52 -0
  51. package/dist/telemetry.d.ts +20 -0
  52. package/dist/telemetry.d.ts.map +1 -0
  53. package/dist/telemetry.js +145 -0
  54. package/dist/transport.d.ts +47 -0
  55. package/dist/transport.d.ts.map +1 -0
  56. package/dist/transport.js +145 -0
  57. package/dist/types.d.ts +234 -0
  58. package/dist/types.d.ts.map +1 -0
  59. package/dist/types.js +8 -0
  60. package/dist/vitals.d.ts +30 -0
  61. package/dist/vitals.d.ts.map +1 -0
  62. package/dist/vitals.js +92 -0
  63. package/package.json +44 -0
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
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
+ // ─── Global singleton ───────────────────────────────────────────────────────
26
+ import { BrowserClient } from "./client";
27
+ let _client = null;
28
+ /** Initialize the global Squasher browser client. Throws if called twice. */
29
+ export function init(config) {
30
+ if (_client) {
31
+ throw new Error("squasher.init() called more than once. Call close() first if re-initializing.");
32
+ }
33
+ _client = new BrowserClient(config);
34
+ return _client;
35
+ }
36
+ /** Get the global client (throws if not initialized). */
37
+ export function getClient() {
38
+ if (!_client) {
39
+ throw new Error("Squasher not initialized. Call init() first.");
40
+ }
41
+ return _client;
42
+ }
43
+ /** Capture an error using the global client. */
44
+ export function captureError(error, extra) {
45
+ getClient().captureError(error, extra);
46
+ }
47
+ /** Capture a message using the global client. */
48
+ export function captureMessage(message, level) {
49
+ getClient().captureMessage(message, level);
50
+ }
51
+ /** Capture a generalized telemetry event using the global client. */
52
+ export function captureTelemetry(event) {
53
+ getClient().captureTelemetry(event);
54
+ }
55
+ /** Track a product analytics event using the global client. */
56
+ export function track(eventName, properties, context) {
57
+ getClient().track(eventName, properties, context);
58
+ }
59
+ /** Identify a distinct user or visitor using the global client. */
60
+ export function identify(distinctId, traits, context) {
61
+ getClient().identify(distinctId, traits, context);
62
+ }
63
+ /** Capture a page navigation event using the global client. */
64
+ export function page(name, properties, context) {
65
+ getClient().page(name, properties, context);
66
+ }
67
+ /** Capture a screen event using the global client. */
68
+ export function screen(name, properties, context) {
69
+ getClient().screen(name, properties, context);
70
+ }
71
+ /** Capture an agent span using the global client. */
72
+ export function captureSpan(name, context) {
73
+ getClient().captureSpan(name, context);
74
+ }
75
+ /** Capture a tool call using the global client. */
76
+ export function captureToolCall(name, context) {
77
+ getClient().captureToolCall(name, context);
78
+ }
79
+ /** Capture an LLM generation event using the global client. */
80
+ export function captureGeneration(message, context) {
81
+ getClient().captureGeneration(message, context);
82
+ }
83
+ /** Set user context on the global client. */
84
+ export function setUser(user) {
85
+ getClient().setUser(user);
86
+ }
87
+ /** Set a single tag on the global client. */
88
+ export function setTag(key, value) {
89
+ getClient().setTag(key, value);
90
+ }
91
+ /** Set multiple tags on the global client. */
92
+ export function setTags(tags) {
93
+ getClient().setTags(tags);
94
+ }
95
+ /** Add a breadcrumb on the global client. */
96
+ export function addBreadcrumb(crumb) {
97
+ getClient().addBreadcrumb(crumb);
98
+ }
99
+ /** Close the global client (stop timers, flush vitals, remove handlers). */
100
+ export function close() {
101
+ if (_client) {
102
+ _client.close();
103
+ _client = null;
104
+ }
105
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * React integration for @squasher-ai/browser.
3
+ *
4
+ * Provides a SquasherErrorBoundary component that captures React render
5
+ * errors to Squasher, including the componentStack for debugging.
6
+ *
7
+ * Usage:
8
+ * import { SquasherErrorBoundary } from '@squasher-ai/browser/react';
9
+ *
10
+ * <SquasherErrorBoundary fallback={<div>Something went wrong</div>}>
11
+ * <App />
12
+ * </SquasherErrorBoundary>
13
+ */
14
+ import { Component, type ErrorInfo, type ReactNode } from "react";
15
+ interface Props {
16
+ children: ReactNode;
17
+ /** Fallback UI to render when an error is caught. */
18
+ fallback?: ReactNode;
19
+ /** Optional callback when an error is caught. */
20
+ onError?: (error: Error, errorInfo: ErrorInfo) => void;
21
+ }
22
+ interface State {
23
+ hasError: boolean;
24
+ }
25
+ export declare class SquasherErrorBoundary extends Component<Props, State> {
26
+ constructor(props: Props);
27
+ static getDerivedStateFromError(): State;
28
+ componentDidCatch(error: Error, errorInfo: ErrorInfo): void;
29
+ render(): ReactNode;
30
+ }
31
+ export {};
32
+ //# sourceMappingURL=react.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGlE,UAAU,KAAK;IACb,QAAQ,EAAE,SAAS,CAAC;IACpB,qDAAqD;IACrD,QAAQ,CAAC,EAAE,SAAS,CAAC;IACrB,iDAAiD;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,KAAK,IAAI,CAAC;CACxD;AAED,UAAU,KAAK;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,qBAAa,qBAAsB,SAAQ,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC;gBACpD,KAAK,EAAE,KAAK;IAKxB,MAAM,CAAC,wBAAwB,IAAI,KAAK;IAI/B,iBAAiB,CAAC,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,SAAS,GAAG,IAAI;IAa3D,MAAM,IAAI,SAAS;CAM7B"}
package/dist/react.js ADDED
@@ -0,0 +1,42 @@
1
+ /**
2
+ * React integration for @squasher-ai/browser.
3
+ *
4
+ * Provides a SquasherErrorBoundary component that captures React render
5
+ * errors to Squasher, including the componentStack for debugging.
6
+ *
7
+ * Usage:
8
+ * import { SquasherErrorBoundary } from '@squasher-ai/browser/react';
9
+ *
10
+ * <SquasherErrorBoundary fallback={<div>Something went wrong</div>}>
11
+ * <App />
12
+ * </SquasherErrorBoundary>
13
+ */
14
+ import { Component } from "react";
15
+ import { getClient } from "./index";
16
+ export class SquasherErrorBoundary extends Component {
17
+ constructor(props) {
18
+ super(props);
19
+ this.state = { hasError: false };
20
+ }
21
+ static getDerivedStateFromError() {
22
+ return { hasError: true };
23
+ }
24
+ componentDidCatch(error, errorInfo) {
25
+ try {
26
+ const client = getClient();
27
+ client.captureError(error, {
28
+ componentStack: errorInfo.componentStack ?? undefined,
29
+ });
30
+ }
31
+ catch {
32
+ // SDK not initialized — silently ignore
33
+ }
34
+ this.props.onError?.(error, errorInfo);
35
+ }
36
+ render() {
37
+ if (this.state.hasError) {
38
+ return this.props.fallback ?? null;
39
+ }
40
+ return this.props.children;
41
+ }
42
+ }
@@ -0,0 +1,16 @@
1
+ import type { ReplayPrivacyConfig } from "./types";
2
+ type ReplayRecordFn = typeof import("rrweb")["record"];
3
+ type ReplayRecordOptions = NonNullable<Parameters<ReplayRecordFn>[0]>;
4
+ type ReplayEmit = NonNullable<ReplayRecordOptions["emit"]>;
5
+ type ReplayEvent = Parameters<ReplayEmit>[0];
6
+ export type ReplayPrivacyRecordOptions = ReplayRecordOptions & {
7
+ maskAllText?: boolean;
8
+ };
9
+ export declare const MASK_ALL_TEXT_SELECTOR = "body, body *";
10
+ export declare function buildReplayRecordOptions(input: {
11
+ emit: ReplayEmit;
12
+ privacy?: ReplayPrivacyConfig;
13
+ }): ReplayPrivacyRecordOptions;
14
+ export declare function applyReplayEventPrivacy<T extends ReplayEvent>(event: T, privacy?: ReplayPrivacyConfig): T;
15
+ export {};
16
+ //# sourceMappingURL=replay-privacy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay-privacy.d.ts","sourceRoot":"","sources":["../src/replay-privacy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAEnD,KAAK,cAAc,GAAG,cAAc,OAAO,CAAC,CAAC,QAAQ,CAAC,CAAC;AACvD,KAAK,mBAAmB,GAAG,WAAW,CAAC,UAAU,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AACtE,KAAK,UAAU,GAAG,WAAW,CAAC,mBAAmB,CAAC,MAAM,CAAC,CAAC,CAAC;AAC3D,KAAK,WAAW,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC;AAc7C,MAAM,MAAM,0BAA0B,GAAG,mBAAmB,GAAG;IAC7D,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAEF,eAAO,MAAM,sBAAsB,iBAAiB,CAAC;AAWrD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B,GAAG,0BAA0B,CAgB7B;AAED,wBAAgB,uBAAuB,CAAC,CAAC,SAAS,WAAW,EAC3D,KAAK,EAAE,CAAC,EACR,OAAO,CAAC,EAAE,mBAAmB,GAC5B,CAAC,CASH"}
@@ -0,0 +1,64 @@
1
+ export const MASK_ALL_TEXT_SELECTOR = "body, body *";
2
+ const MASKED_TEXT_KEYS = new Set([
3
+ "aria-label",
4
+ "placeholder",
5
+ "text",
6
+ "textContent",
7
+ "title",
8
+ "value",
9
+ ]);
10
+ export function buildReplayRecordOptions(input) {
11
+ const options = {
12
+ emit: input.emit,
13
+ };
14
+ if (input.privacy?.blockSelector) {
15
+ options.blockSelector = input.privacy.blockSelector;
16
+ }
17
+ if (input.privacy?.maskAllText) {
18
+ options.maskAllInputs = true;
19
+ options.maskAllText = true;
20
+ options.maskTextSelector = MASK_ALL_TEXT_SELECTOR;
21
+ }
22
+ return options;
23
+ }
24
+ export function applyReplayEventPrivacy(event, privacy) {
25
+ if (!privacy?.maskAllText) {
26
+ return event;
27
+ }
28
+ return redactReplayValue(event, {
29
+ currentTagName: null,
30
+ key: "",
31
+ });
32
+ }
33
+ function redactReplayValue(value, context) {
34
+ if (typeof value === "string") {
35
+ return shouldMaskString(context) ? maskReplayText(value) : value;
36
+ }
37
+ if (Array.isArray(value)) {
38
+ return value.map((entry) => redactReplayValue(entry, context));
39
+ }
40
+ if (!isReplayPrivacyObject(value)) {
41
+ return value;
42
+ }
43
+ const nextTagName = readTagName(value) ?? context.currentTagName;
44
+ const nextValue = {};
45
+ for (const [entryKey, entryValue] of Object.entries(value)) {
46
+ nextValue[entryKey] = redactReplayValue(entryValue, {
47
+ currentTagName: nextTagName,
48
+ key: entryKey,
49
+ });
50
+ }
51
+ return nextValue;
52
+ }
53
+ function isReplayPrivacyObject(value) {
54
+ return typeof value === "object" && value !== null && !Array.isArray(value);
55
+ }
56
+ function readTagName(value) {
57
+ return typeof value.tagName === "string" ? value.tagName.toLowerCase() : null;
58
+ }
59
+ function maskReplayText(value) {
60
+ return value.replace(/[^\s]/g, "*");
61
+ }
62
+ function shouldMaskString(context) {
63
+ return MASKED_TEXT_KEYS.has(context.key);
64
+ }
@@ -0,0 +1,30 @@
1
+ import type { ReplayPrivacyConfig } from "./types";
2
+ interface ReplayRecorderConfig {
3
+ endpoint: string;
4
+ projectId: string;
5
+ apiKey: string;
6
+ debug: boolean;
7
+ sampleRate: number;
8
+ privacy?: ReplayPrivacyConfig;
9
+ }
10
+ export declare class ReplayRecorder {
11
+ readonly ready: Promise<void>;
12
+ private readonly sessionId;
13
+ private readonly startedAt;
14
+ private readonly cleanups;
15
+ private readonly config;
16
+ private buffer;
17
+ private flushTimer;
18
+ private stopRecording;
19
+ private disposed;
20
+ constructor(config: ReplayRecorderConfig);
21
+ dispose(): void;
22
+ private start;
23
+ private enqueue;
24
+ private flushBufferedEvents;
25
+ private buildReplayUrl;
26
+ private trySendBeacon;
27
+ private sendFetch;
28
+ }
29
+ export {};
30
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../src/replay.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,SAAS,CAAC;AAWnD,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAwBD,qBAAa,cAAc;IACzB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAkB;IAC5C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4B;IACtD,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAyB;IAClD,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAuB;IAC9C,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,aAAa,CAA8B;IACnD,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,oBAAoB;IAcxC,OAAO,IAAI,IAAI;YAuBD,KAAK;IA8CnB,OAAO,CAAC,OAAO;IAUf,OAAO,CAAC,mBAAmB;IAwB3B,OAAO,CAAC,cAAc;IAQtB,OAAO,CAAC,aAAa;YAaP,SAAS;CAqBxB"}
package/dist/replay.js ADDED
@@ -0,0 +1,158 @@
1
+ import { getSessionId } from "./session";
2
+ import { applyReplayEventPrivacy, buildReplayRecordOptions } from "./replay-privacy";
3
+ const REPLAY_BUFFER_LIMIT = 50;
4
+ const REPLAY_FLUSH_INTERVAL_MS = 5_000;
5
+ function shouldRecord(sampleRate) {
6
+ if (sampleRate >= 1)
7
+ return true;
8
+ if (sampleRate <= 0)
9
+ return false;
10
+ return Math.random() < sampleRate;
11
+ }
12
+ function getPageUrl() {
13
+ return typeof location === "undefined" ? "" : location.href;
14
+ }
15
+ export class ReplayRecorder {
16
+ ready;
17
+ sessionId = getSessionId();
18
+ startedAt = new Date().toISOString();
19
+ cleanups = [];
20
+ config;
21
+ buffer = [];
22
+ flushTimer = null;
23
+ stopRecording = null;
24
+ disposed = false;
25
+ constructor(config) {
26
+ this.config = config;
27
+ if (!shouldRecord(config.sampleRate)) {
28
+ if (this.config.debug) {
29
+ console.log("[squasher] Replay recording skipped by sample rate");
30
+ }
31
+ this.ready = Promise.resolve();
32
+ return;
33
+ }
34
+ this.ready = this.start();
35
+ }
36
+ dispose() {
37
+ if (this.disposed)
38
+ return;
39
+ this.disposed = true;
40
+ if (this.flushTimer) {
41
+ clearInterval(this.flushTimer);
42
+ this.flushTimer = null;
43
+ }
44
+ if (this.stopRecording) {
45
+ this.stopRecording();
46
+ this.stopRecording = null;
47
+ }
48
+ for (const cleanup of this.cleanups) {
49
+ cleanup();
50
+ }
51
+ this.cleanups.length = 0;
52
+ this.flushBufferedEvents({ final: true, allowWhenDisposed: true });
53
+ }
54
+ async start() {
55
+ if (typeof window === "undefined" || typeof document === "undefined") {
56
+ return;
57
+ }
58
+ const { record } = await import("rrweb");
59
+ if (this.disposed) {
60
+ return;
61
+ }
62
+ const stopRecording = record(buildReplayRecordOptions({
63
+ emit: (event) => {
64
+ this.enqueue(applyReplayEventPrivacy(event, this.config.privacy));
65
+ },
66
+ privacy: this.config.privacy,
67
+ }));
68
+ if (!stopRecording) {
69
+ return;
70
+ }
71
+ this.stopRecording = stopRecording;
72
+ this.flushTimer = setInterval(() => {
73
+ this.flushBufferedEvents();
74
+ }, REPLAY_FLUSH_INTERVAL_MS);
75
+ const finalFlush = () => {
76
+ this.flushBufferedEvents({ final: true });
77
+ };
78
+ window.addEventListener("pagehide", finalFlush);
79
+ window.addEventListener("beforeunload", finalFlush);
80
+ this.cleanups.push(() => window.removeEventListener("pagehide", finalFlush));
81
+ this.cleanups.push(() => window.removeEventListener("beforeunload", finalFlush));
82
+ if (this.config.debug) {
83
+ console.log("[squasher] Replay recording started", {
84
+ sessionId: this.sessionId,
85
+ });
86
+ }
87
+ }
88
+ enqueue(event) {
89
+ if (this.disposed)
90
+ return;
91
+ this.buffer.push(event);
92
+ if (this.buffer.length >= REPLAY_BUFFER_LIMIT) {
93
+ this.flushBufferedEvents();
94
+ }
95
+ }
96
+ flushBufferedEvents(options) {
97
+ if (this.buffer.length === 0)
98
+ return;
99
+ if (this.disposed && !options?.allowWhenDisposed)
100
+ return;
101
+ const events = this.buffer.splice(0);
102
+ const payload = {
103
+ session_id: this.sessionId,
104
+ events,
105
+ page_url: getPageUrl(),
106
+ started_at: this.startedAt,
107
+ };
108
+ const body = JSON.stringify(payload);
109
+ const url = this.buildReplayUrl();
110
+ if (options?.final && this.trySendBeacon(this.buildReplayUrl({ beacon: true }), body)) {
111
+ if (this.config.debug) {
112
+ console.log(`[squasher] Flushed ${events.length} replay events via sendBeacon`);
113
+ }
114
+ return;
115
+ }
116
+ void this.sendFetch(url, body);
117
+ }
118
+ buildReplayUrl(options) {
119
+ const baseUrl = `${this.config.endpoint}/v1/projects/${this.config.projectId}/replays`;
120
+ if (!options?.beacon) {
121
+ return baseUrl;
122
+ }
123
+ return `${baseUrl}?key=${encodeURIComponent(this.config.apiKey)}`;
124
+ }
125
+ trySendBeacon(url, body) {
126
+ if (typeof navigator === "undefined" || !navigator.sendBeacon) {
127
+ return false;
128
+ }
129
+ try {
130
+ const blob = new Blob([body], { type: "application/json" });
131
+ return navigator.sendBeacon(url, blob);
132
+ }
133
+ catch {
134
+ return false;
135
+ }
136
+ }
137
+ async sendFetch(url, body) {
138
+ try {
139
+ await fetch(url, {
140
+ method: "POST",
141
+ headers: {
142
+ "Content-Type": "application/json",
143
+ "x-squasher-key": this.config.apiKey,
144
+ },
145
+ body,
146
+ keepalive: true,
147
+ });
148
+ if (this.config.debug) {
149
+ console.log("[squasher] Replay events sent via fetch");
150
+ }
151
+ }
152
+ catch (error) {
153
+ if (this.config.debug) {
154
+ console.warn("[squasher] Failed to upload replay events:", error);
155
+ }
156
+ }
157
+ }
158
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Session management for the browser SDK.
3
+ *
4
+ * Generates a unique session ID per tab visit using crypto.randomUUID().
5
+ * Stored in sessionStorage so it persists across SPA navigations but dies
6
+ * when the tab closes — one session per tab visit.
7
+ *
8
+ * The session ID is attached to both error events and Web Vitals for
9
+ * cross-correlation in the dashboard.
10
+ */
11
+ /**
12
+ * Get (or create) the session ID for this tab.
13
+ * Uses sessionStorage for persistence across SPA navigations.
14
+ * Falls back to in-memory cache if sessionStorage is unavailable (SSR, iframes).
15
+ */
16
+ export declare function getSessionId(): string;
17
+ /**
18
+ * Reset the session ID. Useful for testing or explicit session boundaries.
19
+ */
20
+ export declare function resetSessionId(): void;
21
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAMH;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAuBrC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAOrC"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Session management for the browser SDK.
3
+ *
4
+ * Generates a unique session ID per tab visit using crypto.randomUUID().
5
+ * Stored in sessionStorage so it persists across SPA navigations but dies
6
+ * when the tab closes — one session per tab visit.
7
+ *
8
+ * The session ID is attached to both error events and Web Vitals for
9
+ * cross-correlation in the dashboard.
10
+ */
11
+ const SESSION_KEY = "__sq_sid";
12
+ let cachedSessionId = null;
13
+ /**
14
+ * Get (or create) the session ID for this tab.
15
+ * Uses sessionStorage for persistence across SPA navigations.
16
+ * Falls back to in-memory cache if sessionStorage is unavailable (SSR, iframes).
17
+ */
18
+ export function getSessionId() {
19
+ if (cachedSessionId)
20
+ return cachedSessionId;
21
+ try {
22
+ const stored = sessionStorage.getItem(SESSION_KEY);
23
+ if (stored) {
24
+ cachedSessionId = stored;
25
+ return stored;
26
+ }
27
+ }
28
+ catch {
29
+ // sessionStorage unavailable (e.g. SSR, sandboxed iframes)
30
+ }
31
+ const id = crypto.randomUUID();
32
+ cachedSessionId = id;
33
+ try {
34
+ sessionStorage.setItem(SESSION_KEY, id);
35
+ }
36
+ catch {
37
+ // Storage full or unavailable — fall back to in-memory
38
+ }
39
+ return id;
40
+ }
41
+ /**
42
+ * Reset the session ID. Useful for testing or explicit session boundaries.
43
+ */
44
+ export function resetSessionId() {
45
+ cachedSessionId = null;
46
+ try {
47
+ sessionStorage.removeItem(SESSION_KEY);
48
+ }
49
+ catch {
50
+ // Ignore
51
+ }
52
+ }
@@ -0,0 +1,20 @@
1
+ import type { Breadcrumb, BrowserErrorEvent, JsonObject, UserContext } from "./types";
2
+ interface PrepareBrowserEventInput {
3
+ beforeSend?: (event: BrowserErrorEvent) => BrowserErrorEvent | null;
4
+ breadcrumbs: Breadcrumb[];
5
+ environment?: string;
6
+ release?: string;
7
+ tags: Record<string, string>;
8
+ user?: UserContext;
9
+ }
10
+ export declare function mergeIdentifiedUser(currentUser: UserContext | undefined, contextUser: UserContext | undefined, distinctId: string): UserContext;
11
+ export declare function buildTrackEvent(eventName: string, properties: JsonObject | undefined, context: Partial<BrowserErrorEvent>): BrowserErrorEvent;
12
+ export declare function buildIdentifyEvent(distinctId: string, traits: JsonObject | undefined, context: Partial<BrowserErrorEvent>): BrowserErrorEvent;
13
+ export declare function buildPageEvent(name: string, properties: JsonObject | undefined, context: Partial<BrowserErrorEvent>): BrowserErrorEvent;
14
+ export declare function buildScreenEvent(name: string, properties: JsonObject | undefined, context: Partial<BrowserErrorEvent>): BrowserErrorEvent;
15
+ export declare function buildSpanEvent(name: string, context: Partial<BrowserErrorEvent>): BrowserErrorEvent;
16
+ export declare function buildToolCallEvent(name: string, context: Partial<BrowserErrorEvent>): BrowserErrorEvent;
17
+ export declare function buildGenerationEvent(message: string, context: Partial<BrowserErrorEvent>): BrowserErrorEvent;
18
+ export declare function prepareBrowserEvent(event: BrowserErrorEvent, input: PrepareBrowserEventInput): BrowserErrorEvent | null;
19
+ export {};
20
+ //# sourceMappingURL=telemetry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"telemetry.d.ts","sourceRoot":"","sources":["../src/telemetry.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAEtF,UAAU,wBAAwB;IAChC,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,iBAAiB,GAAG,IAAI,CAAC;IACpE,WAAW,EAAE,UAAU,EAAE,CAAC;IAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,WAAW,EAAE,WAAW,GAAG,SAAS,EACpC,UAAU,EAAE,MAAM,GACjB,WAAW,CAMb;AAED,wBAAgB,eAAe,CAC7B,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,UAAU,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB,CAanB;AAED,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB,CAkBnB;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB,CAuBnB;AAED,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GAAG,SAAS,EAClC,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB,CAkBnB;AAED,wBAAgB,cAAc,CAC5B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB,CAYnB;AAED,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB,CAYnB;AAED,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAClC,iBAAiB,CAQnB;AAED,wBAAgB,mBAAmB,CACjC,KAAK,EAAE,iBAAiB,EACxB,KAAK,EAAE,wBAAwB,GAC9B,iBAAiB,GAAG,IAAI,CAkC1B"}