Skip to content

Commit 95edc3c

Browse files
authored
fix: Correctly remove all event listeners (#2725)
1 parent 3f12c54 commit 95edc3c

File tree

3 files changed

+48
-15
lines changed

3 files changed

+48
-15
lines changed

packages/browser/src/integrations/trycatch.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -174,11 +174,34 @@ export class TryCatch implements Integration {
174174
fn: EventListenerObject,
175175
options?: boolean | EventListenerOptions,
176176
): () => void {
177-
let callback = (fn as any) as WrappedFunction;
177+
/**
178+
* There are 2 possible scenarios here:
179+
*
180+
* 1. Someone passes a callback, which was attached prior to Sentry initialization, or by using unmodified
181+
* method, eg. `document.addEventListener.call(el, name, handler). In this case, we treat this function
182+
* as a pass-through, and call original `removeEventListener` with it.
183+
*
184+
* 2. Someone passes a callback, which was attached after Sentry was initialized, which means that it was using
185+
* our wrapped version of `addEventListener`, which internally calls `wrap` helper.
186+
* This helper "wraps" whole callback inside a try/catch statement, and attached appropriate metadata to it,
187+
* in order for us to make a distinction between wrapped/non-wrapped functions possible.
188+
* If a function has `__sentry__` property, it means that it was wrapped, and it has additional property
189+
* of `__sentry__original__`, holding the handler. And this original handler, has a reversed link,
190+
* with `__sentry_wrapped__` property, which holds the wrapped version.
191+
*
192+
* When someone adds a handler prior to initialization, and then do it again, but after,
193+
* then we have to detach both of them. Otherwise, if we'd detach only wrapped one, it'd be impossible
194+
* to get rid of the initial handler and it'd stick there forever.
195+
* In case of second scenario, `__sentry_original__` refers to initial handler, and passed function
196+
* is a wrapped version.
197+
*/
198+
const callback = (fn as any) as WrappedFunction;
178199
try {
179-
callback = callback && (callback.__sentry_wrapped__ || callback);
200+
if (callback && callback.__sentry__) {
201+
original.call(this, eventName, callback.__sentry_original__, options);
202+
}
180203
} catch (e) {
181-
// ignore, accessing __sentry_wrapped__ will throw in some Selenium environments
204+
// ignore, accessing __sentry__ will throw in some Selenium environments
182205
}
183206
return original.call(this, eventName, callback, options);
184207
};

packages/browser/test/integration/common/init.js

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,7 @@
33
// - make assertions re: wrapped functions
44
var originalBuiltIns = {
55
setTimeout: setTimeout,
6-
setInterval: setInterval,
7-
requestAnimationFrame: requestAnimationFrame,
8-
xhrProtoOpen: XMLHttpRequest.prototype.open,
9-
headAddEventListener: document.head.addEventListener, // use <head> 'cause body isn't closed yet
10-
headRemoveEventListener: document.head.removeEventListener,
11-
consoleDebug: console.debug,
12-
consoleInfo: console.info,
13-
consoleWarn: console.warn,
14-
consoleError: console.error,
15-
consoleLog: console.log,
6+
addEventListener: document.addEventListener,
167
};
178

189
var events = [];

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

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,13 @@ describe("wrapped built-ins", function() {
2727
return runInSandbox(sandbox, function() {
2828
var div = document.createElement("div");
2929
document.body.appendChild(div);
30-
var click = new MouseEvent("click");
3130
var fooFn = function() {
3231
foo();
3332
};
3433
var barFn = function() {
3534
bar();
3635
};
37-
div.addEventListener("click", fooFn, false);
36+
div.addEventListener("click", fooFn);
3837
div.addEventListener("click", barFn);
3938
div.removeEventListener("click", barFn);
4039
div.dispatchEvent(new MouseEvent("click"));
@@ -43,6 +42,26 @@ describe("wrapped built-ins", function() {
4342
});
4443
});
4544

45+
it("should remove the original callback if it was registered before Sentry initialized (w. original method)", function() {
46+
return runInSandbox(sandbox, function() {
47+
var div = document.createElement("div");
48+
document.body.appendChild(div);
49+
window.capturedCall = false;
50+
var captureFn = function() {
51+
window.capturedCall = true;
52+
};
53+
// Use original addEventListener to simulate non-wrapped behavior (callback is attached without __sentry_wrapped__)
54+
window.originalBuiltIns.addEventListener.call(div, "click", captureFn);
55+
// Then attach the same callback again, but with already wrapped method
56+
div.addEventListener("click", captureFn);
57+
div.removeEventListener("click", captureFn);
58+
div.dispatchEvent(new MouseEvent("click"));
59+
}).then(function(summary) {
60+
assert.equal(summary.window.capturedCall, false);
61+
delete summary.window.capturedCalls;
62+
});
63+
});
64+
4665
it("should capture exceptions inside setTimeout", function() {
4766
return runInSandbox(sandbox, function() {
4867
setTimeout(function() {

0 commit comments

Comments
 (0)