Skip to content

Commit 596732e

Browse files
committed
feat(browser): Add unregisterOriginalCallbacks option to browserApiErrorsIntegration
1 parent 589d813 commit 596732e

File tree

3 files changed

+76
-2
lines changed
  • dev-packages/browser-integration-tests/suites/integrations/browserApiErrors/unregisterOriginalCallbacks
  • packages/browser/src/integrations

3 files changed

+76
-2
lines changed
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import * as Sentry from '@sentry/browser';
2+
3+
window.Sentry = Sentry;
4+
5+
const btn = document.getElementById('btn');
6+
7+
const myClickListener = () => {
8+
// eslint-disable-next-line no-console
9+
console.log('clicked');
10+
};
11+
12+
btn.addEventListener('click', myClickListener);
13+
14+
Sentry.init({
15+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
16+
integrations: [
17+
Sentry.browserApiErrorsIntegration({
18+
unregisterOriginalCallbacks: true,
19+
}),
20+
],
21+
});
22+
23+
btn.addEventListener('click', myClickListener);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { expect } from '@playwright/test';
2+
import { sentryTest } from '../../../../utils/fixtures';
3+
4+
/**
5+
* By setting `unregisterOriginalCallbacks` to `true`, we can avoid the issue of double-invocations
6+
* (see other test for more details).
7+
*/
8+
sentryTest(
9+
'causes listeners to be invoked twice if registered before and after Sentry initialization',
10+
async ({ getLocalTestUrl, page }) => {
11+
const consoleLogs: string[] = [];
12+
page.on('console', msg => {
13+
consoleLogs.push(msg.text());
14+
});
15+
16+
await page.goto(await getLocalTestUrl({ testDir: __dirname }));
17+
18+
await page.waitForFunction('window.Sentry');
19+
20+
await page.locator('#btn').click();
21+
22+
expect(consoleLogs).toHaveLength(1);
23+
expect(consoleLogs).toEqual(['clicked']);
24+
},
25+
);

packages/browser/src/integrations/browserapierrors.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { IntegrationFn, WrappedFunction } from '@sentry/core';
22
import { defineIntegration, fill, getFunctionName, getOriginalFunction } from '@sentry/core';
33
import { WINDOW, wrap } from '../helpers';
4+
import { __propKey } from 'tslib';
45

56
const DEFAULT_EVENT_TARGET = [
67
'EventTarget',
@@ -46,6 +47,15 @@ interface BrowserApiErrorsOptions {
4647
requestAnimationFrame: boolean;
4748
XMLHttpRequest: boolean;
4849
eventTarget: boolean | string[];
50+
51+
/**
52+
* If you experience issues with this integration causing double-invocations of event listeners,
53+
* try setting this option to `true`. It will unregister the original callbacks from the event targets
54+
* before adding the instrumented callback.
55+
*
56+
* @default false
57+
*/
58+
unregisterOriginalCallbacks: boolean;
4959
}
5060

5161
const _browserApiErrorsIntegration = ((options: Partial<BrowserApiErrorsOptions> = {}) => {
@@ -55,6 +65,7 @@ const _browserApiErrorsIntegration = ((options: Partial<BrowserApiErrorsOptions>
5565
requestAnimationFrame: true,
5666
setInterval: true,
5767
setTimeout: true,
68+
unregisterOriginalCallbacks: false,
5869
...options,
5970
};
6071

@@ -82,7 +93,7 @@ const _browserApiErrorsIntegration = ((options: Partial<BrowserApiErrorsOptions>
8293
const eventTargetOption = _options.eventTarget;
8394
if (eventTargetOption) {
8495
const eventTarget = Array.isArray(eventTargetOption) ? eventTargetOption : DEFAULT_EVENT_TARGET;
85-
eventTarget.forEach(_wrapEventTarget);
96+
eventTarget.forEach(target => _wrapEventTarget(target, _options));
8697
}
8798
},
8899
};
@@ -160,7 +171,7 @@ function _wrapXHR(originalSend: () => void): () => void {
160171
};
161172
}
162173

163-
function _wrapEventTarget(target: string): void {
174+
function _wrapEventTarget(target: string, integrationOptions: BrowserApiErrorsOptions): void {
164175
const globalObject = WINDOW as unknown as Record<string, { prototype?: object }>;
165176
const proto = globalObject[target]?.prototype;
166177

@@ -197,6 +208,10 @@ function _wrapEventTarget(target: string): void {
197208
// can sometimes get 'Permission denied to access property "handle Event'
198209
}
199210

211+
if (integrationOptions.unregisterOriginalCallbacks) {
212+
unregisterOriginalCallback(this, eventName, fn);
213+
}
214+
200215
return original.apply(this, [
201216
eventName,
202217
wrap(fn, {
@@ -253,3 +268,14 @@ function _wrapEventTarget(target: string): void {
253268
function isEventListenerObject(obj: unknown): obj is EventListenerObject {
254269
return typeof (obj as EventListenerObject).handleEvent === 'function';
255270
}
271+
272+
function unregisterOriginalCallback(target: unknown, eventName: string, fn: EventListenerOrEventListenerObject): void {
273+
if (
274+
target &&
275+
typeof target === 'object' &&
276+
'removeEventListener' in target &&
277+
typeof target.removeEventListener === 'function'
278+
) {
279+
target.removeEventListener(eventName, fn);
280+
}
281+
}

0 commit comments

Comments
 (0)