@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,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":""}
@@ -1,217 +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", () => {
19
- let listeners;
20
- let emitReplayEvent;
21
- beforeEach(() => {
22
- resetSessionId();
23
- listeners = {};
24
- emitReplayEvent = undefined;
25
- vi.useFakeTimers();
26
- vi.restoreAllMocks();
27
- const storage = {};
28
- vi.stubGlobal("crypto", {
29
- randomUUID: vi.fn().mockReturnValue("11111111-2222-4333-8444-555555555555"),
30
- });
31
- vi.stubGlobal("sessionStorage", {
32
- getItem: (key) => storage[key] ?? null,
33
- setItem: (key, value) => {
34
- storage[key] = value;
35
- },
36
- removeItem: (key) => {
37
- delete storage[key];
38
- },
39
- });
40
- vi.stubGlobal("location", {
41
- href: "https://example.com/dashboard",
42
- });
43
- vi.stubGlobal("fetch", vi.fn().mockResolvedValue({ ok: true }));
44
- vi.stubGlobal("navigator", {
45
- sendBeacon: vi.fn().mockReturnValue(true),
46
- });
47
- vi.stubGlobal("document", {});
48
- vi.stubGlobal("window", {
49
- addEventListener: vi.fn((event, listener) => {
50
- listeners[event] = [...(listeners[event] ?? []), listener];
51
- }),
52
- removeEventListener: vi.fn((event, listener) => {
53
- listeners[event] = (listeners[event] ?? []).filter((value) => value !== listener);
54
- }),
55
- });
56
- rrwebStopMock.mockReset();
57
- rrwebRecordMock.mockReset();
58
- rrwebRecordMock.mockImplementation((options) => {
59
- emitReplayEvent = options?.emit;
60
- return rrwebStopMock;
61
- });
62
- });
63
- afterEach(() => {
64
- vi.useRealTimers();
65
- vi.unstubAllGlobals();
66
- });
67
- it("starts rrweb recording when replay is enabled", async () => {
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
- expect(rrwebRecordMock).toHaveBeenCalledOnce();
77
- recorder.dispose();
78
- expect(rrwebStopMock).toHaveBeenCalledOnce();
79
- });
80
- it("flushes replay events once the buffer reaches 50 events", async () => {
81
- const recorder = new ReplayRecorder({
82
- endpoint: "https://ingest.squasher.ai",
83
- projectId: "project-123",
84
- apiKey: "sq_pk_test",
85
- debug: false,
86
- sampleRate: 1,
87
- });
88
- await recorder.ready;
89
- for (let index = 0; index < 50; index += 1) {
90
- emitReplayEvent?.(createReplayEvent(index === 0 ? 2 : 3, index));
91
- }
92
- const fetchMock = vi.mocked(fetch);
93
- expect(fetchMock).toHaveBeenCalledOnce();
94
- const [url, requestInit] = fetchMock.mock.calls[0] ?? [];
95
- expect(url).toBe("https://ingest.squasher.ai/v1/projects/project-123/replays");
96
- expect(requestInit?.headers).toEqual({
97
- "Content-Type": "application/json",
98
- "x-squasher-key": "sq_pk_test",
99
- });
100
- const requestBody = requestInit?.body;
101
- expect(typeof requestBody).toBe("string");
102
- if (typeof requestBody !== "string") {
103
- throw new Error("expected replay request body to be a string");
104
- }
105
- const payload = JSON.parse(requestBody);
106
- expect(payload.session_id).toBe("11111111-2222-4333-8444-555555555555");
107
- expect(payload.page_url).toBe("https://example.com/dashboard");
108
- expect(payload.events).toHaveLength(50);
109
- expect(payload.events[0]?.type).toBe(2);
110
- expect(payload.events[49]?.type).toBe(3);
111
- recorder.dispose();
112
- });
113
- it("flushes queued replay events every five seconds", async () => {
114
- const recorder = new ReplayRecorder({
115
- endpoint: "https://ingest.squasher.ai",
116
- projectId: "project-123",
117
- apiKey: "sq_pk_test",
118
- debug: false,
119
- sampleRate: 1,
120
- });
121
- await recorder.ready;
122
- emitReplayEvent?.(createReplayEvent(2, 1));
123
- expect(fetch).not.toHaveBeenCalled();
124
- vi.advanceTimersByTime(5_000);
125
- expect(fetch).toHaveBeenCalledOnce();
126
- recorder.dispose();
127
- });
128
- it("uses sendBeacon for the final pagehide flush", async () => {
129
- const recorder = new ReplayRecorder({
130
- endpoint: "https://ingest.squasher.ai",
131
- projectId: "project-123",
132
- apiKey: "sq_pk_test",
133
- debug: false,
134
- sampleRate: 1,
135
- });
136
- await recorder.ready;
137
- emitReplayEvent?.(createReplayEvent(3, 99));
138
- listeners.pagehide?.forEach((listener) => listener());
139
- const sendBeaconMock = vi.mocked(navigator.sendBeacon);
140
- expect(sendBeaconMock).toHaveBeenCalledOnce();
141
- expect(fetch).not.toHaveBeenCalled();
142
- const [url, payloadBlob] = sendBeaconMock.mock.calls[0] ?? [];
143
- expect(url).toBe("https://ingest.squasher.ai/v1/projects/project-123/replays?key=sq_pk_test");
144
- expect(payloadBlob).toBeInstanceOf(Blob);
145
- if (!(payloadBlob instanceof Blob)) {
146
- throw new Error("expected sendBeacon payload to be a Blob");
147
- }
148
- const payload = JSON.parse(await payloadBlob.text());
149
- expect(payload.session_id).toBe("11111111-2222-4333-8444-555555555555");
150
- expect(payload.events).toHaveLength(1);
151
- expect(payload.events[0]?.type).toBe(3);
152
- recorder.dispose();
153
- });
154
- it("passes privacy controls to rrweb and redacts masked text in serialized payloads", async () => {
155
- const recorder = new ReplayRecorder({
156
- endpoint: "https://ingest.squasher.ai",
157
- projectId: "project-123",
158
- apiKey: "sq_pk_test",
159
- debug: false,
160
- privacy: {
161
- blockSelector: ".pii-container",
162
- maskAllText: true,
163
- },
164
- sampleRate: 1,
165
- });
166
- await recorder.ready;
167
- expect(rrwebRecordMock).toHaveBeenCalledWith(expect.objectContaining({
168
- blockSelector: ".pii-container",
169
- maskAllInputs: true,
170
- maskAllText: true,
171
- maskTextSelector: "body, body *",
172
- }));
173
- emitReplayEvent?.({
174
- data: {
175
- node: {
176
- attributes: {
177
- placeholder: "customer@example.com",
178
- value: "customer@example.com",
179
- },
180
- childNodes: [
181
- { textContent: "Jane Example", type: 3 },
182
- {
183
- childNodes: [{ textContent: "body{color:#0f172a;}", type: 3 }],
184
- tagName: "style",
185
- type: 2,
186
- },
187
- {
188
- childNodes: [
189
- { textContent: 'window.__STATE__={email:"customer@example.com"};', type: 3 },
190
- ],
191
- tagName: "script",
192
- type: 2,
193
- },
194
- ],
195
- type: 2,
196
- },
197
- text: "customer@example.com",
198
- },
199
- timestamp: 1,
200
- type: 3,
201
- });
202
- vi.advanceTimersByTime(5_000);
203
- const requestBody = vi.mocked(fetch).mock.calls[0]?.[1]?.body;
204
- expect(typeof requestBody).toBe("string");
205
- if (typeof requestBody !== "string") {
206
- throw new Error("expected replay request body to be a string");
207
- }
208
- const payload = JSON.parse(requestBody);
209
- const serializedEvents = JSON.stringify(payload.events);
210
- expect(serializedEvents).not.toContain("Jane Example");
211
- expect(serializedEvents).not.toContain("customer@example.com");
212
- expect(serializedEvents).not.toContain("body{color:#0f172a;}");
213
- expect(serializedEvents).not.toContain('window.__STATE__={email:"customer@example.com"};');
214
- expect(serializedEvents).toContain("************");
215
- recorder.dispose();
216
- });
217
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=session.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"session.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/session.test.ts"],"names":[],"mappings":""}
@@ -1,46 +0,0 @@
1
- import { describe, it, expect, beforeEach, vi } from "vitest";
2
- import { getSessionId, resetSessionId } from "../session";
3
- describe("session", () => {
4
- beforeEach(() => {
5
- resetSessionId();
6
- // Mock sessionStorage
7
- const store = {};
8
- vi.stubGlobal("sessionStorage", {
9
- getItem: (key) => store[key] ?? null,
10
- setItem: (key, val) => {
11
- store[key] = val;
12
- },
13
- removeItem: (key) => {
14
- delete store[key];
15
- },
16
- });
17
- });
18
- it("generates a UUID session ID", () => {
19
- const id = getSessionId();
20
- expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
21
- });
22
- it("returns the same ID on subsequent calls", () => {
23
- const id1 = getSessionId();
24
- const id2 = getSessionId();
25
- expect(id1).toBe(id2);
26
- });
27
- it("generates a new ID after reset", () => {
28
- const id1 = getSessionId();
29
- resetSessionId();
30
- const id2 = getSessionId();
31
- expect(id1).not.toBe(id2);
32
- });
33
- it("persists to sessionStorage", () => {
34
- const id = getSessionId();
35
- expect(sessionStorage.getItem("__sq_sid")).toBe(id);
36
- });
37
- it("restores from sessionStorage", () => {
38
- sessionStorage.setItem("__sq_sid", "existing-session-id");
39
- // Clear the in-memory cache
40
- resetSessionId();
41
- // Re-set the storage value after reset cleared it
42
- sessionStorage.setItem("__sq_sid", "existing-session-id");
43
- const id = getSessionId();
44
- expect(id).toBe("existing-session-id");
45
- });
46
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=transport.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"transport.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/transport.test.ts"],"names":[],"mappings":""}
@@ -1,101 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
2
- import { Transport } from "../transport";
3
- describe("Transport", () => {
4
- let transport;
5
- beforeEach(() => {
6
- vi.useFakeTimers();
7
- // Mock sendBeacon
8
- vi.stubGlobal("navigator", {
9
- sendBeacon: vi.fn().mockReturnValue(true),
10
- });
11
- // Mock document visibility
12
- vi.stubGlobal("document", {
13
- addEventListener: vi.fn(),
14
- visibilityState: "visible",
15
- });
16
- // Mock window
17
- vi.stubGlobal("window", {
18
- addEventListener: vi.fn(),
19
- });
20
- transport = new Transport({
21
- endpoint: "https://ingest.squasher.ai",
22
- projectId: "test-project",
23
- apiKey: "sq_pk_test",
24
- debug: false,
25
- vitalsBufferSize: 3,
26
- vitalsFlushIntervalMs: 10_000,
27
- });
28
- });
29
- afterEach(() => {
30
- transport.dispose();
31
- vi.useRealTimers();
32
- vi.restoreAllMocks();
33
- });
34
- it("sends errors immediately via sendBeacon", () => {
35
- transport.sendError({
36
- message: "Test error",
37
- level: "error",
38
- sdk: { name: "@squasher-ai/browser", version: "0.1.0" },
39
- });
40
- expect(navigator.sendBeacon).toHaveBeenCalledOnce();
41
- const [url, blob] = navigator.sendBeacon.mock.calls[0];
42
- expect(url).toBe("https://ingest.squasher.ai/v1/ingest/test-project?key=sq_pk_test");
43
- expect(blob).toBeInstanceOf(Blob);
44
- });
45
- it("buffers vitals and flushes when buffer is full", () => {
46
- const vital = {
47
- type: "vital",
48
- name: "LCP",
49
- value: 2400,
50
- rating: "needs-improvement",
51
- delta: 2400,
52
- navigationType: "navigate",
53
- url: "https://example.com",
54
- session_id: "test-session",
55
- timestamp: Date.now(),
56
- };
57
- // Send 2 vitals (below buffer size of 3)
58
- transport.enqueueVital(vital);
59
- transport.enqueueVital(vital);
60
- expect(navigator.sendBeacon).not.toHaveBeenCalled();
61
- // Third vital triggers flush
62
- transport.enqueueVital(vital);
63
- expect(navigator.sendBeacon).toHaveBeenCalledOnce();
64
- const [url] = navigator.sendBeacon.mock.calls[0];
65
- expect(url).toBe("https://ingest.squasher.ai/v1/vitals/test-project");
66
- });
67
- it("flushes on dispose", () => {
68
- const vital = {
69
- type: "vital",
70
- name: "FCP",
71
- value: 1000,
72
- rating: "good",
73
- delta: 1000,
74
- navigationType: "navigate",
75
- url: "https://example.com",
76
- session_id: "s1",
77
- timestamp: Date.now(),
78
- };
79
- transport.enqueueVital(vital);
80
- expect(navigator.sendBeacon).not.toHaveBeenCalled();
81
- transport.dispose();
82
- expect(navigator.sendBeacon).toHaveBeenCalledOnce();
83
- });
84
- it("does not enqueue after dispose", () => {
85
- transport.dispose();
86
- vi.mocked(navigator.sendBeacon).mockClear();
87
- transport.enqueueVital({
88
- type: "vital",
89
- name: "CLS",
90
- value: 0.05,
91
- rating: "good",
92
- delta: 0.05,
93
- navigationType: "navigate",
94
- url: "https://example.com",
95
- session_id: "s1",
96
- timestamp: Date.now(),
97
- });
98
- // Should not send after dispose
99
- expect(navigator.sendBeacon).not.toHaveBeenCalled();
100
- });
101
- });
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=types.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"types.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/types.test.ts"],"names":[],"mappings":""}
@@ -1,139 +0,0 @@
1
- import { describe, it, expect } from "vitest";
2
- /**
3
- * Type-level parity tests.
4
- *
5
- * These tests verify that the browser SDK types match the ingestion wire
6
- * protocol and API spec shapes. They don't test runtime behavior — they
7
- * ensure type definitions are structurally compatible.
8
- */
9
- describe("type parity", () => {
10
- it("VitalEvent matches the ingestion payload schema", () => {
11
- const event = {
12
- type: "vital",
13
- name: "LCP",
14
- value: 2400,
15
- rating: "needs-improvement",
16
- delta: 2400,
17
- navigationType: "navigate",
18
- url: "https://example.com/checkout",
19
- route: "/checkout",
20
- session_id: "abc-123",
21
- timestamp: 1711234567890,
22
- environment: "production",
23
- release: "1.0.0",
24
- connection: "4g",
25
- device_type: "desktop",
26
- referrer: "https://google.com",
27
- };
28
- // Verify all fields are serializable to JSON
29
- const json = JSON.parse(JSON.stringify(event));
30
- expect(json.type).toBe("vital");
31
- expect(json.name).toBe("LCP");
32
- expect(json.value).toBe(2400);
33
- expect(json.rating).toBe("needs-improvement");
34
- expect(json.session_id).toBe("abc-123");
35
- });
36
- it("VitalsPayload wraps events array", () => {
37
- const payload = {
38
- events: [
39
- {
40
- type: "vital",
41
- name: "CLS",
42
- value: 0.05,
43
- rating: "good",
44
- delta: 0.01,
45
- navigationType: "navigate",
46
- url: "https://example.com",
47
- session_id: "s1",
48
- timestamp: 1000,
49
- },
50
- ],
51
- };
52
- const json = JSON.parse(JSON.stringify(payload));
53
- expect(json.events).toHaveLength(1);
54
- expect(json.events[0].name).toBe("CLS");
55
- });
56
- it("BrowserErrorEvent includes session_id", () => {
57
- const event = {
58
- message: "Test error",
59
- type: "TypeError",
60
- level: "error",
61
- session_id: "browser-session-123",
62
- sdk: { name: "@squasher-ai/browser", version: "0.1.0" },
63
- timestamp: new Date().toISOString(),
64
- };
65
- expect(event.session_id).toBe("browser-session-123");
66
- });
67
- it("BrowserErrorEvent supports analytics and agent telemetry fields", () => {
68
- const event = {
69
- message: "tool call completed",
70
- kind: "tool_call",
71
- event_name: "db.lookup",
72
- distinct_id: "agent-123",
73
- analytics: {
74
- event: "checkout_started",
75
- properties: {
76
- step: 1,
77
- },
78
- },
79
- trace: {
80
- trace_id: "trace-1",
81
- span_id: "span-1",
82
- },
83
- tool_call: {
84
- name: "db.lookup",
85
- duration_ms: 42,
86
- },
87
- llm: {
88
- provider: "anthropic",
89
- model: "claude-sonnet",
90
- },
91
- };
92
- expect(event.kind).toBe("tool_call");
93
- expect(event.analytics?.event).toBe("checkout_started");
94
- expect(event.trace?.trace_id).toBe("trace-1");
95
- expect(event.tool_call?.name).toBe("db.lookup");
96
- expect(event.llm?.model).toBe("claude-sonnet");
97
- });
98
- it("BrowserConfig has vitals-specific fields", () => {
99
- const config = {
100
- apiKey: "sq_pk_test",
101
- projectId: "test-project",
102
- enableVitals: true,
103
- vitalsSampleRate: 0.5,
104
- vitalsBufferSize: 10,
105
- vitalsFlushIntervalMs: 10_000,
106
- enableErrorCapture: true,
107
- enableAutoBreadcrumbs: true,
108
- };
109
- expect(config.enableVitals).toBe(true);
110
- expect(config.vitalsSampleRate).toBe(0.5);
111
- });
112
- it("BrowserConfig supports replay recording options", () => {
113
- const config = {
114
- apiKey: "sq_pk_test",
115
- projectId: "test-project",
116
- replay: {
117
- enabled: true,
118
- privacy: {
119
- blockSelector: ".pii-container",
120
- maskAllText: true,
121
- },
122
- sampleRate: 0.25,
123
- },
124
- };
125
- expect(config.replay?.enabled).toBe(true);
126
- expect(config.replay?.privacy?.blockSelector).toBe(".pii-container");
127
- expect(config.replay?.privacy?.maskAllText).toBe(true);
128
- expect(config.replay?.sampleRate).toBe(0.25);
129
- });
130
- it("VitalMetricName only allows valid metric names", () => {
131
- // TypeScript compile-time check — these are the only valid values
132
- const validNames = ["LCP", "CLS", "INP", "FCP", "TTFB"];
133
- expect(validNames).toHaveLength(5);
134
- });
135
- it("VitalRating only allows valid ratings", () => {
136
- const validRatings = ["good", "needs-improvement", "poor"];
137
- expect(validRatings).toHaveLength(3);
138
- });
139
- });