Skip to content

Commit ace0360

Browse files
committed
test(integrations): Add unit tests for ReportingObserver
1 parent ac0bc26 commit ace0360

File tree

2 files changed

+315
-16
lines changed

2 files changed

+315
-16
lines changed

packages/integrations/src/reportingobserver.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,29 @@
1-
/* eslint-disable @typescript-eslint/no-explicit-any */
21
import { EventProcessor, Hub, Integration } from '@sentry/types';
32
import { getGlobalObject, supportsReportingObserver } from '@sentry/utils';
43

5-
/** JSDoc */
64
interface Report {
7-
[key: string]: any;
5+
[key: string]: unknown;
86
type: ReportTypes;
97
url: string;
108
body?: ReportBody;
119
}
1210

13-
/** JSDoc */
1411
const enum ReportTypes {
15-
/** JSDoc */
1612
Crash = 'crash',
17-
/** JSDoc */
1813
Deprecation = 'deprecation',
19-
/** JSDoc */
2014
Intervention = 'intervention',
2115
}
2216

23-
/** JSDoc */
2417
type ReportBody = CrashReportBody | DeprecationReportBody | InterventionReportBody;
2518

26-
/** JSDoc */
2719
interface CrashReportBody {
28-
[key: string]: any;
20+
[key: string]: unknown;
2921
crashId: string;
3022
reason?: string;
3123
}
3224

33-
/** JSDoc */
3425
interface DeprecationReportBody {
35-
[key: string]: any;
26+
[key: string]: unknown;
3627
id: string;
3728
anticipatedRemoval?: Date;
3829
message: string;
@@ -41,9 +32,8 @@ interface DeprecationReportBody {
4132
columnNumber?: number;
4233
}
4334

44-
/** JSDoc */
4535
interface InterventionReportBody {
46-
[key: string]: any;
36+
[key: string]: unknown;
4737
id: string;
4838
message: string;
4939
sourceFile?: string;
@@ -89,7 +79,7 @@ export class ReportingObserver implements Integration {
8979

9080
this._getCurrentHub = getCurrentHub;
9181

92-
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
82+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
9383
const observer = new (getGlobalObject<any>().ReportingObserver)(this.handler.bind(this), {
9484
buffered: true,
9585
types: this._options.types,
@@ -117,7 +107,7 @@ export class ReportingObserver implements Integration {
117107
if (report.body) {
118108
// Object.keys doesn't work on ReportBody, as all properties are inheirted
119109
const plainBody: {
120-
[key: string]: any;
110+
[key: string]: unknown;
121111
} = {};
122112

123113
// eslint-disable-next-line guard-for-in
Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import { Integration } from '@sentry/types';
2+
3+
import { ReportingObserver, ReportTypes } from '../src/reportingobserver';
4+
5+
const mockScope = {
6+
setExtra: jest.fn(),
7+
};
8+
9+
const mockHub = {
10+
withScope: jest.fn(callback => {
11+
callback(mockScope);
12+
}),
13+
captureMessage: jest.fn(),
14+
};
15+
16+
const getMockHubWithIntegration = (integration: Integration) => ({
17+
...mockHub,
18+
getIntegration: jest.fn(() => integration),
19+
});
20+
21+
const mockReportingObserverConstructor = jest.fn();
22+
const mockObserve = jest.fn();
23+
24+
class MockReportingObserver {
25+
public observe: () => void = mockObserve;
26+
27+
constructor(callback: () => void, options: unknown) {
28+
mockReportingObserverConstructor(callback, options);
29+
}
30+
}
31+
32+
describe('ReportingObserver', () => {
33+
beforeEach(() => {
34+
(global as any).ReportingObserver = MockReportingObserver;
35+
});
36+
37+
afterEach(() => {
38+
jest.clearAllMocks();
39+
delete (global as any).ReportingObserver;
40+
});
41+
42+
describe('setup', () => {
43+
it('should abort gracefully and not do anything when ReportingObserbver is not available in the runtime', () => {
44+
// Act like ReportingObserver is unavailable
45+
delete (global as any).ReportingObserver;
46+
47+
const reportingObserverIntegration = new ReportingObserver();
48+
49+
expect(() => {
50+
reportingObserverIntegration.setupOnce(
51+
() => undefined,
52+
() => getMockHubWithIntegration(null) as any,
53+
);
54+
}).not.toThrow();
55+
56+
expect(mockReportingObserverConstructor).not.toHaveBeenCalled();
57+
expect(mockObserve).not.toHaveBeenCalled();
58+
});
59+
60+
it('should use default report types', () => {
61+
const reportingObserverIntegration = new ReportingObserver();
62+
reportingObserverIntegration.setupOnce(
63+
() => undefined,
64+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
65+
);
66+
67+
expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1);
68+
expect(mockReportingObserverConstructor).toHaveBeenCalledWith(
69+
expect.anything(),
70+
expect.objectContaining({ types: ['crash', 'deprecation', 'intervention'] }),
71+
);
72+
});
73+
74+
it('should use user-provided report types', () => {
75+
const reportingObserverIntegration = new ReportingObserver({ types: [ReportTypes.Crash] });
76+
reportingObserverIntegration.setupOnce(
77+
() => undefined,
78+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
79+
);
80+
81+
expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1);
82+
expect(mockReportingObserverConstructor).toHaveBeenCalledWith(
83+
expect.anything(),
84+
expect.objectContaining({ types: ['crash'] }),
85+
);
86+
});
87+
88+
it('should use `buffered` option', () => {
89+
const reportingObserverIntegration = new ReportingObserver();
90+
reportingObserverIntegration.setupOnce(
91+
() => undefined,
92+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
93+
);
94+
95+
expect(mockReportingObserverConstructor).toHaveBeenCalledTimes(1);
96+
expect(mockReportingObserverConstructor).toHaveBeenCalledWith(
97+
expect.anything(),
98+
expect.objectContaining({ buffered: true }),
99+
);
100+
});
101+
102+
it('should call `observe` function', () => {
103+
const reportingObserverIntegration = new ReportingObserver();
104+
reportingObserverIntegration.setupOnce(
105+
() => undefined,
106+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
107+
);
108+
109+
expect(mockObserve).toHaveBeenCalledTimes(1);
110+
});
111+
});
112+
113+
describe('handler', () => {
114+
it('should abort gracefully and not do anything when integration is not installed', () => {
115+
const reportingObserverIntegration = new ReportingObserver();
116+
reportingObserverIntegration.setupOnce(
117+
() => undefined,
118+
() => getMockHubWithIntegration(null) as any,
119+
);
120+
121+
expect(() => {
122+
reportingObserverIntegration.handler([{ type: ReportTypes.Crash, url: 'some url' }]);
123+
}).not.toThrow();
124+
125+
expect(mockHub.captureMessage).not.toHaveBeenCalled();
126+
});
127+
128+
it('should capture messages', () => {
129+
const reportingObserverIntegration = new ReportingObserver();
130+
reportingObserverIntegration.setupOnce(
131+
() => undefined,
132+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
133+
);
134+
135+
reportingObserverIntegration.handler([
136+
{ type: ReportTypes.Crash, url: 'some url' },
137+
{ type: ReportTypes.Deprecation, url: 'some url' },
138+
]);
139+
140+
expect(mockHub.captureMessage).toHaveBeenCalledTimes(2);
141+
});
142+
143+
it('should set extra including the url of a report', () => {
144+
const reportingObserverIntegration = new ReportingObserver();
145+
reportingObserverIntegration.setupOnce(
146+
() => undefined,
147+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
148+
);
149+
150+
reportingObserverIntegration.handler([
151+
{ type: ReportTypes.Crash, url: 'some url 1' },
152+
{ type: ReportTypes.Deprecation, url: 'some url 2' },
153+
]);
154+
155+
expect(mockScope.setExtra).toHaveBeenCalledWith('url', 'some url 1');
156+
expect(mockScope.setExtra).toHaveBeenCalledWith('url', 'some url 2');
157+
});
158+
159+
it('should set extra including the report body if available', () => {
160+
const reportingObserverIntegration = new ReportingObserver();
161+
reportingObserverIntegration.setupOnce(
162+
() => undefined,
163+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
164+
);
165+
166+
const report1 = { type: ReportTypes.Crash, url: 'some url 1', body: { crashId: 'id1' } };
167+
const report2 = { type: ReportTypes.Deprecation, url: 'some url 2', body: { id: 'id2', message: 'message' } };
168+
169+
reportingObserverIntegration.handler([report1, report2]);
170+
171+
expect(mockScope.setExtra).toHaveBeenCalledWith('body', report1.body);
172+
expect(mockScope.setExtra).toHaveBeenCalledWith('body', report2.body);
173+
});
174+
175+
it('should not set extra report body extra when no body is set', () => {
176+
const reportingObserverIntegration = new ReportingObserver();
177+
reportingObserverIntegration.setupOnce(
178+
() => undefined,
179+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
180+
);
181+
182+
reportingObserverIntegration.handler([{ type: ReportTypes.Crash, url: 'some url' }]);
183+
184+
expect(mockScope.setExtra).not.toHaveBeenCalledWith('body', expect.anything());
185+
});
186+
187+
it('should capture report details from body on crash report', () => {
188+
const reportingObserverIntegration = new ReportingObserver();
189+
reportingObserverIntegration.setupOnce(
190+
() => undefined,
191+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
192+
);
193+
194+
const report = {
195+
type: ReportTypes.Crash,
196+
url: 'some url',
197+
body: { crashId: 'some id', reason: 'some reason' },
198+
};
199+
reportingObserverIntegration.handler([report]);
200+
201+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type));
202+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.crashId));
203+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.reason));
204+
});
205+
206+
it('should capture report message from body on deprecation report', () => {
207+
const reportingObserverIntegration = new ReportingObserver();
208+
reportingObserverIntegration.setupOnce(
209+
() => undefined,
210+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
211+
);
212+
213+
const report = {
214+
type: ReportTypes.Deprecation,
215+
url: 'some url',
216+
body: { id: 'some id', message: 'some message' },
217+
};
218+
reportingObserverIntegration.handler([report]);
219+
220+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type));
221+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message));
222+
});
223+
224+
it('should capture report message from body on intervention report', () => {
225+
const reportingObserverIntegration = new ReportingObserver();
226+
reportingObserverIntegration.setupOnce(
227+
() => undefined,
228+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
229+
);
230+
231+
const report = {
232+
type: ReportTypes.Intervention,
233+
url: 'some url',
234+
body: { id: 'some id', message: 'some message' },
235+
};
236+
reportingObserverIntegration.handler([report]);
237+
238+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type));
239+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.body.message));
240+
});
241+
242+
it('should use fallback message when no body is available', () => {
243+
const reportingObserverIntegration = new ReportingObserver();
244+
reportingObserverIntegration.setupOnce(
245+
() => undefined,
246+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
247+
);
248+
249+
const report = {
250+
type: ReportTypes.Intervention,
251+
url: 'some url',
252+
};
253+
reportingObserverIntegration.handler([report]);
254+
255+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type));
256+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available'));
257+
});
258+
259+
it('should use fallback message when no body details are available for crash report', () => {
260+
const reportingObserverIntegration = new ReportingObserver();
261+
reportingObserverIntegration.setupOnce(
262+
() => undefined,
263+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
264+
);
265+
266+
const report = { type: ReportTypes.Crash, url: 'some url', body: { crashId: '', reason: '' } };
267+
reportingObserverIntegration.handler([report]);
268+
269+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type));
270+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available'));
271+
});
272+
273+
it('should use fallback message when no body message is available for deprecation report', () => {
274+
const reportingObserverIntegration = new ReportingObserver();
275+
reportingObserverIntegration.setupOnce(
276+
() => undefined,
277+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
278+
);
279+
280+
const report = {
281+
type: ReportTypes.Deprecation,
282+
url: 'some url',
283+
body: { id: 'some id', message: '' },
284+
};
285+
reportingObserverIntegration.handler([report]);
286+
287+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type));
288+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available'));
289+
});
290+
291+
it('should use fallback message when no body message is available for intervention report', () => {
292+
const reportingObserverIntegration = new ReportingObserver();
293+
reportingObserverIntegration.setupOnce(
294+
() => undefined,
295+
() => getMockHubWithIntegration(reportingObserverIntegration) as any,
296+
);
297+
298+
const report = {
299+
type: ReportTypes.Intervention,
300+
url: 'some url',
301+
body: { id: 'some id', message: '' },
302+
};
303+
reportingObserverIntegration.handler([report]);
304+
305+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining(report.type));
306+
expect(mockHub.captureMessage).toHaveBeenCalledWith(expect.stringContaining('No details available'));
307+
});
308+
});
309+
});

0 commit comments

Comments
 (0)