Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cloudflare/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class CloudflareClient extends ServerRuntimeClient {
// TODO: Grab version information
runtime: { name: 'cloudflare' },
// TODO: Add server name
_flushInterval: 0,
};

super(clientOptions);
Expand Down
27 changes: 15 additions & 12 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,18 +137,21 @@ function setupWeightBasedFlushing<
if (weight >= 800_000) {
flushFn(client);
} else if (!isTimerActive) {
// Only start timer if one isn't already running.
// This prevents flushing being delayed by items that arrive close to the timeout limit
// and thus resetting the flushing timeout and delaying items being flushed.
isTimerActive = true;
// Use safeUnref so the timer doesn't prevent the process from exiting
flushTimeout = safeUnref(
setTimeout(() => {
flushFn(client);
// Note: isTimerActive is reset by the flushHook handler above, not here,
// to avoid race conditions when new items arrive during the flush.
}, DEFAULT_FLUSH_INTERVAL),
);
const flushInterval = client.getOptions()._flushInterval ?? DEFAULT_FLUSH_INTERVAL;
if (flushInterval > 0) {
// Only start timer if one isn't already running.
// This prevents flushing being delayed by items that arrive close to the timeout limit
// and thus resetting the flushing timeout and delaying items being flushed.
isTimerActive = true;
// Use safeUnref so the timer doesn't prevent the process from exiting
flushTimeout = safeUnref(
setTimeout(() => {
flushFn(client);
// Note: isTimerActive is reset by the flushHook handler above, not here,
// to avoid race conditions when new items arrive during the flush.
}, flushInterval),
);
}
}
});

Expand Down
12 changes: 12 additions & 0 deletions packages/core/src/types-hoist/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,18 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
*/
enableMetrics?: boolean;

/**
* Interval in ms for the idle flush timer used by logs and metrics.
* Set to 0 to disable timer-based flushing entirely — useful for
* serverless runtimes that forbid background timers (e.g. Cloudflare Workers).
*
* Size-based flushing and explicit `flush()` calls still work regardless.
*
* @default 5000
* @internal
*/
_flushInterval?: number;

/**
* An event-processing callback for metrics, guaranteed to be invoked after all other metric
* processors. This allows a metric to be modified or dropped before it's sent.
Expand Down
58 changes: 58 additions & 0 deletions packages/core/test/lib/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3245,6 +3245,64 @@ describe('Client', () => {

expect(flushMetricsHandler).toHaveBeenCalledTimes(1);
});

it('does not create a flush timer when _flushInterval is 0', () => {
const safeUnrefSpy = vi.spyOn(timerModule, 'safeUnref');

const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
_flushInterval: 0,
});
const client = new TestClient(options);
const scope = new Scope();
scope.setClient(client);

_INTERNAL_captureMetric({ name: 'test_metric', value: 42, type: 'counter', attributes: {} }, { scope });

expect(safeUnrefSpy).not.toHaveBeenCalled();

safeUnrefSpy.mockRestore();
});

it('still flushes metrics via flush event when _flushInterval is 0', () => {
const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
_flushInterval: 0,
});
const client = new TestClient(options);
const scope = new Scope();
scope.setClient(client);

const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');

_INTERNAL_captureMetric({ name: 'metric1', value: 1, type: 'counter', attributes: {} }, { scope });

expect(sendEnvelopeSpy).not.toHaveBeenCalled();

client.emit('flush');

expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
});

it('still flushes metrics on size threshold when _flushInterval is 0', () => {
const options = getDefaultTestClientOptions({
dsn: PUBLIC_DSN,
_flushInterval: 0,
});
const client = new TestClient(options);
const scope = new Scope();
scope.setClient(client);

const sendEnvelopeSpy = vi.spyOn(client, 'sendEnvelope');

const largeValue = 'x'.repeat(400_000);
_INTERNAL_captureMetric(
{ name: 'large_metric', value: 1, type: 'counter', attributes: { large_value: largeValue } },
{ scope },
);

expect(sendEnvelopeSpy).toHaveBeenCalledTimes(1);
});
});

describe('promise buffer usage', () => {
Expand Down
1 change: 1 addition & 0 deletions packages/vercel-edge/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export class VercelEdgeClient extends ServerRuntimeClient<VercelEdgeClientOption
// Use provided runtime or default to 'vercel-edge'
runtime: options.runtime || { name: 'vercel-edge' },
serverName: options.serverName || process.env.SENTRY_NAME,
_flushInterval: 0,
};

super(clientOptions);
Expand Down
Loading