Skip to content

Commit 2787643

Browse files
authored
feat(utils): Refactor addInstrumentationHandler to dedicated methods (#9542)
This is mostly an internal utility, but changes a bit. The previous implementation for adding instrumentation had a few issues: * It was impossible to tree shake. So even if some instrumentation is never used, the whole code for it is always included. Splitting this up opens up future improvements there as well. * It was not type safe, and actually there _were_ some subtle issues in the code because of this - things could fail in unexpected ways, as we have been doing a lot of type casting from/to any etc. This PR splits up `addInstrumentationHandler` into e.g. `addFetchInstrumentationHandler` etc. methods, which improves the actual type safety a lot, because currently we've been duplicating the types everywhere, sometimes in slightly differing ways, leading to potential issues. **We had a bunch of issues with xhr & fetch data in Replay recently, so I figured it would finally be a good time to clean this up so we can ensure that the data we get is consistent & reliable.** Hopefully this change will prevent issues like we have/had in replay from popping up in the future. Some additional notes: * For the dom instrumentation, I picked the name `addClickKeypressInstrumentationHandler()`. I'm open for other naming ideas, but I found `addDomInstrumentation` a bit too broad, as this could mean a lot of things - and we are strictly only instrumenting clicks and keypresses here. Another name (if we want to be a bit broader) could be e.g. `addDomEventInstrumentation` or something like this? * I updated the type for the XHR data handler to make the method & url required. This was previously optional (but not in some of the code, where we expected this to always be), plus we also did not handle the case that `url` can be a `URL` instead of a string, which we now convert. * On the XHR data, we have one field `args` that 1. is not really used and 2. was actually "buggy", as we always returned the args, but for `open` and `send` the args are very different things (but we pretended it's always the "open" args). Now, this is actually always the method & url, and I also deprecated this for v8. * On the XHR data, we also had an unused/unset `data` field, which I removed (this was not set anywhere) * All old code should still work normally, but I deprecated the generic `addInstrumentationHandler()` method, for removal in v8. * I also took the opportunity and split the instrumentations into dedicated files, previously this was one mega-file which made it harder to reason about things.
1 parent c99b2ae commit 2787643

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+1333
-1099
lines changed

packages/browser/src/integrations/breadcrumbs.ts

+30-20
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1-
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
21
/* eslint-disable max-lines */
32
import { getCurrentHub } from '@sentry/core';
4-
import type { Event as SentryEvent, HandlerDataFetch, HandlerDataXhr, Integration } from '@sentry/types';
3+
import type {
4+
Event as SentryEvent,
5+
HandlerDataConsole,
6+
HandlerDataDom,
7+
HandlerDataFetch,
8+
HandlerDataHistory,
9+
HandlerDataXhr,
10+
Integration,
11+
} from '@sentry/types';
512
import type {
613
FetchBreadcrumbData,
714
FetchBreadcrumbHint,
815
XhrBreadcrumbData,
916
XhrBreadcrumbHint,
1017
} from '@sentry/types/build/types/breadcrumb';
1118
import {
12-
addInstrumentationHandler,
19+
addClickKeypressInstrumentationHandler,
20+
addConsoleInstrumentationHandler,
21+
addFetchInstrumentationHandler,
22+
addHistoryInstrumentationHandler,
23+
addXhrInstrumentationHandler,
1324
getEventDescription,
1425
htmlTreeAsString,
1526
logger,
@@ -22,8 +33,6 @@ import {
2233
import { getClient } from '../exports';
2334
import { WINDOW } from '../helpers';
2435

25-
type HandlerData = Record<string, unknown>;
26-
2736
/** JSDoc */
2837
interface BreadcrumbsOptions {
2938
console: boolean;
@@ -89,19 +98,19 @@ export class Breadcrumbs implements Integration {
8998
*/
9099
public setupOnce(): void {
91100
if (this.options.console) {
92-
addInstrumentationHandler('console', _consoleBreadcrumb);
101+
addConsoleInstrumentationHandler(_consoleBreadcrumb);
93102
}
94103
if (this.options.dom) {
95-
addInstrumentationHandler('dom', _domBreadcrumb(this.options.dom));
104+
addClickKeypressInstrumentationHandler(_domBreadcrumb(this.options.dom));
96105
}
97106
if (this.options.xhr) {
98-
addInstrumentationHandler('xhr', _xhrBreadcrumb);
107+
addXhrInstrumentationHandler(_xhrBreadcrumb);
99108
}
100109
if (this.options.fetch) {
101-
addInstrumentationHandler('fetch', _fetchBreadcrumb);
110+
addFetchInstrumentationHandler(_fetchBreadcrumb);
102111
}
103112
if (this.options.history) {
104-
addInstrumentationHandler('history', _historyBreadcrumb);
113+
addHistoryInstrumentationHandler(_historyBreadcrumb);
105114
}
106115
if (this.options.sentry) {
107116
const client = getClient();
@@ -131,8 +140,8 @@ function addSentryBreadcrumb(event: SentryEvent): void {
131140
* A HOC that creaes a function that creates breadcrumbs from DOM API calls.
132141
* This is a HOC so that we get access to dom options in the closure.
133142
*/
134-
function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerData) => void {
135-
function _innerDomBreadcrumb(handlerData: HandlerData): void {
143+
function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDataDom) => void {
144+
function _innerDomBreadcrumb(handlerData: HandlerDataDom): void {
136145
let target;
137146
let keyAttrs = typeof dom === 'object' ? dom.serializeAttribute : undefined;
138147

@@ -183,7 +192,7 @@ function _domBreadcrumb(dom: BreadcrumbsOptions['dom']): (handlerData: HandlerDa
183192
/**
184193
* Creates breadcrumbs from console API calls
185194
*/
186-
function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level: string }): void {
195+
function _consoleBreadcrumb(handlerData: HandlerDataConsole): void {
187196
const breadcrumb = {
188197
category: 'console',
189198
data: {
@@ -213,7 +222,7 @@ function _consoleBreadcrumb(handlerData: HandlerData & { args: unknown[]; level:
213222
/**
214223
* Creates breadcrumbs from XHR API calls
215224
*/
216-
function _xhrBreadcrumb(handlerData: HandlerData & HandlerDataXhr): void {
225+
function _xhrBreadcrumb(handlerData: HandlerDataXhr): void {
217226
const { startTimestamp, endTimestamp } = handlerData;
218227

219228
const sentryXhrData = handlerData.xhr[SENTRY_XHR_DATA_KEY];
@@ -251,7 +260,7 @@ function _xhrBreadcrumb(handlerData: HandlerData & HandlerDataXhr): void {
251260
/**
252261
* Creates breadcrumbs from fetch API calls
253262
*/
254-
function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch & { response?: Response }): void {
263+
function _fetchBreadcrumb(handlerData: HandlerDataFetch): void {
255264
const { startTimestamp, endTimestamp } = handlerData;
256265

257266
// We only capture complete fetch requests
@@ -283,13 +292,14 @@ function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch & { respon
283292
hint,
284293
);
285294
} else {
295+
const response = handlerData.response as Response | undefined;
286296
const data: FetchBreadcrumbData = {
287297
...handlerData.fetchData,
288-
status_code: handlerData.response && handlerData.response.status,
298+
status_code: response && response.status,
289299
};
290300
const hint: FetchBreadcrumbHint = {
291301
input: handlerData.args,
292-
response: handlerData.response,
302+
response,
293303
startTimestamp,
294304
endTimestamp,
295305
};
@@ -307,15 +317,15 @@ function _fetchBreadcrumb(handlerData: HandlerData & HandlerDataFetch & { respon
307317
/**
308318
* Creates breadcrumbs from history API calls
309319
*/
310-
function _historyBreadcrumb(handlerData: HandlerData & { from: string; to: string }): void {
320+
function _historyBreadcrumb(handlerData: HandlerDataHistory): void {
311321
let from: string | undefined = handlerData.from;
312322
let to: string | undefined = handlerData.to;
313323
const parsedLoc = parseUrl(WINDOW.location.href);
314-
let parsedFrom = parseUrl(from);
324+
let parsedFrom = from ? parseUrl(from) : undefined;
315325
const parsedTo = parseUrl(to);
316326

317327
// Initial pushState doesn't provide `from` information
318-
if (!parsedFrom.path) {
328+
if (!parsedFrom || !parsedFrom.path) {
319329
parsedFrom = parsedLoc;
320330
}
321331

packages/browser/src/integrations/globalhandlers.ts

+90-81
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
22
import { getCurrentHub } from '@sentry/core';
33
import type { Event, Hub, Integration, Primitive, StackParser } from '@sentry/types';
4-
import { addInstrumentationHandler, getLocationHref, isErrorEvent, isPrimitive, isString, logger } from '@sentry/utils';
4+
import {
5+
addGlobalErrorInstrumentationHandler,
6+
addGlobalUnhandledRejectionInstrumentationHandler,
7+
getLocationHref,
8+
isErrorEvent,
9+
isPrimitive,
10+
isString,
11+
logger,
12+
} from '@sentry/utils';
513

614
import type { BrowserClient } from '../client';
715
import { eventFromUnknownInput } from '../eventbuilder';
@@ -68,96 +76,97 @@ export class GlobalHandlers implements Integration {
6876
}
6977
}
7078

71-
/** JSDoc */
7279
function _installGlobalOnErrorHandler(): void {
73-
addInstrumentationHandler(
74-
'error',
75-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
76-
(data: { msg: any; url: any; line: any; column: any; error: any }) => {
77-
const [hub, stackParser, attachStacktrace] = getHubAndOptions();
78-
if (!hub.getIntegration(GlobalHandlers)) {
79-
return;
80-
}
81-
const { msg, url, line, column, error } = data;
82-
if (shouldIgnoreOnError() || (error && error.__sentry_own_request__)) {
83-
return;
84-
}
80+
addGlobalErrorInstrumentationHandler(data => {
81+
const [hub, stackParser, attachStacktrace] = getHubAndOptions();
82+
if (!hub.getIntegration(GlobalHandlers)) {
83+
return;
84+
}
85+
const { msg, url, line, column, error } = data;
86+
if (shouldIgnoreOnError()) {
87+
return;
88+
}
8589

86-
const event =
87-
error === undefined && isString(msg)
88-
? _eventFromIncompleteOnError(msg, url, line, column)
89-
: _enhanceEventWithInitialFrame(
90-
eventFromUnknownInput(stackParser, error || msg, undefined, attachStacktrace, false),
91-
url,
92-
line,
93-
column,
94-
);
95-
96-
event.level = 'error';
97-
98-
hub.captureEvent(event, {
99-
originalException: error,
100-
mechanism: {
101-
handled: false,
102-
type: 'onerror',
103-
},
104-
});
105-
},
106-
);
90+
const event =
91+
error === undefined && isString(msg)
92+
? _eventFromIncompleteOnError(msg, url, line, column)
93+
: _enhanceEventWithInitialFrame(
94+
eventFromUnknownInput(stackParser, error || msg, undefined, attachStacktrace, false),
95+
url,
96+
line,
97+
column,
98+
);
99+
100+
event.level = 'error';
101+
102+
hub.captureEvent(event, {
103+
originalException: error,
104+
mechanism: {
105+
handled: false,
106+
type: 'onerror',
107+
},
108+
});
109+
});
107110
}
108111

109-
/** JSDoc */
110112
function _installGlobalOnUnhandledRejectionHandler(): void {
111-
addInstrumentationHandler(
112-
'unhandledrejection',
113-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
114-
(e: any) => {
115-
const [hub, stackParser, attachStacktrace] = getHubAndOptions();
116-
if (!hub.getIntegration(GlobalHandlers)) {
117-
return;
118-
}
119-
let error = e;
120-
121-
// dig the object of the rejection out of known event types
122-
try {
123-
// PromiseRejectionEvents store the object of the rejection under 'reason'
124-
// see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
125-
if ('reason' in e) {
126-
error = e.reason;
127-
}
128-
// something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
129-
// to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
130-
// the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
131-
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
132-
// https://github.com/getsentry/sentry-javascript/issues/2380
133-
else if ('detail' in e && 'reason' in e.detail) {
134-
error = e.detail.reason;
135-
}
136-
} catch (_oO) {
137-
// no-empty
138-
}
113+
addGlobalUnhandledRejectionInstrumentationHandler(e => {
114+
const [hub, stackParser, attachStacktrace] = getHubAndOptions();
115+
if (!hub.getIntegration(GlobalHandlers)) {
116+
return;
117+
}
139118

140-
if (shouldIgnoreOnError() || (error && error.__sentry_own_request__)) {
141-
return true;
142-
}
119+
if (shouldIgnoreOnError()) {
120+
return true;
121+
}
143122

144-
const event = isPrimitive(error)
145-
? _eventFromRejectionWithPrimitive(error)
146-
: eventFromUnknownInput(stackParser, error, undefined, attachStacktrace, true);
123+
const error = _getUnhandledRejectionError(e as unknown);
147124

148-
event.level = 'error';
125+
const event = isPrimitive(error)
126+
? _eventFromRejectionWithPrimitive(error)
127+
: eventFromUnknownInput(stackParser, error, undefined, attachStacktrace, true);
149128

150-
hub.captureEvent(event, {
151-
originalException: error,
152-
mechanism: {
153-
handled: false,
154-
type: 'onunhandledrejection',
155-
},
156-
});
129+
event.level = 'error';
157130

158-
return;
159-
},
160-
);
131+
hub.captureEvent(event, {
132+
originalException: error,
133+
mechanism: {
134+
handled: false,
135+
type: 'onunhandledrejection',
136+
},
137+
});
138+
139+
return;
140+
});
141+
}
142+
143+
function _getUnhandledRejectionError(error: unknown): unknown {
144+
if (isPrimitive(error)) {
145+
return error;
146+
}
147+
148+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
149+
const e = error as any;
150+
151+
// dig the object of the rejection out of known event types
152+
try {
153+
// PromiseRejectionEvents store the object of the rejection under 'reason'
154+
// see https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent
155+
if ('reason' in e) {
156+
return e.reason;
157+
}
158+
159+
// something, somewhere, (likely a browser extension) effectively casts PromiseRejectionEvents
160+
// to CustomEvents, moving the `promise` and `reason` attributes of the PRE into
161+
// the CustomEvent's `detail` attribute, since they're not part of CustomEvent's spec
162+
// see https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent and
163+
// https://github.com/getsentry/sentry-javascript/issues/2380
164+
else if ('detail' in e && 'reason' in e.detail) {
165+
return e.detail.reason;
166+
}
167+
} catch {} // eslint-disable-line no-empty
168+
169+
return error;
161170
}
162171

163172
/**

packages/browser/src/sdk.ts

+8-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,12 @@ import {
88
Integrations as CoreIntegrations,
99
} from '@sentry/core';
1010
import type { UserFeedback } from '@sentry/types';
11-
import { addInstrumentationHandler, logger, stackParserFromStackParserOptions, supportsFetch } from '@sentry/utils';
11+
import {
12+
addHistoryInstrumentationHandler,
13+
logger,
14+
stackParserFromStackParserOptions,
15+
supportsFetch,
16+
} from '@sentry/utils';
1217

1318
import type { BrowserClientOptions, BrowserOptions } from './client';
1419
import { BrowserClient } from './client';
@@ -255,9 +260,9 @@ function startSessionTracking(): void {
255260
startSessionOnHub(hub);
256261

257262
// We want to create a session for every navigation as well
258-
addInstrumentationHandler('history', ({ from, to }) => {
263+
addHistoryInstrumentationHandler(({ from, to }) => {
259264
// Don't create an additional session for the initial route or if the location did not change
260-
if (!(from === undefined || from === to)) {
265+
if (from !== undefined && from !== to) {
261266
startSessionOnHub(getCurrentHub());
262267
}
263268
});

packages/browser/test/integration/suites/onunhandledrejection.js

-20
Original file line numberDiff line numberDiff line change
@@ -243,24 +243,4 @@ describe('window.onunhandledrejection', function () {
243243
}
244244
});
245245
});
246-
247-
it('should skip our own failed requests that somehow bubbled-up to unhandledrejection handler', function () {
248-
return runInSandbox(sandbox, function () {
249-
if (supportsOnunhandledRejection()) {
250-
Promise.reject({
251-
__sentry_own_request__: true,
252-
});
253-
Promise.reject({
254-
__sentry_own_request__: false,
255-
});
256-
Promise.reject({});
257-
} else {
258-
window.resolveTest({ window: window });
259-
}
260-
}).then(function (summary) {
261-
if (summary.window.supportsOnunhandledRejection()) {
262-
assert.equal(summary.events.length, 2);
263-
}
264-
});
265-
});
266246
});

0 commit comments

Comments
 (0)