diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/.gitignore b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.gitignore
new file mode 100644
index 000000000000..4b56acfbebf4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.gitignore
@@ -0,0 +1,56 @@
+# compiled output
+/dist
+/node_modules
+/build
+
+# Logs
+logs
+*.log
+npm-debug.log*
+pnpm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+
+# OS
+.DS_Store
+
+# Tests
+/coverage
+/.nyc_output
+
+# IDEs and editors
+/.idea
+.project
+.classpath
+.c9/
+*.launch
+.settings/
+*.sublime-workspace
+
+# IDE - VSCode
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# temp directory
+.temp
+.tmp
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc
new file mode 100644
index 000000000000..070f80f05092
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc
@@ -0,0 +1,2 @@
+@sentry:registry=http://127.0.0.1:4873
+@sentry-internal:registry=http://127.0.0.1:4873
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/README.md b/dev-packages/e2e-tests/test-applications/nestjs-fastify/README.md
new file mode 100644
index 000000000000..63c3e90d9b1a
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/README.md
@@ -0,0 +1,85 @@
+
+
+
+
+[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
+[circleci-url]: https://circleci.com/gh/nestjs/nest
+
+ A progressive Node.js framework for building efficient and scalable server-side applications.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+## Description
+
+[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
+
+## Project setup
+
+```bash
+$ yarn install
+```
+
+## Compile and run the project
+
+```bash
+# development
+$ yarn run start
+
+# watch mode
+$ yarn run start:dev
+
+# production mode
+$ yarn run start:prod
+```
+
+## Run tests
+
+```bash
+# unit tests
+$ yarn run test
+
+# e2e tests
+$ yarn run test:e2e
+
+# test coverage
+$ yarn run test:cov
+```
+
+## Resources
+
+Check out a few resources that may come in handy when working with NestJS:
+
+- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
+- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
+- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
+- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
+- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
+- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
+- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
+
+## Support
+
+Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
+
+## Stay in touch
+
+- Author - [Kamil MyĆliwiec](https://twitter.com/kammysliwiec)
+- Website - [https://nestjs.com](https://nestjs.com/)
+- Twitter - [@nestframework](https://twitter.com/nestframework)
+
+## License
+
+Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/nest-cli.json
new file mode 100644
index 000000000000..f9aa683b1ad5
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/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-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
new file mode 100644
index 000000000000..6da132e74a4c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "nestjs-fastify",
+ "version": "0.0.1",
+ "private": true,
+ "scripts": {
+ "build": "nest build",
+ "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
+ "start": "nest start",
+ "start:dev": "nest start --watch",
+ "start:debug": "nest start --debug --watch",
+ "start:prod": "node dist/main",
+ "clean": "npx rimraf node_modules pnpm-lock.yaml",
+ "test": "playwright test",
+ "test:build": "pnpm install",
+ "test:assert": "pnpm test"
+ },
+ "dependencies": {
+ "@nestjs/common": "^10.0.0",
+ "@nestjs/core": "^10.0.0",
+ "@nestjs/microservices": "^10.0.0",
+ "@nestjs/schedule": "^4.1.0",
+ "@nestjs/platform-fastify": "^10.0.0",
+ "@sentry/nestjs": "latest || *",
+ "reflect-metadata": "^0.2.0",
+ "rxjs": "^7.8.1",
+ "fastify": "^4.28.1"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.44.1",
+ "@sentry-internal/test-utils": "link:../../../test-utils",
+ "@nestjs/cli": "^10.0.0",
+ "@nestjs/schematics": "^10.0.0",
+ "@nestjs/testing": "^10.0.0",
+ "@types/node": "18.15.1",
+ "@types/supertest": "^6.0.0",
+ "@typescript-eslint/eslint-plugin": "^6.0.0",
+ "@typescript-eslint/parser": "^6.0.0",
+ "eslint": "^8.42.0",
+ "eslint-config-prettier": "^9.0.0",
+ "eslint-plugin-prettier": "^5.0.0",
+ "prettier": "^3.0.0",
+ "source-map-support": "^0.5.21",
+ "supertest": "^6.3.3",
+ "ts-loader": "^9.4.3",
+ "tsconfig-paths": "^4.2.0",
+ "typescript": "^5.1.3"
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-fastify/playwright.config.mjs
new file mode 100644
index 000000000000..31f2b913b58b
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/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-fastify/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts
new file mode 100644
index 000000000000..33a6b1957d99
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts
@@ -0,0 +1,124 @@
+import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
+import { flush } from '@sentry/nestjs';
+import { AppService } from './app.service';
+import { AsyncInterceptor } from './async-example.interceptor';
+import { ExampleInterceptor1 } from './example-1.interceptor';
+import { ExampleInterceptor2 } from './example-2.interceptor';
+import { ExampleExceptionGlobalFilter } from './example-global-filter.exception';
+import { ExampleExceptionLocalFilter } from './example-local-filter.exception';
+import { ExampleLocalFilter } from './example-local.filter';
+import { ExampleGuard } from './example.guard';
+
+@Controller()
+@UseFilters(ExampleLocalFilter)
+export class AppController {
+ constructor(private readonly appService: AppService) {}
+
+ @Get('test-transaction')
+ testTransaction() {
+ return this.appService.testTransaction();
+ }
+
+ @Get('test-middleware-instrumentation')
+ testMiddlewareInstrumentation() {
+ return this.appService.testSpan();
+ }
+
+ @Get('test-guard-instrumentation')
+ @UseGuards(ExampleGuard)
+ testGuardInstrumentation() {
+ return {};
+ }
+
+ @Get('test-interceptor-instrumentation')
+ @UseInterceptors(ExampleInterceptor1, ExampleInterceptor2)
+ testInterceptorInstrumentation() {
+ return this.appService.testSpan();
+ }
+
+ @Get('test-async-interceptor-instrumentation')
+ @UseInterceptors(AsyncInterceptor)
+ testAsyncInterceptorInstrumentation() {
+ return this.appService.testSpan();
+ }
+
+ @Get('test-pipe-instrumentation/:id')
+ testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) {
+ return { value: id };
+ }
+
+ @Get('test-exception/:id')
+ async testException(@Param('id') id: string) {
+ return this.appService.testException(id);
+ }
+
+ @Get('test-expected-400-exception/:id')
+ async testExpected400Exception(@Param('id') id: string) {
+ return this.appService.testExpected400Exception(id);
+ }
+
+ @Get('test-expected-500-exception/:id')
+ async testExpected500Exception(@Param('id') id: string) {
+ return this.appService.testExpected500Exception(id);
+ }
+
+ @Get('test-expected-rpc-exception/:id')
+ async testExpectedRpcException(@Param('id') id: string) {
+ return this.appService.testExpectedRpcException(id);
+ }
+
+ @Get('test-span-decorator-async')
+ async testSpanDecoratorAsync() {
+ return { result: await this.appService.testSpanDecoratorAsync() };
+ }
+
+ @Get('test-span-decorator-sync')
+ async testSpanDecoratorSync() {
+ return { result: await this.appService.testSpanDecoratorSync() };
+ }
+
+ @Get('kill-test-cron/:job')
+ async killTestCron(@Param('job') job: string) {
+ this.appService.killTestCron(job);
+ }
+
+ @Get('flush')
+ async flush() {
+ await flush();
+ }
+
+ @Get('example-exception-global-filter')
+ async exampleExceptionGlobalFilter() {
+ throw new ExampleExceptionGlobalFilter();
+ }
+
+ @Get('example-exception-local-filter')
+ async exampleExceptionLocalFilter() {
+ throw new ExampleExceptionLocalFilter();
+ }
+
+ @Get('test-service-use')
+ testServiceWithUseMethod() {
+ return this.appService.use();
+ }
+
+ @Get('test-service-transform')
+ testServiceWithTransform() {
+ return this.appService.transform();
+ }
+
+ @Get('test-service-intercept')
+ testServiceWithIntercept() {
+ return this.appService.intercept();
+ }
+
+ @Get('test-service-canActivate')
+ testServiceWithCanActivate() {
+ return this.appService.canActivate();
+ }
+
+ @Get('test-function-name')
+ testFunctionName() {
+ return this.appService.getFunctionName();
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.module.ts
new file mode 100644
index 000000000000..3de3c82dc925
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.module.ts
@@ -0,0 +1,29 @@
+import { MiddlewareConsumer, 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 { ExampleGlobalFilter } from './example-global.filter';
+import { ExampleMiddleware } from './example.middleware';
+
+@Module({
+ imports: [SentryModule.forRoot(), ScheduleModule.forRoot()],
+ controllers: [AppController],
+ providers: [
+ AppService,
+ {
+ provide: APP_FILTER,
+ useClass: SentryGlobalFilter,
+ },
+ {
+ provide: APP_FILTER,
+ useClass: ExampleGlobalFilter,
+ },
+ ],
+})
+export class AppModule {
+ configure(consumer: MiddlewareConsumer): void {
+ consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts
new file mode 100644
index 000000000000..242b4c778a0e
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts
@@ -0,0 +1,113 @@
+import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
+import { RpcException } from '@nestjs/microservices';
+import { Cron, SchedulerRegistry } from '@nestjs/schedule';
+import type { MonitorConfig } from '@sentry/core';
+import * as Sentry from '@sentry/nestjs';
+import { SentryCron, SentryTraced } from '@sentry/nestjs';
+
+const monitorConfig: MonitorConfig = {
+ schedule: {
+ type: 'crontab',
+ value: '* * * * *',
+ },
+};
+
+@Injectable()
+export class AppService {
+ constructor(private schedulerRegistry: SchedulerRegistry) {}
+
+ testTransaction() {
+ Sentry.startSpan({ name: 'test-span' }, () => {
+ Sentry.startSpan({ name: 'child-span' }, () => {});
+ });
+ }
+
+ testSpan() {
+ // span that should not be a child span of the middleware span
+ Sentry.startSpan({ name: 'test-controller-span' }, () => {});
+ }
+
+ testException(id: string) {
+ throw new Error(`This is an exception with id ${id}`);
+ }
+
+ testExpected400Exception(id: string) {
+ throw new HttpException(`This is an expected 400 exception with id ${id}`, HttpStatus.BAD_REQUEST);
+ }
+
+ testExpected500Exception(id: string) {
+ throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR);
+ }
+
+ testExpectedRpcException(id: string) {
+ throw new RpcException(`This is an expected RPC exception with id ${id}`);
+ }
+
+ @SentryTraced('wait and return a string')
+ async wait() {
+ await new Promise(resolve => setTimeout(resolve, 500));
+ return 'test';
+ }
+
+ async testSpanDecoratorAsync() {
+ return await this.wait();
+ }
+
+ @SentryTraced('return a string')
+ getString(): { result: string } {
+ return { result: 'test' };
+ }
+
+ @SentryTraced('return the function name')
+ getFunctionName(): { result: string } {
+ return { result: this.getFunctionName.name };
+ }
+
+ async testSpanDecoratorSync() {
+ const returned = this.getString();
+ // Will fail if getString() is async, because returned will be a Promise<>
+ return returned.result;
+ }
+
+ /*
+ Actual cron schedule differs from schedule defined in config because Sentry
+ only supports minute granularity, but we don't want to wait (worst case) a
+ full minute for the tests to finish.
+ */
+ @Cron('*/5 * * * * *', { name: 'test-cron-job' })
+ @SentryCron('test-cron-slug', monitorConfig)
+ async testCron() {
+ console.log('Test cron!');
+ }
+
+ /*
+ Actual cron schedule differs from schedule defined in config because Sentry
+ only supports minute granularity, but we don't want to wait (worst case) a
+ full minute for the tests to finish.
+ */
+ @Cron('*/5 * * * * *', { name: 'test-cron-error' })
+ @SentryCron('test-cron-error-slug', monitorConfig)
+ async testCronError() {
+ throw new Error('Test error from cron job');
+ }
+
+ async killTestCron(job: string) {
+ this.schedulerRegistry.deleteCronJob(job);
+ }
+
+ use() {
+ console.log('Test use!');
+ }
+
+ transform() {
+ console.log('Test transform!');
+ }
+
+ intercept() {
+ console.log('Test intercept!');
+ }
+
+ canActivate() {
+ console.log('Test canActivate!');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/async-example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/async-example.interceptor.ts
new file mode 100644
index 000000000000..ac0ee60acc51
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/async-example.interceptor.ts
@@ -0,0 +1,17 @@
+import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import * as Sentry from '@sentry/nestjs';
+import { tap } from 'rxjs';
+
+@Injectable()
+export class AsyncInterceptor implements NestInterceptor {
+ intercept(context: ExecutionContext, next: CallHandler) {
+ Sentry.startSpan({ name: 'test-async-interceptor-span' }, () => {});
+ return Promise.resolve(
+ next.handle().pipe(
+ tap(() => {
+ Sentry.startSpan({ name: 'test-async-interceptor-span-after-route' }, () => {});
+ }),
+ ),
+ );
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-1.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-1.interceptor.ts
new file mode 100644
index 000000000000..81c9f70d30e2
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-1.interceptor.ts
@@ -0,0 +1,15 @@
+import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import * as Sentry from '@sentry/nestjs';
+import { tap } from 'rxjs';
+
+@Injectable()
+export class ExampleInterceptor1 implements NestInterceptor {
+ intercept(context: ExecutionContext, next: CallHandler) {
+ Sentry.startSpan({ name: 'test-interceptor-span-1' }, () => {});
+ return next.handle().pipe(
+ tap(() => {
+ Sentry.startSpan({ name: 'test-interceptor-span-after-route' }, () => {});
+ }),
+ );
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-2.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-2.interceptor.ts
new file mode 100644
index 000000000000..2cf9dfb9e043
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-2.interceptor.ts
@@ -0,0 +1,10 @@
+import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
+import * as Sentry from '@sentry/nestjs';
+
+@Injectable()
+export class ExampleInterceptor2 implements NestInterceptor {
+ intercept(context: ExecutionContext, next: CallHandler) {
+ Sentry.startSpan({ name: 'test-interceptor-span-2' }, () => {});
+ return next.handle().pipe();
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global-filter.exception.ts
new file mode 100644
index 000000000000..41981ba748fe
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global-filter.exception.ts
@@ -0,0 +1,5 @@
+export class ExampleExceptionGlobalFilter extends Error {
+ constructor() {
+ super('Original global example exception!');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global.filter.ts
new file mode 100644
index 000000000000..fba749f2232c
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global.filter.ts
@@ -0,0 +1,19 @@
+import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
+import { FastifyReply, FastifyRequest } from 'fastify';
+import { ExampleExceptionGlobalFilter } from './example-global-filter.exception';
+
+@Catch(ExampleExceptionGlobalFilter)
+export class ExampleGlobalFilter implements ExceptionFilter {
+ catch(exception: BadRequestException, host: ArgumentsHost): void {
+ const ctx = host.switchToHttp();
+ const response = ctx.getResponse();
+ const request = ctx.getRequest();
+
+ response.status(400).send({
+ statusCode: 400,
+ timestamp: new Date().toISOString(),
+ path: request.url,
+ message: 'Example exception was handled by global filter!',
+ });
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local-filter.exception.ts
new file mode 100644
index 000000000000..8f76520a3b94
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local-filter.exception.ts
@@ -0,0 +1,5 @@
+export class ExampleExceptionLocalFilter extends Error {
+ constructor() {
+ super('Original local example exception!');
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local.filter.ts
new file mode 100644
index 000000000000..aadf09983947
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local.filter.ts
@@ -0,0 +1,19 @@
+import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
+import { FastifyReply, FastifyRequest } from 'fastify';
+import { ExampleExceptionLocalFilter } from './example-local-filter.exception';
+
+@Catch(ExampleExceptionLocalFilter)
+export class ExampleLocalFilter implements ExceptionFilter {
+ catch(exception: BadRequestException, host: ArgumentsHost): void {
+ const ctx = host.switchToHttp();
+ const response = ctx.getResponse();
+ const request = ctx.getRequest();
+
+ response.status(400).send({
+ statusCode: 400,
+ timestamp: new Date().toISOString(),
+ path: request.url,
+ message: 'Example exception was handled by local filter!',
+ });
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.guard.ts
new file mode 100644
index 000000000000..e12bbdc4e994
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.guard.ts
@@ -0,0 +1,10 @@
+import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
+import * as Sentry from '@sentry/nestjs';
+
+@Injectable()
+export class ExampleGuard implements CanActivate {
+ canActivate(context: ExecutionContext): boolean {
+ Sentry.startSpan({ name: 'test-guard-span' }, () => {});
+ return true;
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.middleware.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.middleware.ts
new file mode 100644
index 000000000000..8eb319cef309
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.middleware.ts
@@ -0,0 +1,12 @@
+import { Injectable, NestMiddleware } from '@nestjs/common';
+import * as Sentry from '@sentry/nestjs';
+import { FastifyReply, FastifyRequest } from 'fastify';
+
+@Injectable()
+export class ExampleMiddleware implements NestMiddleware {
+ use(req: FastifyRequest, res: FastifyReply, next: () => void) {
+ // span that should be a child span of the middleware span
+ Sentry.startSpan({ name: 'test-middleware-span' }, () => {});
+ next();
+ }
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts
new file mode 100644
index 000000000000..4f16ebb36d11
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts
@@ -0,0 +1,12 @@
+import * as Sentry from '@sentry/nestjs';
+
+Sentry.init({
+ environment: 'qa', // dynamic sampling bias to keep transactions
+ dsn: process.env.E2E_TEST_DSN,
+ tunnel: `http://localhost:3031/`, // proxy server
+ tracesSampleRate: 1,
+ transportOptions: {
+ // We expect the app to send a lot of events in a short time
+ bufferSize: 1000,
+ },
+});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/main.ts
new file mode 100644
index 000000000000..7c7c6e4142d4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/src/main.ts
@@ -0,0 +1,16 @@
+// Import this first
+import './instrument';
+
+// Import other modules
+import { NestFactory } from '@nestjs/core';
+import { FastifyAdapter, NestFastifyApplication } from '@nestjs/platform-fastify';
+import { AppModule } from './app.module';
+
+const PORT = 3030;
+
+async function bootstrap() {
+ const app = await NestFactory.create(AppModule, new FastifyAdapter());
+ await app.listen(PORT);
+}
+
+bootstrap();
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-fastify/start-event-proxy.mjs
new file mode 100644
index 000000000000..c6a92da76970
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/start-event-proxy.mjs
@@ -0,0 +1,6 @@
+import { startEventProxyServer } from '@sentry-internal/test-utils';
+
+startEventProxyServer({
+ port: 3031,
+ proxyServerName: 'nestjs-fastify',
+});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/cron-decorator.test.ts
new file mode 100644
index 000000000000..e352e8fdba8f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/cron-decorator.test.ts
@@ -0,0 +1,81 @@
+import { expect, test } from '@playwright/test';
+import { waitForEnvelopeItem, waitForError } from '@sentry-internal/test-utils';
+
+test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => {
+ const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs-fastify', envelope => {
+ return (
+ envelope[0].type === 'check_in' &&
+ envelope[1]['monitor_slug'] === 'test-cron-slug' &&
+ envelope[1]['status'] === 'in_progress'
+ );
+ });
+
+ const okEnvelopePromise = waitForEnvelopeItem('nestjs-fastify', envelope => {
+ return (
+ envelope[0].type === 'check_in' &&
+ envelope[1]['monitor_slug'] === 'test-cron-slug' &&
+ envelope[1]['status'] === 'ok'
+ );
+ });
+
+ const inProgressEnvelope = await inProgressEnvelopePromise;
+ const okEnvelope = await okEnvelopePromise;
+
+ expect(inProgressEnvelope[1]).toEqual(
+ expect.objectContaining({
+ check_in_id: expect.any(String),
+ monitor_slug: 'test-cron-slug',
+ status: 'in_progress',
+ environment: 'qa',
+ monitor_config: {
+ schedule: {
+ type: 'crontab',
+ value: '* * * * *',
+ },
+ },
+ contexts: {
+ trace: {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ }),
+ );
+
+ expect(okEnvelope[1]).toEqual(
+ expect.objectContaining({
+ check_in_id: expect.any(String),
+ monitor_slug: 'test-cron-slug',
+ status: 'ok',
+ environment: 'qa',
+ duration: expect.any(Number),
+ contexts: {
+ trace: {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ },
+ },
+ }),
+ );
+
+ // kill cron so tests don't get stuck
+ await fetch(`${baseURL}/kill-test-cron/test-cron-job`);
+});
+
+test('Sends exceptions to Sentry on error in cron job', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('nestjs-fastify', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'Test error from cron job';
+ });
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('Test error from cron job');
+ expect(errorEvent.contexts?.trace).toEqual({
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+
+ // kill cron so tests don't get stuck
+ await fetch(`${baseURL}/kill-test-cron/test-cron-error`);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts
new file mode 100644
index 000000000000..4eea05edd36f
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts
@@ -0,0 +1,167 @@
+import { expect, test } from '@playwright/test';
+import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends exception to Sentry', async ({ baseURL }) => {
+ const errorEventPromise = waitForError('nestjs-fastify', event => {
+ return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123';
+ });
+
+ const response = await fetch(`${baseURL}/test-exception/123`);
+ expect(response.status).toBe(500);
+
+ const errorEvent = await errorEventPromise;
+
+ expect(errorEvent.exception?.values).toHaveLength(1);
+ expect(errorEvent.exception?.values?.[0]?.value).toBe('This is an exception with id 123');
+
+ expect(errorEvent.request).toEqual({
+ method: 'GET',
+ cookies: {},
+ headers: expect.any(Object),
+ url: 'http://localhost:3030/test-exception/123',
+ });
+
+ expect(errorEvent.transaction).toEqual('GET /test-exception/:id');
+
+ expect(errorEvent.contexts?.trace).toEqual({
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ });
+});
+
+test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => {
+ let errorEventOccurred = false;
+
+ waitForError('nestjs-fastify', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') {
+ errorEventOccurred = true;
+ }
+
+ return event?.transaction === 'GET /test-expected-400-exception/:id';
+ });
+
+ waitForError('nestjs-fastify', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') {
+ errorEventOccurred = true;
+ }
+
+ return event?.transaction === 'GET /test-expected-500-exception/:id';
+ });
+
+ const transactionEventPromise400 = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id';
+ });
+
+ const transactionEventPromise500 = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id';
+ });
+
+ const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`);
+ expect(response400.status).toBe(400);
+
+ const response500 = await fetch(`${baseURL}/test-expected-500-exception/123`);
+ expect(response500.status).toBe(500);
+
+ await transactionEventPromise400;
+ await transactionEventPromise500;
+
+ (await fetch(`${baseURL}/flush`)).text();
+
+ expect(errorEventOccurred).toBe(false);
+});
+
+test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => {
+ let errorEventOccurred = false;
+
+ waitForError('nestjs-fastify', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') {
+ errorEventOccurred = true;
+ }
+
+ return event?.transaction === 'GET /test-expected-rpc-exception/:id';
+ });
+
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id';
+ });
+
+ const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`);
+ expect(response.status).toBe(500);
+
+ await transactionEventPromise;
+
+ (await fetch(`${baseURL}/flush`)).text();
+
+ expect(errorEventOccurred).toBe(false);
+});
+
+test('Global exception filter registered in main module is applied and exception is not sent to Sentry', async ({
+ baseURL,
+}) => {
+ let errorEventOccurred = false;
+
+ waitForError('nestjs-fastify', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by global filter!') {
+ errorEventOccurred = true;
+ }
+
+ return event?.transaction === 'GET /example-exception-global-filter';
+ });
+
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /example-exception-global-filter';
+ });
+
+ const response = await fetch(`${baseURL}/example-exception-global-filter`);
+ const responseBody = await response.json();
+
+ expect(response.status).toBe(400);
+ expect(responseBody).toEqual({
+ statusCode: 400,
+ timestamp: expect.any(String),
+ path: '/example-exception-global-filter',
+ message: 'Example exception was handled by global filter!',
+ });
+
+ await transactionEventPromise;
+
+ (await fetch(`${baseURL}/flush`)).text();
+
+ expect(errorEventOccurred).toBe(false);
+});
+
+test('Local exception filter registered in main module is applied and exception is not sent to Sentry', async ({
+ baseURL,
+}) => {
+ let errorEventOccurred = false;
+
+ waitForError('nestjs-fastify', event => {
+ if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by local filter!') {
+ errorEventOccurred = true;
+ }
+
+ return event?.transaction === 'GET /example-exception-local-filter';
+ });
+
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return transactionEvent?.transaction === 'GET /example-exception-local-filter';
+ });
+
+ const response = await fetch(`${baseURL}/example-exception-local-filter`);
+ const responseBody = await response.json();
+
+ expect(response.status).toBe(400);
+ expect(responseBody).toEqual({
+ statusCode: 400,
+ timestamp: expect.any(String),
+ path: '/example-exception-local-filter',
+ message: 'Example exception was handled by local filter!',
+ });
+
+ await transactionEventPromise;
+
+ (await fetch(`${baseURL}/flush`)).text();
+
+ expect(errorEventOccurred).toBe(false);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/span-decorator.test.ts
new file mode 100644
index 000000000000..6efb193751b9
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/span-decorator.test.ts
@@ -0,0 +1,79 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Transaction includes span and correct value for decorated async function', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-span-decorator-async'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-span-decorator-async`);
+ const body = await response.json();
+
+ expect(body.result).toEqual('test');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'manual',
+ 'sentry.op': 'wait and return a string',
+ },
+ description: 'wait',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'wait and return a string',
+ origin: 'manual',
+ }),
+ ]),
+ );
+});
+
+test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-span-decorator-sync'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-span-decorator-sync`);
+ const body = await response.json();
+
+ expect(body.result).toEqual('test');
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent.spans).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'manual',
+ 'sentry.op': 'return a string',
+ },
+ description: 'getString',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'return a string',
+ origin: 'manual',
+ }),
+ ]),
+ );
+});
+
+test('preserves original function name on decorated functions', async ({ baseURL }) => {
+ const response = await fetch(`${baseURL}/test-function-name`);
+ const body = await response.json();
+
+ expect(body.result).toEqual('getFunctionName');
+});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
new file mode 100644
index 000000000000..609e01709650
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts
@@ -0,0 +1,810 @@
+import { expect, test } from '@playwright/test';
+import { waitForTransaction } from '@sentry-internal/test-utils';
+
+test('Sends an API route transaction', async ({ baseURL }) => {
+ const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-transaction'
+ );
+ });
+
+ await fetch(`${baseURL}/test-transaction`);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ expect(transactionEvent.contexts?.trace).toEqual({
+ data: {
+ 'sentry.source': 'route',
+ 'sentry.origin': 'auto.http.otel.http',
+ 'sentry.op': 'http.server',
+ 'sentry.sample_rate': 1,
+ url: 'http://localhost:3030/test-transaction',
+ 'otel.kind': 'SERVER',
+ 'http.response.status_code': 200,
+ 'http.url': 'http://localhost:3030/test-transaction',
+ 'http.host': 'localhost:3030',
+ 'net.host.name': 'localhost',
+ 'http.method': 'GET',
+ 'http.scheme': 'http',
+ 'http.target': '/test-transaction',
+ 'http.user_agent': 'node',
+ 'http.flavor': '1.1',
+ 'net.transport': 'ip_tcp',
+ 'net.host.ip': expect.any(String),
+ 'net.host.port': expect.any(Number),
+ 'net.peer.ip': expect.any(String),
+ 'net.peer.port': expect.any(Number),
+ 'http.status_code': 200,
+ 'http.status_text': 'OK',
+ 'http.route': '/test-transaction',
+ },
+ op: 'http.server',
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ status: 'ok',
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ origin: 'auto.http.otel.http',
+ });
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'manual',
+ 'fastify.type': 'middleware',
+ 'plugin.name': 'fastify -> @fastify/middie',
+ 'hook.name': 'onRequest',
+ },
+ description: 'middleware - runMiddie',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'auto.http.otel.fastify',
+ 'sentry.op': 'request_handler.fastify',
+ 'plugin.name': 'fastify -> @fastify/middie',
+ 'fastify.type': 'request_handler',
+ 'http.route': '/test-transaction',
+ },
+ description: '@fastify/middie',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'request_handler.fastify',
+ origin: 'auto.http.otel.fastify',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'auto.http.otel.nestjs',
+ 'sentry.op': 'request_context.nestjs',
+ component: '@nestjs/core',
+ 'nestjs.version': expect.any(String),
+ 'nestjs.type': 'request_context',
+ 'http.method': 'GET',
+ 'http.url': '/test-transaction',
+ 'http.route': '/test-transaction',
+ 'nestjs.controller': 'AppController',
+ 'nestjs.callback': 'testTransaction',
+ url: '/test-transaction',
+ },
+ description: 'GET /test-transaction',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'request_context.nestjs',
+ origin: 'auto.http.otel.nestjs',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'auto.middleware.nestjs',
+ 'sentry.op': 'middleware.nestjs',
+ },
+ description: 'SentryTracingInterceptor',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'auto.middleware.nestjs',
+ 'sentry.op': 'middleware.nestjs',
+ },
+ description: 'SentryTracingInterceptor',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'auto.http.otel.nestjs',
+ 'sentry.op': 'handler.nestjs',
+ component: '@nestjs/core',
+ 'nestjs.version': expect.any(String),
+ 'nestjs.type': 'handler',
+ 'nestjs.callback': 'testTransaction',
+ },
+ description: 'testTransaction',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'handler.nestjs',
+ origin: 'auto.http.otel.nestjs',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: { 'sentry.origin': 'manual' },
+ description: 'test-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: { 'sentry.origin': 'manual' },
+ description: 'child-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.origin': 'auto.middleware.nestjs',
+ 'sentry.op': 'middleware.nestjs',
+ },
+ description: 'Interceptors - After Route',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ transaction: 'GET /test-transaction',
+ type: 'transaction',
+ transaction_info: {
+ source: 'route',
+ },
+ }),
+ );
+});
+
+test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({
+ baseURL,
+}) => {
+ const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-middleware-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-middleware-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ExampleMiddleware',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware');
+ const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-controller-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-middleware-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span');
+ const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
+
+ // 'ExampleMiddleware' is the parent of 'test-middleware-span'
+ expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId);
+
+ // 'ExampleMiddleware' is NOT the parent of 'test-controller-span'
+ expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId);
+});
+
+test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({
+ baseURL,
+}) => {
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-guard-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-guard-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ExampleGuard',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ const exampleGuardSpan = transactionEvent.spans.find(span => span.description === 'ExampleGuard');
+ const exampleGuardSpanId = exampleGuardSpan?.span_id;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-guard-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testGuardSpan = transactionEvent.spans.find(span => span.description === 'test-guard-span');
+
+ // 'ExampleGuard' is the parent of 'test-guard-span'
+ expect(testGuardSpan.parent_span_id).toBe(exampleGuardSpanId);
+});
+
+test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' &&
+ transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123')
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-pipe-instrumentation/123`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ParseIntPipe',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+});
+
+test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => {
+ const transactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' &&
+ transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc')
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-pipe-instrumentation/abc`);
+ expect(response.status).toBe(400);
+
+ const transactionEvent = await transactionEventPromise;
+
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ParseIntPipe',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'unknown_error',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+});
+
+test('API route transaction includes nest interceptor spans before route execution. Spans created in and after interceptor are nested correctly', async ({
+ baseURL,
+}) => {
+ const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-interceptor-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-interceptor-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ // check if interceptor spans before route execution exist
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ExampleInterceptor1',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'ExampleInterceptor2',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ // get interceptor spans
+ const exampleInterceptor1Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor1');
+ const exampleInterceptor1SpanId = exampleInterceptor1Span?.span_id;
+ const exampleInterceptor2Span = transactionEvent.spans.find(span => span.description === 'ExampleInterceptor2');
+ const exampleInterceptor2SpanId = exampleInterceptor2Span?.span_id;
+
+ // check if manually started spans exist
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-controller-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-interceptor-span-1',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-interceptor-span-2',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testInterceptor1Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-1');
+ const testInterceptor2Span = transactionEvent.spans.find(span => span.description === 'test-interceptor-span-2');
+ const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
+
+ // 'ExampleInterceptor1' is the parent of 'test-interceptor-span-1'
+ expect(testInterceptor1Span.parent_span_id).toBe(exampleInterceptor1SpanId);
+
+ // 'ExampleInterceptor1' is NOT the parent of 'test-controller-span'
+ expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor1SpanId);
+
+ // 'ExampleInterceptor2' is the parent of 'test-interceptor-span-2'
+ expect(testInterceptor2Span.parent_span_id).toBe(exampleInterceptor2SpanId);
+
+ // 'ExampleInterceptor2' is NOT the parent of 'test-controller-span'
+ expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptor2SpanId);
+});
+
+test('API route transaction includes exactly one nest interceptor span after route execution. Spans created in controller and in interceptor are nested correctly', async ({
+ baseURL,
+}) => {
+ const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-interceptor-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-interceptor-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ // check if interceptor spans after route execution exist
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'Interceptors - After Route',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ // check that exactly one after route span is sent
+ const allInterceptorSpansAfterRoute = transactionEvent.spans.filter(
+ span => span.description === 'Interceptors - After Route',
+ );
+ expect(allInterceptorSpansAfterRoute.length).toBe(1);
+
+ // get interceptor span
+ const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find(
+ span => span.description === 'Interceptors - After Route',
+ );
+ const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id;
+
+ // check if manually started span in interceptor after route exists
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-interceptor-span-after-route',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testInterceptorSpanAfterRoute = transactionEvent.spans.find(
+ span => span.description === 'test-interceptor-span-after-route',
+ );
+ const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
+
+ // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route'
+ expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId);
+
+ // 'Interceptor - After Route' is NOT the parent of 'test-controller-span'
+ expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId);
+});
+
+test('API route transaction includes nest async interceptor spans before route execution. Spans created in and after async interceptor are nested correctly', async ({
+ baseURL,
+}) => {
+ const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ // check if interceptor spans before route execution exist
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'AsyncInterceptor',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ // get interceptor spans
+ const exampleAsyncInterceptor = transactionEvent.spans.find(span => span.description === 'AsyncInterceptor');
+ const exampleAsyncInterceptorSpanId = exampleAsyncInterceptor?.span_id;
+
+ // check if manually started spans exist
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-controller-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-async-interceptor-span',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testAsyncInterceptorSpan = transactionEvent.spans.find(
+ span => span.description === 'test-async-interceptor-span',
+ );
+ const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
+
+ // 'AsyncInterceptor' is the parent of 'test-async-interceptor-span'
+ expect(testAsyncInterceptorSpan.parent_span_id).toBe(exampleAsyncInterceptorSpanId);
+
+ // 'AsyncInterceptor' is NOT the parent of 'test-controller-span'
+ expect(testControllerSpan.parent_span_id).not.toBe(exampleAsyncInterceptorSpanId);
+});
+
+test('API route transaction includes exactly one nest async interceptor span after route execution. Spans created in controller and in async interceptor are nested correctly', async ({
+ baseURL,
+}) => {
+ const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => {
+ return (
+ transactionEvent?.contexts?.trace?.op === 'http.server' &&
+ transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation'
+ );
+ });
+
+ const response = await fetch(`${baseURL}/test-async-interceptor-instrumentation`);
+ expect(response.status).toBe(200);
+
+ const transactionEvent = await pageloadTransactionEventPromise;
+
+ // check if interceptor spans after route execution exist
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: {
+ 'sentry.op': 'middleware.nestjs',
+ 'sentry.origin': 'auto.middleware.nestjs',
+ },
+ description: 'Interceptors - After Route',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ op: 'middleware.nestjs',
+ origin: 'auto.middleware.nestjs',
+ },
+ ]),
+ }),
+ );
+
+ // check that exactly one after route span is sent
+ const allInterceptorSpansAfterRoute = transactionEvent.spans.filter(
+ span => span.description === 'Interceptors - After Route',
+ );
+ expect(allInterceptorSpansAfterRoute.length).toBe(1);
+
+ // get interceptor span
+ const exampleInterceptorSpanAfterRoute = transactionEvent.spans.find(
+ span => span.description === 'Interceptors - After Route',
+ );
+ const exampleInterceptorSpanAfterRouteId = exampleInterceptorSpanAfterRoute?.span_id;
+
+ // check if manually started span in interceptor after route exists
+ expect(transactionEvent).toEqual(
+ expect.objectContaining({
+ spans: expect.arrayContaining([
+ {
+ span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ trace_id: expect.stringMatching(/[a-f0-9]{32}/),
+ data: expect.any(Object),
+ description: 'test-async-interceptor-span-after-route',
+ parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
+ start_timestamp: expect.any(Number),
+ timestamp: expect.any(Number),
+ status: 'ok',
+ origin: 'manual',
+ },
+ ]),
+ }),
+ );
+
+ // verify correct span parent-child relationships
+ const testInterceptorSpanAfterRoute = transactionEvent.spans.find(
+ span => span.description === 'test-async-interceptor-span-after-route',
+ );
+ const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
+
+ // 'Interceptor - After Route' is the parent of 'test-interceptor-span-after-route'
+ expect(testInterceptorSpanAfterRoute.parent_span_id).toBe(exampleInterceptorSpanAfterRouteId);
+
+ // 'Interceptor - After Route' is NOT the parent of 'test-controller-span'
+ expect(testControllerSpan.parent_span_id).not.toBe(exampleInterceptorSpanAfterRouteId);
+});
+
+test('Calling use method on service with Injectable decorator returns 200', async ({ baseURL }) => {
+ const response = await fetch(`${baseURL}/test-service-use`);
+ expect(response.status).toBe(200);
+});
+
+test('Calling transform method on service with Injectable decorator returns 200', async ({ baseURL }) => {
+ const response = await fetch(`${baseURL}/test-service-transform`);
+ expect(response.status).toBe(200);
+});
+
+test('Calling intercept method on service with Injectable decorator returns 200', async ({ baseURL }) => {
+ const response = await fetch(`${baseURL}/test-service-intercept`);
+ expect(response.status).toBe(200);
+});
+
+test('Calling canActivate method on service with Injectable decorator returns 200', async ({ baseURL }) => {
+ const response = await fetch(`${baseURL}/test-service-canActivate`);
+ expect(response.status).toBe(200);
+});
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.build.json
new file mode 100644
index 000000000000..64f86c6bd2bb
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.build.json
@@ -0,0 +1,4 @@
+{
+ "extends": "./tsconfig.json",
+ "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
+}
diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.json
new file mode 100644
index 000000000000..797d8abe0ead
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/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,
+ }
+}
diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts
index 55e168c53963..e75054d3391d 100644
--- a/packages/nestjs/src/setup.ts
+++ b/packages/nestjs/src/setup.ts
@@ -23,6 +23,23 @@ import {
import type { Observable } from 'rxjs';
import { isExpectedError } from './helpers';
+// Partial extract of FastifyRequest interface
+// https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41
+interface FastifyRequest {
+ routeOptions?: {
+ method?: string;
+ url?: string;
+ };
+}
+
+// Partial extract of ExpressRequest interface
+interface ExpressRequest {
+ route?: {
+ path?: string;
+ };
+ method?: string;
+}
+
/**
* Note: We cannot use @ syntax to add the decorators, so we add them directly below the classes as function wrappers.
*/
@@ -52,11 +69,15 @@ class SentryTracingInterceptor implements NestInterceptor {
}
if (context.getType() === 'http') {
- const req = context.switchToHttp().getRequest();
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
- if (req.route) {
- // eslint-disable-next-line @sentry-internal/sdk/no-optional-chaining,@typescript-eslint/no-unsafe-member-access
- getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`);
+ const req = context.switchToHttp().getRequest() as FastifyRequest | ExpressRequest;
+ if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) {
+ // fastify case
+ getIsolationScope().setTransactionName(
+ `${(req.routeOptions.method || 'GET').toUpperCase()} ${req.routeOptions.url}`,
+ );
+ } else if ('route' in req && req.route && req.route.path) {
+ // express case
+ getIsolationScope().setTransactionName(`${(req.method || 'GET').toUpperCase()} ${req.route.path}`);
}
}
diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts
index 1c63c22783aa..b5c9ea4bb61f 100644
--- a/packages/node/src/integrations/tracing/nest/nest.ts
+++ b/packages/node/src/integrations/tracing/nest/nest.ts
@@ -87,8 +87,15 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE
}
if (context.getType() === 'http') {
+ // getRequest() returns either a FastifyRequest or ExpressRequest, depending on the used adapter
const req = context.switchToHttp().getRequest();
- if (req.route) {
+ if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) {
+ // fastify case
+ getIsolationScope().setTransactionName(
+ `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`,
+ );
+ } else if ('route' in req && req.route && req.route.path) {
+ // express case
getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`);
}
}
diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts
index ed7e968a9600..a983832ac8c6 100644
--- a/packages/node/src/integrations/tracing/nest/types.ts
+++ b/packages/node/src/integrations/tracing/nest/types.ts
@@ -1,5 +1,22 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
+// Partial extract of FastifyRequest interface
+// https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41
+interface FastifyRequest {
+ routeOptions?: {
+ method?: string;
+ url?: string;
+ };
+}
+
+// Partial extract of ExpressRequest interface
+interface ExpressRequest {
+ route?: {
+ path?: string;
+ };
+ method?: string;
+}
+
export interface MinimalNestJsExecutionContext {
getType: () => string;
@@ -7,12 +24,7 @@ export interface MinimalNestJsExecutionContext {
// minimal request object
// according to official types, all properties are required but
// let's play it safe and assume they're optional
- getRequest: () => {
- route?: {
- path?: string;
- };
- method?: string;
- };
+ getRequest: () => FastifyRequest | ExpressRequest;
};
_sentryInterceptorInstrumented?: boolean;