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
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineEventHandler } from '#imports';
import * as Sentry from '@sentry/nuxt';

export default defineEventHandler(() => {
// Seed a pending client-report outcome without sending an event, so the test can observe whether the
// SDK wrongly flushes (and ships) it per HTTP response instead of aggregating it.
Sentry.getClient()?.recordDroppedEvent('before_send', 'error');
return { seeded: true };
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { expect, test } from '@playwright/test';
import { waitForEnvelopeItem } from '@sentry-internal/test-utils';

// Regression test for https://github.com/getsentry/sentry-javascript/issues/23306
//
// The httpIntegration `responseHook` used to call `vercelWaitUntil(flushSafelyWithTimeout())`, which
// evaluates the flush eagerly regardless of platform. On a long-running server that meant `flush()` ran
// on every HTTP response, shipping each pending outcome as its own `client_report` envelope instead of
// aggregating it on the flush interval.
test('does not ship a client_report per HTTP response', async ({ request }) => {
const requestCount = 5;

// Resolve as soon as a second client_report is observed. A single one is tolerated because the 60s
// client-report interval may happen to flush the seeded outcomes once during the window.
let seen = 0;
const twoReportsSeen = waitForEnvelopeItem('nuxt-4', item => item[0].type === 'client_report' && ++seen >= 2)
.then(() => 'two-or-more' as const)
.catch(() => 'two-or-more' as const);

// Each request seeds one pending outcome (see server/api/flush-regression.ts).
for (let i = 0; i < requestCount; i++) {
await request.get('/api/flush-regression');
}

// Buggy build: each response flushes -> one client_report per request, so a second arrives within ms.
// Fixed build: flushIfServerless no-ops on a long-running server, so the outcomes aggregate and at
// most the single interval flush ships within this window.
const result = await Promise.race([
twoReportsSeen,
new Promise<'at-most-one'>(resolve => setTimeout(() => resolve('at-most-one'), 5000)),
]);
Comment thread
logaretm marked this conversation as resolved.

expect(result).toBe('at-most-one');
});
23 changes: 6 additions & 17 deletions packages/nuxt/src/server/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ import {
debug,
DEFAULT_ENVIRONMENT,
DEV_ENVIRONMENT,
flush,
flushIfServerless,
getGlobalScope,
vercelWaitUntil,
} from '@sentry/core';
import {
getDefaultIntegrations as getDefaultNodeIntegrations,
Expand Down Expand Up @@ -109,27 +108,17 @@ function getNuxtDefaultIntegrations(options: NodeOptions): Integration[] {
httpIntegration({
instrumentation: {
responseHook: () => {
// Makes it possible to end the tracing span before closing the Vercel lambda (https://vercel.com/docs/functions/functions-api-reference#waituntil)
vercelWaitUntil(flushSafelyWithTimeout());
// Flush eagerly on serverless platforms, where the function may be frozen before the transport
// sends, handing the flush to a platform `waitUntil` where one exists so it doesn't block. On a
// long-running server this is a no-op, so pending outcomes keep aggregating on the flush interval
// instead of shipping one client_report envelope per response.
void flushIfServerless();
},
},
}),
];
}

/**
* Flushes pending Sentry events with a 2-second timeout and in a way that cannot create unhandled promise rejections.
*/
async function flushSafelyWithTimeout(): Promise<void> {
try {
DEBUG_BUILD && debug.log('Flushing events...');
await flush(2000);
DEBUG_BUILD && debug.log('Done flushing events');
} catch (e) {
DEBUG_BUILD && debug.log('Error while flushing events:\n', e);
}
}

/**
* Checks if the event is a cache event.
*/
Expand Down
Loading