Skip to content

Commit 84a9768

Browse files
authored
feat(nestjs): Instrument @nestjs/bullmq (#19759)
Add automatic instrumentation for BullMQ queue processors in NestJS via the `@Processor` decorator from `@nestjs/bullmq`. Wraps the `process()` method on `@Processor` decorated classes to fork an isolation scope per job, create a `queue.process` transaction, and capture unhandled exceptions. Also adds a dedicated `nestjs-bullmq` e2e test application to test this setup. Note: `@OnWorkerEvent` lifecycle handlers (e.g. `completed`, `failed`) run outside the isolation scope created by `process()`. Breadcrumbs/tags set in these handlers leak to the default isolation scope. This is documented with a test and might be addressed in a follow-up. Closes #12823
1 parent f04a0dc commit 84a9768

File tree

22 files changed

+759
-2
lines changed

22 files changed

+759
-2
lines changed

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@
44

55
- "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott
66

7+
- **feat(nestjs): Instrument `@nestjs/bullmq` `@Processor` decorator**
8+
9+
Automatically capture exceptions and create transactions for BullMQ queue processors in NestJS applications.
10+
11+
When using the `@Processor` decorator from `@nestjs/bullmq`, the SDK now automatically wraps the `process()` method
12+
to create `queue.process` transactions with proper isolation scopes, preventing breadcrumb and scope leakage between
13+
jobs and HTTP requests. Errors thrown in processors are captured with the `auto.queue.nestjs.bullmq` mechanism type.
14+
15+
Requires `@nestjs/bullmq` v10.0.0 or later.
16+
717
- **feat(node): Expose `headersToSpanAttributes` option on `nativeNodeFetchIntegration()` ([#19770](https://github.com/getsentry/sentry-javascript/pull/19770))**
818

919
Response headers like `http.response.header.content-length` were previously captured automatically on outgoing
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
services:
2+
redis:
3+
image: redis:8
4+
restart: always
5+
container_name: e2e-tests-nestjs-bullmq-redis
6+
ports:
7+
- '6379:6379'
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalSetup() {
8+
// Start Redis via Docker Compose
9+
execSync('docker compose up -d --wait', {
10+
cwd: __dirname,
11+
stdio: 'inherit',
12+
});
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { execSync } from 'child_process';
2+
import { dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
7+
export default async function globalTeardown() {
8+
// Stop Redis and remove containers
9+
execSync('docker compose down --volumes', {
10+
cwd: __dirname,
11+
stdio: 'inherit',
12+
});
13+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "https://json.schemastore.org/nest-cli",
3+
"collection": "@nestjs/schematics",
4+
"sourceRoot": "src",
5+
"compilerOptions": {
6+
"deleteOutDir": true
7+
}
8+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "nestjs-bullmq",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"build": "nest build",
7+
"start": "nest start",
8+
"start:dev": "nest start --watch",
9+
"start:prod": "node dist/main",
10+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
11+
"test": "playwright test",
12+
"test:build": "pnpm install && pnpm build",
13+
"test:assert": "pnpm test"
14+
},
15+
"dependencies": {
16+
"@nestjs/common": "^11.0.0",
17+
"@nestjs/core": "^11.0.0",
18+
"@nestjs/platform-express": "^11.0.0",
19+
"@nestjs/bullmq": "^11.0.0",
20+
"bullmq": "^5.0.0",
21+
"@sentry/nestjs": "latest || *",
22+
"reflect-metadata": "^0.2.0",
23+
"rxjs": "^7.8.1"
24+
},
25+
"devDependencies": {
26+
"@playwright/test": "~1.56.0",
27+
"@sentry-internal/test-utils": "link:../../../test-utils",
28+
"@nestjs/cli": "^11.0.0",
29+
"@nestjs/schematics": "^11.0.0",
30+
"@types/node": "^18.19.1",
31+
"typescript": "~5.0.0"
32+
},
33+
"volta": {
34+
"extends": "../../package.json"
35+
}
36+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default {
8+
...config,
9+
globalSetup: './global-setup.mjs',
10+
globalTeardown: './global-teardown.mjs',
11+
};
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { Controller, Get, Param } from '@nestjs/common';
2+
import { InjectQueue } from '@nestjs/bullmq';
3+
import { Queue } from 'bullmq';
4+
5+
@Controller()
6+
export class AppController {
7+
constructor(@InjectQueue('test-queue') private readonly queue: Queue) {}
8+
9+
@Get('enqueue/:name')
10+
async enqueue(@Param('name') name: string) {
11+
await this.queue.add(name, { timestamp: Date.now() });
12+
return { queued: true };
13+
}
14+
15+
@Get('check-isolation')
16+
checkIsolation() {
17+
// This endpoint is called after the processor adds a breadcrumb.
18+
// The test verifies that breadcrumbs from the processor do NOT leak here.
19+
return { message: 'ok' };
20+
}
21+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Module } from '@nestjs/common';
2+
import { APP_FILTER } from '@nestjs/core';
3+
import { BullModule } from '@nestjs/bullmq';
4+
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
5+
import { AppController } from './app.controller';
6+
import { TestProcessor } from './jobs/test.processor';
7+
8+
@Module({
9+
imports: [
10+
SentryModule.forRoot(),
11+
BullModule.forRoot({
12+
connection: { host: 'localhost', port: 6379 },
13+
}),
14+
BullModule.registerQueue({ name: 'test-queue' }),
15+
],
16+
controllers: [AppController],
17+
providers: [
18+
TestProcessor,
19+
{
20+
provide: APP_FILTER,
21+
useClass: SentryGlobalFilter,
22+
},
23+
],
24+
})
25+
export class AppModule {}

0 commit comments

Comments
 (0)