From c42af08e96c4e26378042f5e4203d17952bb61d3 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Fri, 16 Oct 2026 13:19:55 +0200 Subject: [PATCH 1/2] test(nestjs): Fix flaky schedule instrumentation error tests The @Cron and @Interval handlers throw the same error on every tick, so the Dedupe integration drops all but the first one unless another error is sent in between. Once the other erroring cron jobs are killed, that first error can predate the test's listener and it times out. Allow `waitForError` to look back like `waitForStreamedSpan` and use it here. Co-Authored-By: Claude Opus 5.5 --- .../tests/schedule-instrumentation.test.ts | 39 ++++++++++++------- .../test-utils/src/event-proxy-server.ts | 9 ++++- 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts index aa8a90c66cd5..0717b5074b59 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts @@ -2,13 +2,19 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends exceptions to Sentry on error in @Cron decorated method', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs-basic', event => { - return ( - !event.type && - event.exception?.values?.[0]?.value === 'Test error from schedule cron' && - event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.cron' - ); - }); + // The job throws the same error on every tick, so the Dedupe integration drops all but the first + // one unless another error is sent in between. That first error may predate this test. + const errorEventPromise = waitForError( + 'nestjs-basic', + event => { + return ( + !event.type && + event.exception?.values?.[0]?.value === 'Test error from schedule cron' && + event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.cron' + ); + }, + 0, + ); const errorEvent = await errorEventPromise; @@ -23,13 +29,18 @@ test('Sends exceptions to Sentry on error in @Cron decorated method', async ({ b }); test('Sends exceptions to Sentry on error in @Interval decorated method', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs-basic', event => { - return ( - !event.type && - event.exception?.values?.[0]?.value === 'Test error from schedule interval' && - event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.interval' - ); - }); + // See the @Cron test above for why this looks back to app startup. + const errorEventPromise = waitForError( + 'nestjs-basic', + event => { + return ( + !event.type && + event.exception?.values?.[0]?.value === 'Test error from schedule interval' && + event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.interval' + ); + }, + 0, + ); const errorEvent = await errorEventPromise; diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index 627b3995ba05..d5130dc2fe53 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -334,12 +334,17 @@ export function waitForEnvelopeItem( }); } -/** Wait for an error to be sent. */ +/** + * Wait for an error to be sent. + * + * Pass `timestamp` to look further back than the moment of the call, e.g. `0` for an error the app + * emits on its own, before any test can start listening. + */ export function waitForError( proxyServerName: string, callback: (errorEvent: Event) => Promise | boolean, + timestamp: number = getNanosecondTimestamp(), ): Promise { - const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName, From a424a7f4a316c5bf78cd81c0f6d72d0dbd96d990 Mon Sep 17 00:00:00 2001 From: Francesco Novy Date: Mon, 19 Oct 2026 09:55:47 +0200 Subject: [PATCH 2/2] test(nestjs): Make schedule test errors unique instead of looking back Include a timestamp in the thrown errors so the Dedupe integration never drops them, and only accept errors thrown after the test started. This reverts the `waitForError` timestamp option. Co-Authored-By: Claude Opus 5.5 --- .../nestjs-basic/src/schedule.service.ts | 5 ++- .../tests/schedule-instrumentation.test.ts | 43 ++++++++----------- .../test-utils/src/event-proxy-server.ts | 9 +--- 3 files changed, 23 insertions(+), 34 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/schedule.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/schedule.service.ts index 38b56136ab20..791d9aa83c20 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/src/schedule.service.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/src/schedule.service.ts @@ -7,15 +7,16 @@ export class ScheduleService { constructor(private schedulerRegistry: SchedulerRegistry) {} // @Cron error test (auto-instrumentation, no @SentryCron) + // The timestamp keeps the Dedupe integration from dropping the error on repeated ticks. @Cron('*/5 * * * * *', { name: 'test-schedule-cron-error' }) handleCronError() { - throw new Error('Test error from schedule cron'); + throw new Error(`Test error from schedule cron ${Date.now()}`); } // @Interval error test @Interval('test-schedule-interval-error', 2000) async handleIntervalError() { - throw new Error('Test error from schedule interval'); + throw new Error(`Test error from schedule interval ${Date.now()}`); } // @Timeout error test diff --git a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts index 0717b5074b59..b3f5a416f090 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-basic/tests/schedule-instrumentation.test.ts @@ -2,19 +2,15 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForStreamedSpan } from '@sentry-internal/test-utils'; test('Sends exceptions to Sentry on error in @Cron decorated method', async ({ baseURL }) => { - // The job throws the same error on every tick, so the Dedupe integration drops all but the first - // one unless another error is sent in between. That first error may predate this test. - const errorEventPromise = waitForError( - 'nestjs-basic', - event => { - return ( - !event.type && - event.exception?.values?.[0]?.value === 'Test error from schedule cron' && - event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.cron' - ); - }, - 0, - ); + const testStart = Date.now(); + const errorEventPromise = waitForError('nestjs-basic', event => { + const thrownAt = event.exception?.values?.[0]?.value?.match(/^Test error from schedule cron (\d+)$/)?.[1]; + return ( + !event.type && + Number(thrownAt) >= testStart && + event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.cron' + ); + }); const errorEvent = await errorEventPromise; @@ -29,18 +25,15 @@ test('Sends exceptions to Sentry on error in @Cron decorated method', async ({ b }); test('Sends exceptions to Sentry on error in @Interval decorated method', async ({ baseURL }) => { - // See the @Cron test above for why this looks back to app startup. - const errorEventPromise = waitForError( - 'nestjs-basic', - event => { - return ( - !event.type && - event.exception?.values?.[0]?.value === 'Test error from schedule interval' && - event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.interval' - ); - }, - 0, - ); + const testStart = Date.now(); + const errorEventPromise = waitForError('nestjs-basic', event => { + const thrownAt = event.exception?.values?.[0]?.value?.match(/^Test error from schedule interval (\d+)$/)?.[1]; + return ( + !event.type && + Number(thrownAt) >= testStart && + event.exception?.values?.[0]?.mechanism?.type === 'auto.function.nestjs.interval' + ); + }); const errorEvent = await errorEventPromise; diff --git a/dev-packages/test-utils/src/event-proxy-server.ts b/dev-packages/test-utils/src/event-proxy-server.ts index d5130dc2fe53..627b3995ba05 100644 --- a/dev-packages/test-utils/src/event-proxy-server.ts +++ b/dev-packages/test-utils/src/event-proxy-server.ts @@ -334,17 +334,12 @@ export function waitForEnvelopeItem( }); } -/** - * Wait for an error to be sent. - * - * Pass `timestamp` to look further back than the moment of the call, e.g. `0` for an error the app - * emits on its own, before any test can start listening. - */ +/** Wait for an error to be sent. */ export function waitForError( proxyServerName: string, callback: (errorEvent: Event) => Promise | boolean, - timestamp: number = getNanosecondTimestamp(), ): Promise { + const timestamp = getNanosecondTimestamp(); return new Promise((resolve, reject) => { waitForEnvelopeItem( proxyServerName,