@squasher-ai/browser 0.1.1 → 0.3.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 (109) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +3 -42
  3. package/dist/_vendor/result-runtime/attempt.d.ts +17 -0
  4. package/dist/_vendor/result-runtime/attempt.js +62 -0
  5. package/dist/_vendor/sdk-runtime/batching/index.d.ts +42 -0
  6. package/dist/_vendor/sdk-runtime/batching/index.js +48 -0
  7. package/dist/_vendor/sdk-runtime/errors/headers.d.ts +6 -0
  8. package/dist/_vendor/sdk-runtime/errors/headers.js +44 -0
  9. package/dist/_vendor/sdk-runtime/errors/index.d.ts +95 -0
  10. package/dist/_vendor/sdk-runtime/errors/index.js +157 -0
  11. package/dist/_vendor/sdk-runtime/headers.d.ts +48 -0
  12. package/dist/_vendor/sdk-runtime/headers.js +67 -0
  13. package/dist/_vendor/sdk-runtime/platform.d.ts +33 -0
  14. package/dist/_vendor/sdk-runtime/platform.js +173 -0
  15. package/dist/_vendor/sdk-runtime/retry.d.ts +50 -0
  16. package/dist/_vendor/sdk-runtime/retry.js +104 -0
  17. package/dist/_vendor/sdk-runtime/runtime/actionable-error.d.ts +37 -0
  18. package/dist/_vendor/sdk-runtime/runtime/actionable-error.js +123 -0
  19. package/dist/_vendor/sdk-runtime/runtime/environment.d.ts +18 -0
  20. package/dist/_vendor/sdk-runtime/runtime/environment.js +81 -0
  21. package/dist/_vendor/sdk-runtime/runtime/public-sdk-runtime.d.ts +12 -0
  22. package/dist/_vendor/sdk-runtime/runtime/public-sdk-runtime.js +38 -0
  23. package/dist/_vendor/sdk-runtime/runtime/redaction.d.ts +14 -0
  24. package/dist/_vendor/sdk-runtime/runtime/redaction.js +120 -0
  25. package/dist/_vendor/sdk-runtime/runtime/release.d.ts +35 -0
  26. package/dist/_vendor/sdk-runtime/runtime/release.js +120 -0
  27. package/dist/_vendor/sdk-runtime/sampling/index.d.ts +43 -0
  28. package/dist/_vendor/sdk-runtime/sampling/index.js +70 -0
  29. package/dist/_vendor/telemetry-contract/sdk/public-contract.d.ts +175 -0
  30. package/dist/_vendor/telemetry-contract/sdk/public-contract.js +5 -0
  31. package/dist/autocapture.d.ts +14 -19
  32. package/dist/autocapture.js +77 -95
  33. package/dist/client.d.ts +12 -20
  34. package/dist/client.js +56 -72
  35. package/dist/config.d.ts +24 -0
  36. package/dist/config.js +26 -0
  37. package/dist/console-capture.d.ts +7 -0
  38. package/dist/console-capture.js +79 -0
  39. package/dist/errors.d.ts +2 -3
  40. package/dist/errors.js +8 -5
  41. package/dist/index.d.ts +5 -39
  42. package/dist/index.js +4 -82
  43. package/dist/next.d.ts +39 -0
  44. package/dist/next.js +67 -0
  45. package/dist/react.d.ts +0 -1
  46. package/dist/react.js +12 -10
  47. package/dist/replay-network.d.ts +20 -0
  48. package/dist/replay-network.js +193 -0
  49. package/dist/replay-privacy.d.ts +2 -3
  50. package/dist/replay-privacy.js +47 -17
  51. package/dist/replay-retry.d.ts +40 -0
  52. package/dist/replay-retry.js +193 -0
  53. package/dist/replay.d.ts +18 -6
  54. package/dist/replay.js +131 -71
  55. package/dist/runtime/client-singleton.d.ts +24 -0
  56. package/dist/runtime/client-singleton.js +66 -0
  57. package/dist/runtime/client.d.ts +21 -0
  58. package/dist/runtime/client.js +12 -0
  59. package/dist/runtime/replay.d.ts +12 -0
  60. package/dist/runtime/replay.js +15 -0
  61. package/dist/session.d.ts +0 -11
  62. package/dist/session.js +20 -21
  63. package/dist/telemetry.d.ts +1 -2
  64. package/dist/telemetry.js +13 -10
  65. package/dist/trace-context.d.ts +8 -0
  66. package/dist/trace-context.js +13 -0
  67. package/dist/transport.d.ts +11 -20
  68. package/dist/transport.js +86 -66
  69. package/dist/types.d.ts +38 -139
  70. package/dist/types.js +0 -7
  71. package/dist/vitals.d.ts +55 -3
  72. package/dist/vitals.js +115 -16
  73. package/package.json +11 -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
  98. package/dist/autocapture.d.ts.map +0 -1
  99. package/dist/client.d.ts.map +0 -1
  100. package/dist/errors.d.ts.map +0 -1
  101. package/dist/index.d.ts.map +0 -1
  102. package/dist/react.d.ts.map +0 -1
  103. package/dist/replay-privacy.d.ts.map +0 -1
  104. package/dist/replay.d.ts.map +0 -1
  105. package/dist/session.d.ts.map +0 -1
  106. package/dist/telemetry.d.ts.map +0 -1
  107. package/dist/transport.d.ts.map +0 -1
  108. package/dist/types.d.ts.map +0 -1
  109. package/dist/vitals.d.ts.map +0 -1
package/dist/vitals.js CHANGED
@@ -12,15 +12,15 @@
12
12
  * Measurements are mapped to VitalEvent and queued via the transport.
13
13
  */
14
14
  import { onLCP, onCLS, onINP, onFCP, onTTFB } from "web-vitals";
15
- import { getSessionId } from "./session";
15
+ import { getSessionId } from "./session.js";
16
16
  /**
17
17
  * Infer device type from screen width.
18
18
  * Mobile < 768px, Tablet < 1024px, Desktop >= 1024px.
19
19
  */
20
20
  function inferDeviceType() {
21
- if (typeof screen === "undefined")
21
+ if (!globalThis.screen)
22
22
  return undefined;
23
- const width = screen.width;
23
+ const width = globalThis.screen.width;
24
24
  if (width < 768)
25
25
  return "mobile";
26
26
  if (width < 1024)
@@ -32,13 +32,54 @@ function inferDeviceType() {
32
32
  * Returns undefined if not supported (Firefox, Safari).
33
33
  */
34
34
  function getConnectionType() {
35
- const nav = navigator;
36
- return nav.connection?.effectiveType;
35
+ return globalThis.navigator?.connection?.effectiveType;
36
+ }
37
+ function getBrowserTimezone() {
38
+ try {
39
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
40
+ }
41
+ catch {
42
+ return undefined;
43
+ }
44
+ }
45
+ function getUrlDimensions() {
46
+ if (!globalThis.location) {
47
+ return {
48
+ page_path: "",
49
+ query_string: "",
50
+ url: "",
51
+ utm_campaign: undefined,
52
+ utm_medium: undefined,
53
+ utm_source: undefined,
54
+ };
55
+ }
56
+ const href = globalThis.location.href;
57
+ const parsed = href ? safeUrl(href) : undefined;
58
+ const rawSearch = globalThis.location.search || parsed?.search || "";
59
+ const search = rawSearch.startsWith("?") ? rawSearch.slice(1) : rawSearch;
60
+ const params = new URLSearchParams(search);
61
+ return {
62
+ page_path: globalThis.location.pathname || parsed?.pathname || "",
63
+ query_string: search,
64
+ url: href,
65
+ utm_campaign: params.get("utm_campaign") || undefined,
66
+ utm_medium: params.get("utm_medium") || undefined,
67
+ utm_source: params.get("utm_source") || undefined,
68
+ };
69
+ }
70
+ function safeUrl(value) {
71
+ try {
72
+ return new URL(value);
73
+ }
74
+ catch {
75
+ return undefined;
76
+ }
37
77
  }
38
78
  /**
39
79
  * Map a web-vitals Metric to our VitalEvent shape.
40
80
  */
41
- function mapMetric(metric, config) {
81
+ function mapMetric(metric, config, getCurrentSessionId) {
82
+ const url = getUrlDimensions();
42
83
  return {
43
84
  type: "vital",
44
85
  name: metric.name,
@@ -46,16 +87,59 @@ function mapMetric(metric, config) {
46
87
  rating: metric.rating,
47
88
  delta: metric.delta,
48
89
  navigationType: metric.navigationType,
49
- url: location.href,
90
+ url: url.url,
91
+ session_id: getCurrentSessionId(),
92
+ timestamp: Date.now(),
93
+ environment: config.environment,
94
+ release: config.release,
95
+ connection: getConnectionType(),
96
+ device_type: inferDeviceType(),
97
+ language: globalThis.navigator?.language || undefined,
98
+ page_path: url.page_path,
99
+ query_string: url.query_string,
100
+ referrer: globalThis.document?.referrer || undefined,
101
+ timezone: getBrowserTimezone(),
102
+ utm_campaign: url.utm_campaign,
103
+ utm_medium: url.utm_medium,
104
+ utm_source: url.utm_source,
105
+ };
106
+ }
107
+ /**
108
+ * Map an externally-sourced web vital (Next.js custom metric or otherwise)
109
+ * to our VitalEvent wire shape so it can be enqueued through the transport.
110
+ */
111
+ export function mapExternalVital(metric, config, route) {
112
+ const url = getUrlDimensions();
113
+ return {
114
+ type: "vital",
115
+ name: metric.name,
116
+ value: metric.value,
117
+ rating: normalizeRating(metric.rating),
118
+ delta: metric.delta ?? metric.value,
119
+ navigationType: metric.navigationType ?? "navigate",
120
+ url: url.url,
121
+ route,
50
122
  session_id: getSessionId(),
51
123
  timestamp: Date.now(),
52
124
  environment: config.environment,
53
125
  release: config.release,
54
126
  connection: getConnectionType(),
55
127
  device_type: inferDeviceType(),
56
- referrer: document.referrer || undefined,
128
+ language: globalThis.navigator?.language || undefined,
129
+ page_path: url.page_path,
130
+ query_string: url.query_string,
131
+ referrer: globalThis.document?.referrer || undefined,
132
+ timezone: getBrowserTimezone(),
133
+ utm_campaign: url.utm_campaign,
134
+ utm_medium: url.utm_medium,
135
+ utm_source: url.utm_source,
57
136
  };
58
137
  }
138
+ function normalizeRating(rating) {
139
+ if (rating === "poor" || rating === "needs-improvement")
140
+ return rating;
141
+ return "good";
142
+ }
59
143
  /** Decide whether to sample this vital based on the configured rate. */
60
144
  function shouldSample(sampleRate) {
61
145
  if (sampleRate >= 1)
@@ -64,19 +148,33 @@ function shouldSample(sampleRate) {
64
148
  return false;
65
149
  return Math.random() < sampleRate;
66
150
  }
151
+ export const defaultVitalsRuntime = {
152
+ getSessionId,
153
+ observeCLS: (callback, options) => onCLS(callback, options),
154
+ observeFCP: (callback) => onFCP(callback),
155
+ observeINP: (callback, options) => onINP(callback, options),
156
+ observeLCP: (callback) => onLCP(callback),
157
+ observeTTFB: (callback) => onTTFB(callback),
158
+ };
67
159
  /**
68
160
  * Start collecting all 5 Core Web Vitals and feed them to the transport.
69
161
  * Call once during SDK initialization.
70
162
  */
71
- export function startVitalsCollection(transport, config) {
163
+ export function startVitalsCollection(transport, config, runtime = defaultVitalsRuntime) {
164
+ let suppressed = false;
165
+ const suppress = () => {
166
+ suppressed = true;
167
+ };
72
168
  if (!shouldSample(config.sampleRate)) {
73
169
  if (config.debug) {
74
170
  console.log("[squasher] Vitals collection skipped (sampled out)");
75
171
  }
76
- return;
172
+ return { suppress };
77
173
  }
78
174
  const handle = (metric) => {
79
- const event = mapMetric(metric, config);
175
+ if (suppressed)
176
+ return;
177
+ const event = mapMetric(metric, config, runtime.getSessionId);
80
178
  if (config.debug) {
81
179
  console.log(`[squasher] ${metric.name}: ${metric.value.toFixed(2)} (${metric.rating})`);
82
180
  }
@@ -84,9 +182,10 @@ export function startVitalsCollection(transport, config) {
84
182
  };
85
183
  // Register all 5 metric observers.
86
184
  // reportAllChanges: true for CLS/INP so we get cumulative values.
87
- onLCP(handle);
88
- onCLS(handle, { reportAllChanges: true });
89
- onINP(handle, { reportAllChanges: true });
90
- onFCP(handle);
91
- onTTFB(handle);
185
+ runtime.observeLCP(handle);
186
+ runtime.observeCLS(handle, { reportAllChanges: true });
187
+ runtime.observeINP(handle, { reportAllChanges: true });
188
+ runtime.observeFCP(handle);
189
+ runtime.observeTTFB(handle);
190
+ return { suppress };
92
191
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@squasher-ai/browser",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Official @squasher-ai/browser package for Squasher.",
5
- "homepage": "https://docs.squasher.ai",
5
+ "homepage": "https://squasher.ai",
6
6
  "license": "MIT",
7
7
  "publishConfig": {
8
8
  "access": "public"
@@ -18,17 +18,25 @@
18
18
  "./react": {
19
19
  "import": "./dist/react.js",
20
20
  "types": "./dist/react.d.ts"
21
+ },
22
+ "./next": {
23
+ "import": "./dist/next.js",
24
+ "types": "./dist/next.d.ts"
21
25
  }
22
26
  },
23
27
  "sideEffects": false,
24
28
  "dependencies": {
25
- "rrweb": "^2.0.0-alpha.4",
29
+ "rrweb": "^2.0.0-alpha.20",
26
30
  "web-vitals": "^4.2.4"
27
31
  },
28
32
  "peerDependencies": {
33
+ "next": ">=14",
29
34
  "react": ">=18"
30
35
  },
31
36
  "peerDependenciesMeta": {
37
+ "next": {
38
+ "optional": true
39
+ },
32
40
  "react": {
33
41
  "optional": true
34
42
  }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=client.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/client.test.ts"],"names":[],"mappings":""}
@@ -1,103 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { BrowserClient } from "../client";
3
- import { resetSessionId } from "../session";
4
- describe("BrowserClient", () => {
5
- beforeEach(() => {
6
- resetSessionId();
7
- const storage = {};
8
- vi.restoreAllMocks();
9
- vi.stubGlobal("crypto", {
10
- randomUUID: vi.fn().mockReturnValue("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"),
11
- });
12
- vi.stubGlobal("sessionStorage", {
13
- getItem: (key) => storage[key] ?? null,
14
- setItem: (key, value) => {
15
- storage[key] = value;
16
- },
17
- removeItem: (key) => {
18
- delete storage[key];
19
- },
20
- });
21
- vi.stubGlobal("navigator", {
22
- sendBeacon: vi.fn().mockReturnValue(true),
23
- });
24
- vi.stubGlobal("document", {
25
- addEventListener: vi.fn(),
26
- referrer: "https://referrer.example",
27
- title: "Settings",
28
- });
29
- vi.stubGlobal("window", {
30
- addEventListener: vi.fn(),
31
- });
32
- vi.stubGlobal("location", {
33
- href: "https://example.com/settings",
34
- pathname: "/settings",
35
- search: "?tab=profile",
36
- });
37
- });
38
- it("attaches the current session ID to captured error events", async () => {
39
- const client = new BrowserClient({
40
- apiKey: "sq_pk_test",
41
- projectId: "project-123",
42
- enableVitals: false,
43
- enableErrorCapture: false,
44
- enableAutoBreadcrumbs: false,
45
- });
46
- client.captureError(new Error("boom"));
47
- const sendBeaconMock = vi.mocked(navigator.sendBeacon);
48
- expect(sendBeaconMock).toHaveBeenCalledOnce();
49
- const [url, payloadBlob] = sendBeaconMock.mock.calls[0] ?? [];
50
- expect(url).toBe("https://ingest.squasher.ai/v1/ingest/project-123?key=sq_pk_test");
51
- expect(payloadBlob).toBeInstanceOf(Blob);
52
- if (!(payloadBlob instanceof Blob)) {
53
- throw new Error("expected ingest payload to be a Blob");
54
- }
55
- const payload = JSON.parse(await payloadBlob.text());
56
- expect(payload.session_id).toBe("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee");
57
- expect(payload.request?.url).toBe("https://example.com/settings");
58
- client.close();
59
- });
60
- it("track sends analytics metadata with visitor context", async () => {
61
- const client = new BrowserClient({
62
- apiKey: "sq_pk_test",
63
- projectId: "project-123",
64
- enableVitals: false,
65
- enableErrorCapture: false,
66
- enableAutoBreadcrumbs: false,
67
- });
68
- client.track("checkout_started", { step: 1 });
69
- const [_, payloadBlob] = vi.mocked(navigator.sendBeacon).mock.calls[0] ?? [];
70
- if (!(payloadBlob instanceof Blob)) {
71
- throw new Error("expected ingest payload to be a Blob");
72
- }
73
- const payload = JSON.parse(await payloadBlob.text());
74
- expect(payload.kind).toBe("analytics");
75
- expect(payload.event_name).toBe("checkout_started");
76
- expect(payload.analytics?.event).toBe("checkout_started");
77
- expect(payload.analytics?.properties?.step).toBe(1);
78
- expect(payload.visitor?.anonymous_id).toBe("aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee");
79
- client.close();
80
- });
81
- it("page sends normalized page metadata", async () => {
82
- const client = new BrowserClient({
83
- apiKey: "sq_pk_test",
84
- projectId: "project-123",
85
- enableVitals: false,
86
- enableErrorCapture: false,
87
- enableAutoBreadcrumbs: false,
88
- });
89
- client.page("Settings");
90
- const [_, payloadBlob] = vi.mocked(navigator.sendBeacon).mock.calls[0] ?? [];
91
- if (!(payloadBlob instanceof Blob)) {
92
- throw new Error("expected ingest payload to be a Blob");
93
- }
94
- const payload = JSON.parse(await payloadBlob.text());
95
- expect(payload.kind).toBe("page");
96
- expect(payload.page?.name).toBe("Settings");
97
- expect(payload.page?.path).toBe("/settings");
98
- expect(payload.page?.title).toBe("Settings");
99
- expect(payload.page?.referrer).toBe("https://referrer.example");
100
- expect(payload.page?.search).toBe("?tab=profile");
101
- client.close();
102
- });
103
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=errors.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"errors.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/errors.test.ts"],"names":[],"mappings":""}
@@ -1,80 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- import { parseStack, buildErrorEvent, buildMessageEvent } from "../errors";
3
- describe("parseStack", () => {
4
- it("parses V8 stack traces", () => {
5
- const stack = `Error: Something went wrong
6
- at functionName (https://example.com/app.js:10:20)
7
- at anotherFunction (https://example.com/utils.js:5:3)
8
- at Object.<anonymous> (node_modules/some-lib/index.js:100:15)`;
9
- const frames = parseStack(stack);
10
- expect(frames).toBeDefined();
11
- expect(frames).toHaveLength(3);
12
- expect(frames[0]).toEqual({
13
- function: "functionName",
14
- filename: "https://example.com/app.js",
15
- lineno: 10,
16
- colno: 20,
17
- in_app: true,
18
- });
19
- expect(frames[1]).toEqual({
20
- function: "anotherFunction",
21
- filename: "https://example.com/utils.js",
22
- lineno: 5,
23
- colno: 3,
24
- in_app: true,
25
- });
26
- // node_modules should be marked as not in_app
27
- expect(frames[2].in_app).toBe(false);
28
- });
29
- it("handles anonymous functions", () => {
30
- const stack = `Error: test
31
- at https://example.com/app.js:1:1`;
32
- const frames = parseStack(stack);
33
- expect(frames).toBeDefined();
34
- expect(frames).toHaveLength(1);
35
- expect(frames[0].function).toBe("<anonymous>");
36
- });
37
- it("returns undefined for empty stack", () => {
38
- expect(parseStack(undefined)).toBeUndefined();
39
- expect(parseStack("Error: test")).toBeUndefined();
40
- });
41
- it("marks chrome extensions as not in_app", () => {
42
- const stack = `Error: ext
43
- at handler (chrome-extension://abcdef/content.js:5:10)`;
44
- const frames = parseStack(stack);
45
- expect(frames).toBeDefined();
46
- expect(frames[0].in_app).toBe(false);
47
- });
48
- });
49
- describe("buildErrorEvent", () => {
50
- it("builds a complete error event", () => {
51
- const error = new Error("Test error");
52
- error.name = "TypeError";
53
- const event = buildErrorEvent(error, { extra: "data" });
54
- expect(event.message).toBe("Test error");
55
- expect(event.type).toBe("TypeError");
56
- expect(event.level).toBe("error");
57
- expect(event.sdk?.name).toBe("@squasher-ai/browser");
58
- expect(event.sdk?.version).toBe("0.1.0");
59
- expect(event.timestamp).toBeDefined();
60
- expect(event.extra).toEqual({ extra: "data" });
61
- expect(event.stack).toBeDefined();
62
- });
63
- it("builds event without extra", () => {
64
- const error = new Error("Simple error");
65
- const event = buildErrorEvent(error);
66
- expect(event.extra).toBeUndefined();
67
- });
68
- });
69
- describe("buildMessageEvent", () => {
70
- it("builds a message event with default level", () => {
71
- const event = buildMessageEvent("Hello");
72
- expect(event.message).toBe("Hello");
73
- expect(event.level).toBe("info");
74
- expect(event.sdk?.name).toBe("@squasher-ai/browser");
75
- });
76
- it("builds a message event with custom level", () => {
77
- const event = buildMessageEvent("Warning!", "warning");
78
- expect(event.level).toBe("warning");
79
- });
80
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=replay-privacy.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"replay-privacy.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/replay-privacy.test.ts"],"names":[],"mappings":""}
@@ -1,124 +0,0 @@
1
- import { describe, expect, it, vi } from "vitest";
2
- import { applyReplayEventPrivacy, buildReplayRecordOptions, MASK_ALL_TEXT_SELECTOR, } from "../replay-privacy";
3
- describe("replay privacy helpers", () => {
4
- it("builds rrweb options for text masking and blocked selectors", () => {
5
- const emit = vi.fn();
6
- expect(buildReplayRecordOptions({
7
- emit,
8
- privacy: {
9
- blockSelector: ".pii-container",
10
- maskAllText: true,
11
- },
12
- })).toMatchObject({
13
- blockSelector: ".pii-container",
14
- emit,
15
- maskAllInputs: true,
16
- maskAllText: true,
17
- maskTextSelector: MASK_ALL_TEXT_SELECTOR,
18
- });
19
- });
20
- it("redacts replay text fields without touching non-text attributes", () => {
21
- const event = {
22
- data: {
23
- node: {
24
- attributes: {
25
- "aria-label": "Customer email",
26
- placeholder: "you@example.com",
27
- style: "color:#0f172a",
28
- title: "Checkout form",
29
- value: "Customer email",
30
- },
31
- childNodes: [
32
- {
33
- textContent: "Jane Example",
34
- type: 3,
35
- },
36
- ],
37
- tagName: "input",
38
- type: 2,
39
- },
40
- source: 5,
41
- text: "customer@example.com",
42
- },
43
- timestamp: 1,
44
- type: 3,
45
- };
46
- const redactedEvent = applyReplayEventPrivacy(event, { maskAllText: true });
47
- const serializedEvent = JSON.stringify(redactedEvent);
48
- expect(redactedEvent).not.toBe(event);
49
- expect(serializedEvent).not.toContain("Jane Example");
50
- expect(serializedEvent).not.toContain("you@example.com");
51
- expect(serializedEvent).not.toContain("customer@example.com");
52
- expect(serializedEvent).not.toContain("Checkout form");
53
- expect(serializedEvent).toContain("color:#0f172a");
54
- expect(serializedEvent).toContain("************");
55
- expect(event.data.text).toBe("customer@example.com");
56
- });
57
- it("redacts inline style and script text alongside visible text nodes", () => {
58
- const event = {
59
- data: {
60
- node: {
61
- childNodes: [
62
- {
63
- childNodes: [
64
- {
65
- childNodes: [
66
- {
67
- tagName: "style",
68
- childNodes: [
69
- {
70
- textContent: "body{background:#0f172a;color:#e2e8f0;}",
71
- type: 3,
72
- },
73
- ],
74
- type: 2,
75
- },
76
- {
77
- tagName: "script",
78
- childNodes: [
79
- {
80
- textContent: 'window.__BOOTSTRAP__={email:"jane@example.com"};',
81
- type: 3,
82
- },
83
- ],
84
- type: 2,
85
- },
86
- {
87
- childNodes: [
88
- {
89
- textContent: "Private checkout summary",
90
- type: 3,
91
- },
92
- ],
93
- tagName: "h1",
94
- type: 2,
95
- },
96
- ],
97
- tagName: "body",
98
- type: 2,
99
- },
100
- ],
101
- tagName: "html",
102
- type: 2,
103
- },
104
- ],
105
- type: 0,
106
- },
107
- },
108
- timestamp: 1,
109
- type: 2,
110
- };
111
- const redactedEvent = applyReplayEventPrivacy(event, { maskAllText: true });
112
- const serializedEvent = JSON.stringify(redactedEvent);
113
- expect(serializedEvent).not.toContain("body{background:#0f172a;color:#e2e8f0;}");
114
- expect(serializedEvent).not.toContain('window.__BOOTSTRAP__={email:"jane@example.com"};');
115
- expect(serializedEvent).not.toContain("Private checkout summary");
116
- expect(serializedEvent).toContain("******* ******** *******");
117
- expect(serializedEvent).toMatch(/\*{10,}/);
118
- });
119
- it("returns the original replay event when text masking is disabled", () => {
120
- const event = { data: { text: "customer@example.com" }, timestamp: 1, type: 3 };
121
- expect(applyReplayEventPrivacy(event, { maskAllText: false })).toBe(event);
122
- expect(applyReplayEventPrivacy(event)).toBe(event);
123
- });
124
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=replay-startup.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"replay-startup.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/replay-startup.test.ts"],"names":[],"mappings":""}
@@ -1,99 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { ReplayRecorder } from "../replay";
3
- import { resetSessionId } from "../session";
4
- const rrwebRecordMock = vi.hoisted(() => vi.fn());
5
- const rrwebStopMock = vi.hoisted(() => vi.fn());
6
- vi.mock("rrweb", () => ({
7
- record: rrwebRecordMock,
8
- }));
9
- function createReplayEvent(type, timestamp) {
10
- return {
11
- type,
12
- timestamp,
13
- data: {
14
- source: type,
15
- },
16
- };
17
- }
18
- describe("ReplayRecorder startup behavior", () => {
19
- let emitReplayEvent;
20
- beforeEach(() => {
21
- resetSessionId();
22
- emitReplayEvent = undefined;
23
- vi.useFakeTimers();
24
- vi.restoreAllMocks();
25
- const storage = {};
26
- vi.stubGlobal("crypto", {
27
- randomUUID: vi.fn().mockReturnValue("11111111-2222-4333-8444-555555555555"),
28
- });
29
- vi.stubGlobal("sessionStorage", {
30
- getItem: (key) => storage[key] ?? null,
31
- setItem: (key, value) => {
32
- storage[key] = value;
33
- },
34
- removeItem: (key) => {
35
- delete storage[key];
36
- },
37
- });
38
- vi.stubGlobal("location", {
39
- href: "https://example.com/dashboard",
40
- });
41
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
42
- vi.stubGlobal("navigator", {
43
- sendBeacon: vi.fn().mockReturnValue(true),
44
- });
45
- vi.stubGlobal("document", {});
46
- vi.stubGlobal("window", {
47
- addEventListener: vi.fn(),
48
- removeEventListener: vi.fn(),
49
- });
50
- rrwebStopMock.mockReset();
51
- rrwebRecordMock.mockReset();
52
- rrwebRecordMock.mockImplementation((options) => {
53
- emitReplayEvent = options?.emit;
54
- return rrwebStopMock;
55
- });
56
- });
57
- afterEach(() => {
58
- vi.useRealTimers();
59
- vi.unstubAllGlobals();
60
- });
61
- it("preserves rrweb bootstrap events emitted before record() returns", async () => {
62
- rrwebRecordMock.mockImplementation((options) => {
63
- options?.emit?.(createReplayEvent(4, 1));
64
- options?.emit?.(createReplayEvent(2, 2));
65
- emitReplayEvent = options?.emit;
66
- return rrwebStopMock;
67
- });
68
- const recorder = new ReplayRecorder({
69
- endpoint: "https://ingest.squasher.ai",
70
- projectId: "project-123",
71
- apiKey: "sq_pk_test",
72
- debug: false,
73
- sampleRate: 1,
74
- });
75
- await recorder.ready;
76
- emitReplayEvent?.(createReplayEvent(3, 3));
77
- vi.advanceTimersByTime(5_000);
78
- const requestBody = vi.mocked(fetch).mock.calls[0]?.[1]?.body;
79
- expect(typeof requestBody).toBe("string");
80
- if (typeof requestBody !== "string") {
81
- throw new Error("expected replay request body to be a string");
82
- }
83
- const payload = JSON.parse(requestBody);
84
- expect(payload.events.map((event) => event.type)).toEqual([4, 2, 3]);
85
- recorder.dispose();
86
- });
87
- it("skips recording when the replay sample rate drops the session", () => {
88
- vi.spyOn(Math, "random").mockReturnValue(0.9);
89
- const recorder = new ReplayRecorder({
90
- endpoint: "https://ingest.squasher.ai",
91
- projectId: "project-123",
92
- apiKey: "sq_pk_test",
93
- debug: false,
94
- sampleRate: 0.5,
95
- });
96
- expect(rrwebRecordMock).not.toHaveBeenCalled();
97
- recorder.dispose();
98
- });
99
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=replay.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"replay.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/replay.test.ts"],"names":[],"mappings":""}