From c082fc675cb551296c4f41dc1114d97af0c16ceb Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 3 Dec 2024 11:07:04 +0100 Subject: [PATCH 1/9] add basic fastify test with ts5 --- .../nestjs-fastify/.gitignore | 56 ++ .../test-applications/nestjs-fastify/.npmrc | 2 + .../nestjs-fastify/README.md | 85 ++ .../nestjs-fastify/nest-cli.json | 8 + .../nestjs-fastify/package.json | 49 ++ .../nestjs-fastify/playwright.config.mjs | 7 + .../nestjs-fastify/src/app.controller.ts | 124 +++ .../nestjs-fastify/src/app.module.ts | 29 + .../nestjs-fastify/src/app.service.ts | 113 +++ .../src/async-example.interceptor.ts | 17 + .../src/example-1.interceptor.ts | 15 + .../src/example-2.interceptor.ts | 10 + .../src/example-global-filter.exception.ts | 5 + .../src/example-global.filter.ts | 19 + .../src/example-local-filter.exception.ts | 5 + .../src/example-local.filter.ts | 19 + .../nestjs-fastify/src/example.guard.ts | 10 + .../nestjs-fastify/src/example.middleware.ts | 12 + .../nestjs-fastify/src/instrument.ts | 12 + .../nestjs-fastify/src/main.ts | 16 + .../nestjs-fastify/start-event-proxy.mjs | 6 + .../tests/cron-decorator.test.ts | 81 ++ .../nestjs-fastify/tests/errors.test.ts | 166 ++++ .../tests/span-decorator.test.ts | 79 ++ .../nestjs-fastify/tests/transactions.test.ts | 729 ++++++++++++++++++ .../nestjs-fastify/tsconfig.build.json | 4 + .../nestjs-fastify/tsconfig.json | 21 + 27 files changed, 1699 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/README.md create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/nest-cli.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.controller.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.module.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/app.service.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/async-example.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-1.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-2.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global-filter.exception.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-global.filter.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local-filter.exception.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example-local.filter.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.guard.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/example.middleware.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/instrument.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/cron-decorator.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/span-decorator.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.build.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-fastify/tsconfig.json 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 @@ +

+ Nest Logo +

+ +[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.

+

+NPM Version +Package License +NPM Downloads +CircleCI +Coverage +Discord +Backers on Open Collective +Sponsors on Open Collective + Donate us + Support us + Follow us on Twitter +

+ + +## 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..133f31101d27 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json @@ -0,0 +1,49 @@ +{ + "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-express": "^10.0.0", + "@nestjs/platform-fastify": "^10.0.0", + "@sentry/nestjs": "latest || *", + "reflect-metadata": "^0.2.0", + "rxjs": "^7.8.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/express": "^4.17.17", + "@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..988696d0e13d --- /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 { Request, Response } from 'express'; +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).json({ + 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..505217f5dcbd --- /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 { Request, Response } from 'express'; +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).json({ + 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..31d15c9372ea --- /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 { NextFunction, Request, Response } from 'express'; + +@Injectable() +export class ExampleMiddleware implements NestMiddleware { + use(req: Request, res: Response, next: NextFunction) { + // 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..ece5e2b495a8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/errors.test.ts @@ -0,0 +1,166 @@ +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({ + 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..5ad5a31d9a83 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/tests/transactions.test.ts @@ -0,0 +1,729 @@ +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([ + { + data: { + 'express.name': '/test-transaction', + 'express.type': 'request_handler', + 'http.route': '/test-transaction', + 'sentry.origin': 'auto.http.otel.express', + 'sentry.op': 'request_handler.express', + }, + op: 'request_handler.express', + description: '/test-transaction', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'auto.http.otel.express', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'test-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }, + { + data: { + 'sentry.origin': 'manual', + }, + description: 'child-span', + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + span_id: expect.stringMatching(/[a-f0-9]{16}/), + start_timestamp: expect.any(Number), + status: 'ok', + timestamp: expect.any(Number), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), + origin: 'manual', + }, + { + 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', + origin: 'auto.http.otel.nestjs', + op: 'handler.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, + } +} From 221d34708899c1728e043c07cc4fdb8ed655ff39 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 3 Dec 2024 14:10:19 +0100 Subject: [PATCH 2/9] adapt setting tx name to fastify req object --- packages/nestjs/src/setup.ts | 12 +++++++++--- packages/node/src/integrations/tracing/nest/nest.ts | 10 +++++++++- packages/node/src/integrations/tracing/nest/types.ts | 1 + 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 55e168c53963..77872a88314f 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -53,11 +53,17 @@ 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 + /* eslint-disable @typescript-eslint/no-unsafe-member-access, @sentry-internal/sdk/no-optional-chaining */ + if (req.routeOptions && req.routeOptions.url) { + // fastify case + getIsolationScope().setTransactionName( + `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`, + ); + } else if (req.route && req.route.path) { + // express case getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); } + /* eslint-enable @typescript-eslint/no-unsafe-member-access, @sentry-internal/sdk/no-optional-chaining */ } return next.handle(); diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts index 1c63c22783aa..973780395fe6 100644 --- a/packages/node/src/integrations/tracing/nest/nest.ts +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -88,9 +88,17 @@ export function setupNestErrorHandler(app: MinimalNestJsApp, baseFilter: NestJsE if (context.getType() === 'http') { const req = context.switchToHttp().getRequest(); - if (req.route) { + /* eslint-disable @typescript-eslint/no-unsafe-member-access */ + if (req.routeOptions && req.routeOptions.url) { + // fastify case + getIsolationScope().setTransactionName( + `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`, + ); + } else if (req.route && req.route.path) { + // express case getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); } + /* eslint-enable @typescript-eslint/no-unsafe-member-access, @sentry-internal/sdk/no-optional-chaining */ } return next.handle(); diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index ed7e968a9600..13374208b2d6 100644 --- a/packages/node/src/integrations/tracing/nest/types.ts +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -8,6 +8,7 @@ export interface MinimalNestJsExecutionContext { // according to official types, all properties are required but // let's play it safe and assume they're optional getRequest: () => { + routeOptions?: any; route?: { path?: string; }; From 8de63e95d22dab7cfb91fcd32d96ad1d1239bc23 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 3 Dec 2024 14:43:03 +0100 Subject: [PATCH 3/9] fix req + res types in test --- .../test-applications/nestjs-fastify/package.json | 5 ++--- .../nestjs-fastify/src/example-global.filter.ts | 8 ++++---- .../nestjs-fastify/src/example-local.filter.ts | 8 ++++---- .../nestjs-fastify/src/example.middleware.ts | 4 ++-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json index 133f31101d27..6da132e74a4c 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-fastify/package.json @@ -19,11 +19,11 @@ "@nestjs/core": "^10.0.0", "@nestjs/microservices": "^10.0.0", "@nestjs/schedule": "^4.1.0", - "@nestjs/platform-express": "^10.0.0", "@nestjs/platform-fastify": "^10.0.0", "@sentry/nestjs": "latest || *", "reflect-metadata": "^0.2.0", - "rxjs": "^7.8.1" + "rxjs": "^7.8.1", + "fastify": "^4.28.1" }, "devDependencies": { "@playwright/test": "^1.44.1", @@ -31,7 +31,6 @@ "@nestjs/cli": "^10.0.0", "@nestjs/schematics": "^10.0.0", "@nestjs/testing": "^10.0.0", - "@types/express": "^4.17.17", "@types/node": "18.15.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", 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 index 988696d0e13d..23cf24c6bc38 100644 --- 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 @@ -1,15 +1,15 @@ import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; -import { Request, Response } from 'express'; +import { FastifyRequest, FastifyReply } 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(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); - response.status(400).json({ + response.status(400).send({ statusCode: 400, timestamp: new Date().toISOString(), path: request.url, 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 index 505217f5dcbd..32f72ff90e0f 100644 --- 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 @@ -1,15 +1,15 @@ import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; -import { Request, Response } from 'express'; +import { FastifyRequest, FastifyReply } 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(); + const response = ctx.getResponse(); + const request = ctx.getRequest(); - response.status(400).json({ + response.status(400).send({ statusCode: 400, timestamp: new Date().toISOString(), path: request.url, 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 index 31d15c9372ea..4635e92a7c57 100644 --- 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 @@ -1,10 +1,10 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import * as Sentry from '@sentry/nestjs'; -import { NextFunction, Request, Response } from 'express'; +import { FastifyRequest, FastifyReply } from 'fastify'; @Injectable() export class ExampleMiddleware implements NestMiddleware { - use(req: Request, res: Response, next: NextFunction) { + 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(); From 32b1ff6258ae56e0cbc66367b38617c678177513 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 3 Dec 2024 14:54:37 +0100 Subject: [PATCH 4/9] test: adapt expected spans in api tx --- .../nestjs-fastify/tests/transactions.test.ts | 125 +++++++++++++++--- 1 file changed, 103 insertions(+), 22 deletions(-) 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 index 5ad5a31d9a83..609e01709650 100644 --- 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 @@ -50,48 +50,92 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect.objectContaining({ spans: expect.arrayContaining([ { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: { - 'express.name': '/test-transaction', - 'express.type': 'request_handler', - 'http.route': '/test-transaction', - 'sentry.origin': 'auto.http.otel.express', - 'sentry.op': 'request_handler.express', + 'sentry.origin': 'manual', + 'fastify.type': 'middleware', + 'plugin.name': 'fastify -> @fastify/middie', + 'hook.name': 'onRequest', }, - op: 'request_handler.express', - description: '/test-transaction', + description: 'middleware - runMiddie', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), - status: 'ok', timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'auto.http.otel.express', + status: 'ok', + origin: 'manual', }, { + span_id: expect.stringMatching(/[a-f0-9]{16}/), + trace_id: expect.stringMatching(/[a-f0-9]{32}/), data: { - 'sentry.origin': 'manual', + '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: 'test-span', + description: '@fastify/middie', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - span_id: expect.stringMatching(/[a-f0-9]{16}/), start_timestamp: expect.any(Number), - status: 'ok', timestamp: expect.any(Number), - trace_id: expect.stringMatching(/[a-f0-9]{32}/), - origin: 'manual', + 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': 'manual', + '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: 'child-span', + description: 'GET /test-transaction', parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), - 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}/), - origin: 'manual', + 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}/), @@ -109,8 +153,45 @@ test('Sends an API route transaction', async ({ baseURL }) => { start_timestamp: expect.any(Number), timestamp: expect.any(Number), status: 'ok', - origin: 'auto.http.otel.nestjs', 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', From e61a44495a520da3e6252af8c0f4c5a6dd1c5b8e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 3 Dec 2024 15:00:08 +0100 Subject: [PATCH 5/9] organize imports --- .../nestjs-fastify/src/example-global.filter.ts | 2 +- .../nestjs-fastify/src/example-local.filter.ts | 2 +- .../test-applications/nestjs-fastify/src/example.middleware.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 23cf24c6bc38..fba749f2232c 100644 --- 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 @@ -1,5 +1,5 @@ import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; -import { FastifyRequest, FastifyReply } from 'fastify'; +import { FastifyReply, FastifyRequest } from 'fastify'; import { ExampleExceptionGlobalFilter } from './example-global-filter.exception'; @Catch(ExampleExceptionGlobalFilter) 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 index 32f72ff90e0f..aadf09983947 100644 --- 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 @@ -1,5 +1,5 @@ import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common'; -import { FastifyRequest, FastifyReply } from 'fastify'; +import { FastifyReply, FastifyRequest } from 'fastify'; import { ExampleExceptionLocalFilter } from './example-local-filter.exception'; @Catch(ExampleExceptionLocalFilter) 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 index 4635e92a7c57..8eb319cef309 100644 --- 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 @@ -1,6 +1,6 @@ import { Injectable, NestMiddleware } from '@nestjs/common'; import * as Sentry from '@sentry/nestjs'; -import { FastifyRequest, FastifyReply } from 'fastify'; +import { FastifyReply, FastifyRequest } from 'fastify'; @Injectable() export class ExampleMiddleware implements NestMiddleware { From 8b20bcdde9e9d11c4cb8634c3d410706854930e2 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 3 Dec 2024 16:59:35 +0100 Subject: [PATCH 6/9] adapt error test --- .../test-applications/nestjs-fastify/tests/errors.test.ts | 1 + 1 file changed, 1 insertion(+) 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 index ece5e2b495a8..4eea05edd36f 100644 --- 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 @@ -24,6 +24,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { 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}/), }); From 92b7b4355356f79b1b616e42ae410fa171745e9b Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 4 Dec 2024 11:25:17 +0100 Subject: [PATCH 7/9] improve request types --- packages/nestjs/src/setup.ts | 13 +++++----- packages/node/src/index.ts | 2 ++ .../src/integrations/tracing/nest/nest.ts | 7 +++--- .../src/integrations/tracing/nest/types.ts | 25 +++++++++++++------ 4 files changed, 29 insertions(+), 18 deletions(-) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 77872a88314f..632680caea81 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -22,6 +22,7 @@ import { } from '@sentry/core'; import type { Observable } from 'rxjs'; import { isExpectedError } from './helpers'; +import type { FastifyRequest, ExpressRequest } from '@sentry/node'; /** * Note: We cannot use @ syntax to add the decorators, so we add them directly below the classes as function wrappers. @@ -52,18 +53,16 @@ class SentryTracingInterceptor implements NestInterceptor { } if (context.getType() === 'http') { - const req = context.switchToHttp().getRequest(); - /* eslint-disable @typescript-eslint/no-unsafe-member-access, @sentry-internal/sdk/no-optional-chaining */ - if (req.routeOptions && req.routeOptions.url) { + const req = context.switchToHttp().getRequest() as FastifyRequest | ExpressRequest; + if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) { // fastify case getIsolationScope().setTransactionName( - `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`, + `${(req.routeOptions.method || 'GET').toUpperCase()} ${req.routeOptions.url}`, ); - } else if (req.route && req.route.path) { + } else if ('route' in req && req.route && req.route.path) { // express case - getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); + getIsolationScope().setTransactionName(`${(req.method || 'GET').toUpperCase()} ${req.route.path}`); } - /* eslint-enable @typescript-eslint/no-unsafe-member-access, @sentry-internal/sdk/no-optional-chaining */ } return next.handle(); diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 4572cf65b9ce..6b803009092c 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -166,3 +166,5 @@ export type { User, Span, } from '@sentry/core'; + +export type { FastifyRequest, ExpressRequest } from './integrations/tracing/nest/types'; diff --git a/packages/node/src/integrations/tracing/nest/nest.ts b/packages/node/src/integrations/tracing/nest/nest.ts index 973780395fe6..b5c9ea4bb61f 100644 --- a/packages/node/src/integrations/tracing/nest/nest.ts +++ b/packages/node/src/integrations/tracing/nest/nest.ts @@ -87,18 +87,17 @@ 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(); - /* eslint-disable @typescript-eslint/no-unsafe-member-access */ - if (req.routeOptions && req.routeOptions.url) { + if ('routeOptions' in req && req.routeOptions && req.routeOptions.url) { // fastify case getIsolationScope().setTransactionName( `${req.routeOptions.method?.toUpperCase() || 'GET'} ${req.routeOptions.url}`, ); - } else if (req.route && req.route.path) { + } else if ('route' in req && req.route && req.route.path) { // express case getIsolationScope().setTransactionName(`${req.method?.toUpperCase() || 'GET'} ${req.route.path}`); } - /* eslint-enable @typescript-eslint/no-unsafe-member-access, @sentry-internal/sdk/no-optional-chaining */ } return next.handle(); diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index 13374208b2d6..84095cd81a64 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 +export interface FastifyRequest { + routeOptions?: { + method?: string; + url?: string; + }; +} + +// Partial extract of ExpressRequest interface +export interface ExpressRequest { + route?: { + path?: string; + }; + method?: string; +} + export interface MinimalNestJsExecutionContext { getType: () => string; @@ -7,13 +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: () => { - routeOptions?: any; - route?: { - path?: string; - }; - method?: string; - }; + getRequest: () => FastifyRequest | ExpressRequest; }; _sentryInterceptorInstrumented?: boolean; From 5922598e11fc669480f0ea1524e2a47099eb399c Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 4 Dec 2024 12:02:31 +0100 Subject: [PATCH 8/9] organize imports --- packages/nestjs/src/setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index 632680caea81..cee349dd79c1 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -20,9 +20,9 @@ import { logger, spanToJSON, } from '@sentry/core'; +import type { ExpressRequest, FastifyRequest } from '@sentry/node'; import type { Observable } from 'rxjs'; import { isExpectedError } from './helpers'; -import type { FastifyRequest, ExpressRequest } from '@sentry/node'; /** * Note: We cannot use @ syntax to add the decorators, so we add them directly below the classes as function wrappers. From dfd2b9d1577136a0955ce4e98e1a861a37d1d796 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Thu, 5 Dec 2024 11:29:55 +0100 Subject: [PATCH 9/9] get rid of request interface export --- packages/nestjs/src/setup.ts | 18 +++++++++++++++++- packages/node/src/index.ts | 2 -- .../src/integrations/tracing/nest/types.ts | 4 ++-- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/nestjs/src/setup.ts b/packages/nestjs/src/setup.ts index cee349dd79c1..e75054d3391d 100644 --- a/packages/nestjs/src/setup.ts +++ b/packages/nestjs/src/setup.ts @@ -20,10 +20,26 @@ import { logger, spanToJSON, } from '@sentry/core'; -import type { ExpressRequest, FastifyRequest } from '@sentry/node'; 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. */ diff --git a/packages/node/src/index.ts b/packages/node/src/index.ts index 6b803009092c..4572cf65b9ce 100644 --- a/packages/node/src/index.ts +++ b/packages/node/src/index.ts @@ -166,5 +166,3 @@ export type { User, Span, } from '@sentry/core'; - -export type { FastifyRequest, ExpressRequest } from './integrations/tracing/nest/types'; diff --git a/packages/node/src/integrations/tracing/nest/types.ts b/packages/node/src/integrations/tracing/nest/types.ts index 84095cd81a64..a983832ac8c6 100644 --- a/packages/node/src/integrations/tracing/nest/types.ts +++ b/packages/node/src/integrations/tracing/nest/types.ts @@ -2,7 +2,7 @@ // Partial extract of FastifyRequest interface // https://github.com/fastify/fastify/blob/87f9f20687c938828f1138f91682d568d2a31e53/types/request.d.ts#L41 -export interface FastifyRequest { +interface FastifyRequest { routeOptions?: { method?: string; url?: string; @@ -10,7 +10,7 @@ export interface FastifyRequest { } // Partial extract of ExpressRequest interface -export interface ExpressRequest { +interface ExpressRequest { route?: { path?: string; };