Skip to content

Commit 1cc66ab

Browse files
authored
fix(shared): Fix memory leaks in react native environment (#6485)
1 parent 79b926c commit 1cc66ab

File tree

4 files changed

+52
-7
lines changed

4 files changed

+52
-7
lines changed

.changeset/twenty-poems-push.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/shared': patch
3+
---
4+
5+
Fix a potential memory leak in `TelemetryCollector`

packages/shared/src/__tests__/telemetry.test.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,38 @@ describe('TelemetryCollector', () => {
150150
expect(fetchSpy).toHaveBeenCalled();
151151
});
152152

153+
test('events are isolated between batches (no shared buffer reference)', () => {
154+
const collector = new TelemetryCollector({
155+
maxBufferSize: 2,
156+
publishableKey: TEST_PK,
157+
});
158+
const capturedBatches: TelemetryEvent[] = [];
159+
160+
fetchSpy.mockImplementation((_, options) => {
161+
capturedBatches.push(JSON.parse(options.body).events);
162+
return Promise.resolve({ ok: true });
163+
});
164+
165+
// First batch
166+
collector.record({ event: 'A', payload: { id: 1 } });
167+
collector.record({ event: 'B', payload: { id: 2 } });
168+
169+
// Second batch
170+
collector.record({ event: 'C', payload: { id: 3 } });
171+
collector.record({ event: 'D', payload: { id: 4 } });
172+
173+
// If there's a closure leak, events might be mixed or duplicated
174+
expect(capturedBatches[0]).toEqual([
175+
expect.objectContaining({ event: 'A', payload: { id: 1 } }),
176+
expect.objectContaining({ event: 'B', payload: { id: 2 } }),
177+
]);
178+
179+
expect(capturedBatches[1]).toEqual([
180+
expect.objectContaining({ event: 'C', payload: { id: 3 } }),
181+
expect.objectContaining({ event: 'D', payload: { id: 4 } }),
182+
]);
183+
});
184+
153185
describe('with server-side sampling', () => {
154186
test('does not send events if the random seed does not exceed the event-specific sampling rate', async () => {
155187
windowSpy.mockImplementation(() => undefined);

packages/shared/src/telemetry/collector.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -213,21 +213,27 @@ export class TelemetryCollector implements TelemetryCollectorInterface {
213213
}
214214

215215
#flush(): void {
216+
// Capture the current buffer and clear it immediately to avoid closure references
217+
const eventsToSend = [...this.#buffer];
218+
this.#buffer = [];
219+
220+
this.#pendingFlush = null;
221+
222+
if (eventsToSend.length === 0) {
223+
return;
224+
}
225+
216226
fetch(new URL('/v1/event', this.#config.endpoint), {
217227
method: 'POST',
218228
// TODO: We send an array here with that idea that we can eventually send multiple events.
219229
body: JSON.stringify({
220-
events: this.#buffer,
230+
events: eventsToSend,
221231
}),
232+
keepalive: true,
222233
headers: {
223234
'Content-Type': 'application/json',
224235
},
225-
})
226-
.catch(() => void 0)
227-
.then(() => {
228-
this.#buffer = [];
229-
})
230-
.catch(() => void 0);
236+
}).catch(() => void 0);
231237
}
232238

233239
/**

packages/shared/src/telemetry/events/method-called.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { TelemetryEventRaw } from '@clerk/types';
22

33
const EVENT_METHOD_CALLED = 'METHOD_CALLED';
4+
const EVENT_SAMPLING_RATE = 0.1;
45

56
type EventMethodCalled = {
67
method: string;
@@ -15,6 +16,7 @@ export function eventMethodCalled(
1516
): TelemetryEventRaw<EventMethodCalled> {
1617
return {
1718
event: EVENT_METHOD_CALLED,
19+
eventSamplingRate: EVENT_SAMPLING_RATE,
1820
payload: {
1921
method,
2022
...payload,

0 commit comments

Comments
 (0)