-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathbefore-send.test.ts
More file actions
181 lines (147 loc) · 5.54 KB
/
before-send.test.ts
File metadata and controls
181 lines (147 loc) · 5.54 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { CatcherMessage } from '../src/types/catcher-message';
import type { Transport } from '../src/types/transport';
import type { HawkJavaScriptEvent } from '../src/types/event';
import Catcher from '../src/catcher';
const TEST_TOKEN = 'eyJpbnRlZ3JhdGlvbklkIjoiOTU3MmQyOWQtNWJhZS00YmYyLTkwN2MtZDk5ZDg5MGIwOTVmIiwic2VjcmV0IjoiZTExODFiZWItMjdlMS00ZDViLWEwZmEtZmUwYTM1Mzg5OWMyIn0=';
/**
* Wait for fire-and-forget async calls inside hawk.send() to complete
*/
const wait = (): Promise<void> => new Promise((r) => setTimeout(r, 0));
function createTransport() {
const sendSpy = vi.fn<(msg: CatcherMessage) => Promise<void>>().mockResolvedValue(undefined);
const transport: Transport = { send: sendSpy };
return { sendSpy, transport };
}
function getSentPayload(spy: ReturnType<typeof vi.fn>): HawkJavaScriptEvent | null {
const calls = spy.mock.calls;
return calls.length ? calls[calls.length - 1][0].payload : null;
}
/**
* Shared Catcher config — no breadcrumbs, no global handlers, fake transport
*/
function createCatcher(transport: Transport, beforeSend: NonNullable<ConstructorParameters<typeof Catcher>[0] extends object ? ConstructorParameters<typeof Catcher>[0]['beforeSend'] : never>) {
return new Catcher({
token: TEST_TOKEN,
disableGlobalErrorsHandling: true,
breadcrumbs: false,
transport,
beforeSend,
});
}
describe('beforeSend', () => {
let warnSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
});
afterEach(() => {
warnSpy.mockRestore();
});
it('should send event as-is when beforeSend returns it unchanged', async () => {
// Arrange
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport, (event) => event);
// Act
hawk.send(new Error('hello'));
await wait();
// Assert
expect(sendSpy).toHaveBeenCalledOnce();
expect(getSentPayload(sendSpy)!.title).toBe('hello');
});
it('should send modified event when beforeSend mutates and returns it', async () => {
// Arrange
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport, (event) => {
event.context = { sanitized: true };
return event;
});
// Act
hawk.send(new Error('modify'));
await wait();
// Assert
expect(sendSpy).toHaveBeenCalledOnce();
expect(getSentPayload(sendSpy)!.context).toEqual({ sanitized: true });
});
it('should not send event when beforeSend returns false', async () => {
// Arrange
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport, () => false);
// Act
hawk.send(new Error('drop'));
await wait();
// Assert
expect(sendSpy).not.toHaveBeenCalled();
});
it.each([
{ label: 'undefined', value: undefined },
{ label: 'null', value: null },
{ label: 'number (42)', value: 42 },
{ label: 'string ("oops")', value: 'oops' },
{ label: 'true', value: true },
])('should send original event and warn when beforeSend returns $label', async ({ value }) => {
// Arrange
const { sendSpy, transport } = createTransport();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const hawk = createCatcher(transport, () => value as any);
// Act
hawk.send(new Error('invalid'));
await wait();
// Assert
expect(sendSpy).toHaveBeenCalledOnce();
expect(getSentPayload(sendSpy)!.title).toBe('invalid');
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid beforeSend value'),
expect.anything(),
expect.anything()
);
});
it('should send original event and warn when beforeSend deletes required field (title)', async () => {
// Arrange
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport, (event) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (event as any).title;
return event;
});
// Act
hawk.send(new Error('required-field'));
await wait();
// Assert — fallback to original payload, title preserved
expect(sendSpy).toHaveBeenCalledOnce();
expect(getSentPayload(sendSpy)!.title).toBe('required-field');
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('Invalid beforeSend value'),
expect.anything(),
expect.anything()
);
});
it('should still send event when structuredClone throws (non-cloneable payload)', async () => {
// Arrange
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport, (event) => event);
const cloneSpy = vi.spyOn(globalThis, 'structuredClone').mockImplementation(() => {
throw new DOMException('could not be cloned', 'DataCloneError');
});
// Act
hawk.send(new Error('non-cloneable'));
await wait();
// Assert — event is still sent, reporting didn't crash
expect(sendSpy).toHaveBeenCalledOnce();
expect(getSentPayload(sendSpy)!.title).toBe('non-cloneable');
cloneSpy.mockRestore();
});
it('should send event without deleted optional fields', async () => {
// Arrange
const { sendSpy, transport } = createTransport();
const hawk = createCatcher(transport, (event) => {
delete event.release;
return event;
});
// Act
hawk.send(new Error('optional'));
await wait();
// Assert
expect(sendSpy).toHaveBeenCalledOnce();
expect(getSentPayload(sendSpy)!.release).toBeUndefined();
});
});