@squasher-ai/browser 0.1.1 → 0.2.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 (98) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -41
  3. package/dist/__tests__/client/harness.d.ts +18 -0
  4. package/dist/__tests__/client/harness.d.ts.map +1 -0
  5. package/dist/__tests__/client/harness.js +106 -0
  6. package/dist/__tests__/replay/coverage-harness.d.ts +29 -0
  7. package/dist/__tests__/replay/coverage-harness.d.ts.map +1 -0
  8. package/dist/__tests__/replay/coverage-harness.js +123 -0
  9. package/dist/__tests__/replay/test-helpers.d.ts +13 -0
  10. package/dist/__tests__/replay/test-helpers.d.ts.map +1 -0
  11. package/dist/__tests__/replay/test-helpers.js +9 -0
  12. package/dist/autocapture.d.ts +14 -18
  13. package/dist/autocapture.d.ts.map +1 -1
  14. package/dist/autocapture.js +77 -95
  15. package/dist/client.d.ts +12 -19
  16. package/dist/client.d.ts.map +1 -1
  17. package/dist/client.js +56 -72
  18. package/dist/config.d.ts +25 -0
  19. package/dist/config.d.ts.map +1 -0
  20. package/dist/config.js +26 -0
  21. package/dist/console-capture.d.ts +8 -0
  22. package/dist/console-capture.d.ts.map +1 -0
  23. package/dist/console-capture.js +79 -0
  24. package/dist/errors.d.ts +2 -2
  25. package/dist/errors.d.ts.map +1 -1
  26. package/dist/errors.js +8 -5
  27. package/dist/index.d.ts +5 -38
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +4 -82
  30. package/dist/next.d.ts +40 -0
  31. package/dist/next.d.ts.map +1 -0
  32. package/dist/next.js +67 -0
  33. package/dist/react.d.ts.map +1 -1
  34. package/dist/react.js +12 -10
  35. package/dist/replay-network.d.ts +21 -0
  36. package/dist/replay-network.d.ts.map +1 -0
  37. package/dist/replay-network.js +193 -0
  38. package/dist/replay-privacy.d.ts +2 -2
  39. package/dist/replay-privacy.d.ts.map +1 -1
  40. package/dist/replay-privacy.js +47 -17
  41. package/dist/replay-retry.d.ts +41 -0
  42. package/dist/replay-retry.d.ts.map +1 -0
  43. package/dist/replay-retry.js +193 -0
  44. package/dist/replay.d.ts +18 -5
  45. package/dist/replay.d.ts.map +1 -1
  46. package/dist/replay.js +131 -71
  47. package/dist/runtime/client-singleton.d.ts +25 -0
  48. package/dist/runtime/client-singleton.d.ts.map +1 -0
  49. package/dist/runtime/client-singleton.js +66 -0
  50. package/dist/runtime/client.d.ts +22 -0
  51. package/dist/runtime/client.d.ts.map +1 -0
  52. package/dist/runtime/client.js +12 -0
  53. package/dist/runtime/replay.d.ts +13 -0
  54. package/dist/runtime/replay.d.ts.map +1 -0
  55. package/dist/runtime/replay.js +15 -0
  56. package/dist/session.d.ts +0 -10
  57. package/dist/session.d.ts.map +1 -1
  58. package/dist/session.js +20 -21
  59. package/dist/telemetry.d.ts +1 -1
  60. package/dist/telemetry.d.ts.map +1 -1
  61. package/dist/telemetry.js +13 -10
  62. package/dist/trace-context.d.ts +9 -0
  63. package/dist/trace-context.d.ts.map +1 -0
  64. package/dist/trace-context.js +13 -0
  65. package/dist/transport.d.ts +11 -19
  66. package/dist/transport.d.ts.map +1 -1
  67. package/dist/transport.js +86 -66
  68. package/dist/types.d.ts +37 -135
  69. package/dist/types.d.ts.map +1 -1
  70. package/dist/types.js +0 -7
  71. package/dist/vitals.d.ts +55 -2
  72. package/dist/vitals.d.ts.map +1 -1
  73. package/dist/vitals.js +115 -16
  74. package/package.json +25 -3
  75. package/dist/__tests__/client.test.d.ts +0 -2
  76. package/dist/__tests__/client.test.d.ts.map +0 -1
  77. package/dist/__tests__/client.test.js +0 -103
  78. package/dist/__tests__/errors.test.d.ts +0 -2
  79. package/dist/__tests__/errors.test.d.ts.map +0 -1
  80. package/dist/__tests__/errors.test.js +0 -80
  81. package/dist/__tests__/replay-privacy.test.d.ts +0 -2
  82. package/dist/__tests__/replay-privacy.test.d.ts.map +0 -1
  83. package/dist/__tests__/replay-privacy.test.js +0 -124
  84. package/dist/__tests__/replay-startup.test.d.ts +0 -2
  85. package/dist/__tests__/replay-startup.test.d.ts.map +0 -1
  86. package/dist/__tests__/replay-startup.test.js +0 -99
  87. package/dist/__tests__/replay.test.d.ts +0 -2
  88. package/dist/__tests__/replay.test.d.ts.map +0 -1
  89. package/dist/__tests__/replay.test.js +0 -217
  90. package/dist/__tests__/session.test.d.ts +0 -2
  91. package/dist/__tests__/session.test.d.ts.map +0 -1
  92. package/dist/__tests__/session.test.js +0 -46
  93. package/dist/__tests__/transport.test.d.ts +0 -2
  94. package/dist/__tests__/transport.test.d.ts.map +0 -1
  95. package/dist/__tests__/transport.test.js +0 -101
  96. package/dist/__tests__/types.test.d.ts +0 -2
  97. package/dist/__tests__/types.test.d.ts.map +0 -1
  98. package/dist/__tests__/types.test.js +0 -139
package/dist/replay.js CHANGED
@@ -1,7 +1,25 @@
1
- import { getSessionId } from "./session";
2
- import { applyReplayEventPrivacy, buildReplayRecordOptions } from "./replay-privacy";
3
- const REPLAY_BUFFER_LIMIT = 50;
1
+ import { attempt } from "@squasher-ai/result-utils";
2
+ import { applyReplayEventPrivacy, buildReplayRecordOptions } from "./replay-privacy.js";
3
+ import { ReplayUploader } from "./replay-retry.js";
4
+ import { defaultReplayRecorderRuntime, } from "./runtime/replay.js";
5
+ import { getSessionId } from "./session.js";
6
+ // Flush on the same 5s cadence Sentry uses for session-mode replays. Anything
7
+ // shorter creates lots of tiny R2 blobs; anything longer hurts time-to-first-
8
+ // segment on the dashboard.
4
9
  const REPLAY_FLUSH_INTERVAL_MS = 5_000;
10
+ // Soft cap on buffered JSON bytes before we flush ahead of the timer. ~900KB
11
+ // stays well under our worker's 5MB body limit and PostHog's ~1MB-per-event
12
+ // guidance, while still letting a FullSnapshot ship in one segment.
13
+ const REPLAY_FLUSH_BYTES = 900_000;
14
+ const RRWEB_FULL_SNAPSHOT_EVENT = 2;
15
+ function estimateEventBytes(event) {
16
+ try {
17
+ return JSON.stringify(event).length;
18
+ }
19
+ catch {
20
+ return 0;
21
+ }
22
+ }
5
23
  function shouldRecord(sampleRate) {
6
24
  if (sampleRate >= 1)
7
25
  return true;
@@ -9,8 +27,11 @@ function shouldRecord(sampleRate) {
9
27
  return false;
10
28
  return Math.random() < sampleRate;
11
29
  }
30
+ function isReplayFullSnapshotEvent(event) {
31
+ return event instanceof Object && "type" in event && event.type === RRWEB_FULL_SNAPSHOT_EVENT;
32
+ }
12
33
  function getPageUrl() {
13
- return typeof location === "undefined" ? "" : location.href;
34
+ return globalThis.location?.href ?? "";
14
35
  }
15
36
  export class ReplayRecorder {
16
37
  ready;
@@ -18,12 +39,21 @@ export class ReplayRecorder {
18
39
  startedAt = new Date().toISOString();
19
40
  cleanups = [];
20
41
  config;
42
+ runtime;
21
43
  buffer = [];
44
+ bufferBytes = 0;
22
45
  flushTimer = null;
23
46
  stopRecording = null;
24
47
  disposed = false;
25
- constructor(config) {
48
+ uploader;
49
+ constructor(config, runtime = defaultReplayRecorderRuntime) {
26
50
  this.config = config;
51
+ this.runtime = runtime;
52
+ this.uploader = new ReplayUploader({
53
+ url: `${config.endpoint}/v1/projects/${config.projectId}/replays`,
54
+ headerKey: config.apiKey,
55
+ debug: config.debug,
56
+ });
27
57
  if (!shouldRecord(config.sampleRate)) {
28
58
  if (this.config.debug) {
29
59
  console.log("[squasher] Replay recording skipped by sample rate");
@@ -50,70 +80,121 @@ export class ReplayRecorder {
50
80
  }
51
81
  this.cleanups.length = 0;
52
82
  this.flushBufferedEvents({ final: true, allowWhenDisposed: true });
83
+ this.uploader.dispose();
53
84
  }
54
85
  async start() {
55
- if (typeof window === "undefined" || typeof document === "undefined") {
86
+ if (this.disposed || !globalThis.window || !globalThis.document) {
56
87
  return;
57
88
  }
58
- const { record } = await import("rrweb");
59
- if (this.disposed) {
60
- return;
89
+ let recording;
90
+ try {
91
+ recording = await this.runtime.loadRecordingAdapter();
61
92
  }
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) {
93
+ catch (error) {
94
+ if (this.config.debug) {
95
+ console.warn("[squasher] Replay recording could not start", error);
96
+ }
69
97
  return;
70
98
  }
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
- });
99
+ if (!this.disposed) {
100
+ const stopRecording = recording.start(buildReplayRecordOptions({
101
+ emit: (event) => {
102
+ this.enqueue(applyReplayEventPrivacy(event, this.config.privacy));
103
+ },
104
+ privacy: this.config.privacy,
105
+ }));
106
+ if (stopRecording) {
107
+ this.stopRecording = stopRecording;
108
+ this.flushTimer = setInterval(() => {
109
+ this.flushBufferedEvents();
110
+ }, REPLAY_FLUSH_INTERVAL_MS);
111
+ const finalFlush = () => {
112
+ this.flushBufferedEvents({ final: true });
113
+ };
114
+ window.addEventListener("pagehide", finalFlush);
115
+ window.addEventListener("beforeunload", finalFlush);
116
+ this.cleanups.push(() => window.removeEventListener("pagehide", finalFlush));
117
+ this.cleanups.push(() => window.removeEventListener("beforeunload", finalFlush));
118
+ this.cleanups.push(this.runtime.installNetworkCapture({
119
+ debug: this.config.debug,
120
+ emitCustomEvent: recording.emitCustomEvent,
121
+ ignoredUrlPrefixes: [
122
+ `${this.config.endpoint}/v1/ingest/${this.config.projectId}`,
123
+ `${this.config.endpoint}/v1/projects/${this.config.projectId}/replays`,
124
+ `${this.config.endpoint}/v1/vitals/${this.config.projectId}`,
125
+ ],
126
+ replayStartedAtMs: new Date(this.startedAt).getTime(),
127
+ }));
128
+ if (this.config.debug) {
129
+ console.log("[squasher] Replay recording started", {
130
+ sessionId: this.sessionId,
131
+ });
132
+ }
133
+ }
86
134
  }
87
135
  }
88
136
  enqueue(event) {
89
137
  if (this.disposed)
90
138
  return;
91
139
  this.buffer.push(event);
92
- if (this.buffer.length >= REPLAY_BUFFER_LIMIT) {
140
+ this.bufferBytes += estimateEventBytes(event);
141
+ if (isReplayFullSnapshotEvent(event) || this.bufferBytes >= REPLAY_FLUSH_BYTES) {
93
142
  this.flushBufferedEvents();
94
143
  }
95
144
  }
96
145
  flushBufferedEvents(options) {
97
- if (this.buffer.length === 0)
146
+ if (this.buffer.length === 0 && !options?.final)
98
147
  return;
99
148
  if (this.disposed && !options?.allowWhenDisposed)
100
149
  return;
150
+ if (options?.final) {
151
+ this.finalFlush();
152
+ return;
153
+ }
154
+ if (this.buffer.length === 0)
155
+ return;
101
156
  const events = this.buffer.splice(0);
157
+ this.bufferBytes = 0;
158
+ const body = this.buildPayloadBody(events);
159
+ this.uploader.enqueue(body);
160
+ }
161
+ /**
162
+ * Terminal flush during pagehide/beforeunload. Tries sendBeacon first for the
163
+ * current buffer and any pending retries, then falls back to keepalive fetch.
164
+ */
165
+ finalFlush() {
166
+ const pending = this.uploader.drain();
167
+ const bodies = [...pending];
168
+ if (this.buffer.length > 0) {
169
+ const events = this.buffer.splice(0);
170
+ this.bufferBytes = 0;
171
+ bodies.push(this.buildPayloadBody(events));
172
+ }
173
+ if (bodies.length === 0)
174
+ return;
175
+ const beaconUrl = this.buildReplayUrl({ beacon: true });
176
+ for (const body of bodies) {
177
+ if (!this.trySendBeacon(beaconUrl, body)) {
178
+ this.uploader.enqueue(body);
179
+ }
180
+ else if (this.config.debug) {
181
+ console.log("[squasher] Flushed replay payload via sendBeacon");
182
+ }
183
+ }
184
+ }
185
+ buildPayloadBody(events) {
102
186
  const payload = {
103
187
  session_id: this.sessionId,
104
188
  events,
105
189
  page_url: getPageUrl(),
106
190
  started_at: this.startedAt,
191
+ distinct_id: this.config.getDistinctId?.(),
192
+ user_id: this.config.getUserId?.(),
193
+ visitor_id: this.config.getVisitorId?.() ?? this.sessionId,
194
+ environment: this.config.environment,
195
+ release: this.config.release,
107
196
  };
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);
197
+ return JSON.stringify(payload);
117
198
  }
118
199
  buildReplayUrl(options) {
119
200
  const baseUrl = `${this.config.endpoint}/v1/projects/${this.config.projectId}/replays`;
@@ -123,36 +204,15 @@ export class ReplayRecorder {
123
204
  return `${baseUrl}?key=${encodeURIComponent(this.config.apiKey)}`;
124
205
  }
125
206
  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 {
207
+ if (!globalThis.navigator?.sendBeacon) {
134
208
  return false;
135
209
  }
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
- }
210
+ return attempt({
211
+ try: () => {
212
+ const blob = new Blob([body], { type: "application/json" });
213
+ return globalThis.navigator.sendBeacon(url, blob);
214
+ },
215
+ catch: () => false,
216
+ });
157
217
  }
158
218
  }
@@ -0,0 +1,25 @@
1
+ import { BrowserClient } from "../client.js";
2
+ import type { Breadcrumb, BrowserConfig, BrowserErrorEvent, JsonObject, Level, UserContext } from "../types.js";
3
+ type CaptureErrorExtra = Parameters<BrowserClient["captureError"]>[1];
4
+ /** Initialize the global Squasher browser client. Throws if called twice. */
5
+ export declare function init(config: BrowserConfig): BrowserClient;
6
+ /** Get the global client (throws if not initialized). */
7
+ export declare function getClient(): BrowserClient;
8
+ export declare function captureError(error: Error, extra?: CaptureErrorExtra): void;
9
+ export declare function captureMessage(message: string, level?: Level): void;
10
+ export declare function captureTelemetry(event: BrowserErrorEvent): void;
11
+ export declare function track(eventName: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
12
+ export declare function identify(distinctId: string, traits?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
13
+ export declare function page(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
14
+ export declare function screen(name: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
15
+ export declare function captureSpan(name: string, context?: Partial<BrowserErrorEvent>): void;
16
+ export declare function captureToolCall(name: string, context?: Partial<BrowserErrorEvent>): void;
17
+ export declare function captureGeneration(message: string, context?: Partial<BrowserErrorEvent>): void;
18
+ export declare function setUser(user: UserContext | undefined): void;
19
+ export declare function setTag(key: string, value: string): void;
20
+ export declare function setTags(tags: Record<string, string>): void;
21
+ export declare function addBreadcrumb(crumb: Omit<Breadcrumb, "timestamp">): void;
22
+ /** Stops timers, flushes vitals, and removes global handlers. */
23
+ export declare function close(): void;
24
+ export {};
25
+ //# sourceMappingURL=client-singleton.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-singleton.d.ts","sourceRoot":"","sources":["../../src/runtime/client-singleton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC7C,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,KAAK,EACL,WAAW,EACZ,MAAM,aAAa,CAAC;AAErB,KAAK,iBAAiB,GAAG,UAAU,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;AAItE,6EAA6E;AAC7E,wBAAgB,IAAI,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAQzD;AAED,yDAAyD;AACzD,wBAAgB,SAAS,IAAI,aAAa,CAKzC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,iBAAiB,GAAG,IAAI,CAE1E;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,CAEnE;AAED,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAE/D;AAED,wBAAgB,KAAK,CACnB,SAAS,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GACnC,IAAI,CAEN;AAED,wBAAgB,QAAQ,CACtB,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,UAAU,EACnB,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GACnC,IAAI,CAEN;AAED,wBAAgB,IAAI,CAClB,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GACnC,IAAI,CAEN;AAED,wBAAgB,MAAM,CACpB,IAAI,EAAE,MAAM,EACZ,UAAU,CAAC,EAAE,UAAU,EACvB,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GACnC,IAAI,CAEN;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAEpF;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAExF;AAED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,IAAI,CAE7F;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,SAAS,GAAG,IAAI,CAE3D;AAED,wBAAgB,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAEvD;AAED,wBAAgB,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAE1D;AAED,wBAAgB,aAAa,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,GAAG,IAAI,CAExE;AAED,iEAAiE;AACjE,wBAAgB,KAAK,IAAI,IAAI,CAK5B"}
@@ -0,0 +1,66 @@
1
+ import { BrowserClient } from "../client.js";
2
+ let client = null;
3
+ /** Initialize the global Squasher browser client. Throws if called twice. */
4
+ export function init(config) {
5
+ if (client) {
6
+ throw new Error("squasher.init() called more than once. Call close() first if re-initializing.");
7
+ }
8
+ client = new BrowserClient(config);
9
+ return client;
10
+ }
11
+ /** Get the global client (throws if not initialized). */
12
+ export function getClient() {
13
+ if (!client) {
14
+ throw new Error("Squasher not initialized. Call init() first.");
15
+ }
16
+ return client;
17
+ }
18
+ export function captureError(error, extra) {
19
+ getClient().captureError(error, extra);
20
+ }
21
+ export function captureMessage(message, level) {
22
+ getClient().captureMessage(message, level);
23
+ }
24
+ export function captureTelemetry(event) {
25
+ getClient().captureTelemetry(event);
26
+ }
27
+ export function track(eventName, properties, context) {
28
+ getClient().track(eventName, properties, context);
29
+ }
30
+ export function identify(distinctId, traits, context) {
31
+ getClient().identify(distinctId, traits, context);
32
+ }
33
+ export function page(name, properties, context) {
34
+ getClient().page(name, properties, context);
35
+ }
36
+ export function screen(name, properties, context) {
37
+ getClient().screen(name, properties, context);
38
+ }
39
+ export function captureSpan(name, context) {
40
+ getClient().captureSpan(name, context);
41
+ }
42
+ export function captureToolCall(name, context) {
43
+ getClient().captureToolCall(name, context);
44
+ }
45
+ export function captureGeneration(message, context) {
46
+ getClient().captureGeneration(message, context);
47
+ }
48
+ export function setUser(user) {
49
+ getClient().setUser(user);
50
+ }
51
+ export function setTag(key, value) {
52
+ getClient().setTag(key, value);
53
+ }
54
+ export function setTags(tags) {
55
+ getClient().setTags(tags);
56
+ }
57
+ export function addBreadcrumb(crumb) {
58
+ getClient().addBreadcrumb(crumb);
59
+ }
60
+ /** Stops timers, flushes vitals, and removes global handlers. */
61
+ export function close() {
62
+ if (client) {
63
+ client.close();
64
+ client = null;
65
+ }
66
+ }
@@ -0,0 +1,22 @@
1
+ import { type AutocaptureOptions, type BreadcrumbCallback } from "../autocapture.js";
2
+ import { type ReplayRecorderConfig } from "../replay.js";
3
+ import { type TransportConfig } from "../transport.js";
4
+ import type { BrowserErrorEvent, VitalEvent } from "../types.js";
5
+ import { type VitalsCollectorConfig, type VitalsCollectorHandle } from "../vitals.js";
6
+ export interface BrowserTransport {
7
+ dispose(): void;
8
+ enqueueVital(event: VitalEvent): void;
9
+ sendEvent(event: BrowserErrorEvent): void;
10
+ }
11
+ export interface ReplayRecorderHandle {
12
+ dispose(): void;
13
+ }
14
+ export interface BrowserClientRuntime {
15
+ createReplayRecorder(config: ReplayRecorderConfig): ReplayRecorderHandle;
16
+ createTransport(config: TransportConfig): BrowserTransport;
17
+ installAutocapture(onBreadcrumb: BreadcrumbCallback, options: AutocaptureOptions): () => void;
18
+ installGlobalErrorHandlers(onError: (event: BrowserErrorEvent) => void): () => void;
19
+ startVitalsCollection(transport: BrowserTransport, config: VitalsCollectorConfig): VitalsCollectorHandle;
20
+ }
21
+ export declare const defaultBrowserClientRuntime: BrowserClientRuntime;
22
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/runtime/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,kBAAkB,EACvB,KAAK,kBAAkB,EACxB,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAAkB,KAAK,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACzE,OAAO,EAAa,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClE,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACjE,OAAO,EAEL,KAAK,qBAAqB,EAC1B,KAAK,qBAAqB,EAC3B,MAAM,cAAc,CAAC;AAEtB,MAAM,WAAW,gBAAgB;IAC/B,OAAO,IAAI,IAAI,CAAC;IAChB,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IACtC,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC3C;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,IAAI,IAAI,CAAC;CACjB;AAED,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,CAAC,MAAM,EAAE,oBAAoB,GAAG,oBAAoB,CAAC;IACzE,eAAe,CAAC,MAAM,EAAE,eAAe,GAAG,gBAAgB,CAAC;IAC3D,kBAAkB,CAAC,YAAY,EAAE,kBAAkB,EAAE,OAAO,EAAE,kBAAkB,GAAG,MAAM,IAAI,CAAC;IAC9F,0BAA0B,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IACpF,qBAAqB,CACnB,SAAS,EAAE,gBAAgB,EAC3B,MAAM,EAAE,qBAAqB,GAC5B,qBAAqB,CAAC;CAC1B;AAED,eAAO,MAAM,2BAA2B,EAAE,oBAMzC,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { installAutocapture, } from "../autocapture.js";
2
+ import { installGlobalErrorHandlers } from "../errors.js";
3
+ import { ReplayRecorder } from "../replay.js";
4
+ import { Transport } from "../transport.js";
5
+ import { startVitalsCollection, } from "../vitals.js";
6
+ export const defaultBrowserClientRuntime = {
7
+ createReplayRecorder: (config) => new ReplayRecorder(config),
8
+ createTransport: (config) => new Transport(config),
9
+ installAutocapture,
10
+ installGlobalErrorHandlers,
11
+ startVitalsCollection,
12
+ };
@@ -0,0 +1,13 @@
1
+ import type { ReplayNetworkCaptureOptions } from "../replay-network.js";
2
+ import type { ReplayPrivacyRecordOptions } from "../replay-privacy.js";
3
+ export type StopRecording = () => void;
4
+ export interface ReplayRecordingAdapter {
5
+ emitCustomEvent?: ReplayNetworkCaptureOptions["emitCustomEvent"];
6
+ start(options: ReplayPrivacyRecordOptions): StopRecording | undefined;
7
+ }
8
+ export interface ReplayRecorderRuntime {
9
+ installNetworkCapture(options: ReplayNetworkCaptureOptions): () => void;
10
+ loadRecordingAdapter(): Promise<ReplayRecordingAdapter>;
11
+ }
12
+ export declare const defaultReplayRecorderRuntime: ReplayRecorderRuntime;
13
+ //# sourceMappingURL=replay.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"replay.d.ts","sourceRoot":"","sources":["../../src/runtime/replay.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AAExE,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAEvE,MAAM,MAAM,aAAa,GAAG,MAAM,IAAI,CAAC;AAEvC,MAAM,WAAW,sBAAsB;IACrC,eAAe,CAAC,EAAE,2BAA2B,CAAC,iBAAiB,CAAC,CAAC;IACjE,KAAK,CAAC,OAAO,EAAE,0BAA0B,GAAG,aAAa,GAAG,SAAS,CAAC;CACvE;AAED,MAAM,WAAW,qBAAqB;IACpC,qBAAqB,CAAC,OAAO,EAAE,2BAA2B,GAAG,MAAM,IAAI,CAAC;IACxE,oBAAoB,IAAI,OAAO,CAAC,sBAAsB,CAAC,CAAC;CACzD;AAaD,eAAO,MAAM,4BAA4B,EAAE,qBAG1C,CAAC"}
@@ -0,0 +1,15 @@
1
+ import { installReplayNetworkCapture } from "../replay-network.js";
2
+ async function loadRecordingAdapter() {
3
+ const { record } = await import("rrweb");
4
+ const adapter = {
5
+ start: (options) => record(options),
6
+ };
7
+ if (record.addCustomEvent) {
8
+ adapter.emitCustomEvent = record.addCustomEvent;
9
+ }
10
+ return adapter;
11
+ }
12
+ export const defaultReplayRecorderRuntime = {
13
+ installNetworkCapture: installReplayNetworkCapture,
14
+ loadRecordingAdapter,
15
+ };
package/dist/session.d.ts CHANGED
@@ -1,13 +1,3 @@
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
1
  /**
12
2
  * Get (or create) the session ID for this tab.
13
3
  * Uses sessionStorage for persistence across SPA navigations.
@@ -1 +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"}
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAiBA;;;;GAIG;AACH,wBAAgB,YAAY,IAAI,MAAM,CAuBrC;AAED;;GAEG;AACH,wBAAgB,cAAc,IAAI,IAAI,CAQrC"}
package/dist/session.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { attempt } from "@squasher-ai/result-utils";
1
2
  /**
2
3
  * Session management for the browser SDK.
3
4
  *
@@ -18,24 +19,22 @@ let cachedSessionId = null;
18
19
  export function getSessionId() {
19
20
  if (cachedSessionId)
20
21
  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)
22
+ const stored = attempt({
23
+ try: () => sessionStorage.getItem(SESSION_KEY),
24
+ catch: () => null,
25
+ });
26
+ if (stored) {
27
+ cachedSessionId = stored;
28
+ return stored;
30
29
  }
31
30
  const id = crypto.randomUUID();
32
31
  cachedSessionId = id;
33
- try {
34
- sessionStorage.setItem(SESSION_KEY, id);
35
- }
36
- catch {
37
- // Storage full or unavailable — fall back to in-memory
38
- }
32
+ attempt({
33
+ try: () => {
34
+ sessionStorage.setItem(SESSION_KEY, id);
35
+ },
36
+ catch: () => undefined,
37
+ });
39
38
  return id;
40
39
  }
41
40
  /**
@@ -43,10 +42,10 @@ export function getSessionId() {
43
42
  */
44
43
  export function resetSessionId() {
45
44
  cachedSessionId = null;
46
- try {
47
- sessionStorage.removeItem(SESSION_KEY);
48
- }
49
- catch {
50
- // Ignore
51
- }
45
+ attempt({
46
+ try: () => {
47
+ sessionStorage.removeItem(SESSION_KEY);
48
+ },
49
+ catch: () => undefined,
50
+ });
52
51
  }
@@ -1,4 +1,4 @@
1
- import type { Breadcrumb, BrowserErrorEvent, JsonObject, UserContext } from "./types";
1
+ import type { Breadcrumb, BrowserErrorEvent, JsonObject, UserContext } from "./types.js";
2
2
  interface PrepareBrowserEventInput {
3
3
  beforeSend?: (event: BrowserErrorEvent) => BrowserErrorEvent | null;
4
4
  breadcrumbs: Breadcrumb[];
@@ -1 +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"}
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,YAAY,CAAC;AAEzF,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,CAqBnB;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"}
package/dist/telemetry.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getSessionId } from "./session";
1
+ import { getSessionId } from "./session.js";
2
2
  export function mergeIdentifiedUser(currentUser, contextUser, distinctId) {
3
3
  return {
4
4
  ...currentUser,
@@ -54,10 +54,10 @@ export function buildPageEvent(name, properties, context) {
54
54
  page: {
55
55
  ...context.page,
56
56
  name: context.page?.name ?? name,
57
- path: context.page?.path ?? (typeof location !== "undefined" ? location.pathname : undefined),
58
- title: context.page?.title ?? (typeof document !== "undefined" ? document.title : undefined),
59
- referrer: context.page?.referrer ?? (typeof document !== "undefined" ? document.referrer : undefined),
60
- search: context.page?.search ?? (typeof location !== "undefined" ? location.search : undefined),
57
+ path: context.page?.path ?? globalThis.location?.pathname,
58
+ title: context.page?.title ?? globalThis.document?.title,
59
+ referrer: context.page?.referrer ?? globalThis.document?.referrer,
60
+ search: context.page?.search ?? globalThis.location?.search,
61
61
  },
62
62
  };
63
63
  }
@@ -126,19 +126,22 @@ export function prepareBrowserEvent(event, input) {
126
126
  environment: filtered.environment ?? input.environment,
127
127
  release: filtered.release ?? input.release,
128
128
  kind: filtered.kind ?? (filtered.level === "error" || filtered.level === "fatal" ? "error" : "log"),
129
- ...(input.user ? { user: { ...input.user } } : {}),
130
- ...(Object.keys(input.tags).length > 0 ? { tags: { ...input.tags, ...filtered.tags } } : {}),
131
- ...(input.breadcrumbs.length > 0 ? { breadcrumbs: [...input.breadcrumbs] } : {}),
132
129
  };
130
+ if (input.user)
131
+ prepared.user = { ...input.user };
132
+ if (Object.keys(input.tags).length > 0)
133
+ prepared.tags = { ...input.tags, ...filtered.tags };
134
+ if (input.breadcrumbs.length > 0)
135
+ prepared.breadcrumbs = [...input.breadcrumbs];
133
136
  if (!prepared.visitor &&
134
137
  prepared.kind &&
135
138
  ["analytics", "identify", "page", "screen", "visitor"].includes(prepared.kind)) {
136
139
  prepared.visitor = { anonymous_id: prepared.session_id };
137
140
  }
138
- if (typeof location !== "undefined") {
141
+ if (globalThis.location) {
139
142
  prepared.request = {
140
143
  ...prepared.request,
141
- url: prepared.request?.url ?? location.href,
144
+ url: prepared.request?.url ?? globalThis.location.href,
142
145
  };
143
146
  }
144
147
  return prepared;
@@ -0,0 +1,9 @@
1
+ import type { TraceContext } from "./types.js";
2
+ export declare function createTraceId(): string;
3
+ export declare function createSpanId(): string;
4
+ export declare function createTraceContext(parent?: Pick<TraceContext, "span_id" | "trace_id">): {
5
+ parent_span_id: string | undefined;
6
+ span_id: string;
7
+ trace_id: string;
8
+ };
9
+ //# sourceMappingURL=trace-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"trace-context.d.ts","sourceRoot":"","sources":["../src/trace-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE/C,wBAAgB,aAAa,WAE5B;AAED,wBAAgB,YAAY,WAE3B;AAED,wBAAgB,kBAAkB,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,YAAY,EAAE,SAAS,GAAG,UAAU,CAAC;;;;EAMrF"}
@@ -0,0 +1,13 @@
1
+ export function createTraceId() {
2
+ return crypto.randomUUID().replaceAll("-", "");
3
+ }
4
+ export function createSpanId() {
5
+ return crypto.randomUUID().replaceAll("-", "").slice(0, 16);
6
+ }
7
+ export function createTraceContext(parent) {
8
+ return {
9
+ parent_span_id: parent?.span_id,
10
+ span_id: createSpanId(),
11
+ trace_id: parent?.trace_id ?? createTraceId(),
12
+ };
13
+ }