From 2f5cf1da38c3d2eaabfbc5e30179cef04a773dcf Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 20 Feb 2026 08:58:04 +0100 Subject: [PATCH 1/5] Add reproduction for sentry-javascript#17742 Demonstrates breadcrumb leaking between NestJS requests due to missing request isolation scope. Breadcrumbs from earlier requests appear on later error events. Co-Authored-By: Claude Opus 4.6 --- sentry-javascript/17742/README.md | 66 +++++++++++++++++++ sentry-javascript/17742/nest-cli.json | 5 ++ sentry-javascript/17742/package.json | 22 +++++++ sentry-javascript/17742/src/app.controller.ts | 25 +++++++ sentry-javascript/17742/src/app.module.ts | 11 ++++ sentry-javascript/17742/src/app.service.ts | 65 ++++++++++++++++++ sentry-javascript/17742/src/instrument.ts | 33 ++++++++++ sentry-javascript/17742/src/main.ts | 23 +++++++ sentry-javascript/17742/tsconfig.json | 21 ++++++ 9 files changed, 271 insertions(+) create mode 100644 sentry-javascript/17742/README.md create mode 100644 sentry-javascript/17742/nest-cli.json create mode 100644 sentry-javascript/17742/package.json create mode 100644 sentry-javascript/17742/src/app.controller.ts create mode 100644 sentry-javascript/17742/src/app.module.ts create mode 100644 sentry-javascript/17742/src/app.service.ts create mode 100644 sentry-javascript/17742/src/instrument.ts create mode 100644 sentry-javascript/17742/src/main.ts create mode 100644 sentry-javascript/17742/tsconfig.json diff --git a/sentry-javascript/17742/README.md b/sentry-javascript/17742/README.md new file mode 100644 index 0000000..a41833b --- /dev/null +++ b/sentry-javascript/17742/README.md @@ -0,0 +1,66 @@ +# Reproduction for sentry-javascript#17742 + +**Issue:** https://github.com/getsentry/sentry-javascript/issues/17742 + +## Description + +Breadcrumbs from earlier, unrelated requests leak into later Sentry error events in NestJS. This indicates that request isolation isn't working as expected — breadcrumbs are being stored on the default (global) isolation scope instead of per-request scopes. + +## Steps to Reproduce + +1. Export your Sentry DSN (or run without one to see local debug output): + ```bash + export SENTRY_DSN= + ``` + +2. Install dependencies: + ```bash + npm install + ``` + +3. Build and start the server: + ```bash + npm run build && npm start + ``` + +4. In another terminal, hit the routes in sequence: + ```bash + curl http://localhost:3000/route-a + sleep 1 + curl http://localhost:3000/route-b + sleep 1 + curl http://localhost:3000/trigger-error + ``` + +5. Check the server output — the `beforeSend` hook logs all breadcrumbs attached to the error event. + +## Expected Behavior + +The error event from `/trigger-error` should only contain its own breadcrumb: +``` +=== Sentry Event Breadcrumbs (1 total) === + [0] category=trigger-error, message=About to trigger an error +``` + +## Actual Behavior + +The error event contains breadcrumbs from all previous requests: +``` +=== Sentry Event Breadcrumbs (6 total) === + [0] category=route-a, message=Processing route A - step 1 + [1] category=route-a, message=Processing route A - step 2 + [2] category=route-a, message=Route A completed successfully + [3] category=route-b, message=Processing route B - step 1 + [4] category=route-b, message=Route B completed successfully + [5] category=trigger-error, message=About to trigger an error + +*** BUG CONFIRMED: Breadcrumbs leaked from other requests! *** +``` + +The Sentry debug logs also show: `"Isolation scope is still the default isolation scope, skipping setting transactionName."` — confirming that requests are not getting their own isolation scopes. + +## Environment + +- Node.js: v18+ +- @sentry/nestjs: ^10.2.0 +- @nestjs/core: ^10.0.0 diff --git a/sentry-javascript/17742/nest-cli.json b/sentry-javascript/17742/nest-cli.json new file mode 100644 index 0000000..2566481 --- /dev/null +++ b/sentry-javascript/17742/nest-cli.json @@ -0,0 +1,5 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src" +} diff --git a/sentry-javascript/17742/package.json b/sentry-javascript/17742/package.json new file mode 100644 index 0000000..d8481de --- /dev/null +++ b/sentry-javascript/17742/package.json @@ -0,0 +1,22 @@ +{ + "name": "repro-sentry-javascript-17742", + "version": "1.0.0", + "description": "Reproduction for sentry-javascript#17742 - NestJS leaking breadcrumbs", + "private": true, + "scripts": { + "build": "nest build", + "start": "nest start" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@sentry/nestjs": "^10.2.0", + "reflect-metadata": "^0.2.2", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@nestjs/cli": "^10.0.0", + "typescript": "^5.0.0" + } +} diff --git a/sentry-javascript/17742/src/app.controller.ts b/sentry-javascript/17742/src/app.controller.ts new file mode 100644 index 0000000..92142ed --- /dev/null +++ b/sentry-javascript/17742/src/app.controller.ts @@ -0,0 +1,25 @@ +import { Controller, Get } from "@nestjs/common"; +import { AppService } from "./app.service"; + +@Controller() +export class AppController { + constructor(private readonly appService: AppService) {} + + // Route A: adds some breadcrumbs, responds successfully + @Get("route-a") + routeA(): string { + return this.appService.handleRouteA(); + } + + // Route B: adds different breadcrumbs, responds successfully + @Get("route-b") + routeB(): string { + return this.appService.handleRouteB(); + } + + // This route triggers an error — check if breadcrumbs from route-a/route-b leak here + @Get("trigger-error") + triggerError(): string { + return this.appService.triggerError(); + } +} diff --git a/sentry-javascript/17742/src/app.module.ts b/sentry-javascript/17742/src/app.module.ts new file mode 100644 index 0000000..fd45a05 --- /dev/null +++ b/sentry-javascript/17742/src/app.module.ts @@ -0,0 +1,11 @@ +import { Module } from "@nestjs/common"; +import { SentryModule } from "@sentry/nestjs/setup"; +import { AppController } from "./app.controller"; +import { AppService } from "./app.service"; + +@Module({ + imports: [SentryModule.forRoot()], + controllers: [AppController], + providers: [AppService], +}) +export class AppModule {} diff --git a/sentry-javascript/17742/src/app.service.ts b/sentry-javascript/17742/src/app.service.ts new file mode 100644 index 0000000..a894871 --- /dev/null +++ b/sentry-javascript/17742/src/app.service.ts @@ -0,0 +1,65 @@ +import { Injectable } from "@nestjs/common"; +import * as Sentry from "@sentry/nestjs"; + +@Injectable() +export class AppService { + handleRouteA(): string { + // Add distinctive breadcrumbs for route A + Sentry.addBreadcrumb({ + category: "route-a", + message: "Processing route A - step 1", + level: "info", + }); + Sentry.addBreadcrumb({ + category: "route-a", + message: "Processing route A - step 2", + level: "info", + }); + Sentry.addBreadcrumb({ + category: "route-a", + message: "Route A completed successfully", + level: "info", + }); + + console.log("[Route A] Request handled, 3 breadcrumbs added"); + return "Route A OK"; + } + + handleRouteB(): string { + // Add distinctive breadcrumbs for route B + Sentry.addBreadcrumb({ + category: "route-b", + message: "Processing route B - step 1", + level: "info", + }); + Sentry.addBreadcrumb({ + category: "route-b", + message: "Route B completed successfully", + level: "info", + }); + + console.log("[Route B] Request handled, 2 breadcrumbs added"); + return "Route B OK"; + } + + triggerError(): string { + // Add a breadcrumb specific to this request + Sentry.addBreadcrumb({ + category: "trigger-error", + message: "About to trigger an error", + level: "error", + }); + + // Capture an error — check the Sentry event to see if breadcrumbs from + // route-a and route-b leaked into this event. + // EXPECTED: Only the "trigger-error" breadcrumb should appear + // ACTUAL (BUG): Breadcrumbs from route-a and route-b may also appear + const error = new Error("Test error to check breadcrumb isolation"); + Sentry.captureException(error); + + console.log( + "[Trigger Error] Error captured — check Sentry for leaked breadcrumbs" + ); + return "Error triggered — check Sentry dashboard for breadcrumb leaking"; + } +} diff --git a/sentry-javascript/17742/src/instrument.ts b/sentry-javascript/17742/src/instrument.ts new file mode 100644 index 0000000..d876cd6 --- /dev/null +++ b/sentry-javascript/17742/src/instrument.ts @@ -0,0 +1,33 @@ +// Sentry must be imported and initialized before anything else +import * as Sentry from "@sentry/nestjs"; + +Sentry.init({ + dsn: process.env.SENTRY_DSN || "", + debug: true, + tracesSampleRate: 1.0, + beforeSend(event) { + // Log all breadcrumbs attached to the error event to show leaking + const breadcrumbs = event.breadcrumbs || []; + console.log( + `\n=== Sentry Event Breadcrumbs (${breadcrumbs.length} total) ===` + ); + breadcrumbs.forEach((bc, i) => { + console.log(` [${i}] category=${bc.category}, message=${bc.message}`); + }); + + // Check for leaked breadcrumbs from other routes + const leakedFromA = breadcrumbs.filter((b) => b.category === "route-a"); + const leakedFromB = breadcrumbs.filter((b) => b.category === "route-b"); + if (leakedFromA.length > 0 || leakedFromB.length > 0) { + console.log( + "\n*** BUG CONFIRMED: Breadcrumbs leaked from other requests! ***" + ); + console.log(` - Leaked from route-a: ${leakedFromA.length} breadcrumbs`); + console.log(` - Leaked from route-b: ${leakedFromB.length} breadcrumbs`); + } else { + console.log("\n No leaked breadcrumbs detected (isolation working)."); + } + console.log("=== End Breadcrumbs ===\n"); + return event; + }, +}); diff --git a/sentry-javascript/17742/src/main.ts b/sentry-javascript/17742/src/main.ts new file mode 100644 index 0000000..c2b6200 --- /dev/null +++ b/sentry-javascript/17742/src/main.ts @@ -0,0 +1,23 @@ +import "./instrument"; + +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(3000); + console.log("Server running on http://localhost:3000"); + console.log(""); + console.log("To reproduce the breadcrumb leaking issue:"); + console.log("1. curl http://localhost:3000/route-a"); + console.log("2. Wait a moment, then: curl http://localhost:3000/route-b"); + console.log("3. curl http://localhost:3000/trigger-error"); + console.log(""); + console.log( + "Expected: The error event should only contain breadcrumbs from /trigger-error" + ); + console.log( + "Actual: The error event may contain breadcrumbs from /route-a and /route-b as well" + ); +} +bootstrap(); diff --git a/sentry-javascript/17742/tsconfig.json b/sentry-javascript/17742/tsconfig.json new file mode 100644 index 0000000..95f5641 --- /dev/null +++ b/sentry-javascript/17742/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false + } +} From 275cfa6ad967edf521982ac0433fa31a6eee2076 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 20 Feb 2026 09:05:59 +0100 Subject: [PATCH 2/5] Align reproduction with official Sentry NestJS docs - Add SentryGlobalFilter as APP_FILTER provider - Throw Error instead of manually calling captureException() Co-Authored-By: Claude Opus 4.6 --- sentry-javascript/17742/src/app.module.ts | 11 +++++++++-- sentry-javascript/17742/src/app.service.ts | 13 ++++--------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/sentry-javascript/17742/src/app.module.ts b/sentry-javascript/17742/src/app.module.ts index fd45a05..c89a3e5 100644 --- a/sentry-javascript/17742/src/app.module.ts +++ b/sentry-javascript/17742/src/app.module.ts @@ -1,11 +1,18 @@ import { Module } from "@nestjs/common"; -import { SentryModule } from "@sentry/nestjs/setup"; +import { APP_FILTER } from "@nestjs/core"; +import { SentryGlobalFilter, SentryModule } from "@sentry/nestjs/setup"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; @Module({ imports: [SentryModule.forRoot()], controllers: [AppController], - providers: [AppService], + providers: [ + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, + AppService, + ], }) export class AppModule {} diff --git a/sentry-javascript/17742/src/app.service.ts b/sentry-javascript/17742/src/app.service.ts index a894871..290583d 100644 --- a/sentry-javascript/17742/src/app.service.ts +++ b/sentry-javascript/17742/src/app.service.ts @@ -50,16 +50,11 @@ export class AppService { level: "error", }); - // Capture an error — check the Sentry event to see if breadcrumbs from - // route-a and route-b leaked into this event. + // Throw an exception — the SentryGlobalFilter will capture it automatically. + // Check the Sentry event to see if breadcrumbs from route-a and route-b + // leaked into this event. // EXPECTED: Only the "trigger-error" breadcrumb should appear // ACTUAL (BUG): Breadcrumbs from route-a and route-b may also appear - const error = new Error("Test error to check breadcrumb isolation"); - Sentry.captureException(error); - - console.log( - "[Trigger Error] Error captured — check Sentry for leaked breadcrumbs" - ); - return "Error triggered — check Sentry dashboard for breadcrumb leaking"; + throw new Error("Test error to check breadcrumb isolation"); } } From 717f0741cae84b00e3d0c70c67a664cbe6d2afb6 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 20 Feb 2026 11:15:17 +0100 Subject: [PATCH 3/5] Reproduce background job breadcrumb leaking into HTTP requests Rewrote reproduction to demonstrate the actual root cause: background jobs (cron/graphile-worker/BullMQ) run outside HTTP request context and pollute the default isolation scope. When httpServerIntegration clones this scope for new requests, it inherits stale breadcrumbs. Added test-repro.sh for one-command reproduction. Co-Authored-By: Claude Opus 4.6 --- sentry-javascript/17742/README.md | 58 +++++++++---------- sentry-javascript/17742/package.json | 5 +- sentry-javascript/17742/src/app.controller.ts | 13 ----- sentry-javascript/17742/src/app.module.ts | 5 +- sentry-javascript/17742/src/app.service.ts | 55 +++--------------- .../17742/src/background-job.service.ts | 37 ++++++++++++ sentry-javascript/17742/src/instrument.ts | 15 ++--- sentry-javascript/17742/src/main.ts | 11 ++-- sentry-javascript/17742/test-repro.sh | 19 ++++++ 9 files changed, 111 insertions(+), 107 deletions(-) create mode 100644 sentry-javascript/17742/src/background-job.service.ts create mode 100755 sentry-javascript/17742/test-repro.sh diff --git a/sentry-javascript/17742/README.md b/sentry-javascript/17742/README.md index a41833b..6d0c8b4 100644 --- a/sentry-javascript/17742/README.md +++ b/sentry-javascript/17742/README.md @@ -4,63 +4,59 @@ ## Description -Breadcrumbs from earlier, unrelated requests leak into later Sentry error events in NestJS. This indicates that request isolation isn't working as expected — breadcrumbs are being stored on the default (global) isolation scope instead of per-request scopes. +Breadcrumbs from background jobs (cron tasks, graphile-worker, BullMQ, etc.) leak into HTTP request error events in NestJS. Background jobs run outside the HTTP request context, so they add breadcrumbs to the default isolation scope. When a new HTTP request arrives, `httpServerIntegration` clones the default scope — inheriting all those stale breadcrumbs. ## Steps to Reproduce -1. Export your Sentry DSN (or run without one to see local debug output): +1. Add a `.env` file with your Sentry DSN: ```bash - export SENTRY_DSN= + echo "SENTRY_DSN=" > .env ``` -2. Install dependencies: +2. Install dependencies and run the reproduction: ```bash npm install + npm run test:repro ``` -3. Build and start the server: - ```bash - npm run build && npm start - ``` - -4. In another terminal, hit the routes in sequence: - ```bash - curl http://localhost:3000/route-a - sleep 1 - curl http://localhost:3000/route-b - sleep 1 - curl http://localhost:3000/trigger-error - ``` + This builds the app, starts it, waits for background jobs to pollute the default scope, then triggers an error via HTTP. -5. Check the server output — the `beforeSend` hook logs all breadcrumbs attached to the error event. +3. Check the output — the `beforeSend` hook logs all breadcrumbs on the error event. ## Expected Behavior -The error event from `/trigger-error` should only contain its own breadcrumb: +The error event from `GET /trigger-error` should only contain its own breadcrumb: ``` === Sentry Event Breadcrumbs (1 total) === - [0] category=trigger-error, message=About to trigger an error + [0] category=http-request, message=About to trigger an error in HTTP handler ``` ## Actual Behavior -The error event contains breadcrumbs from all previous requests: +The error event contains breadcrumbs from background jobs that ran before the request: +``` +=== Sentry Event Breadcrumbs (21 total) === + ... + [9] category=background-job, message=Background job #1 started + [10] category=background-job, message=Background job #1 completed + ... + [20] category=http-request, message=About to trigger an error in HTTP handler + +*** BUG CONFIRMED: 6 breadcrumbs leaked from background jobs! *** ``` -=== Sentry Event Breadcrumbs (6 total) === - [0] category=route-a, message=Processing route A - step 1 - [1] category=route-a, message=Processing route A - step 2 - [2] category=route-a, message=Route A completed successfully - [3] category=route-b, message=Processing route B - step 1 - [4] category=route-b, message=Route B completed successfully - [5] category=trigger-error, message=About to trigger an error - -*** BUG CONFIRMED: Breadcrumbs leaked from other requests! *** + +## Root Cause + +In `packages/node-core/src/integrations/http/httpServerIntegration.ts:185`: +```ts +const isolationScope = getIsolationScope().clone(); ``` -The Sentry debug logs also show: `"Isolation scope is still the default isolation scope, skipping setting transactionName."` — confirming that requests are not getting their own isolation scopes. +This clones the **default** isolation scope, which has been polluted by background job breadcrumbs. The cloned scope inherits all those breadcrumbs, causing them to appear on HTTP request error events. ## Environment - Node.js: v18+ - @sentry/nestjs: ^10.2.0 - @nestjs/core: ^10.0.0 +- @nestjs/schedule: ^6.1.1 diff --git a/sentry-javascript/17742/package.json b/sentry-javascript/17742/package.json index d8481de..68de584 100644 --- a/sentry-javascript/17742/package.json +++ b/sentry-javascript/17742/package.json @@ -5,13 +5,16 @@ "private": true, "scripts": { "build": "nest build", - "start": "nest start" + "start": "nest start", + "test:repro": "npm run build && bash test-repro.sh" }, "dependencies": { "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/schedule": "^6.1.1", "@sentry/nestjs": "^10.2.0", + "dotenv": "^17.3.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/sentry-javascript/17742/src/app.controller.ts b/sentry-javascript/17742/src/app.controller.ts index 92142ed..a7817bd 100644 --- a/sentry-javascript/17742/src/app.controller.ts +++ b/sentry-javascript/17742/src/app.controller.ts @@ -5,19 +5,6 @@ import { AppService } from "./app.service"; export class AppController { constructor(private readonly appService: AppService) {} - // Route A: adds some breadcrumbs, responds successfully - @Get("route-a") - routeA(): string { - return this.appService.handleRouteA(); - } - - // Route B: adds different breadcrumbs, responds successfully - @Get("route-b") - routeB(): string { - return this.appService.handleRouteB(); - } - - // This route triggers an error — check if breadcrumbs from route-a/route-b leak here @Get("trigger-error") triggerError(): string { return this.appService.triggerError(); diff --git a/sentry-javascript/17742/src/app.module.ts b/sentry-javascript/17742/src/app.module.ts index c89a3e5..f2ec282 100644 --- a/sentry-javascript/17742/src/app.module.ts +++ b/sentry-javascript/17742/src/app.module.ts @@ -1,11 +1,13 @@ import { Module } from "@nestjs/common"; import { APP_FILTER } from "@nestjs/core"; +import { ScheduleModule } from "@nestjs/schedule"; import { SentryGlobalFilter, SentryModule } from "@sentry/nestjs/setup"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; +import { BackgroundJobService } from "./background-job.service"; @Module({ - imports: [SentryModule.forRoot()], + imports: [SentryModule.forRoot(), ScheduleModule.forRoot()], controllers: [AppController], providers: [ { @@ -13,6 +15,7 @@ import { AppService } from "./app.service"; useClass: SentryGlobalFilter, }, AppService, + BackgroundJobService, ], }) export class AppModule {} diff --git a/sentry-javascript/17742/src/app.service.ts b/sentry-javascript/17742/src/app.service.ts index 290583d..a110ea7 100644 --- a/sentry-javascript/17742/src/app.service.ts +++ b/sentry-javascript/17742/src/app.service.ts @@ -3,58 +3,19 @@ import * as Sentry from "@sentry/nestjs"; @Injectable() export class AppService { - handleRouteA(): string { - // Add distinctive breadcrumbs for route A - Sentry.addBreadcrumb({ - category: "route-a", - message: "Processing route A - step 1", - level: "info", - }); - Sentry.addBreadcrumb({ - category: "route-a", - message: "Processing route A - step 2", - level: "info", - }); - Sentry.addBreadcrumb({ - category: "route-a", - message: "Route A completed successfully", - level: "info", - }); - - console.log("[Route A] Request handled, 3 breadcrumbs added"); - return "Route A OK"; - } - - handleRouteB(): string { - // Add distinctive breadcrumbs for route B - Sentry.addBreadcrumb({ - category: "route-b", - message: "Processing route B - step 1", - level: "info", - }); - Sentry.addBreadcrumb({ - category: "route-b", - message: "Route B completed successfully", - level: "info", - }); - - console.log("[Route B] Request handled, 2 breadcrumbs added"); - return "Route B OK"; - } - triggerError(): string { - // Add a breadcrumb specific to this request + // Add a breadcrumb specific to this HTTP request Sentry.addBreadcrumb({ - category: "trigger-error", - message: "About to trigger an error", + category: "http-request", + message: "About to trigger an error in HTTP handler", level: "error", }); - // Throw an exception — the SentryGlobalFilter will capture it automatically. - // Check the Sentry event to see if breadcrumbs from route-a and route-b - // leaked into this event. - // EXPECTED: Only the "trigger-error" breadcrumb should appear - // ACTUAL (BUG): Breadcrumbs from route-a and route-b may also appear + // Throw an exception — the SentryGlobalFilter captures it automatically. + // EXPECTED: Only the "http-request" breadcrumb should appear on the event + // ACTUAL (BUG): Breadcrumbs from background jobs leak into this event + // because the HTTP request's isolation scope was cloned from the default + // scope, which was polluted by background job breadcrumbs throw new Error("Test error to check breadcrumb isolation"); } } diff --git a/sentry-javascript/17742/src/background-job.service.ts b/sentry-javascript/17742/src/background-job.service.ts new file mode 100644 index 0000000..1d1529a --- /dev/null +++ b/sentry-javascript/17742/src/background-job.service.ts @@ -0,0 +1,37 @@ +import { Injectable } from "@nestjs/common"; +import { Interval } from "@nestjs/schedule"; +import * as Sentry from "@sentry/nestjs"; + +/** + * Simulates a background job runner (like graphile-worker or BullMQ). + * These jobs run outside the HTTP request context, so they execute on the + * default isolation scope. Any breadcrumbs they add pollute the default scope, + * which then gets cloned into every new HTTP request's isolation scope. + */ +@Injectable() +export class BackgroundJobService { + private jobCount = 0; + + @Interval(3000) + handleBackgroundJob() { + this.jobCount++; + console.log(`[Background Job #${this.jobCount}] Running...`); + + // These breadcrumbs are added to the default isolation scope + // because this code runs outside any HTTP request context + Sentry.addBreadcrumb({ + category: "background-job", + message: `Background job #${this.jobCount} started`, + level: "info", + }); + Sentry.addBreadcrumb({ + category: "background-job", + message: `Background job #${this.jobCount} completed`, + level: "info", + }); + + console.log( + `[Background Job #${this.jobCount}] Done — 2 breadcrumbs added to default scope` + ); + } +} diff --git a/sentry-javascript/17742/src/instrument.ts b/sentry-javascript/17742/src/instrument.ts index d876cd6..7e5b6f8 100644 --- a/sentry-javascript/17742/src/instrument.ts +++ b/sentry-javascript/17742/src/instrument.ts @@ -1,4 +1,4 @@ -// Sentry must be imported and initialized before anything else +import "dotenv/config"; import * as Sentry from "@sentry/nestjs"; Sentry.init({ @@ -6,7 +6,7 @@ Sentry.init({ debug: true, tracesSampleRate: 1.0, beforeSend(event) { - // Log all breadcrumbs attached to the error event to show leaking + // Log all breadcrumbs attached to the error event const breadcrumbs = event.breadcrumbs || []; console.log( `\n=== Sentry Event Breadcrumbs (${breadcrumbs.length} total) ===` @@ -15,15 +15,12 @@ Sentry.init({ console.log(` [${i}] category=${bc.category}, message=${bc.message}`); }); - // Check for leaked breadcrumbs from other routes - const leakedFromA = breadcrumbs.filter((b) => b.category === "route-a"); - const leakedFromB = breadcrumbs.filter((b) => b.category === "route-b"); - if (leakedFromA.length > 0 || leakedFromB.length > 0) { + // Check for leaked breadcrumbs from background jobs + const leaked = breadcrumbs.filter((b) => b.category === "background-job"); + if (leaked.length > 0) { console.log( - "\n*** BUG CONFIRMED: Breadcrumbs leaked from other requests! ***" + `\n*** BUG CONFIRMED: ${leaked.length} breadcrumbs leaked from background jobs! ***` ); - console.log(` - Leaked from route-a: ${leakedFromA.length} breadcrumbs`); - console.log(` - Leaked from route-b: ${leakedFromB.length} breadcrumbs`); } else { console.log("\n No leaked breadcrumbs detected (isolation working)."); } diff --git a/sentry-javascript/17742/src/main.ts b/sentry-javascript/17742/src/main.ts index c2b6200..4e6cf8b 100644 --- a/sentry-javascript/17742/src/main.ts +++ b/sentry-javascript/17742/src/main.ts @@ -9,15 +9,16 @@ async function bootstrap() { console.log("Server running on http://localhost:3000"); console.log(""); console.log("To reproduce the breadcrumb leaking issue:"); - console.log("1. curl http://localhost:3000/route-a"); - console.log("2. Wait a moment, then: curl http://localhost:3000/route-b"); - console.log("3. curl http://localhost:3000/trigger-error"); + console.log( + "1. Wait ~10 seconds for a few background jobs to run and pollute the default scope" + ); + console.log("2. curl http://localhost:3000/trigger-error"); console.log(""); console.log( - "Expected: The error event should only contain breadcrumbs from /trigger-error" + "Expected: The error event should only contain breadcrumbs from the HTTP request" ); console.log( - "Actual: The error event may contain breadcrumbs from /route-a and /route-b as well" + "Actual: The error event also contains breadcrumbs from background jobs" ); } bootstrap(); diff --git a/sentry-javascript/17742/test-repro.sh b/sentry-javascript/17742/test-repro.sh new file mode 100755 index 0000000..9588e5f --- /dev/null +++ b/sentry-javascript/17742/test-repro.sh @@ -0,0 +1,19 @@ +#!/bin/bash +# Reproduction script for sentry-javascript#17742 +# Background job breadcrumbs leaking into HTTP request error events + +echo "=== Starting NestJS server ===" +node dist/main.js & +APP_PID=$! + +echo "Waiting for server to start and background jobs to run..." +sleep 10 + +echo "" +echo "=== Triggering error after background jobs have polluted the default scope ===" +curl -s http://localhost:3000/trigger-error +echo "" + +sleep 3 +kill $APP_PID 2>/dev/null +wait $APP_PID 2>/dev/null From 3272650f15e7da1c9949846b8ad8f4be05ce4577 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 20 Feb 2026 12:08:43 +0100 Subject: [PATCH 4/5] Extend reproduction to cover all NestJS background job frameworks Add breadcrumb leaking reproductions for: - @nestjs/schedule (@Interval) - always active - @nestjs/event-emitter (@OnEvent) - always active - @nestjs/bullmq (@Processor) - requires REDIS_URL - nestjs-graphile-worker (@Task) - requires DATABASE_URL Both schedule and event-emitter confirmed leaking breadcrumbs into HTTP request error events. Co-Authored-By: Claude Opus 4.6 --- sentry-javascript/17742/README.md | 40 +++++---- sentry-javascript/17742/package.json | 5 ++ sentry-javascript/17742/src/app.module.ts | 82 ++++++++++++++++++- .../17742/src/background-job.service.ts | 37 --------- .../17742/src/bullmq-job.processor.ts | 49 +++++++++++ .../17742/src/event-job.service.ts | 36 ++++++++ .../17742/src/graphile-job.service.ts | 58 +++++++++++++ sentry-javascript/17742/src/instrument.ts | 29 +++++-- sentry-javascript/17742/src/main.ts | 13 ++- .../17742/src/schedule-job.service.ts | 26 ++++++ sentry-javascript/17742/test-repro.sh | 10 ++- 11 files changed, 310 insertions(+), 75 deletions(-) delete mode 100644 sentry-javascript/17742/src/background-job.service.ts create mode 100644 sentry-javascript/17742/src/bullmq-job.processor.ts create mode 100644 sentry-javascript/17742/src/event-job.service.ts create mode 100644 sentry-javascript/17742/src/graphile-job.service.ts create mode 100644 sentry-javascript/17742/src/schedule-job.service.ts diff --git a/sentry-javascript/17742/README.md b/sentry-javascript/17742/README.md index 6d0c8b4..d120a41 100644 --- a/sentry-javascript/17742/README.md +++ b/sentry-javascript/17742/README.md @@ -4,24 +4,34 @@ ## Description -Breadcrumbs from background jobs (cron tasks, graphile-worker, BullMQ, etc.) leak into HTTP request error events in NestJS. Background jobs run outside the HTTP request context, so they add breadcrumbs to the default isolation scope. When a new HTTP request arrives, `httpServerIntegration` clones the default scope — inheriting all those stale breadcrumbs. +Breadcrumbs from background jobs leak into HTTP request error events in NestJS. Background jobs run outside the HTTP request context, so they add breadcrumbs to the default isolation scope. When a new HTTP request arrives, `httpServerIntegration` clones the default scope — inheriting all those stale breadcrumbs. + +This reproduction covers **all four** common NestJS background job patterns: + +| Framework | Decorator | External Dep | Env Var | +|-----------|-----------|-------------|---------| +| `@nestjs/schedule` | `@Interval` / `@Cron` | None | Always active | +| `@nestjs/event-emitter` | `@OnEvent` | None | Always active | +| `@nestjs/bullmq` | `@Processor` | Redis | `REDIS_URL` | +| `nestjs-graphile-worker` | `@Task` | PostgreSQL | `DATABASE_URL` | ## Steps to Reproduce -1. Add a `.env` file with your Sentry DSN: +1. Add a `.env` file with your Sentry DSN (and optionally Redis/PostgreSQL): ```bash - echo "SENTRY_DSN=" > .env + SENTRY_DSN= + # Optional: + # REDIS_URL=redis://localhost:6379 + # DATABASE_URL=postgres://user:pass@localhost:5432/dbname ``` -2. Install dependencies and run the reproduction: +2. Install dependencies and run: ```bash npm install npm run test:repro ``` - This builds the app, starts it, waits for background jobs to pollute the default scope, then triggers an error via HTTP. - -3. Check the output — the `beforeSend` hook logs all breadcrumbs on the error event. +3. Check the output for leaked breadcrumbs. ## Expected Behavior @@ -33,16 +43,9 @@ The error event from `GET /trigger-error` should only contain its own breadcrumb ## Actual Behavior -The error event contains breadcrumbs from background jobs that ran before the request: ``` -=== Sentry Event Breadcrumbs (21 total) === - ... - [9] category=background-job, message=Background job #1 started - [10] category=background-job, message=Background job #1 completed - ... - [20] category=http-request, message=About to trigger an error in HTTP handler - -*** BUG CONFIRMED: 6 breadcrumbs leaked from background jobs! *** +*** BUG CONFIRMED: Breadcrumbs leaked from background jobs! *** + Leaked: schedule-job: 3, event-job: 2 ``` ## Root Cause @@ -52,7 +55,7 @@ In `packages/node-core/src/integrations/http/httpServerIntegration.ts:185`: const isolationScope = getIsolationScope().clone(); ``` -This clones the **default** isolation scope, which has been polluted by background job breadcrumbs. The cloned scope inherits all those breadcrumbs, causing them to appear on HTTP request error events. +Background jobs execute on the default isolation scope (no HTTP request forked a new one). Their breadcrumbs accumulate on the default scope. When `httpServerIntegration` handles a new request, it clones the default scope — including all stale breadcrumbs from background jobs. ## Environment @@ -60,3 +63,6 @@ This clones the **default** isolation scope, which has been polluted by backgrou - @sentry/nestjs: ^10.2.0 - @nestjs/core: ^10.0.0 - @nestjs/schedule: ^6.1.1 +- @nestjs/event-emitter: latest +- @nestjs/bullmq: latest (optional) +- nestjs-graphile-worker: latest (optional) diff --git a/sentry-javascript/17742/package.json b/sentry-javascript/17742/package.json index 68de584..89f427c 100644 --- a/sentry-javascript/17742/package.json +++ b/sentry-javascript/17742/package.json @@ -9,12 +9,17 @@ "test:repro": "npm run build && bash test-repro.sh" }, "dependencies": { + "@nestjs/bullmq": "^11.0.4", "@nestjs/common": "^10.0.0", "@nestjs/core": "^10.0.0", + "@nestjs/event-emitter": "^3.0.1", "@nestjs/platform-express": "^10.0.0", "@nestjs/schedule": "^6.1.1", "@sentry/nestjs": "^10.2.0", + "bullmq": "^5.69.3", "dotenv": "^17.3.1", + "graphile-worker": "^0.16.6", + "nestjs-graphile-worker": "^0.9.1", "reflect-metadata": "^0.2.2", "rxjs": "^7.8.1" }, diff --git a/sentry-javascript/17742/src/app.module.ts b/sentry-javascript/17742/src/app.module.ts index f2ec282..5c956d1 100644 --- a/sentry-javascript/17742/src/app.module.ts +++ b/sentry-javascript/17742/src/app.module.ts @@ -1,13 +1,85 @@ -import { Module } from "@nestjs/common"; +import { DynamicModule, Module } from "@nestjs/common"; import { APP_FILTER } from "@nestjs/core"; +import { EventEmitterModule } from "@nestjs/event-emitter"; import { ScheduleModule } from "@nestjs/schedule"; import { SentryGlobalFilter, SentryModule } from "@sentry/nestjs/setup"; import { AppController } from "./app.controller"; import { AppService } from "./app.service"; -import { BackgroundJobService } from "./background-job.service"; +import { ScheduleJobService } from "./schedule-job.service"; +import { EventJobService } from "./event-job.service"; + +/** + * Build imports and providers dynamically based on available services. + * - Schedule + EventEmitter: always enabled (no external deps) + * - BullMQ: enabled when REDIS_URL is set + * - Graphile Worker: enabled when DATABASE_URL is set + */ +function getOptionalImports(): DynamicModule[] { + const imports: DynamicModule[] = []; + + if (process.env.REDIS_URL) { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { BullModule } = require("@nestjs/bullmq"); + imports.push( + BullModule.forRoot({ connection: { url: process.env.REDIS_URL } }) + ); + imports.push(BullModule.registerQueue({ name: "background-queue" })); + console.log("[Config] BullMQ enabled (REDIS_URL set)"); + } catch (e) { + console.log("[Config] BullMQ not available"); + } + } else { + console.log("[Config] BullMQ disabled (set REDIS_URL to enable)"); + } + + if (process.env.DATABASE_URL) { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { GraphileWorkerModule } = require("nestjs-graphile-worker"); + imports.push( + GraphileWorkerModule.forRoot({ + connectionString: process.env.DATABASE_URL, + }) + ); + console.log("[Config] Graphile Worker enabled (DATABASE_URL set)"); + } catch (e) { + console.log("[Config] Graphile Worker not available"); + } + } else { + console.log("[Config] Graphile Worker disabled (set DATABASE_URL to enable)"); + } + + return imports; +} + +function getOptionalProviders(): any[] { + const providers: any[] = []; + + if (process.env.REDIS_URL) { + try { + const { BullmqJobProcessor, BullmqJobProducer } = require("./bullmq-job.processor"); + providers.push(BullmqJobProcessor, BullmqJobProducer); + } catch (e) {} + } + + if (process.env.DATABASE_URL) { + try { + const { GraphileJobHandler, GraphileJobProducer } = require("./graphile-job.service"); + providers.push(GraphileJobHandler, GraphileJobProducer); + } catch (e) {} + } + + return providers; +} @Module({ - imports: [SentryModule.forRoot(), ScheduleModule.forRoot()], + imports: [ + SentryModule.forRoot(), + ScheduleModule.forRoot(), + EventEmitterModule.forRoot(), + ...getOptionalImports(), + ], controllers: [AppController], providers: [ { @@ -15,7 +87,9 @@ import { BackgroundJobService } from "./background-job.service"; useClass: SentryGlobalFilter, }, AppService, - BackgroundJobService, + ScheduleJobService, + EventJobService, + ...getOptionalProviders(), ], }) export class AppModule {} diff --git a/sentry-javascript/17742/src/background-job.service.ts b/sentry-javascript/17742/src/background-job.service.ts deleted file mode 100644 index 1d1529a..0000000 --- a/sentry-javascript/17742/src/background-job.service.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { Injectable } from "@nestjs/common"; -import { Interval } from "@nestjs/schedule"; -import * as Sentry from "@sentry/nestjs"; - -/** - * Simulates a background job runner (like graphile-worker or BullMQ). - * These jobs run outside the HTTP request context, so they execute on the - * default isolation scope. Any breadcrumbs they add pollute the default scope, - * which then gets cloned into every new HTTP request's isolation scope. - */ -@Injectable() -export class BackgroundJobService { - private jobCount = 0; - - @Interval(3000) - handleBackgroundJob() { - this.jobCount++; - console.log(`[Background Job #${this.jobCount}] Running...`); - - // These breadcrumbs are added to the default isolation scope - // because this code runs outside any HTTP request context - Sentry.addBreadcrumb({ - category: "background-job", - message: `Background job #${this.jobCount} started`, - level: "info", - }); - Sentry.addBreadcrumb({ - category: "background-job", - message: `Background job #${this.jobCount} completed`, - level: "info", - }); - - console.log( - `[Background Job #${this.jobCount}] Done — 2 breadcrumbs added to default scope` - ); - } -} diff --git a/sentry-javascript/17742/src/bullmq-job.processor.ts b/sentry-javascript/17742/src/bullmq-job.processor.ts new file mode 100644 index 0000000..554126b --- /dev/null +++ b/sentry-javascript/17742/src/bullmq-job.processor.ts @@ -0,0 +1,49 @@ +import { Processor, WorkerHost } from "@nestjs/bullmq"; +import { Injectable, OnModuleInit } from "@nestjs/common"; +import { InjectQueue } from "@nestjs/bullmq"; +import { Queue, Job } from "bullmq"; +import * as Sentry from "@sentry/nestjs"; + +/** + * Case 3: @nestjs/bullmq + * BullMQ processors run outside HTTP request context on the default isolation scope. + */ +@Processor("background-queue") +export class BullmqJobProcessor extends WorkerHost { + private jobCount = 0; + + async process(job: Job): Promise { + this.jobCount++; + console.log( + `[@nestjs/bullmq] Processing job #${this.jobCount}: ${job.name}` + ); + + Sentry.addBreadcrumb({ + category: "bullmq-job", + message: `BullMQ job #${this.jobCount} processed`, + level: "info", + }); + + console.log(`[@nestjs/bullmq] Job #${this.jobCount} done`); + } +} + +/** + * Service that periodically adds jobs to the BullMQ queue. + */ +@Injectable() +export class BullmqJobProducer implements OnModuleInit { + private jobCount = 0; + + constructor(@InjectQueue("background-queue") private queue: Queue) {} + + onModuleInit() { + setInterval(async () => { + this.jobCount++; + await this.queue.add("background-task", { + id: this.jobCount, + }); + console.log(`[@nestjs/bullmq] Added job #${this.jobCount} to queue`); + }, 5000); + } +} diff --git a/sentry-javascript/17742/src/event-job.service.ts b/sentry-javascript/17742/src/event-job.service.ts new file mode 100644 index 0000000..8145126 --- /dev/null +++ b/sentry-javascript/17742/src/event-job.service.ts @@ -0,0 +1,36 @@ +import { Injectable } from "@nestjs/common"; +import { EventEmitter2 } from "@nestjs/event-emitter"; +import { OnEvent } from "@nestjs/event-emitter"; +import * as Sentry from "@sentry/nestjs"; + +/** + * Case 2: @nestjs/event-emitter + * Event handlers run outside HTTP request context on the default isolation scope. + */ +@Injectable() +export class EventJobService { + private eventCount = 0; + + constructor(private eventEmitter: EventEmitter2) { + // Emit events periodically to simulate background event processing + setInterval(() => { + this.eventEmitter.emit("background.task", { + id: Date.now(), + }); + }, 4000); + } + + @OnEvent("background.task") + handleBackgroundEvent(payload: { id: number }) { + this.eventCount++; + console.log(`[@nestjs/event-emitter] Event #${this.eventCount} received`); + + Sentry.addBreadcrumb({ + category: "event-job", + message: `Event handler #${this.eventCount} executed`, + level: "info", + }); + + console.log(`[@nestjs/event-emitter] Event #${this.eventCount} done`); + } +} diff --git a/sentry-javascript/17742/src/graphile-job.service.ts b/sentry-javascript/17742/src/graphile-job.service.ts new file mode 100644 index 0000000..9ad5545 --- /dev/null +++ b/sentry-javascript/17742/src/graphile-job.service.ts @@ -0,0 +1,58 @@ +import { Injectable, OnModuleInit } from "@nestjs/common"; +import { Task, TaskHandler } from "nestjs-graphile-worker"; +import { WorkerService } from "nestjs-graphile-worker"; +import * as Sentry from "@sentry/nestjs"; + +/** + * Case 4: nestjs-graphile-worker + * Graphile worker tasks run outside HTTP request context on the default isolation scope. + */ +@Injectable() +@Task("background-graphile-task") +export class GraphileJobHandler { + private taskCount = 0; + + @TaskHandler() + async handler(payload: { id: number }) { + this.taskCount++; + console.log( + `[nestjs-graphile-worker] Task #${this.taskCount} running` + ); + + Sentry.addBreadcrumb({ + category: "graphile-job", + message: `Graphile task #${this.taskCount} executed`, + level: "info", + }); + + console.log( + `[nestjs-graphile-worker] Task #${this.taskCount} done` + ); + } +} + +/** + * Service that periodically adds tasks to the graphile-worker queue. + */ +@Injectable() +export class GraphileJobProducer implements OnModuleInit { + private taskCount = 0; + + constructor(private readonly workerService: WorkerService) {} + + onModuleInit() { + setInterval(async () => { + this.taskCount++; + try { + await this.workerService.addJob("background-graphile-task", { + id: this.taskCount, + }); + console.log( + `[nestjs-graphile-worker] Added task #${this.taskCount} to queue` + ); + } catch (e) { + // Silently ignore if worker service isn't ready + } + }, 6000); + } +} diff --git a/sentry-javascript/17742/src/instrument.ts b/sentry-javascript/17742/src/instrument.ts index 7e5b6f8..1f891fb 100644 --- a/sentry-javascript/17742/src/instrument.ts +++ b/sentry-javascript/17742/src/instrument.ts @@ -3,24 +3,39 @@ import * as Sentry from "@sentry/nestjs"; Sentry.init({ dsn: process.env.SENTRY_DSN || "", - debug: true, + debug: false, tracesSampleRate: 1.0, beforeSend(event) { - // Log all breadcrumbs attached to the error event const breadcrumbs = event.breadcrumbs || []; console.log( `\n=== Sentry Event Breadcrumbs (${breadcrumbs.length} total) ===` ); + + // Group breadcrumbs by category + const categories = new Map(); breadcrumbs.forEach((bc, i) => { - console.log(` [${i}] category=${bc.category}, message=${bc.message}`); + const cat = bc.category || "unknown"; + categories.set(cat, (categories.get(cat) || 0) + 1); + console.log(` [${i}] category=${cat}, message=${bc.message}`); }); - // Check for leaked breadcrumbs from background jobs - const leaked = breadcrumbs.filter((b) => b.category === "background-job"); - if (leaked.length > 0) { + // Check for leaked breadcrumbs from each background job type + const leakCategories = [ + "schedule-job", + "event-job", + "bullmq-job", + "graphile-job", + ]; + + const leaks = leakCategories + .filter((cat) => categories.has(cat)) + .map((cat) => `${cat}: ${categories.get(cat)}`); + + if (leaks.length > 0) { console.log( - `\n*** BUG CONFIRMED: ${leaked.length} breadcrumbs leaked from background jobs! ***` + `\n*** BUG CONFIRMED: Breadcrumbs leaked from background jobs! ***` ); + console.log(` Leaked: ${leaks.join(", ")}`); } else { console.log("\n No leaked breadcrumbs detected (isolation working)."); } diff --git a/sentry-javascript/17742/src/main.ts b/sentry-javascript/17742/src/main.ts index 4e6cf8b..c1f0591 100644 --- a/sentry-javascript/17742/src/main.ts +++ b/sentry-javascript/17742/src/main.ts @@ -10,15 +10,14 @@ async function bootstrap() { console.log(""); console.log("To reproduce the breadcrumb leaking issue:"); console.log( - "1. Wait ~10 seconds for a few background jobs to run and pollute the default scope" + "1. Wait ~10 seconds for background jobs to pollute the default scope" ); console.log("2. curl http://localhost:3000/trigger-error"); console.log(""); - console.log( - "Expected: The error event should only contain breadcrumbs from the HTTP request" - ); - console.log( - "Actual: The error event also contains breadcrumbs from background jobs" - ); + console.log("Background job sources:"); + console.log(" - @nestjs/schedule @Interval (always active)"); + console.log(" - @nestjs/event-emitter @OnEvent (always active)"); + console.log(" - @nestjs/bullmq @Processor (requires REDIS_URL)"); + console.log(" - nestjs-graphile-worker @Task (requires DATABASE_URL)"); } bootstrap(); diff --git a/sentry-javascript/17742/src/schedule-job.service.ts b/sentry-javascript/17742/src/schedule-job.service.ts new file mode 100644 index 0000000..0479604 --- /dev/null +++ b/sentry-javascript/17742/src/schedule-job.service.ts @@ -0,0 +1,26 @@ +import { Injectable } from "@nestjs/common"; +import { Interval } from "@nestjs/schedule"; +import * as Sentry from "@sentry/nestjs"; + +/** + * Case 1: @nestjs/schedule + * Scheduled jobs run outside HTTP request context on the default isolation scope. + */ +@Injectable() +export class ScheduleJobService { + private jobCount = 0; + + @Interval(3000) + handleScheduledJob() { + this.jobCount++; + console.log(`[@nestjs/schedule] Job #${this.jobCount} running`); + + Sentry.addBreadcrumb({ + category: "schedule-job", + message: `Scheduled job #${this.jobCount} executed`, + level: "info", + }); + + console.log(`[@nestjs/schedule] Job #${this.jobCount} done`); + } +} diff --git a/sentry-javascript/17742/test-repro.sh b/sentry-javascript/17742/test-repro.sh index 9588e5f..a921ef3 100755 --- a/sentry-javascript/17742/test-repro.sh +++ b/sentry-javascript/17742/test-repro.sh @@ -1,16 +1,20 @@ #!/bin/bash # Reproduction script for sentry-javascript#17742 -# Background job breadcrumbs leaking into HTTP request error events +# Tests breadcrumb leaking from all background job frameworks: +# - @nestjs/schedule (always active) +# - @nestjs/event-emitter (always active) +# - @nestjs/bullmq (requires REDIS_URL) +# - nestjs-graphile-worker (requires DATABASE_URL) echo "=== Starting NestJS server ===" node dist/main.js & APP_PID=$! echo "Waiting for server to start and background jobs to run..." -sleep 10 +sleep 12 echo "" -echo "=== Triggering error after background jobs have polluted the default scope ===" +echo "=== Triggering error to check for leaked breadcrumbs ===" curl -s http://localhost:3000/trigger-error echo "" From 7f1f96b7ed291ad5d3af78eb9992bd96a6196fbb Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 20 Feb 2026 12:24:57 +0100 Subject: [PATCH 5/5] Fix graphile worker runner not starting and increase wait time The graphile-worker runner wasn't being started, so tasks were queued but never processed. Now all 4 frameworks confirmed leaking: schedule-job, event-job, bullmq-job, graphile-job. Co-Authored-By: Claude Opus 4.6 --- sentry-javascript/17742/src/graphile-job.service.ts | 7 ++++++- sentry-javascript/17742/test-repro.sh | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/sentry-javascript/17742/src/graphile-job.service.ts b/sentry-javascript/17742/src/graphile-job.service.ts index 9ad5545..815a226 100644 --- a/sentry-javascript/17742/src/graphile-job.service.ts +++ b/sentry-javascript/17742/src/graphile-job.service.ts @@ -40,7 +40,12 @@ export class GraphileJobProducer implements OnModuleInit { constructor(private readonly workerService: WorkerService) {} - onModuleInit() { + async onModuleInit() { + // Start the graphile-worker runner so it actually processes tasks + this.workerService.run().catch((err) => { + console.error("[nestjs-graphile-worker] Runner error:", err); + }); + setInterval(async () => { this.taskCount++; try { diff --git a/sentry-javascript/17742/test-repro.sh b/sentry-javascript/17742/test-repro.sh index a921ef3..64e65b9 100755 --- a/sentry-javascript/17742/test-repro.sh +++ b/sentry-javascript/17742/test-repro.sh @@ -11,7 +11,7 @@ node dist/main.js & APP_PID=$! echo "Waiting for server to start and background jobs to run..." -sleep 12 +sleep 18 echo "" echo "=== Triggering error to check for leaked breadcrumbs ==="