@squasher-ai/browser 0.1.0 → 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 (97) hide show
  1. package/LICENSE +21 -21
  2. package/dist/__tests__/client/harness.d.ts +18 -0
  3. package/dist/__tests__/client/harness.d.ts.map +1 -0
  4. package/dist/__tests__/client/harness.js +106 -0
  5. package/dist/__tests__/replay/coverage-harness.d.ts +29 -0
  6. package/dist/__tests__/replay/coverage-harness.d.ts.map +1 -0
  7. package/dist/__tests__/replay/coverage-harness.js +123 -0
  8. package/dist/__tests__/replay/test-helpers.d.ts +13 -0
  9. package/dist/__tests__/replay/test-helpers.d.ts.map +1 -0
  10. package/dist/__tests__/replay/test-helpers.js +9 -0
  11. package/dist/autocapture.d.ts +14 -18
  12. package/dist/autocapture.d.ts.map +1 -1
  13. package/dist/autocapture.js +77 -95
  14. package/dist/client.d.ts +12 -19
  15. package/dist/client.d.ts.map +1 -1
  16. package/dist/client.js +56 -72
  17. package/dist/config.d.ts +25 -0
  18. package/dist/config.d.ts.map +1 -0
  19. package/dist/config.js +26 -0
  20. package/dist/console-capture.d.ts +8 -0
  21. package/dist/console-capture.d.ts.map +1 -0
  22. package/dist/console-capture.js +79 -0
  23. package/dist/errors.d.ts +2 -2
  24. package/dist/errors.d.ts.map +1 -1
  25. package/dist/errors.js +8 -5
  26. package/dist/index.d.ts +5 -38
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +4 -82
  29. package/dist/next.d.ts +40 -0
  30. package/dist/next.d.ts.map +1 -0
  31. package/dist/next.js +67 -0
  32. package/dist/react.d.ts.map +1 -1
  33. package/dist/react.js +12 -10
  34. package/dist/replay-network.d.ts +21 -0
  35. package/dist/replay-network.d.ts.map +1 -0
  36. package/dist/replay-network.js +193 -0
  37. package/dist/replay-privacy.d.ts +2 -2
  38. package/dist/replay-privacy.d.ts.map +1 -1
  39. package/dist/replay-privacy.js +47 -17
  40. package/dist/replay-retry.d.ts +41 -0
  41. package/dist/replay-retry.d.ts.map +1 -0
  42. package/dist/replay-retry.js +193 -0
  43. package/dist/replay.d.ts +18 -5
  44. package/dist/replay.d.ts.map +1 -1
  45. package/dist/replay.js +131 -71
  46. package/dist/runtime/client-singleton.d.ts +25 -0
  47. package/dist/runtime/client-singleton.d.ts.map +1 -0
  48. package/dist/runtime/client-singleton.js +66 -0
  49. package/dist/runtime/client.d.ts +22 -0
  50. package/dist/runtime/client.d.ts.map +1 -0
  51. package/dist/runtime/client.js +12 -0
  52. package/dist/runtime/replay.d.ts +13 -0
  53. package/dist/runtime/replay.d.ts.map +1 -0
  54. package/dist/runtime/replay.js +15 -0
  55. package/dist/session.d.ts +0 -10
  56. package/dist/session.d.ts.map +1 -1
  57. package/dist/session.js +20 -21
  58. package/dist/telemetry.d.ts +1 -1
  59. package/dist/telemetry.d.ts.map +1 -1
  60. package/dist/telemetry.js +13 -10
  61. package/dist/trace-context.d.ts +9 -0
  62. package/dist/trace-context.d.ts.map +1 -0
  63. package/dist/trace-context.js +13 -0
  64. package/dist/transport.d.ts +11 -19
  65. package/dist/transport.d.ts.map +1 -1
  66. package/dist/transport.js +86 -66
  67. package/dist/types.d.ts +37 -135
  68. package/dist/types.d.ts.map +1 -1
  69. package/dist/types.js +0 -7
  70. package/dist/vitals.d.ts +55 -2
  71. package/dist/vitals.d.ts.map +1 -1
  72. package/dist/vitals.js +115 -16
  73. package/package.json +17 -3
  74. package/dist/__tests__/client.test.d.ts +0 -2
  75. package/dist/__tests__/client.test.d.ts.map +0 -1
  76. package/dist/__tests__/client.test.js +0 -103
  77. package/dist/__tests__/errors.test.d.ts +0 -2
  78. package/dist/__tests__/errors.test.d.ts.map +0 -1
  79. package/dist/__tests__/errors.test.js +0 -80
  80. package/dist/__tests__/replay-privacy.test.d.ts +0 -2
  81. package/dist/__tests__/replay-privacy.test.d.ts.map +0 -1
  82. package/dist/__tests__/replay-privacy.test.js +0 -124
  83. package/dist/__tests__/replay-startup.test.d.ts +0 -2
  84. package/dist/__tests__/replay-startup.test.d.ts.map +0 -1
  85. package/dist/__tests__/replay-startup.test.js +0 -99
  86. package/dist/__tests__/replay.test.d.ts +0 -2
  87. package/dist/__tests__/replay.test.d.ts.map +0 -1
  88. package/dist/__tests__/replay.test.js +0 -217
  89. package/dist/__tests__/session.test.d.ts +0 -2
  90. package/dist/__tests__/session.test.d.ts.map +0 -1
  91. package/dist/__tests__/session.test.js +0 -46
  92. package/dist/__tests__/transport.test.d.ts +0 -2
  93. package/dist/__tests__/transport.test.d.ts.map +0 -1
  94. package/dist/__tests__/transport.test.js +0 -101
  95. package/dist/__tests__/types.test.d.ts +0 -2
  96. package/dist/__tests__/types.test.d.ts.map +0 -1
  97. package/dist/__tests__/types.test.js +0 -139
@@ -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
+ }
@@ -1,19 +1,14 @@
1
1
  /**
2
- * Transport layer for the browser SDK.
2
+ * Browser transport.
3
3
  *
4
- * Two send paths:
5
- * 1. Errors → POST /v1/ingest/{project_id} (immediate, same wire format as sdk-node)
6
- * 2. Vitals → POST /v1/vitals/{project_id} (batched array)
4
+ * - Errors → POST /v1/ingest/{project_id} (per-event, immediate).
5
+ * - Vitals → POST /v1/vitals/{project_id} (batched).
7
6
  *
8
- * Primary transport: navigator.sendBeacon() fire-and-forget, survives page unload.
9
- * Fallback: fetch() with keepalive: true.
10
- *
11
- * Vitals are buffered and flushed on:
12
- * - Buffer reaching vitalsBufferSize (default: 10)
13
- * - Timer tick (default: 10s)
14
- * - Page visibility change to "hidden" (user leaving)
7
+ * Uses navigator.sendBeacon when available so requests survive page unload,
8
+ * falling back to `fetch({ keepalive: true })`. Vitals flush on buffer full,
9
+ * interval tick, and page visibilitychange/pagehide.
15
10
  */
16
- import type { BrowserErrorEvent, VitalEvent } from "./types";
11
+ import type { BrowserErrorEvent, VitalEvent } from "./types.js";
17
12
  export interface TransportConfig {
18
13
  endpoint: string;
19
14
  projectId: string;
@@ -28,20 +23,17 @@ export declare class Transport {
28
23
  private flushTimer;
29
24
  private disposed;
30
25
  constructor(config: TransportConfig);
31
- /** Send a single telemetry event immediately. Fire-and-forget. */
26
+ /** Fire-and-forget. */
32
27
  sendEvent(event: BrowserErrorEvent): void;
33
- /** Backward-compatible alias for error events. */
28
+ /** @deprecated use {@link sendEvent}. */
34
29
  sendError(event: BrowserErrorEvent): void;
35
- /** Buffer a vital event. Auto-flushes when buffer is full. */
36
30
  enqueueVital(event: VitalEvent): void;
37
- /** Flush all buffered vitals immediately. */
38
31
  flushVitals(): void;
39
- /** Stop all timers and flush remaining data. */
40
32
  dispose(): void;
41
33
  private buildIngestUrl;
42
- /** Try sending via navigator.sendBeacon. Returns true if successful. */
34
+ private buildVitalsUrl;
35
+ private buildUrl;
43
36
  private trySendBeacon;
44
- /** Send via fetch with keepalive and the API key header. */
45
37
  private sendFetch;
46
38
  }
47
39
  //# sourceMappingURL=transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAiB,MAAM,SAAS,CAAC;AAE5E,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAED,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,eAAe;IA2BnC,kEAAkE;IAClE,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAoBzC,kDAAkD;IAClD,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAMzC,8DAA8D;IAC9D,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAUrC,6CAA6C;IAC7C,WAAW,IAAI,IAAI;IAoBnB,gDAAgD;IAChD,OAAO,IAAI,IAAI;IAWf,OAAO,CAAC,cAAc;IAQtB,wEAAwE;IACxE,OAAO,CAAC,aAAa;IAarB,4DAA4D;YAC9C,SAAS;CAoBxB"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,KAAK,EAAE,iBAAiB,EAAE,UAAU,EAAiB,MAAM,YAAY,CAAC;AAa/E,MAAM,WAAW,eAAe;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,OAAO,CAAC;IACf,gBAAgB,EAAE,MAAM,CAAC;IACzB,qBAAqB,EAAE,MAAM,CAAC;CAC/B;AAWD,qBAAa,SAAS;IACpB,OAAO,CAAC,MAAM,CAAkB;IAChC,OAAO,CAAC,YAAY,CAAoB;IACxC,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,QAAQ,CAAS;gBAEb,MAAM,EAAE,eAAe;IAuBnC,uBAAuB;IACvB,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAuBzC,yCAAyC;IACzC,SAAS,CAAC,KAAK,EAAE,iBAAiB,GAAG,IAAI;IAIzC,YAAY,CAAC,KAAK,EAAE,UAAU,GAAG,IAAI;IAUrC,WAAW,IAAI,IAAI;IAuBnB,OAAO,IAAI,IAAI;IASf,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,cAAc;IAItB,OAAO,CAAC,QAAQ;IAahB,OAAO,CAAC,aAAa;YAcP,SAAS;CAwBxB"}
package/dist/transport.js CHANGED
@@ -1,18 +1,31 @@
1
1
  /**
2
- * Transport layer for the browser SDK.
2
+ * Browser transport.
3
3
  *
4
- * Two send paths:
5
- * 1. Errors → POST /v1/ingest/{project_id} (immediate, same wire format as sdk-node)
6
- * 2. Vitals → POST /v1/vitals/{project_id} (batched array)
4
+ * - Errors → POST /v1/ingest/{project_id} (per-event, immediate).
5
+ * - Vitals → POST /v1/vitals/{project_id} (batched).
7
6
  *
8
- * Primary transport: navigator.sendBeacon() fire-and-forget, survives page unload.
9
- * Fallback: fetch() with keepalive: true.
10
- *
11
- * Vitals are buffered and flushed on:
12
- * - Buffer reaching vitalsBufferSize (default: 10)
13
- * - Timer tick (default: 10s)
14
- * - Page visibility change to "hidden" (user leaving)
7
+ * Uses navigator.sendBeacon when available so requests survive page unload,
8
+ * falling back to `fetch({ keepalive: true })`. Vitals flush on buffer full,
9
+ * interval tick, and page visibilitychange/pagehide.
15
10
  */
11
+ import { attempt, attemptAsync } from "@squasher-ai/result-utils";
12
+ import { getDefaultHeaders } from "@squasher-ai/sdk-core";
13
+ const SDK_NAME = "@squasher-ai/browser";
14
+ const SDK_VERSION = "0.2.0";
15
+ const BEACON_CONTENT_TYPE = "text/plain";
16
+ // One allocation per process; getDefaultHeaders is pure given package
17
+ // metadata and platform detection memoizes inside sdk-core.
18
+ const DEFAULT_TELEMETRY_HEADERS = getDefaultHeaders({
19
+ packageName: SDK_NAME,
20
+ packageVersion: SDK_VERSION,
21
+ });
22
+ function normalizeEndpoint(endpoint) {
23
+ const trimmed = endpoint.trim();
24
+ if (trimmed.length === 0) {
25
+ return null;
26
+ }
27
+ return trimmed.replace(/\/+$/, "");
28
+ }
16
29
  export class Transport {
17
30
  config;
18
31
  vitalsBuffer = [];
@@ -20,50 +33,47 @@ export class Transport {
20
33
  disposed = false;
21
34
  constructor(config) {
22
35
  this.config = config;
23
- // Start the vitals flush timer
24
36
  this.flushTimer = setInterval(() => {
25
37
  this.flushVitals();
26
38
  }, config.vitalsFlushIntervalMs);
27
- // Flush vitals when the page is being hidden (user leaving/switching tabs)
28
- if (typeof document !== "undefined") {
29
- document.addEventListener("visibilitychange", () => {
30
- if (document.visibilityState === "hidden") {
39
+ if (globalThis.document) {
40
+ globalThis.document.addEventListener("visibilitychange", () => {
41
+ if (globalThis.document.visibilityState === "hidden") {
31
42
  this.flushVitals();
32
43
  }
33
44
  });
34
45
  }
35
- // Also flush on pagehide (more reliable than unload)
36
- if (typeof window !== "undefined") {
37
- window.addEventListener("pagehide", () => {
46
+ // `pagehide` fires in cases where `unload` does not (bfcache, iOS Safari).
47
+ if (globalThis.window) {
48
+ globalThis.window.addEventListener("pagehide", () => {
38
49
  this.flushVitals();
39
50
  });
40
51
  }
41
52
  }
42
- // ─── Error sending (immediate) ──────────────────────────────────────
43
- /** Send a single telemetry event immediately. Fire-and-forget. */
53
+ /** Fire-and-forget. */
44
54
  sendEvent(event) {
45
55
  if (this.disposed)
46
56
  return;
47
57
  const url = this.buildIngestUrl();
58
+ const beaconUrl = this.buildIngestUrl({ beacon: true });
59
+ if (!url || !beaconUrl) {
60
+ return;
61
+ }
48
62
  const body = JSON.stringify(event);
49
- // Try sendBeacon first (survives page unload)
50
- if (this.trySendBeacon(this.buildIngestUrl({ beacon: true }), body)) {
63
+ if (this.trySendBeacon(beaconUrl, body)) {
51
64
  if (this.config.debug) {
52
65
  console.log("[squasher] Error sent via sendBeacon");
53
66
  }
54
67
  return;
55
68
  }
56
- // Fallback to fetch with keepalive
57
69
  this.sendFetch(url, body).catch(() => {
58
- // Fire-and-forget — errors are best-effort in the browser
70
+ // Best-effort in the browser.
59
71
  });
60
72
  }
61
- /** Backward-compatible alias for error events. */
73
+ /** @deprecated use {@link sendEvent}. */
62
74
  sendError(event) {
63
75
  this.sendEvent(event);
64
76
  }
65
- // ─── Vitals buffering ───────────────────────────────────────────────
66
- /** Buffer a vital event. Auto-flushes when buffer is full. */
67
77
  enqueueVital(event) {
68
78
  if (this.disposed)
69
79
  return;
@@ -72,25 +82,25 @@ export class Transport {
72
82
  this.flushVitals();
73
83
  }
74
84
  }
75
- /** Flush all buffered vitals immediately. */
76
85
  flushVitals() {
77
86
  if (this.vitalsBuffer.length === 0)
78
87
  return;
88
+ const url = this.buildVitalsUrl();
89
+ const beaconUrl = this.buildVitalsUrl({ beacon: true });
90
+ if (!url || !beaconUrl) {
91
+ return;
92
+ }
79
93
  const events = this.vitalsBuffer.splice(0);
80
- const url = `${this.config.endpoint}/v1/vitals/${this.config.projectId}`;
81
94
  const payload = { events };
82
95
  const body = JSON.stringify(payload);
83
- if (this.trySendBeacon(url, body)) {
96
+ if (this.trySendBeacon(beaconUrl, body)) {
84
97
  if (this.config.debug) {
85
98
  console.log(`[squasher] Flushed ${events.length} vitals via sendBeacon`);
86
99
  }
87
100
  return;
88
101
  }
89
- this.sendFetch(url, body).catch(() => {
90
- // Best-effort
91
- });
102
+ this.sendFetch(url, body).catch(() => { });
92
103
  }
93
- /** Stop all timers and flush remaining data. */
94
104
  dispose() {
95
105
  this.disposed = true;
96
106
  if (this.flushTimer) {
@@ -99,47 +109,57 @@ export class Transport {
99
109
  }
100
110
  this.flushVitals();
101
111
  }
102
- // ─── Private helpers ────────────────────────────────────────────────
103
112
  buildIngestUrl(options) {
104
- const baseUrl = `${this.config.endpoint}/v1/ingest/${this.config.projectId}`;
113
+ return this.buildUrl("ingest", options);
114
+ }
115
+ buildVitalsUrl(options) {
116
+ return this.buildUrl("vitals", options);
117
+ }
118
+ buildUrl(kind, options) {
119
+ const endpoint = normalizeEndpoint(this.config.endpoint);
120
+ if (!endpoint) {
121
+ return null;
122
+ }
123
+ const baseUrl = `${endpoint}/v1/${kind}/${this.config.projectId}`;
105
124
  if (!options?.beacon) {
106
125
  return baseUrl;
107
126
  }
108
127
  return `${baseUrl}?key=${encodeURIComponent(this.config.apiKey)}`;
109
128
  }
110
- /** Try sending via navigator.sendBeacon. Returns true if successful. */
111
129
  trySendBeacon(url, body) {
112
- if (typeof navigator === "undefined" || !navigator.sendBeacon) {
113
- return false;
114
- }
115
- try {
116
- const blob = new Blob([body], { type: "application/json" });
117
- return navigator.sendBeacon(url, blob);
118
- }
119
- catch {
130
+ if (!globalThis.navigator?.sendBeacon) {
120
131
  return false;
121
132
  }
133
+ return attempt({
134
+ try: () => {
135
+ const blob = new Blob([body], { type: BEACON_CONTENT_TYPE });
136
+ return globalThis.navigator.sendBeacon(url, blob);
137
+ },
138
+ catch: () => false,
139
+ });
122
140
  }
123
- /** Send via fetch with keepalive and the API key header. */
124
141
  async sendFetch(url, body) {
125
- try {
126
- await fetch(url, {
127
- method: "POST",
128
- headers: {
129
- "Content-Type": "application/json",
130
- "x-squasher-key": this.config.apiKey,
131
- },
132
- body,
133
- keepalive: true,
134
- });
135
- if (this.config.debug) {
136
- console.log("[squasher] Event sent via fetch");
137
- }
138
- }
139
- catch (err) {
140
- if (this.config.debug) {
141
- console.warn("[squasher] Failed to send event:", err);
142
- }
143
- }
142
+ await attemptAsync({
143
+ try: async () => {
144
+ await fetch(url, {
145
+ method: "POST",
146
+ headers: {
147
+ ...DEFAULT_TELEMETRY_HEADERS,
148
+ "Content-Type": "application/json",
149
+ "x-squasher-key": this.config.apiKey,
150
+ },
151
+ body,
152
+ keepalive: true,
153
+ });
154
+ if (this.config.debug) {
155
+ console.log("[squasher] Event sent via fetch");
156
+ }
157
+ },
158
+ catch: ({ cause }) => {
159
+ if (this.config.debug) {
160
+ console.warn("[squasher] Failed to send event:", cause);
161
+ }
162
+ },
163
+ });
144
164
  }
145
165
  }