From 611a260189390fe17808f5e7d546f1179ac7ae6b Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 4 Mar 2026 16:29:34 +0100 Subject: [PATCH 1/5] Add websockets e2e for nestjs --- .../nestjs-websockets/nest-cli.json | 8 +++++ .../nestjs-websockets/package.json | 33 +++++++++++++++++++ .../nestjs-websockets/playwright.config.mjs | 7 ++++ .../nestjs-websockets/src/app.controller.ts | 11 +++++++ .../nestjs-websockets/src/app.gateway.ts | 25 ++++++++++++++ .../nestjs-websockets/src/app.module.ts | 18 ++++++++++ .../nestjs-websockets/src/instrument.ts | 11 +++++++ .../nestjs-websockets/src/main.ts | 15 +++++++++ .../nestjs-websockets/start-event-proxy.mjs | 6 ++++ .../nestjs-websockets/tests/errors.test.ts | 23 +++++++++++++ .../tests/transactions.test.ts | 18 ++++++++++ .../nestjs-websockets/tsconfig.json | 22 +++++++++++++ 12 files changed, 197 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/nest-cli.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.module.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-websockets/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-websockets/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/nest-cli.json @@ -0,0 +1,8 @@ +{ + "$schema": "https://json.schemastore.org/nest-cli", + "collection": "@nestjs/schematics", + "sourceRoot": "src", + "compilerOptions": { + "deleteOutDir": true + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json new file mode 100644 index 000000000000..3544c9bed7cd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json @@ -0,0 +1,33 @@ +{ + "name": "nestjs-websockets", + "version": "0.0.1", + "private": true, + "scripts": { + "build": "nest build", + "start": "nest start", + "test": "playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test" + }, + "dependencies": { + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", + "@nestjs/platform-socket.io": "^10.0.0", + "@sentry/nestjs": "latest || *", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.1" + }, + "devDependencies": { + "@playwright/test": "~1.56.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "@nestjs/cli": "^10.0.0", + "@types/node": "^18.19.1", + "socket.io-client": "^4.0.0", + "typescript": "~5.0.0" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-websockets/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/playwright.config.mjs @@ -0,0 +1,7 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `pnpm start`, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts new file mode 100644 index 000000000000..c1f085b11291 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts @@ -0,0 +1,11 @@ +import { Controller, Get } from '@nestjs/common'; +import { flush } from '@sentry/nestjs'; + +@Controller() +export class AppController { + @Get('/flush') + async flush() { + await flush(); + return 'ok'; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts new file mode 100644 index 000000000000..5231385ef80b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts @@ -0,0 +1,25 @@ +import { SubscribeMessage, WebSocketGateway, MessageBody } from '@nestjs/websockets'; +import * as Sentry from '@sentry/nestjs'; + +@WebSocketGateway({ cors: true }) +export class AppGateway { + @SubscribeMessage('test-message') + handleTestMessage(@MessageBody() data: { message: string }) { + return { event: 'test-response', data: { message: data.message } }; + } + + @SubscribeMessage('test-exception') + handleTestException() { + throw new Error('This is an exception in a WebSocket handler'); + } + + @SubscribeMessage('test-manual-capture') + handleManualCapture() { + try { + throw new Error('Manually captured WebSocket error'); + } catch (e) { + Sentry.captureException(e); + } + return { event: 'capture-response', data: { success: true } }; + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.module.ts new file mode 100644 index 000000000000..96386d3cf29f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { APP_FILTER } from '@nestjs/core'; +import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup'; +import { AppController } from './app.controller'; +import { AppGateway } from './app.gateway'; + +@Module({ + imports: [SentryModule.forRoot()], + controllers: [AppController], + providers: [ + { + provide: APP_FILTER, + useClass: SentryGlobalFilter, + }, + AppGateway, + ], +}) +export class AppModule {} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts new file mode 100644 index 000000000000..e0a1cead1153 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/instrument.ts @@ -0,0 +1,11 @@ +import * as Sentry from '@sentry/nestjs'; + +Sentry.init({ + environment: 'qa', + dsn: process.env.E2E_TEST_DSN, + tunnel: `http://localhost:3031/`, + tracesSampleRate: 1, + transportOptions: { + bufferSize: 1000, + }, +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/main.ts new file mode 100644 index 000000000000..71ce685f4d61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/main.ts @@ -0,0 +1,15 @@ +// Import this first +import './instrument'; + +// Import other modules +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +const PORT = 3030; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(PORT); +} + +bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-websockets/start-event-proxy.mjs new file mode 100644 index 000000000000..1fe76699833c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nestjs-websockets', +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts new file mode 100644 index 000000000000..99d938e1c05a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts @@ -0,0 +1,23 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import { io } from 'socket.io-client'; + +test('Captures manually reported error in WebSocket gateway handler', async ({ baseURL }) => { + const errorPromise = waitForError('nestjs-websockets', event => { + return event.exception?.values?.[0]?.value === 'Manually captured WebSocket error'; + }); + + const socket = io(baseURL!); + await new Promise(resolve => socket.on('connect', resolve)); + + socket.emit('test-manual-capture', {}); + + const error = await errorPromise; + + expect(error.exception?.values?.[0]).toMatchObject({ + type: 'Error', + value: 'Manually captured WebSocket error', + }); + + socket.disconnect(); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts new file mode 100644 index 000000000000..bdff08f2ff67 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('Sends an HTTP transaction for the flush endpoint', async ({ baseURL }) => { + const txPromise = waitForTransaction('nestjs-websockets', tx => { + return tx?.contexts?.trace?.op === 'http.server' && tx?.transaction === 'GET /flush'; + }); + + await fetch(`${baseURL}/flush`); + + const tx = await txPromise; + + expect(tx.contexts?.trace).toEqual( + expect.objectContaining({ + op: 'http.server', + }), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tsconfig.json new file mode 100644 index 000000000000..cf79f029c781 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tsconfig.json @@ -0,0 +1,22 @@ +{ + "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, + "moduleResolution": "Node16" + } +} From 930412d376ec1e62e3d2e4c026dfbf258eb34d12 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 4 Mar 2026 16:39:40 +0100 Subject: [PATCH 2/5] . --- .../nestjs-websockets/src/app.controller.ts | 8 +++----- .../nestjs-websockets/src/app.gateway.ts | 7 +------ .../nestjs-websockets/tests/transactions.test.ts | 6 +++--- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts index c1f085b11291..e5e867d95312 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.controller.ts @@ -1,11 +1,9 @@ import { Controller, Get } from '@nestjs/common'; -import { flush } from '@sentry/nestjs'; @Controller() export class AppController { - @Get('/flush') - async flush() { - await flush(); - return 'ok'; + @Get('/test-transaction') + testTransaction() { + return { message: 'ok' }; } } diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts index 5231385ef80b..712d47aba4d2 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/src/app.gateway.ts @@ -1,13 +1,8 @@ import { SubscribeMessage, WebSocketGateway, MessageBody } from '@nestjs/websockets'; import * as Sentry from '@sentry/nestjs'; -@WebSocketGateway({ cors: true }) +@WebSocketGateway() export class AppGateway { - @SubscribeMessage('test-message') - handleTestMessage(@MessageBody() data: { message: string }) { - return { event: 'test-response', data: { message: data.message } }; - } - @SubscribeMessage('test-exception') handleTestException() { throw new Error('This is an exception in a WebSocket handler'); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts index bdff08f2ff67..d701897cfa56 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/transactions.test.ts @@ -1,12 +1,12 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test('Sends an HTTP transaction for the flush endpoint', async ({ baseURL }) => { +test('Sends an HTTP transaction', async ({ baseURL }) => { const txPromise = waitForTransaction('nestjs-websockets', tx => { - return tx?.contexts?.trace?.op === 'http.server' && tx?.transaction === 'GET /flush'; + return tx?.contexts?.trace?.op === 'http.server' && tx?.transaction === 'GET /test-transaction'; }); - await fetch(`${baseURL}/flush`); + await fetch(`${baseURL}/test-transaction`); const tx = await txPromise; From 9dcfb1319c9b75c17ff20d7023115aa36d59da68 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 4 Mar 2026 16:57:06 +0100 Subject: [PATCH 3/5] Add test that verifies that exceptions are not auto-captured --- .../nestjs-websockets/tests/errors.test.ts | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts index 99d938e1c05a..e6843799f05d 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/tests/errors.test.ts @@ -21,3 +21,35 @@ test('Captures manually reported error in WebSocket gateway handler', async ({ b socket.disconnect(); }); + +// There is no good mechanism to verify that an event was NOT sent to Sentry. +// The idea here is that we first send a message that triggers an exception which won't be auto-captured, +// and then send a message that triggers a manually captured error which will be sent to Sentry. +// If the manually captured error arrives, we can deduce that the first exception was not sent, +// because Socket.IO guarantees message ordering: https://socket.io/docs/v4/delivery-guarantees +test('Does not automatically capture exceptions in WebSocket gateway handler', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-websockets', event => { + if (!event.type && event.exception?.values?.[0]?.value === 'This is an exception in a WebSocket handler') { + errorEventOccurred = true; + } + + return false; + }); + + const manualCapturePromise = waitForError('nestjs-websockets', event => { + return event.exception?.values?.[0]?.value === 'Manually captured WebSocket error'; + }); + + const socket = io(baseURL!); + await new Promise(resolve => socket.on('connect', resolve)); + + socket.emit('test-exception', {}); + socket.emit('test-manual-capture', {}); + await manualCapturePromise; + + expect(errorEventOccurred).toBe(false); + + socket.disconnect(); +}); From 8df4927494c140be5bfeb655225206365222b3cb Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 4 Mar 2026 17:28:39 +0100 Subject: [PATCH 4/5] v11 --- .../test-applications/nestjs-websockets/package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json index 3544c9bed7cd..8fd9301c7ce0 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json @@ -10,11 +10,11 @@ "test:assert": "pnpm test" }, "dependencies": { - "@nestjs/common": "^10.0.0", - "@nestjs/core": "^10.0.0", - "@nestjs/platform-express": "^10.0.0", - "@nestjs/websockets": "^10.0.0", - "@nestjs/platform-socket.io": "^10.0.0", + "@nestjs/common": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/platform-express": "^11.0.0", + "@nestjs/websockets": "^11.0.0", + "@nestjs/platform-socket.io": "^11.0.0", "@sentry/nestjs": "latest || *", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1" @@ -22,7 +22,7 @@ "devDependencies": { "@playwright/test": "~1.56.0", "@sentry-internal/test-utils": "link:../../../test-utils", - "@nestjs/cli": "^10.0.0", + "@nestjs/cli": "^11.0.0", "@types/node": "^18.19.1", "socket.io-client": "^4.0.0", "typescript": "~5.0.0" From 7d9e99b764a82009734a321abe7aef362965f2c5 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Wed, 4 Mar 2026 17:33:34 +0100 Subject: [PATCH 5/5] add latest variant to canary tests --- .github/workflows/canary.yml | 3 +++ .../e2e-tests/test-applications/nestjs-websockets/package.json | 1 + 2 files changed, 4 insertions(+) diff --git a/.github/workflows/canary.yml b/.github/workflows/canary.yml index 252bbc831239..0235dc00e00a 100644 --- a/.github/workflows/canary.yml +++ b/.github/workflows/canary.yml @@ -114,6 +114,9 @@ jobs: - test-application: 'nestjs-11' build-command: 'test:build-latest' label: 'nestjs-11 (latest)' + - test-application: 'nestjs-websockets' + build-command: 'test:build-latest' + label: 'nestjs-websockets (latest)' steps: - name: Check out current commit diff --git a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json index 8fd9301c7ce0..6356b48b322f 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-websockets/package.json @@ -7,6 +7,7 @@ "start": "nest start", "test": "playwright test", "test:build": "pnpm install && pnpm build", + "test:build-latest": "pnpm install && pnpm add @nestjs/common@latest @nestjs/core@latest @nestjs/platform-express@latest @nestjs/websockets@latest @nestjs/platform-socket.io@latest && pnpm add -D @nestjs/cli@latest && pnpm build", "test:assert": "pnpm test" }, "dependencies": {