@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
@@ -1,26 +1,28 @@
1
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
2
+ * Auto-capture breadcrumbs. Categories emitted:
3
+ * - "navigation" — pushState/replaceState/popstate
4
+ * - "ui.click" — clicks on elements
5
+ * - "fetch" — 4xx/5xx fetch responses
6
+ * - "console" — console.error / console.warn calls
13
7
  */
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) {
8
+ import { attemptAsync } from "@squasher-ai/result-utils";
9
+ import { readRuntimeProperty, readRuntimeString } from "@squasher-ai/sdk-core";
10
+ import { captureConsole } from "./console-capture.js";
11
+ /** Installs all hooks. Returned function removes them and restores originals. */
12
+ export function installAutocapture(onBreadcrumb, options = {}) {
19
13
  const cleanups = [];
20
- cleanups.push(captureNavigation(onBreadcrumb));
21
- cleanups.push(captureClicks(onBreadcrumb));
22
- cleanups.push(captureFetch(onBreadcrumb));
23
- cleanups.push(captureConsole(onBreadcrumb));
14
+ const captureBreadcrumbs = options.captureBreadcrumbs ?? true;
15
+ if (captureBreadcrumbs) {
16
+ cleanups.push(captureNavigation(onBreadcrumb));
17
+ cleanups.push(captureClicks(onBreadcrumb));
18
+ cleanups.push(captureFetch(onBreadcrumb));
19
+ }
20
+ if (captureBreadcrumbs || options.onConsoleError) {
21
+ cleanups.push(captureConsole(onBreadcrumb, {
22
+ captureBreadcrumbs,
23
+ onConsoleError: options.onConsoleError,
24
+ }));
25
+ }
24
26
  return () => {
25
27
  for (const cleanup of cleanups) {
26
28
  cleanup();
@@ -43,13 +45,13 @@ function captureNavigation(onBreadcrumb) {
43
45
  };
44
46
  // Patch history.pushState
45
47
  const origPushState = history.pushState.bind(history);
46
- history.pushState = function (...args) {
48
+ history.pushState = (...args) => {
47
49
  origPushState(...args);
48
50
  emitNavCrumb();
49
51
  };
50
52
  // Patch history.replaceState
51
53
  const origReplaceState = history.replaceState.bind(history);
52
- history.replaceState = function (...args) {
54
+ history.replaceState = (...args) => {
53
55
  origReplaceState(...args);
54
56
  emitNavCrumb();
55
57
  };
@@ -65,20 +67,21 @@ function captureNavigation(onBreadcrumb) {
65
67
  // ─── Clicks ─────────────────────────────────────────────────────────────
66
68
  function captureClicks(onBreadcrumb) {
67
69
  const handler = (event) => {
68
- const target = event.target;
70
+ const target = readClickTarget(event.target);
69
71
  if (!target)
70
72
  return;
71
73
  const descriptor = describeElement(target);
72
74
  if (!descriptor)
73
75
  return;
76
+ const data = { tag: target.tagName.toLowerCase() };
77
+ if (target.id)
78
+ data.id = target.id;
79
+ if (target.className)
80
+ data.className = target.className;
74
81
  onBreadcrumb({
75
82
  category: "ui.click",
76
83
  message: descriptor,
77
- data: {
78
- tag: target.tagName?.toLowerCase(),
79
- id: target.id || undefined,
80
- className: target.className || undefined,
81
- },
84
+ data,
82
85
  });
83
86
  };
84
87
  // Use capture phase to catch clicks even if stopPropagation is called
@@ -87,27 +90,35 @@ function captureClicks(onBreadcrumb) {
87
90
  document.removeEventListener("click", handler, true);
88
91
  };
89
92
  }
93
+ function readClickTarget(target) {
94
+ const tagName = readRuntimeString(readRuntimeProperty(target, "tagName"));
95
+ if (!tagName)
96
+ return null;
97
+ return {
98
+ className: readRuntimeString(readRuntimeProperty(target, "className")) ?? "",
99
+ id: readRuntimeString(readRuntimeProperty(target, "id")) ?? "",
100
+ tagName,
101
+ textContent: readRuntimeString(readRuntimeProperty(target, "textContent")) ?? "",
102
+ };
103
+ }
90
104
  /**
91
105
  * Build a human-readable descriptor for a DOM element.
92
106
  * e.g. "button#submit-btn.primary" or "a.nav-link"
93
107
  */
94
108
  function describeElement(el) {
95
- const tag = el.tagName?.toLowerCase();
96
- if (!tag)
97
- return null;
109
+ const tag = el.tagName.toLowerCase();
98
110
  let desc = tag;
99
- const htmlEl = el;
100
- if (htmlEl.id) {
101
- desc += `#${htmlEl.id}`;
111
+ if (el.id) {
112
+ desc += `#${el.id}`;
102
113
  }
103
- if (htmlEl.className && typeof htmlEl.className === "string") {
104
- const classes = htmlEl.className.trim().split(/\s+/).slice(0, 2).join(".");
114
+ if (el.className) {
115
+ const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
105
116
  if (classes) {
106
117
  desc += `.${classes}`;
107
118
  }
108
119
  }
109
120
  // Add text content for buttons/links (truncated)
110
- const textContent = htmlEl.textContent?.trim();
121
+ const textContent = el.textContent?.trim();
111
122
  if (textContent && (tag === "button" || tag === "a")) {
112
123
  const truncated = textContent.length > 30 ? `${textContent.slice(0, 30)}...` : textContent;
113
124
  desc += `[${truncated}]`;
@@ -116,76 +127,47 @@ function describeElement(el) {
116
127
  }
117
128
  // ─── Fetch ──────────────────────────────────────────────────────────────
118
129
  function captureFetch(onBreadcrumb) {
119
- if (typeof window === "undefined" || !window.fetch)
130
+ if (!globalThis.window?.fetch)
120
131
  return () => { };
121
132
  const origFetch = window.fetch.bind(window);
122
- window.fetch = async function (input, init) {
133
+ window.fetch = async (input, init) => {
123
134
  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
+ const url = input instanceof URL ? input.href : input instanceof Request ? input.url : String(input);
136
+ return attemptAsync({
137
+ try: async () => {
138
+ const response = await origFetch(input, init);
139
+ // Only breadcrumb on error responses (4xx/5xx)
140
+ if (response.status >= 400) {
141
+ onBreadcrumb({
142
+ category: "fetch",
143
+ message: `${method} ${url} [${response.status}]`,
144
+ level: response.status >= 500 ? "error" : "warning",
145
+ data: {
146
+ method,
147
+ url,
148
+ status: response.status,
149
+ },
150
+ });
151
+ }
152
+ return response;
153
+ },
154
+ catch: ({ cause, error }) => {
155
+ // Network error
135
156
  onBreadcrumb({
136
157
  category: "fetch",
137
- message: `${method} ${url} [${response.status}]`,
138
- level: response.status >= 500 ? "error" : "warning",
158
+ message: `${method} ${url} [network error]`,
159
+ level: "error",
139
160
  data: {
140
161
  method,
141
162
  url,
142
- status: response.status,
163
+ error: error.message,
143
164
  },
144
165
  });
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",
166
+ throw cause;
167
+ },
184
168
  });
185
- origWarn(...args);
186
169
  };
187
170
  return () => {
188
- console.error = origError;
189
- console.warn = origWarn;
171
+ window.fetch = origFetch;
190
172
  };
191
173
  }
package/dist/client.d.ts CHANGED
@@ -1,15 +1,6 @@
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";
1
+ import { type BrowserClientRuntime } from "./runtime/client.js";
2
+ import type { Breadcrumb, BrowserConfig, BrowserErrorEvent, JsonObject, Level, UserContext } from "./types.js";
3
+ import { type ExternalVitalInput } from "./vitals.js";
13
4
  export declare class BrowserClient {
14
5
  private config;
15
6
  private transport;
@@ -19,12 +10,13 @@ export declare class BrowserClient {
19
10
  private disabled;
20
11
  private cleanups;
21
12
  private replayRecorder;
22
- constructor(config: BrowserConfig);
13
+ private vitalsHandle;
14
+ constructor(config: BrowserConfig, runtime?: BrowserClientRuntime);
23
15
  setUser(user: UserContext | undefined): void;
24
16
  setTag(key: string, value: string): void;
25
17
  setTags(tags: Record<string, string>): void;
26
18
  addBreadcrumb(crumb: Omit<Breadcrumb, "timestamp">): void;
27
- captureError(error: Error, extra?: Record<string, unknown>): void;
19
+ captureError(error: Error, extra?: JsonObject): void;
28
20
  captureMessage(message: string, level?: Level): void;
29
21
  captureTelemetry(event: BrowserErrorEvent): void;
30
22
  track(eventName: string, properties?: JsonObject, context?: Partial<BrowserErrorEvent>): void;
@@ -34,12 +26,13 @@ export declare class BrowserClient {
34
26
  captureSpan(name: string, context?: Partial<BrowserErrorEvent>): void;
35
27
  captureToolCall(name: string, context?: Partial<BrowserErrorEvent>): void;
36
28
  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
- */
29
+ /** Record a web vital from an external collector (e.g. Next.js `useReportWebVitals`). */
30
+ recordWebVital(metric: ExternalVitalInput, route?: string): void;
31
+ /** Stop the SDK's native `web-vitals` collector to avoid double-counting. */
32
+ suppressNativeVitalsCollection(): void;
33
+ private vitalsConfig;
34
+ /** Flush vitals and stop timers. Call before unmounting a SPA. */
41
35
  close(): void;
42
- private shouldSample;
43
36
  /** Enrich an error event with current context and send it. */
44
37
  private sendErrorEvent;
45
38
  }
@@ -1 +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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,oBAAoB,EAG1B,MAAM,qBAAqB,CAAC;AAa7B,OAAO,KAAK,EACV,UAAU,EACV,aAAa,EACb,iBAAiB,EACjB,UAAU,EACV,KAAK,EACL,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAoB,KAAK,kBAAkB,EAA8B,MAAM,aAAa,CAAC;AAEpG,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAiB;IAC/B,OAAO,CAAC,SAAS,CAAmB;IACpC,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,CAAqC;IAC3D,OAAO,CAAC,YAAY,CAAsC;gBAE9C,MAAM,EAAE,aAAa,EAAE,OAAO,GAAE,oBAAkD;IA4F9F,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;IAUzD,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,EAAE,UAAU,GAAG,IAAI;IAIpD,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,KAAc,GAAG,IAAI;IAI5D,gBAAgB,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAKhD,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;IAIlF,yFAAyF;IACzF,cAAc,CAAC,MAAM,EAAE,kBAAkB,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI;IAMhE,6EAA6E;IAC7E,8BAA8B,IAAI,IAAI;IAItC,OAAO,CAAC,YAAY;IAKpB,kEAAkE;IAClE,KAAK,IAAI,IAAI;IAWb,8DAA8D;IAC9D,OAAO,CAAC,cAAc;CAqBvB"}
package/dist/client.js CHANGED
@@ -1,43 +1,10 @@
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
- }
1
+ import { shouldSampleTelemetry } from "@squasher-ai/sdk-core";
2
+ import { resolveConfig } from "./config.js";
3
+ import { buildErrorEvent, buildMessageEvent } from "./errors.js";
4
+ import { defaultBrowserClientRuntime, } from "./runtime/client.js";
5
+ import { getSessionId } from "./session.js";
6
+ import { buildGenerationEvent, buildIdentifyEvent, buildPageEvent, buildScreenEvent, buildSpanEvent, buildToolCallEvent, buildTrackEvent, mergeIdentifiedUser, prepareBrowserEvent, } from "./telemetry.js";
7
+ import { mapExternalVital } from "./vitals.js";
41
8
  export class BrowserClient {
42
9
  config;
43
10
  transport;
@@ -47,7 +14,8 @@ export class BrowserClient {
47
14
  disabled;
48
15
  cleanups = [];
49
16
  replayRecorder = null;
50
- constructor(config) {
17
+ vitalsHandle = null;
18
+ constructor(config, runtime = defaultBrowserClientRuntime) {
51
19
  if (!config.apiKey)
52
20
  throw new Error("Squasher: apiKey is required");
53
21
  if (!config.projectId)
@@ -58,8 +26,7 @@ export class BrowserClient {
58
26
  if (this.config.debug) {
59
27
  console.warn("[squasher] SDK initialized in disabled mode");
60
28
  }
61
- // Create a no-op transport
62
- this.transport = new Transport({
29
+ this.transport = runtime.createTransport({
63
30
  endpoint: this.config.endpoint,
64
31
  projectId: this.config.projectId,
65
32
  apiKey: this.config.apiKey,
@@ -69,8 +36,7 @@ export class BrowserClient {
69
36
  });
70
37
  return;
71
38
  }
72
- // Initialize transport
73
- this.transport = new Transport({
39
+ this.transport = runtime.createTransport({
74
40
  endpoint: this.config.endpoint,
75
41
  projectId: this.config.projectId,
76
42
  apiKey: this.config.apiKey,
@@ -78,36 +44,45 @@ export class BrowserClient {
78
44
  vitalsBufferSize: this.config.vitalsBufferSize,
79
45
  vitalsFlushIntervalMs: this.config.vitalsFlushIntervalMs,
80
46
  });
81
- // Start Web Vitals collection
82
47
  if (this.config.enableVitals) {
83
- startVitalsCollection(this.transport, {
48
+ this.vitalsHandle = runtime.startVitalsCollection(this.transport, {
84
49
  sampleRate: this.config.vitalsSampleRate,
85
50
  environment: this.config.environment,
86
51
  release: this.config.release,
87
52
  debug: this.config.debug,
88
53
  });
89
54
  }
90
- // Install global error handlers
91
55
  if (this.config.enableErrorCapture) {
92
- const cleanup = installGlobalErrorHandlers((event) => {
56
+ const cleanup = runtime.installGlobalErrorHandlers((event) => {
93
57
  this.sendErrorEvent(event);
94
58
  });
95
59
  this.cleanups.push(cleanup);
96
60
  }
97
- // Install auto-capture breadcrumbs
98
- if (this.config.enableAutoBreadcrumbs) {
99
- const cleanup = installAutocapture((crumb) => {
61
+ if (this.config.enableAutoBreadcrumbs || this.config.captureConsoleErrors) {
62
+ const cleanup = runtime.installAutocapture((crumb) => {
100
63
  this.addBreadcrumb(crumb);
64
+ }, {
65
+ captureBreadcrumbs: this.config.enableAutoBreadcrumbs,
66
+ onConsoleError: this.config.captureConsoleErrors
67
+ ? (event) => {
68
+ this.sendErrorEvent(event);
69
+ }
70
+ : undefined,
101
71
  });
102
72
  this.cleanups.push(cleanup);
103
73
  }
104
74
  if (this.config.replayEnabled) {
105
- this.replayRecorder = new ReplayRecorder({
75
+ this.replayRecorder = runtime.createReplayRecorder({
106
76
  endpoint: this.config.endpoint,
107
77
  projectId: this.config.projectId,
108
78
  apiKey: this.config.apiKey,
109
79
  debug: this.config.debug,
110
80
  sampleRate: this.config.replaySampleRate,
81
+ environment: this.config.environment,
82
+ release: this.config.release,
83
+ getDistinctId: () => this.user?.id,
84
+ getUserId: () => this.user?.id,
85
+ getVisitorId: () => getSessionId(),
111
86
  privacy: this.config.replayPrivacy,
112
87
  });
113
88
  }
@@ -117,11 +92,11 @@ export class BrowserClient {
117
92
  vitals: this.config.enableVitals,
118
93
  errorCapture: this.config.enableErrorCapture,
119
94
  autoBreadcrumbs: this.config.enableAutoBreadcrumbs,
95
+ consoleErrors: this.config.captureConsoleErrors,
120
96
  replay: this.config.replayEnabled,
121
97
  });
122
98
  }
123
99
  }
124
- // ─── Context ──────────────────────────────────────────────────────────
125
100
  setUser(user) {
126
101
  this.user = user;
127
102
  }
@@ -140,7 +115,6 @@ export class BrowserClient {
140
115
  this.breadcrumbs = this.breadcrumbs.slice(-this.config.maxBreadcrumbs);
141
116
  }
142
117
  }
143
- // ─── Capture ──────────────────────────────────────────────────────────
144
118
  captureError(error, extra) {
145
119
  this.captureTelemetry(buildErrorEvent(error, extra));
146
120
  }
@@ -150,8 +124,6 @@ export class BrowserClient {
150
124
  captureTelemetry(event) {
151
125
  if (this.disabled)
152
126
  return;
153
- if (!this.shouldSample())
154
- return;
155
127
  this.sendErrorEvent(event);
156
128
  }
157
129
  track(eventName, properties, context = {}) {
@@ -176,11 +148,22 @@ export class BrowserClient {
176
148
  captureGeneration(message, context = {}) {
177
149
  this.captureTelemetry(buildGenerationEvent(message, context));
178
150
  }
179
- // ─── Lifecycle ────────────────────────────────────────────────────────
180
- /**
181
- * Flush remaining vitals and stop all timers.
182
- * Call this before unmounting a SPA or during cleanup.
183
- */
151
+ /** Record a web vital from an external collector (e.g. Next.js `useReportWebVitals`). */
152
+ recordWebVital(metric, route) {
153
+ if (this.disabled)
154
+ return;
155
+ const event = mapExternalVital(metric, this.vitalsConfig(), route);
156
+ this.transport.enqueueVital(event);
157
+ }
158
+ /** Stop the SDK's native `web-vitals` collector to avoid double-counting. */
159
+ suppressNativeVitalsCollection() {
160
+ this.vitalsHandle?.suppress();
161
+ }
162
+ vitalsConfig() {
163
+ const { vitalsSampleRate: sampleRate, environment, release, debug } = this.config;
164
+ return { sampleRate, environment, release, debug };
165
+ }
166
+ /** Flush vitals and stop timers. Call before unmounting a SPA. */
184
167
  close() {
185
168
  this.replayRecorder?.dispose();
186
169
  this.replayRecorder = null;
@@ -190,14 +173,6 @@ export class BrowserClient {
190
173
  this.cleanups = [];
191
174
  this.transport.dispose();
192
175
  }
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
176
  /** Enrich an error event with current context and send it. */
202
177
  sendErrorEvent(event) {
203
178
  const prepared = prepareBrowserEvent(event, {
@@ -208,8 +183,17 @@ export class BrowserClient {
208
183
  tags: this.tags,
209
184
  user: this.user,
210
185
  });
211
- if (prepared) {
212
- this.transport.sendEvent(prepared);
186
+ if (!prepared)
187
+ return;
188
+ if (!this.config.debug && !shouldSampleTelemetry(prepared, this.config.sampling)) {
189
+ if (!prepared.measurements?.length)
190
+ return;
191
+ this.transport.sendEvent({
192
+ ...prepared,
193
+ attributes: { ...prepared.attributes, "squasher.sampled": false },
194
+ });
195
+ return;
213
196
  }
197
+ this.transport.sendEvent(prepared);
214
198
  }
215
199
  }
@@ -0,0 +1,25 @@
1
+ import { type ResolvedTelemetrySampling } from "@squasher-ai/sdk-core";
2
+ import type { BrowserConfig, BrowserErrorEvent, ReplayPrivacyConfig } from "./types.js";
3
+ export interface ResolvedConfig {
4
+ apiKey: string;
5
+ projectId: string;
6
+ endpoint: string;
7
+ environment?: string;
8
+ release?: string;
9
+ debug: boolean;
10
+ sampling: ResolvedTelemetrySampling;
11
+ vitalsSampleRate: number;
12
+ enableVitals: boolean;
13
+ enableErrorCapture: boolean;
14
+ enableAutoBreadcrumbs: boolean;
15
+ captureConsoleErrors: boolean;
16
+ beforeSend?: (event: BrowserErrorEvent) => BrowserErrorEvent | null;
17
+ maxBreadcrumbs: number;
18
+ vitalsBufferSize: number;
19
+ vitalsFlushIntervalMs: number;
20
+ replayEnabled: boolean;
21
+ replaySampleRate: number;
22
+ replayPrivacy?: ReplayPrivacyConfig;
23
+ }
24
+ export declare function resolveConfig(config: BrowserConfig): ResolvedConfig;
25
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAKL,KAAK,yBAAyB,EAC/B,MAAM,oBAAoB,CAAC;AAC5B,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAExF,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,OAAO,CAAC;IACf,QAAQ,EAAE,yBAAyB,CAAC;IACpC,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,OAAO,CAAC;IACtB,kBAAkB,EAAE,OAAO,CAAC;IAC5B,qBAAqB,EAAE,OAAO,CAAC;IAC/B,oBAAoB,EAAE,OAAO,CAAC;IAC9B,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,iBAAiB,GAAG,IAAI,CAAC;IACpE,cAAc,EAAE,MAAM,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,aAAa,EAAE,OAAO,CAAC;IACvB,gBAAgB,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,mBAAmB,CAAC;CACrC;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,cAAc,CAwBnE"}
package/dist/config.js ADDED
@@ -0,0 +1,26 @@
1
+ import { DEFAULT_INGEST_ENDPOINT, detectRelease, resolveDebugFlag, resolveTelemetrySampling, } from "@squasher-ai/sdk-core";
2
+ export function resolveConfig(config) {
3
+ return {
4
+ apiKey: config.apiKey,
5
+ projectId: config.projectId,
6
+ endpoint: config.endpoint ?? DEFAULT_INGEST_ENDPOINT,
7
+ environment: config.environment,
8
+ // In the browser, detectRelease only picks up build-time-injected vars
9
+ // (Next.js inlines `process.env.NEXT_PUBLIC_*` etc).
10
+ release: config.release ?? detectRelease() ?? undefined,
11
+ debug: resolveDebugFlag(config.debug),
12
+ sampling: resolveTelemetrySampling(config.sampling),
13
+ vitalsSampleRate: config.vitalsSampleRate ?? 1,
14
+ enableVitals: config.enableVitals ?? true,
15
+ enableErrorCapture: config.enableErrorCapture ?? true,
16
+ enableAutoBreadcrumbs: config.enableAutoBreadcrumbs ?? true,
17
+ captureConsoleErrors: config.captureConsoleErrors ?? false,
18
+ beforeSend: config.beforeSend,
19
+ maxBreadcrumbs: config.maxBreadcrumbs ?? 30,
20
+ vitalsBufferSize: config.vitalsBufferSize ?? 10,
21
+ vitalsFlushIntervalMs: config.vitalsFlushIntervalMs ?? 10_000,
22
+ replayEnabled: config.replay?.enabled ?? true,
23
+ replaySampleRate: config.replay?.sampleRate ?? 1,
24
+ replayPrivacy: config.replay?.privacy,
25
+ };
26
+ }
@@ -0,0 +1,8 @@
1
+ import type { Breadcrumb, BrowserErrorEvent } from "./types.js";
2
+ export type BreadcrumbCallback = (crumb: Omit<Breadcrumb, "timestamp">) => void;
3
+ export interface ConsoleCaptureOptions {
4
+ captureBreadcrumbs: boolean;
5
+ onConsoleError?: (event: BrowserErrorEvent) => void;
6
+ }
7
+ export declare function captureConsole(onBreadcrumb: BreadcrumbCallback, options: ConsoleCaptureOptions): () => void;
8
+ //# sourceMappingURL=console-capture.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"console-capture.d.ts","sourceRoot":"","sources":["../src/console-capture.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGhE,MAAM,MAAM,kBAAkB,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAC,KAAK,IAAI,CAAC;AAEhF,MAAM,WAAW,qBAAqB;IACpC,kBAAkB,EAAE,OAAO,CAAC;IAC5B,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,CAAC;CACrD;AAED,wBAAgB,cAAc,CAC5B,YAAY,EAAE,kBAAkB,EAChC,OAAO,EAAE,qBAAqB,GAC7B,MAAM,IAAI,CAkCZ"}