Skip to content

Commit 68155d7

Browse files
sergicalclaude
authored andcommitted
fix(cloudflare, vercel-edge): Disable timer-based flush for serverless runtimes
The weight-based flushing mechanism for logs and metrics schedules a `setTimeout(fn, 5000)` after each capture. In Cloudflare Workers built with `@cloudflare/vite-plugin` (native ESM + `no_bundle: true`), workerd rejects this timer as running outside request context. Add a `_flushInterval` internal option to `ClientOptions`. When set to 0, the idle flush timer is skipped entirely. Size-based flushing (800KB) and explicit `flush()` calls (via `withSentry` → `flushAndDispose`) still work. Set `_flushInterval: 0` in `CloudflareClient` and `VercelEdgeClient`. Fixes #20888 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 2df6847 commit 68155d7

5 files changed

Lines changed: 87 additions & 12 deletions

File tree

packages/cloudflare/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ export class CloudflareClient extends ServerRuntimeClient {
3434
// TODO: Grab version information
3535
runtime: { name: 'cloudflare' },
3636
// TODO: Add server name
37+
_flushInterval: 0,
3738
};
3839

3940
super(clientOptions);

packages/core/src/client.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -137,18 +137,21 @@ function setupWeightBasedFlushing<
137137
if (weight >= 800_000) {
138138
flushFn(client);
139139
} else if (!isTimerActive) {
140-
// Only start timer if one isn't already running.
141-
// This prevents flushing being delayed by items that arrive close to the timeout limit
142-
// and thus resetting the flushing timeout and delaying items being flushed.
143-
isTimerActive = true;
144-
// Use safeUnref so the timer doesn't prevent the process from exiting
145-
flushTimeout = safeUnref(
146-
setTimeout(() => {
147-
flushFn(client);
148-
// Note: isTimerActive is reset by the flushHook handler above, not here,
149-
// to avoid race conditions when new items arrive during the flush.
150-
}, DEFAULT_FLUSH_INTERVAL),
151-
);
140+
const flushInterval = client.getOptions()._flushInterval ?? DEFAULT_FLUSH_INTERVAL;
141+
if (flushInterval > 0) {
142+
// Only start timer if one isn't already running.
143+
// This prevents flushing being delayed by items that arrive close to the timeout limit
144+
// and thus resetting the flushing timeout and delaying items being flushed.
145+
isTimerActive = true;
146+
// Use safeUnref so the timer doesn't prevent the process from exiting
147+
flushTimeout = safeUnref(
148+
setTimeout(() => {
149+
flushFn(client);
150+
// Note: isTimerActive is reset by the flushHook handler above, not here,
151+
// to avoid race conditions when new items arrive during the flush.
152+
}, flushInterval),
153+
);
154+
}
152155
}
153156
});
154157

packages/core/src/types/options.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,18 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
597597
*/
598598
enableMetrics?: boolean;
599599

600+
/**
601+
* Interval in ms for the idle flush timer used by logs and metrics.
602+
* Set to 0 to disable timer-based flushing entirely — useful for
603+
* serverless runtimes that forbid background timers (e.g. Cloudflare Workers).
604+
*
605+
* Size-based flushing and explicit `flush()` calls still work regardless.
606+
*
607+
* @default 5000
608+
* @internal
609+
*/
610+
_flushInterval?: number;
611+
600612
/**
601613
* An event-processing callback for metrics, guaranteed to be invoked after all other metric
602614
* processors. This allows a metric to be modified or dropped before it's sent.

packages/core/test/lib/client.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3245,6 +3245,64 @@ describe('Client', () => {
32453245

32463246
expect(flushMetricsHandler).toHaveBeenCalledTimes(1);
32473247
});
3248+
3249+
it('does not create a flush timer when _flushInterval is 0', () => {
3250+
const safeUnrefSpy = vi.spyOn(timerModule, 'safeUnref');
3251+
3252+
const options = getDefaultTestClientOptions({
3253+
dsn: PUBLIC_DSN,
3254+
_flushInterval: 0,
3255+
});
3256+
const client = new TestClient(options);
3257+
const scope = new Scope();
3258+
scope.setClient(client);
3259+
3260+
_INTERNAL_captureMetric({ name: 'test_metric', value: 42, type: 'counter', attributes: {} }, { scope });
3261+
3262+
expect(safeUnrefSpy).not.toHaveBeenCalled();
3263+
3264+
safeUnrefSpy.mockRestore();
3265+
});
3266+
3267+
it('still flushes metrics via flush event when _flushInterval is 0', () => {
3268+
const options = getDefaultTestClientOptions({
3269+
dsn: PUBLIC_DSN,
3270+
_flushInterval: 0,
3271+
});
3272+
const client = new TestClient(options);
3273+
const scope = new Scope();
3274+
scope.setClient(client);
3275+
3276+
const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
3277+
3278+
_INTERNAL_captureMetric({ name: 'metric1', value: 1, type: 'counter', attributes: {} }, { scope });
3279+
3280+
expect(sendEnvelopeSpy).not.toHaveBeenCalled();
3281+
3282+
client.emit('flush');
3283+
3284+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
3285+
});
3286+
3287+
it('still flushes metrics on size threshold when _flushInterval is 0', () => {
3288+
const options = getDefaultTestClientOptions({
3289+
dsn: PUBLIC_DSN,
3290+
_flushInterval: 0,
3291+
});
3292+
const client = new TestClient(options);
3293+
const scope = new Scope();
3294+
scope.setClient(client);
3295+
3296+
const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');
3297+
3298+
const largeValue = 'x'.repeat(400_000);
3299+
_INTERNAL_captureMetric(
3300+
{ name: 'large_metric', value: 1, type: 'counter', attributes: { large_value: largeValue } },
3301+
{ scope },
3302+
);
3303+
3304+
expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
3305+
});
32483306
});
32493307

32503308
describe('promise buffer usage', () => {

packages/vercel-edge/src/client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export class VercelEdgeClient extends ServerRuntimeClient<VercelEdgeClientOption
3030
// Use provided runtime or default to 'vercel-edge'
3131
runtime: options.runtime || { name: 'vercel-edge' },
3232
serverName: options.serverName || process.env.SENTRY_NAME,
33+
_flushInterval: 0,
3334
};
3435

3536
super(clientOptions);

0 commit comments

Comments
 (0)