Skip to content
Merged
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ module.exports = [
path: createCDNPath('bundle.tracing.logs.metrics.min.js'),
gzip: false,
brotli: false,
limit: '130 KB',
limit: '131 KB',
},
{
name: 'CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed',
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import { safeMathRandom } from './utils/randomSafeContext';
import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span';
import { showSpanDropWarning } from './utils/spanUtils';
import { rejectedSyncPromise } from './utils/syncpromise';
import { safeUnref } from './utils/timer';
import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent';

const ALREADY_SEEN_ERROR = "Not capturing exception because it's already been captured.";
Expand Down Expand Up @@ -1150,9 +1151,8 @@ export abstract class Client<O extends ClientOptions = ClientOptions> {
protected async _isClientDoneProcessing(timeout?: number): Promise<boolean> {
let ticked = 0;

// if no timeout is provided, we wait "forever" until everything is processed
while (!timeout || ticked < timeout) {
await new Promise(resolve => setTimeout(resolve, 1));
await new Promise(resolve => safeUnref(setTimeout(resolve, 1)));

if (!this._numProcessing) {
return true;
Expand Down
8 changes: 5 additions & 3 deletions packages/core/src/utils/promisebuffer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { rejectedSyncPromise, resolvedSyncPromise } from './syncpromise';
import { safeUnref } from './timer';

export interface PromiseBuffer<T> {
// exposes the internal array so tests can assert on the state of it.
Expand Down Expand Up @@ -77,10 +78,11 @@ export function makePromiseBuffer<T>(limit: number = 100): PromiseBuffer<T> {
return drainPromise;
}

const promises = [drainPromise, new Promise<boolean>(resolve => setTimeout(() => resolve(false), timeout))];
const promises = [
drainPromise,
new Promise<boolean>(resolve => safeUnref(setTimeout(() => resolve(false), timeout))),
];

// Promise.race will resolve to the first promise that resolves or rejects
// So if the drainPromise resolves, the timeout promise will be ignored
return Promise.race(promises);
}

Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/utils/timer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* Calls `unref` on a timer, if the method is available on @param timer.
*
* `unref()` is used to allow processes to exit immediately, even if the timer
* is still running and hasn't resolved yet.
*
* Use this in places where code can run on browser or server, since browsers
* do not support `unref`.
*/
export function safeUnref(timer: ReturnType<typeof setTimeout>): ReturnType<typeof setTimeout> {
if (typeof timer === 'object' && typeof timer.unref === 'function') {
timer.unref();
}
return timer;
}
Comment thread
cursor[bot] marked this conversation as resolved.
47 changes: 47 additions & 0 deletions packages/core/test/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2205,6 +2205,53 @@ describe('Client', () => {
}),
]);
});

test('flush returns immediately when nothing is processing', async () => {
vi.useFakeTimers();
expect.assertions(2);

const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN });
const client = new TestClient(options);

// just to ensure the client init'd
vi.advanceTimersByTime(100);

const elapsed = Date.now();
const done = client.flush(1000).then(result => {
expect(result).toBe(true);
expect(Date.now() - elapsed).toBeLessThan(2);
});

// ensures that only after 1 ms, we're already done flushing
vi.advanceTimersByTime(1);
await done;
});

test('flush with early exit when processing completes', async () => {
vi.useRealTimers();
expect.assertions(3);

const { makeTransport, getSendCalled, getSentCount } = makeFakeTransport(50);

const client = new TestClient(
getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
enableSend: true,
transport: makeTransport,
}),
);

client.captureMessage('test');
expect(getSendCalled()).toEqual(1);

const startTime = Date.now();
await client.flush(5000);
const elapsed = Date.now() - startTime;

expect(getSentCount()).toEqual(1);
// if this flakes, remove the test
expect(elapsed).toBeLessThan(1000);
});
});

describe('sendEvent', () => {
Expand Down
12 changes: 12 additions & 0 deletions packages/core/test/lib/utils/promisebuffer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,4 +252,16 @@ describe('PromiseBuffer', () => {
expect(e).toEqual(new Error('whoops'));
}
});

test('drain returns immediately when buffer is empty', async () => {
const buffer = makePromiseBuffer();
expect(buffer.$.length).toEqual(0);

const startTime = Date.now();
const result = await buffer.drain(5000);
const elapsed = Date.now() - startTime;

expect(result).toBe(true);
expect(elapsed).toBeLessThan(100);
});
});
32 changes: 32 additions & 0 deletions packages/core/test/lib/utils/timer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { describe, expect, it, vi } from 'vitest';
import { safeUnref } from '../../../src/utils/timer';

describe('safeUnref', () => {
it('calls unref on a NodeJS timer', () => {
const timeout = setTimeout(() => {}, 1000);
const unrefSpy = vi.spyOn(timeout, 'unref');
safeUnref(timeout);
expect(unrefSpy).toHaveBeenCalledOnce();
});

it('returns the timer', () => {
const timeout = setTimeout(() => {}, 1000);
const result = safeUnref(timeout);
expect(result).toBe(timeout);
});

it('handles multiple unref calls', () => {
const timeout = setTimeout(() => {}, 1000);
const unrefSpy = vi.spyOn(timeout, 'unref');

const result = safeUnref(timeout);
result.unref();

expect(unrefSpy).toHaveBeenCalledTimes(2);
});

it("doesn't throw for a browser timer", () => {
const timer = safeUnref(385 as unknown as ReturnType<typeof setTimeout>);
expect(timer).toBe(385);
});
});
22 changes: 22 additions & 0 deletions packages/node-core/test/sdk/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -386,4 +386,26 @@ describe('NodeClient', () => {
expect(processOffSpy).toHaveBeenNthCalledWith(1, 'beforeExit', expect.any(Function));
});
});

describe('flush', () => {
it('flush returns immediately when nothing is processing', async () => {
const options = getDefaultNodeClientOptions();
const client = new NodeClient(options);

const startTime = Date.now();
const result = await client.flush(1000);
const elapsed = Date.now() - startTime;

expect(result).toBe(true);
expect(elapsed).toBeLessThan(100);
});

it('flush does not block process exit with unref timers', async () => {
const options = getDefaultNodeClientOptions();
const client = new NodeClient(options);

const result = await client.flush(5000);
expect(result).toBe(true);
});
});
});
Loading