Skip to content

Commit d6a90d2

Browse files
committed
remove envent listeners on client close.
1 parent 7241b13 commit d6a90d2

2 files changed

Lines changed: 66 additions & 32 deletions

File tree

‎packages/browser/src/client.ts‎

Lines changed: 47 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -138,40 +138,56 @@ export class BrowserClient extends Client<BrowserClientOptions> {
138138

139139
const { sendClientReports } = this._options;
140140

141-
// Flush buffered data when the page becomes hidden (e.g. tab switch, navigation, or the page
142-
// being discarded). `flush()` emits the `flush` hook, which drains the span (streaming), log and
143-
// metric buffers and hands the resulting envelopes to the transport (which uses `keepalive`).
144-
// Client report outcomes don't listen to the `flush` hook, so we flush them separately.
145-
if (WINDOW.document) {
146-
WINDOW.document.addEventListener('visibilitychange', () => {
147-
// Devices usually hide the page before they sleep and show it again after they wake up. Checking for drift
148-
// at both points makes the time origin correction happen at the sleep, rather than at the next regular timestamp call.
149-
timestampInSeconds();
150-
151-
if (WINDOW.document.visibilityState === 'hidden') {
152-
if (sendClientReports) {
153-
this._flushOutcomes();
141+
// Devices usually hide the page before they sleep and show it again after they wake up. Checking for drift
142+
// at both points makes the time origin correction happen at the sleep, rather than at the next regular timestamp call.
143+
const checkDrift = (): void => {
144+
timestampInSeconds();
145+
};
146+
147+
// Most bundle-size-efficient way to add and remove listeners: Create a table to avoid repeated
148+
// addeventlistener and removeeventlistener calls. The alternative, using an AbortController, can
149+
// lead to app breakage on old Angular<>Zone.js < 0.14.3 versions.
150+
const listeners: [EventTarget | undefined, string, () => void][] = [
151+
// Flush buffered data when the page becomes hidden (e.g. tab switch, navigation, or the page
152+
// being discarded). `flush()` emits the `flush` hook, which drains the span (streaming), log and
153+
// metric buffers and hands the resulting envelopes to the transport (which uses `keepalive`).
154+
// Client report outcomes don't listen to the `flush` hook, so we flush them separately.
155+
[
156+
WINDOW.document,
157+
'visibilitychange',
158+
() => {
159+
// Devices usually hide the page before they sleep and show it again after they wake up. Checking for drift
160+
// at both points makes the time origin correction happen at the sleep, rather than at the next regular timestamp call.
161+
checkDrift();
162+
163+
if (WINDOW.document.visibilityState === 'hidden') {
164+
if (sendClientReports) {
165+
this._flushOutcomes();
166+
}
167+
// Defer the flush to a microtask so that visibilitychange listeners registered after this
168+
// one have already run. In particular, browser tracing's background-tab detection ends the
169+
// active pageload/navigation (segment) span when the page is hidden. Deferring ensures that
170+
// segment span has been added to the streaming buffer before we flush, so it is sent
171+
// together with its child spans instead of being orphaned.
172+
queueMicrotask(() => {
173+
void this.flush();
174+
});
154175
}
155-
// Defer the flush to a microtask so that visibilitychange listeners registered after this
156-
// one have already run. In particular, browser tracing's background-tab detection ends the
157-
// active pageload/navigation (segment) span when the page is hidden. Deferring ensures that
158-
// segment span has been added to the streaming buffer before we flush, so it is sent
159-
// together with its child spans instead of being orphaned.
160-
queueMicrotask(() => {
161-
void this.flush();
162-
});
163-
}
164-
});
165-
176+
},
177+
],
166178
// `freeze` and `resume` are only fired by Chromium browsers. Other browsers never fire them, so these listeners
167179
// are no-ops there.
168-
WINDOW.document.addEventListener('freeze', () => timestampInSeconds());
169-
WINDOW.document.addEventListener('resume', () => timestampInSeconds());
170-
}
171-
172-
// Pages restored from the back/forward cache were paused while they were cached.
173-
WINDOW.addEventListener?.('pagehide', () => timestampInSeconds());
174-
WINDOW.addEventListener?.('pageshow', () => timestampInSeconds());
180+
[WINDOW.document, 'freeze', checkDrift],
181+
[WINDOW.document, 'resume', checkDrift],
182+
// Pages restored from the back/forward cache were paused while they were cached.
183+
[WINDOW, 'pagehide', checkDrift],
184+
[WINDOW, 'pageshow', checkDrift],
185+
];
186+
187+
listeners.forEach(([target, name, listener]) => target?.addEventListener?.(name, listener));
188+
this.on('close', () =>
189+
listeners.forEach(([target, name, listener]) => target?.removeEventListener?.(name, listener)),
190+
);
175191

176192
if (userInfo) {
177193
this.on('beforeSendSession', addAutoIpAddressToSession);

‎packages/browser/test/client.test.ts‎

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,10 @@ function setDocumentHidden(): void {
2929
describe('BrowserClient', () => {
3030
let client: BrowserClient;
3131

32-
afterEach(() => {
32+
afterEach(async () => {
3333
vi.useRealTimers();
3434
vi.clearAllMocks();
35+
await client?.close();
3536
});
3637

3738
it('flushes the client (spans, logs, metrics) when the page becomes hidden', async () => {
@@ -77,6 +78,23 @@ describe('BrowserClient', () => {
7778
expect(SentryCore.timestampInSeconds).toHaveBeenCalled();
7879
});
7980

81+
it('removes its page lifecycle listeners when closed', async () => {
82+
client = new BrowserClient(getDefaultBrowserClientOptions());
83+
await client.close();
84+
const flushSpy = vi.spyOn(client, 'flush');
85+
vi.mocked(SentryCore.timestampInSeconds).mockClear();
86+
87+
setDocumentHidden();
88+
WINDOW.document.dispatchEvent(new Event('freeze'));
89+
WINDOW.document.dispatchEvent(new Event('resume'));
90+
WINDOW.dispatchEvent(new Event('pagehide'));
91+
WINDOW.dispatchEvent(new Event('pageshow'));
92+
await Promise.resolve();
93+
94+
expect(SentryCore.timestampInSeconds).not.toHaveBeenCalled();
95+
expect(flushSpy).not.toHaveBeenCalled();
96+
});
97+
8098
it('does not flush outcomes when sendClientReports is disabled but still flushes the client', async () => {
8199
client = new BrowserClient(getDefaultBrowserClientOptions({ sendClientReports: false }));
82100
const flushSpy = vi.spyOn(client, 'flush').mockReturnValue(Promise.resolve(true) as any);

0 commit comments

Comments
 (0)