diff --git a/sdk/__tests__/NetworkInterceptor.test.ts b/sdk/__tests__/NetworkInterceptor.test.ts index 1ee3f41..90a8165 100644 --- a/sdk/__tests__/NetworkInterceptor.test.ts +++ b/sdk/__tests__/NetworkInterceptor.test.ts @@ -32,6 +32,12 @@ class FakeXMLHttpRequest { } } + removeEventListener(type: string, listener: () => void) { + if (type === 'loadend') { + this.listeners = this.listeners.filter(existing => existing !== listener); + } + } + complete() { this.readyState = this.DONE; this.listeners.forEach(listener => listener()); @@ -111,6 +117,21 @@ describe('NetworkInterceptor', () => { expect(xhr.listenedTo).not.toContain('progress'); }); + it("doesn't keep a response listener per request when an XMLHttpRequest is reused", () => { + const callbacks = createCallbacks(); + installNetworkInterceptor(callbacks); + + const xhr = new XMLHttpRequest() as unknown as FakeXMLHttpRequest; + + for (let i = 0; i < 3; i++) { + xhr.open('GET', 'https://example.com/items'); + xhr.send(); + xhr.complete(); + } + + expect(callbacks.onResponse).toHaveBeenCalledTimes(3); + }); + it('still opens and sends the request when a callback throws', () => { installNetworkInterceptor({ onOpen: () => { diff --git a/sdk/__tests__/RealUserMonitor.test.ts b/sdk/__tests__/RealUserMonitor.test.ts index 7e69c77..f29deee 100644 --- a/sdk/__tests__/RealUserMonitor.test.ts +++ b/sdk/__tests__/RealUserMonitor.test.ts @@ -30,6 +30,12 @@ class FakeXMLHttpRequest { } } + removeEventListener(type: string, listener: () => void) { + if (type === 'loadend') { + this.listeners = this.listeners.filter(existing => existing !== listener); + } + } + complete() { this.readyState = this.DONE; this.listeners.forEach(listener => listener()); diff --git a/sdk/src/NetworkInterceptor.ts b/sdk/src/NetworkInterceptor.ts index fd67892..7abcc8e 100644 --- a/sdk/src/NetworkInterceptor.ts +++ b/sdk/src/NetworkInterceptor.ts @@ -68,8 +68,15 @@ const patchXMLHttpRequest = () => { if (active && fetchCallDepth === 0) { runSafely('send', () => active.onSend(this)); // loadend rather than readystatechange: in React Native, a readystatechange listener turns on incremental - // response updates from native, which completion alone doesn't need - this.addEventListener('loadend', () => runSafely('response', () => active.onResponse(this))); + // response updates from native, which completion alone doesn't need. The listener removes itself so that an + // XMLHttpRequest reused after abort() doesn't keep one per request; { once: true } can't be used, because React + // Native's XMLHttpRequest.addEventListener drops the options argument + const onLoadEnd = () => { + this.removeEventListener('loadend', onLoadEnd); + runSafely('response', () => active.onResponse(this)); + }; + + this.addEventListener('loadend', onLoadEnd); } return (send as (...sendArgs: any[]) => void).apply(this, args);