From cdf43512d285935f975504c7ae945985a4f4945e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 21 Jan 2025 18:46:37 +0100 Subject: [PATCH 01/18] add basic e2e test --- .../test-applications/nestjs-11/.gitignore | 56 ++ .../test-applications/nestjs-11/.npmrc | 2 + .../test-applications/nestjs-11/nest-cli.json | 8 + .../test-applications/nestjs-11/package.json | 48 ++ .../nestjs-11/playwright.config.mjs | 7 + .../nestjs-11/src/app.controller.ts | 124 +++ .../nestjs-11/src/app.module.ts | 29 + .../nestjs-11/src/app.service.ts | 113 +++ .../src/async-example.interceptor.ts | 17 + .../nestjs-11/src/example-1.interceptor.ts | 15 + .../nestjs-11/src/example-2.interceptor.ts | 10 + .../src/example-global-filter.exception.ts | 5 + .../nestjs-11/src/example-global.filter.ts | 19 + .../src/example-local-filter.exception.ts | 5 + .../nestjs-11/src/example-local.filter.ts | 19 + .../nestjs-11/src/example.guard.ts | 10 + .../nestjs-11/src/example.middleware.ts | 12 + .../nestjs-11/src/instrument.ts | 12 + .../test-applications/nestjs-11/src/main.ts | 15 + .../nestjs-11/start-event-proxy.mjs | 6 + .../nestjs-11/tests/cron-decorator.test.ts | 81 ++ .../nestjs-11/tests/errors.test.ts | 167 ++++ .../nestjs-11/tests/span-decorator.test.ts | 79 ++ .../nestjs-11/tests/transactions.test.ts | 729 ++++++++++++++++++ .../nestjs-11/tsconfig.build.json | 4 + .../test-applications/nestjs-11/tsconfig.json | 22 + 26 files changed, 1614 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/nest-cli.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/package.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/app.controller.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/app.module.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/app.service.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/async-example.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example-1.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example-2.interceptor.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example-global-filter.exception.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example-global.filter.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example-local-filter.exception.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example-local.filter.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example.guard.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/example.middleware.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/src/main.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/tests/cron-decorator.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/tests/span-decorator.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/tsconfig.build.json create mode 100644 dev-packages/e2e-tests/test-applications/nestjs-11/tsconfig.json diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/.gitignore b/dev-packages/e2e-tests/test-applications/nestjs-11/.gitignore new file mode 100644 index 000000000000..4b56acfbebf4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/.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-11/.npmrc b/dev-packages/e2e-tests/test-applications/nestjs-11/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/.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-11/nest-cli.json b/dev-packages/e2e-tests/test-applications/nestjs-11/nest-cli.json new file mode 100644 index 000000000000..f9aa683b1ad5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json new file mode 100644 index 000000000000..9ba374954190 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json @@ -0,0 +1,48 @@ +{ + "name": "nestjs-11", + "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": "^11.0.0", + "@nestjs/core": "^11.0.0", + "@nestjs/microservices": "^11.0.0", + "@nestjs/schedule": "^5.0.0", + "@nestjs/platform-express": "^11.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": "^11.0.0", + "@nestjs/schematics": "^11.0.0", + "@nestjs/testing": "^11.0.0", + "@types/express": "^4.17.17", + "@types/node": "^18.19.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.0.0" + } +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/nestjs-11/playwright.config.mjs new file mode 100644 index 000000000000..31f2b913b58b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/app.controller.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/app.controller.ts new file mode 100644 index 000000000000..33a6b1957d99 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/app.module.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/app.module.ts new file mode 100644 index 000000000000..3de3c82dc925 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/app.service.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/app.service.ts new file mode 100644 index 000000000000..242b4c778a0e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/async-example.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/async-example.interceptor.ts new file mode 100644 index 000000000000..ac0ee60acc51 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example-1.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example-1.interceptor.ts new file mode 100644 index 000000000000..81c9f70d30e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example-2.interceptor.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example-2.interceptor.ts new file mode 100644 index 000000000000..2cf9dfb9e043 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example-global-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example-global-filter.exception.ts new file mode 100644 index 000000000000..41981ba748fe --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example-global.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example-global.filter.ts new file mode 100644 index 000000000000..988696d0e13d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example-local-filter.exception.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example-local-filter.exception.ts new file mode 100644 index 000000000000..8f76520a3b94 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example-local.filter.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example-local.filter.ts new file mode 100644 index 000000000000..505217f5dcbd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example.guard.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example.guard.ts new file mode 100644 index 000000000000..e12bbdc4e994 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/example.middleware.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/example.middleware.ts new file mode 100644 index 000000000000..31d15c9372ea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/instrument.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/instrument.ts new file mode 100644 index 000000000000..4f16ebb36d11 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-11/src/main.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/src/main.ts new file mode 100644 index 000000000000..71ce685f4d61 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/src/main.ts @@ -0,0 +1,15 @@ +// Import this first +import './instrument'; + +// Import other modules +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +const PORT = 3030; + +async function bootstrap() { + const app = await NestFactory.create(AppModule); + await app.listen(PORT); +} + +bootstrap(); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/nestjs-11/start-event-proxy.mjs new file mode 100644 index 000000000000..d61ed3b5d609 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'nestjs-11', +}); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/cron-decorator.test.ts new file mode 100644 index 000000000000..7896603b3bd9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-basic', envelope => { + return ( + envelope[0].type === 'check_in' && + envelope[1]['monitor_slug'] === 'test-cron-slug' && + envelope[1]['status'] === 'in_progress' + ); + }); + + const okEnvelopePromise = waitForEnvelopeItem('nestjs-basic', 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-basic', 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-11/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts new file mode 100644 index 000000000000..748730985cf6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-basic', 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}/), + parent_span_id: expect.stringMatching(/[a-f0-9]{16}/), + }); +}); + +test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { + let errorEventOccurred = false; + + waitForError('nestjs-basic', 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-basic', 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-basic', transactionEvent => { + return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; + }); + + const transactionEventPromise500 = waitForTransaction('nestjs-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-11/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/span-decorator.test.ts new file mode 100644 index 000000000000..b82213e6157a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-basic', 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-basic', 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-11/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts new file mode 100644 index 000000000000..c37eb8da7cc1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/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-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-basic', 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-11/tsconfig.build.json b/dev-packages/e2e-tests/test-applications/nestjs-11/tsconfig.build.json new file mode 100644 index 000000000000..26c30d4eddf2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tsconfig.build.json @@ -0,0 +1,4 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "test", "dist"] +} diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tsconfig.json b/dev-packages/e2e-tests/test-applications/nestjs-11/tsconfig.json new file mode 100644 index 000000000000..cf79f029c781 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true, + "strictNullChecks": false, + "noImplicitAny": false, + "strictBindCallApply": false, + "forceConsistentCasingInFileNames": false, + "noFallthroughCasesInSwitch": false, + "moduleResolution": "Node16" + } +} From 21e46bc9b0af50078d4b2b7eb3df1e6669260eef Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 21 Jan 2025 18:47:03 +0100 Subject: [PATCH 02/18] inline otel instrumentation for testing --- .../sentry-nest-core-instrumentation.ts | 308 ++++++++++++++++++ 1 file changed, 308 insertions(+) create mode 100644 packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts diff --git a/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts new file mode 100644 index 000000000000..07fec8f19d09 --- /dev/null +++ b/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts @@ -0,0 +1,308 @@ +/* + * This file is based on code from the OpenTelemetry Authors + * Source: https://github.com/open-telemetry/opentelemetry-js-contrib + * + * Modified for immediate requirements while maintaining compliance + * with the original Apache 2.0 license terms. + * + * Original License: + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as api from '@opentelemetry/api'; +import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; +import { + InstrumentationBase, + InstrumentationNodeModuleDefinition, + InstrumentationNodeModuleFile, + isWrapped, +} from '@opentelemetry/instrumentation'; +import type { NestFactory } from '@nestjs/core/nest-factory.js'; +import type { RouterExecutionContext } from '@nestjs/core/router/router-execution-context.js'; +import type { Controller } from '@nestjs/common/interfaces'; +import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; + +import { SDK_VERSION } from '@sentry/core'; + +const supportedVersions = ['>=4.0.0 <12']; +const COMPONENT = '@nestjs/core'; + +enum AttributeNames { + VERSION = 'nestjs.version', + TYPE = 'nestjs.type', + MODULE = 'nestjs.module', + CONTROLLER = 'nestjs.controller', + CALLBACK = 'nestjs.callback', + PIPES = 'nestjs.pipes', + INTERCEPTORS = 'nestjs.interceptors', + GUARDS = 'nestjs.guards', +} + +export enum NestType { + APP_CREATION = 'app_creation', + REQUEST_CONTEXT = 'request_context', + REQUEST_HANDLER = 'handler', +} + +/** + * + */ +export class NestInstrumentation extends InstrumentationBase { + public constructor(config: InstrumentationConfig = {}) { + super('sentry-nestjs', SDK_VERSION, config); + } + + /** + * + */ + public init(): InstrumentationNodeModuleDefinition { + const module = new InstrumentationNodeModuleDefinition(COMPONENT, supportedVersions); + + module.files.push( + this._getNestFactoryFileInstrumentation(supportedVersions), + this._getRouterExecutionContextFileInstrumentation(supportedVersions), + ); + + return module; + } + + /** + * + */ + private _getNestFactoryFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { + return new InstrumentationNodeModuleFile( + '@nestjs/core/nest-factory.js', + versions, + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (NestFactoryStatic: any, moduleVersion?: string) => { + this._ensureWrapped( + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + NestFactoryStatic.NestFactoryStatic.prototype, + 'create', + createWrapNestFactoryCreate(this.tracer, moduleVersion), + ); + return NestFactoryStatic; + }, + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (NestFactoryStatic: any) => { + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this._unwrap(NestFactoryStatic.NestFactoryStatic.prototype, 'create'); + }, + ); + } + + /** + * + */ + private _getRouterExecutionContextFileInstrumentation(versions: string[]): InstrumentationNodeModuleFile { + return new InstrumentationNodeModuleFile( + '@nestjs/core/router/router-execution-context.js', + versions, + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (RouterExecutionContext: any, moduleVersion?: string) => { + this._ensureWrapped( + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + RouterExecutionContext.RouterExecutionContext.prototype, + 'create', + createWrapCreateHandler(this.tracer, moduleVersion), + ); + return RouterExecutionContext; + }, + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (RouterExecutionContext: any) => { + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + this._unwrap(RouterExecutionContext.RouterExecutionContext.prototype, 'create'); + }, + ); + } + + /** + * + */ + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private _ensureWrapped(obj: any, methodName: string, wrapper: (original: any) => any): void { + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (isWrapped(obj[methodName])) { + this._unwrap(obj, methodName); + } + this._wrap(obj, methodName, wrapper); + } +} + +function createWrapNestFactoryCreate(tracer: api.Tracer, moduleVersion?: string) { + return function wrapCreate(original: typeof NestFactory.create) { + return function createWithTrace( + this: typeof NestFactory, + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + nestModule: any, + /* serverOrOptions */ + ) { + const span = tracer.startSpan('Create Nest App', { + attributes: { + component: COMPONENT, + [AttributeNames.TYPE]: NestType.APP_CREATION, + [AttributeNames.VERSION]: moduleVersion, + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + [AttributeNames.MODULE]: nestModule.name, + }, + }); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + // todo + // eslint-disable-next-line prefer-rest-params, @typescript-eslint/no-explicit-any + return await original.apply(this, arguments as any); + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + }; +} + +function createWrapCreateHandler(tracer: api.Tracer, moduleVersion?: string) { + return function wrapCreateHandler(original: RouterExecutionContext['create']) { + return function createHandlerWithTrace( + this: RouterExecutionContext, + instance: Controller, + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback: (...args: any[]) => unknown, + ) { + // todo + // eslint-disable-next-line prefer-rest-params + arguments[1] = createWrapHandler(tracer, moduleVersion, callback); + // todo + // eslint-disable-next-line prefer-rest-params, @typescript-eslint/no-explicit-any + const handler = original.apply(this, arguments as any); + const callbackName = callback.name; + const instanceName = + // todo + // eslint-disable-next-line @typescript-eslint/prefer-optional-chain + instance.constructor && instance.constructor.name ? instance.constructor.name : 'UnnamedInstance'; + const spanName = callbackName ? `${instanceName}.${callbackName}` : instanceName; + + // todo + // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-explicit-any + return function (this: any, req: any, res: any, next: (...args: any[]) => unknown) { + const span = tracer.startSpan(spanName, { + attributes: { + component: COMPONENT, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.TYPE]: NestType.REQUEST_CONTEXT, + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + [ATTR_HTTP_REQUEST_METHOD]: req.method, + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, deprecation/deprecation + [SEMATTRS_HTTP_URL]: req.originalUrl || req.url, + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + [ATTR_HTTP_ROUTE]: req.route?.path || req.routeOptions?.url || req.routerPath, + [AttributeNames.CONTROLLER]: instanceName, + [AttributeNames.CALLBACK]: callbackName, + }, + }); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + // todo + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, prefer-rest-params + return await handler.apply(this, arguments as unknown); + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + }; + }; +} + +function createWrapHandler( + tracer: api.Tracer, + moduleVersion: string | undefined, + // todo + // eslint-disable-next-line @typescript-eslint/ban-types + handler: Function, + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): (this: RouterExecutionContext) => Promise { + const spanName = handler.name || 'anonymous nest handler'; + const options = { + attributes: { + component: COMPONENT, + [AttributeNames.VERSION]: moduleVersion, + [AttributeNames.TYPE]: NestType.REQUEST_HANDLER, + [AttributeNames.CALLBACK]: handler.name, + }, + }; + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const wrappedHandler = function (this: RouterExecutionContext): Promise { + const span = tracer.startSpan(spanName, options); + const spanContext = api.trace.setSpan(api.context.active(), span); + + return api.context.with(spanContext, async () => { + try { + // todo + // eslint-disable-next-line prefer-rest-params + return await handler.apply(this, arguments); + // todo + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (e: any) { + throw addError(span, e); + } finally { + span.end(); + } + }); + }; + + if (handler.name) { + Object.defineProperty(wrappedHandler, 'name', { value: handler.name }); + } + + // Get the current metadata and set onto the wrapper to ensure other decorators ( ie: NestJS EventPattern / RolesGuard ) + // won't be affected by the use of this instrumentation + Reflect.getMetadataKeys(handler).forEach(metadataKey => { + Reflect.defineMetadata(metadataKey, Reflect.getMetadata(metadataKey, handler), wrappedHandler); + }); + return wrappedHandler; +} + +const addError = (span: api.Span, error: Error): Error => { + span.recordException(error); + span.setStatus({ code: api.SpanStatusCode.ERROR, message: error.message }); + return error; +}; From 29af416e5c3db60124cea55c671b9e86561d7c72 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 21 Jan 2025 18:47:21 +0100 Subject: [PATCH 03/18] bump versions --- packages/nestjs/package.json | 8 ++++---- packages/nestjs/src/integrations/nest.ts | 2 +- .../src/integrations/sentry-nest-instrumentation.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 23f8cabfe903..c09c2f440d85 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -51,12 +51,12 @@ "@sentry/node": "9.0.0-alpha.1" }, "devDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" }, "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" }, "scripts": { "build": "run-p build:transpile build:types", diff --git a/packages/nestjs/src/integrations/nest.ts b/packages/nestjs/src/integrations/nest.ts index b7f1a8ef1485..fe205a4a69b8 100644 --- a/packages/nestjs/src/integrations/nest.ts +++ b/packages/nestjs/src/integrations/nest.ts @@ -1,4 +1,4 @@ -import { NestInstrumentation } from '@opentelemetry/instrumentation-nestjs-core'; +import { NestInstrumentation } from './sentry-nest-core-instrumentation'; import { defineIntegration } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node'; import { SentryNestEventInstrumentation } from './sentry-nest-event-instrumentation'; diff --git a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts index ea7d65176aed..c4f36728c906 100644 --- a/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-instrumentation.ts @@ -19,7 +19,7 @@ import { import { getMiddlewareSpanOptions, getNextProxy, instrumentObservable, isPatched } from './helpers'; import type { CallHandler, CatchTarget, InjectableTarget, MinimalNestJsExecutionContext, Observable } from './types'; -const supportedVersions = ['>=8.0.0 <11']; +const supportedVersions = ['>=8.0.0 <12']; const COMPONENT = '@nestjs/common'; /** From fc5684d80e13a831a59d274062236dda33734fa6 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 22 Jan 2025 09:17:52 +0100 Subject: [PATCH 04/18] update nest-11 tests --- .../nestjs-11/tests/cron-decorator.test.ts | 6 ++--- .../nestjs-11/tests/errors.test.ts | 24 +++++++++---------- .../nestjs-11/tests/span-decorator.test.ts | 4 ++-- .../nestjs-11/tests/transactions.test.ts | 18 +++++++------- 4 files changed, 26 insertions(+), 26 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/cron-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/cron-decorator.test.ts index 7896603b3bd9..e6ac7ae855ae 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/cron-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/cron-decorator.test.ts @@ -2,7 +2,7 @@ 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-basic', envelope => { + const inProgressEnvelopePromise = waitForEnvelopeItem('nestjs-11', envelope => { return ( envelope[0].type === 'check_in' && envelope[1]['monitor_slug'] === 'test-cron-slug' && @@ -10,7 +10,7 @@ test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { ); }); - const okEnvelopePromise = waitForEnvelopeItem('nestjs-basic', envelope => { + const okEnvelopePromise = waitForEnvelopeItem('nestjs-11', envelope => { return ( envelope[0].type === 'check_in' && envelope[1]['monitor_slug'] === 'test-cron-slug' && @@ -63,7 +63,7 @@ test('Cron job triggers send of in_progress envelope', async ({ baseURL }) => { }); test('Sends exceptions to Sentry on error in cron job', async ({ baseURL }) => { - const errorEventPromise = waitForError('nestjs-basic', event => { + const errorEventPromise = waitForError('nestjs-11', event => { return !event.type && event.exception?.values?.[0]?.value === 'Test error from cron job'; }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts index 748730985cf6..4eb20da60746 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts @@ -1,8 +1,8 @@ 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-basic', event => { +test.only('Sends exception to Sentry', async ({ baseURL }) => { + const errorEventPromise = waitForError('nestjs-11', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); @@ -33,7 +33,7 @@ test('Sends exception to Sentry', async ({ baseURL }) => { test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs-basic', event => { + waitForError('nestjs-11', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 400 exception with id 123') { errorEventOccurred = true; } @@ -41,7 +41,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-400-exception/:id'; }); - waitForError('nestjs-basic', event => { + waitForError('nestjs-11', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected 500 exception with id 123') { errorEventOccurred = true; } @@ -49,11 +49,11 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-500-exception/:id'; }); - const transactionEventPromise400 = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise400 = waitForTransaction('nestjs-11', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; }); - const transactionEventPromise500 = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise500 = waitForTransaction('nestjs-11', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; }); @@ -74,7 +74,7 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { let errorEventOccurred = false; - waitForError('nestjs-basic', event => { + waitForError('nestjs-11', event => { if (!event.type && event.exception?.values?.[0]?.value === 'This is an expected RPC exception with id 123') { errorEventOccurred = true; } @@ -82,7 +82,7 @@ test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { return event?.transaction === 'GET /test-expected-rpc-exception/:id'; }); - const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; }); @@ -101,7 +101,7 @@ test('Global exception filter registered in main module is applied and exception }) => { let errorEventOccurred = false; - waitForError('nestjs-basic', event => { + waitForError('nestjs-11', event => { if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by global filter!') { errorEventOccurred = true; } @@ -109,7 +109,7 @@ test('Global exception filter registered in main module is applied and exception return event?.transaction === 'GET /example-exception-global-filter'; }); - const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return transactionEvent?.transaction === 'GET /example-exception-global-filter'; }); @@ -136,7 +136,7 @@ test('Local exception filter registered in main module is applied and exception }) => { let errorEventOccurred = false; - waitForError('nestjs-basic', event => { + waitForError('nestjs-11', event => { if (!event.type && event.exception?.values?.[0]?.value === 'Example exception was handled by local filter!') { errorEventOccurred = true; } @@ -144,7 +144,7 @@ test('Local exception filter registered in main module is applied and exception return event?.transaction === 'GET /example-exception-local-filter'; }); - const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return transactionEvent?.transaction === 'GET /example-exception-local-filter'; }); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/span-decorator.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/span-decorator.test.ts index b82213e6157a..3d63c1d17953 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/span-decorator.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/span-decorator.test.ts @@ -2,7 +2,7 @@ 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-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-async' @@ -37,7 +37,7 @@ test('Transaction includes span and correct value for decorated async function', }); test('Transaction includes span and correct value for decorated sync function', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-span-decorator-sync' diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts index c37eb8da7cc1..1209eae1ada9 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts @@ -2,7 +2,7 @@ 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-basic', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-transaction' @@ -125,7 +125,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({ baseURL, }) => { - const pageloadTransactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-middleware-instrumentation' @@ -205,7 +205,7 @@ test('API route transaction includes nest middleware span. Spans created in and test('API route transaction includes nest guard span and span started in guard is nested correctly', async ({ baseURL, }) => { - const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-guard-instrumentation' @@ -268,7 +268,7 @@ test('API route transaction includes nest guard span and span started in guard i }); test('API route transaction includes nest pipe span for valid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && @@ -305,7 +305,7 @@ test('API route transaction includes nest pipe span for valid request', async ({ }); test('API route transaction includes nest pipe span for invalid request', async ({ baseURL }) => { - const transactionEventPromise = waitForTransaction('nestjs-basic', transactionEvent => { + const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && @@ -344,7 +344,7 @@ test('API route transaction includes nest pipe span for invalid request', async 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-basic', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' @@ -462,7 +462,7 @@ test('API route transaction includes nest interceptor spans before route executi 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-basic', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-interceptor-instrumentation' @@ -544,7 +544,7 @@ test('API route transaction includes exactly one nest interceptor span after rou 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-basic', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' @@ -629,7 +629,7 @@ test('API route transaction includes nest async interceptor spans before route e 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-basic', transactionEvent => { + const pageloadTransactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && transactionEvent?.transaction === 'GET /test-async-interceptor-instrumentation' From 3f8d55a723650c3c86865554b4caffaf9b0741d8 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 22 Jan 2025 09:19:43 +0100 Subject: [PATCH 05/18] ... --- .../e2e-tests/test-applications/nestjs-11/tests/errors.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts index 4eb20da60746..a24d1010eca4 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { waitForError, waitForTransaction } from '@sentry-internal/test-utils'; -test.only('Sends exception to Sentry', async ({ baseURL }) => { +test('Sends exception to Sentry', async ({ baseURL }) => { const errorEventPromise = waitForError('nestjs-11', event => { return !event.type && event.exception?.values?.[0]?.value === 'This is an exception with id 123'; }); From 90d93d6e251cb2b4698e56ff605a3e51fdd18d86 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 22 Jan 2025 09:22:27 +0100 Subject: [PATCH 06/18] biome --- .../src/integrations/sentry-nest-core-instrumentation.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts b/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts index 07fec8f19d09..a7fdb66558ba 100644 --- a/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts +++ b/packages/nestjs/src/integrations/sentry-nest-core-instrumentation.ts @@ -19,6 +19,9 @@ * limitations under the License. */ +import type { Controller } from '@nestjs/common/interfaces'; +import type { NestFactory } from '@nestjs/core/nest-factory.js'; +import type { RouterExecutionContext } from '@nestjs/core/router/router-execution-context.js'; import * as api from '@opentelemetry/api'; import type { InstrumentationConfig } from '@opentelemetry/instrumentation'; import { @@ -27,9 +30,6 @@ import { InstrumentationNodeModuleFile, isWrapped, } from '@opentelemetry/instrumentation'; -import type { NestFactory } from '@nestjs/core/nest-factory.js'; -import type { RouterExecutionContext } from '@nestjs/core/router/router-execution-context.js'; -import type { Controller } from '@nestjs/common/interfaces'; import { ATTR_HTTP_REQUEST_METHOD, ATTR_HTTP_ROUTE, SEMATTRS_HTTP_URL } from '@opentelemetry/semantic-conventions'; import { SDK_VERSION } from '@sentry/core'; From ac60b21689e5435a8e3a831c29e0c10e7bd44fad Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 22 Jan 2025 11:10:09 +0100 Subject: [PATCH 07/18] use nest 10 dev dependencies --- packages/nestjs/package.json | 4 ++-- yarn.lock | 25 +++++++++++++++---------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index c09c2f440d85..01c9fca623cc 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -51,8 +51,8 @@ "@sentry/node": "9.0.0-alpha.1" }, "devDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0" + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0 || ^11.0.0", diff --git a/yarn.lock b/yarn.lock index 83ff270a83f9..2da833fa441e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4768,14 +4768,14 @@ iterare "1.2.1" tslib "2.7.0" -"@nestjs/common@^8.0.0 || ^9.0.0 || ^10.0.0": - version "10.4.7" - resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.7.tgz#076cb77c06149805cb1e193d8cdc69bbe8446c75" - integrity sha512-gIOpjD3Mx8gfYGxYm/RHPcJzqdknNNFCyY+AxzBT3gc5Xvvik1Dn5OxaMGw5EbVfhZgJKVP0n83giUOAlZQe7w== +"@nestjs/common@^10.0.0": + version "10.4.15" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-10.4.15.tgz#27c291466d9100eb86fdbe6f7bbb4d1a6ad55f70" + integrity sha512-vaLg1ZgwhG29BuLDxPA9OAcIlgqzp9/N8iG0wGapyUNTf4IY4O6zAHgN6QalwLhFxq7nOI021vdRojR1oF3bqg== dependencies: uid "2.0.2" iterare "1.2.1" - tslib "2.7.0" + tslib "2.8.1" "@nestjs/core@10.4.6": version "10.4.6" @@ -4789,17 +4789,17 @@ path-to-regexp "3.3.0" tslib "2.7.0" -"@nestjs/core@^8.0.0 || ^9.0.0 || ^10.0.0": - version "10.4.7" - resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.7.tgz#adb27067a8c40b79f0713b417457fdfc6cf3406a" - integrity sha512-AIpQzW/vGGqSLkKvll1R7uaSNv99AxZI2EFyVJPNGDgFsfXaohfV1Ukl6f+s75Km+6Fj/7aNl80EqzNWQCS8Ig== +"@nestjs/core@^10.0.0": + version "10.4.15" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-10.4.15.tgz#1343a3395d5c54e9b792608cb75eef39053806d5" + integrity sha512-UBejmdiYwaH6fTsz2QFBlC1cJHM+3UDeLZN+CiP9I1fRv2KlBZsmozGLbV5eS1JAVWJB4T5N5yQ0gjN8ZvcS2w== dependencies: uid "2.0.2" "@nuxtjs/opencollective" "0.3.2" fast-safe-stringify "2.1.1" iterare "1.2.1" path-to-regexp "3.3.0" - tslib "2.7.0" + tslib "2.8.1" "@nestjs/platform-express@10.4.6": version "10.4.6" @@ -28554,6 +28554,11 @@ tslib@2.7.0, tslib@^2.0.0, tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.2. resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01" integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA== +tslib@2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + tslib@^1.11.1, tslib@^1.8.1, tslib@^1.9.0: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" From 0d68954a37fb18ee2b36fbe2f9963a1729f99507 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 24 Jan 2025 14:45:12 +0100 Subject: [PATCH 08/18] add missing otel dependency --- packages/nestjs/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 01c9fca623cc..36a49b83e61d 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -45,6 +45,7 @@ }, "dependencies": { "@opentelemetry/core": "^1.30.1", + "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/instrumentation-nestjs-core": "0.44.0", "@sentry/core": "9.0.0-alpha.1", From 9741129d0c362f8cc04068b1b48c62cfeedd8b69 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Fri, 24 Jan 2025 14:47:31 +0100 Subject: [PATCH 09/18] update e2e to run in node 20 --- dev-packages/e2e-tests/run.ts | 29 ++++++++++++++++--- .../test-applications/nestjs-11/package.json | 3 ++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index f8aafa5eaa01..087600f2f51f 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -5,14 +5,34 @@ import * as dotenv from 'dotenv'; import { sync as globSync } from 'glob'; import { registrySetup } from './registrySetup'; +import { readFileSync } from 'fs'; const DEFAULT_DSN = 'https://username@domain/123'; const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; -function asyncExec(command: string, options: { env: Record; cwd: string }): Promise { +interface PackageJson { + volta?: { + node?: string; + }; +} + +function getVoltaNodeVersion(packageJsonPath: string): string | undefined { + try { + const packageJson: PackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); + return packageJson.volta?.node; + } catch { + return undefined; + } +} + +function asyncExec( + command: string, + options: { env: Record; cwd: string; nodeVersion?: string }, +): Promise { return new Promise((resolve, reject) => { - const process = spawn(command, { ...options, shell: true }); + const finalCommand = options.nodeVersion ? `volta run --node ${options.nodeVersion} ${command}` : command; + const process = spawn(finalCommand, { ...options, shell: true }); process.stdout.on('data', data => { console.log(`${data}`); @@ -75,12 +95,13 @@ async function run(): Promise { for (const testAppPath of testAppPaths) { const cwd = resolve('test-applications', testAppPath); + const nodeVersion = getVoltaNodeVersion(resolve(cwd, 'package.json')); console.log(`Building ${testAppPath}...`); - await asyncExec('pnpm test:build', { env, cwd }); + await asyncExec('pnpm test:build', { env, cwd, nodeVersion }); console.log(`Testing ${testAppPath}...`); - await asyncExec('pnpm test:assert', { env, cwd }); + await asyncExec('pnpm test:assert', { env, cwd, nodeVersion }); } } catch (error) { console.error(error); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json index 9ba374954190..cb7b500faa8d 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json @@ -44,5 +44,8 @@ "ts-loader": "^9.4.3", "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" + }, + "volta": { + "node": "20.18.2" } } From 0703ba61026f1b504cf35ac76fa048913ea51c9a Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 6 Feb 2025 15:41:38 +0100 Subject: [PATCH 10/18] update tests for nestjs 11 --- .../nestjs-11/tests/errors.test.ts | 12 ++++++++---- .../nestjs-11/tests/transactions.test.ts | 15 +++++++++------ packages/nestjs/package.json | 1 + 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts index a24d1010eca4..0fa13fea32aa 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/errors.test.ts @@ -50,11 +50,13 @@ test('Does not send HttpExceptions to Sentry', async ({ baseURL }) => { }); const transactionEventPromise400 = waitForTransaction('nestjs-11', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-expected-400-exception/:id'; + // todo(express-5): parametrize /test-expected-400-exception/:id + return transactionEvent?.transaction === 'GET /test-expected-400-exception/123'; }); const transactionEventPromise500 = waitForTransaction('nestjs-11', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-expected-500-exception/:id'; + // todo(express-5): parametrize /test-expected-500-exception/:id + return transactionEvent?.transaction === 'GET /test-expected-500-exception/123'; }); const response400 = await fetch(`${baseURL}/test-expected-400-exception/123`); @@ -79,11 +81,13 @@ test('Does not send RpcExceptions to Sentry', async ({ baseURL }) => { errorEventOccurred = true; } - return event?.transaction === 'GET /test-expected-rpc-exception/:id'; + // todo(express-5): parametrize /test-expected-rpc-exception/:id + return event?.transaction === 'GET /test-expected-rpc-exception/123'; }); const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { - return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/:id'; + // todo(express-5): parametrize /test-expected-rpc-exception/:id + return transactionEvent?.transaction === 'GET /test-expected-rpc-exception/123'; }); const response = await fetch(`${baseURL}/test-expected-rpc-exception/123`); diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts index 1209eae1ada9..7e0947d53ec1 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/tests/transactions.test.ts @@ -15,7 +15,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect(transactionEvent.contexts?.trace).toEqual({ data: { - 'sentry.source': 'route', + 'sentry.source': 'url', // todo(express-5): 'route' 'sentry.origin': 'auto.http.otel.http', 'sentry.op': 'http.server', 'sentry.sample_rate': 1, @@ -37,7 +37,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'net.peer.port': expect.any(Number), 'http.status_code': 200, 'http.status_text': 'OK', - 'http.route': '/test-transaction', + // 'http.route': '/test-transaction', // todo(express-5): add this line again }, op: 'http.server', span_id: expect.stringMatching(/[a-f0-9]{16}/), @@ -49,6 +49,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { expect(transactionEvent).toEqual( expect.objectContaining({ spans: expect.arrayContaining([ + /* todo(express-5): add this part again { data: { 'express.name': '/test-transaction', @@ -66,7 +67,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { timestamp: expect.any(Number), trace_id: expect.stringMatching(/[a-f0-9]{32}/), origin: 'auto.http.otel.express', - }, + }, */ { data: { 'sentry.origin': 'manual', @@ -116,7 +117,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { transaction: 'GET /test-transaction', type: 'transaction', transaction_info: { - source: 'route', + source: 'url', // todo(express-5): 'route' }, }), ); @@ -271,7 +272,8 @@ test('API route transaction includes nest pipe span for valid request', async ({ const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + // todo(express-5): parametrize test-pipe-instrumentation/:id + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/123' && transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/123') ); }); @@ -308,7 +310,8 @@ test('API route transaction includes nest pipe span for invalid request', async const transactionEventPromise = waitForTransaction('nestjs-11', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && - transactionEvent?.transaction === 'GET /test-pipe-instrumentation/:id' && + // todo(express-5): parametrize test-pipe-instrumentation/:id + transactionEvent?.transaction === 'GET /test-pipe-instrumentation/abc' && transactionEvent?.request?.url?.includes('/test-pipe-instrumentation/abc') ); }); diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index 36a49b83e61d..54bf231585e7 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -48,6 +48,7 @@ "@opentelemetry/api": "^1.9.0", "@opentelemetry/instrumentation": "^0.57.1", "@opentelemetry/instrumentation-nestjs-core": "0.44.0", + "@opentelemetry/semantic-conventions": "^1.27.0", "@sentry/core": "9.0.0-alpha.1", "@sentry/node": "9.0.0-alpha.1" }, From 2cc6f13c2dc09382b6fd6ccc977103a5b16d9550 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 6 Feb 2025 15:52:32 +0100 Subject: [PATCH 11/18] fix formatting --- dev-packages/e2e-tests/run.ts | 2 +- packages/nestjs/src/integrations/nest.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index 087600f2f51f..234cb9b96fcd 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -4,8 +4,8 @@ import { resolve } from 'path'; import * as dotenv from 'dotenv'; import { sync as globSync } from 'glob'; -import { registrySetup } from './registrySetup'; import { readFileSync } from 'fs'; +import { registrySetup } from './registrySetup'; const DEFAULT_DSN = 'https://username@domain/123'; const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; diff --git a/packages/nestjs/src/integrations/nest.ts b/packages/nestjs/src/integrations/nest.ts index fe205a4a69b8..4cc68c720541 100644 --- a/packages/nestjs/src/integrations/nest.ts +++ b/packages/nestjs/src/integrations/nest.ts @@ -1,6 +1,6 @@ -import { NestInstrumentation } from './sentry-nest-core-instrumentation'; import { defineIntegration } from '@sentry/core'; import { generateInstrumentOnce } from '@sentry/node'; +import { NestInstrumentation } from './sentry-nest-core-instrumentation'; import { SentryNestEventInstrumentation } from './sentry-nest-event-instrumentation'; import { SentryNestInstrumentation } from './sentry-nest-instrumentation'; From fdd7f4cff2826891789fd0930b8f04a867c7cf08 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 6 Feb 2025 16:43:58 +0100 Subject: [PATCH 12/18] fix fastify semantic conventions --- .../nestjs-fastify/tests/transactions.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 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 96b60e5d976f..09870b9ce1cb 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 @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test('Sends an API route transaction', async ({ baseURL }) => { +test.only('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && @@ -25,7 +25,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { 'http.url': 'http://localhost:3030/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', - 'http.method': 'GET', + 'http.request.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-transaction', 'http.user_agent': 'node', @@ -92,7 +92,7 @@ test('Sends an API route transaction', async ({ baseURL }) => { component: '@nestjs/core', 'nestjs.version': expect.any(String), 'nestjs.type': 'request_context', - 'http.method': 'GET', + 'http.request.method': 'GET', 'http.url': '/test-transaction', 'http.route': '/test-transaction', 'nestjs.controller': 'AppController', From e592e4ace0304473aba03cbeb3739ad49f7ed4ff Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 6 Feb 2025 16:45:47 +0100 Subject: [PATCH 13/18] fix test 2 --- .../test-applications/nestjs-fastify/tests/transactions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 09870b9ce1cb..078c9aafc496 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 @@ -25,7 +25,7 @@ test.only('Sends an API route transaction', async ({ baseURL }) => { 'http.url': 'http://localhost:3030/test-transaction', 'http.host': 'localhost:3030', 'net.host.name': 'localhost', - 'http.request.method': 'GET', + 'http.method': 'GET', 'http.scheme': 'http', 'http.target': '/test-transaction', 'http.user_agent': 'node', From a562ae65645757a8dc466750baf938e39074fb22 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 6 Feb 2025 17:07:23 +0100 Subject: [PATCH 14/18] remove .only --- .../test-applications/nestjs-fastify/tests/transactions.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 078c9aafc496..337e98decc31 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 @@ -1,7 +1,7 @@ import { expect, test } from '@playwright/test'; import { waitForTransaction } from '@sentry-internal/test-utils'; -test.only('Sends an API route transaction', async ({ baseURL }) => { +test('Sends an API route transaction', async ({ baseURL }) => { const pageloadTransactionEventPromise = waitForTransaction('nestjs-fastify', transactionEvent => { return ( transactionEvent?.contexts?.trace?.op === 'http.server' && From 1e433fcc5c479b44f59b0ee624620cb39780db72 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 10 Feb 2025 15:39:50 +0100 Subject: [PATCH 15/18] undo changes in run script --- dev-packages/e2e-tests/run.ts | 26 ++++---------------------- 1 file changed, 4 insertions(+), 22 deletions(-) diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index 234cb9b96fcd..158dd2a6f7e6 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -1,38 +1,21 @@ /* eslint-disable no-console */ import { spawn } from 'child_process'; -import { resolve } from 'path'; import * as dotenv from 'dotenv'; import { sync as globSync } from 'glob'; +import { resolve } from 'path'; -import { readFileSync } from 'fs'; import { registrySetup } from './registrySetup'; const DEFAULT_DSN = 'https://username@domain/123'; const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; -interface PackageJson { - volta?: { - node?: string; - }; -} - -function getVoltaNodeVersion(packageJsonPath: string): string | undefined { - try { - const packageJson: PackageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); - return packageJson.volta?.node; - } catch { - return undefined; - } -} - function asyncExec( command: string, options: { env: Record; cwd: string; nodeVersion?: string }, ): Promise { return new Promise((resolve, reject) => { - const finalCommand = options.nodeVersion ? `volta run --node ${options.nodeVersion} ${command}` : command; - const process = spawn(finalCommand, { ...options, shell: true }); + const process = spawn(command, { ...options, shell: true }); process.stdout.on('data', data => { console.log(`${data}`); @@ -95,13 +78,12 @@ async function run(): Promise { for (const testAppPath of testAppPaths) { const cwd = resolve('test-applications', testAppPath); - const nodeVersion = getVoltaNodeVersion(resolve(cwd, 'package.json')); console.log(`Building ${testAppPath}...`); - await asyncExec('pnpm test:build', { env, cwd, nodeVersion }); + await asyncExec('pnpm test:build', { env, cwd }); console.log(`Testing ${testAppPath}...`); - await asyncExec('pnpm test:assert', { env, cwd, nodeVersion }); + await asyncExec('pnpm test:assert', { env, cwd }); } } catch (error) { console.error(error); From 993cc4d27f97bf3df5e8441775ac9c8ed840e411 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 10 Feb 2025 15:40:53 +0100 Subject: [PATCH 16/18] remove unused option --- dev-packages/e2e-tests/run.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index 158dd2a6f7e6..c20978ddf2e1 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -10,10 +10,7 @@ const DEFAULT_DSN = 'https://username@domain/123'; const DEFAULT_SENTRY_ORG_SLUG = 'sentry-javascript-sdks'; const DEFAULT_SENTRY_PROJECT = 'sentry-javascript-e2e-tests'; -function asyncExec( - command: string, - options: { env: Record; cwd: string; nodeVersion?: string }, -): Promise { +function asyncExec(command: string, options: { env: Record; cwd: string }): Promise { return new Promise((resolve, reject) => { const process = spawn(command, { ...options, shell: true }); From ed2c87ddc5460e5c32542e3d0ddf7ccc052e7c8c Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 10 Feb 2025 15:41:55 +0100 Subject: [PATCH 17/18] rm volta from test app --- .../e2e-tests/test-applications/nestjs-11/package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json index cb7b500faa8d..9ba374954190 100644 --- a/dev-packages/e2e-tests/test-applications/nestjs-11/package.json +++ b/dev-packages/e2e-tests/test-applications/nestjs-11/package.json @@ -44,8 +44,5 @@ "ts-loader": "^9.4.3", "tsconfig-paths": "^4.2.0", "typescript": "~5.0.0" - }, - "volta": { - "node": "20.18.2" } } From 81f6c7f1219f94575565f8b52e7171595b839232 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Mon, 10 Feb 2025 15:48:03 +0100 Subject: [PATCH 18/18] biome --- dev-packages/e2e-tests/run.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev-packages/e2e-tests/run.ts b/dev-packages/e2e-tests/run.ts index c20978ddf2e1..f8aafa5eaa01 100644 --- a/dev-packages/e2e-tests/run.ts +++ b/dev-packages/e2e-tests/run.ts @@ -1,8 +1,8 @@ /* eslint-disable no-console */ import { spawn } from 'child_process'; +import { resolve } from 'path'; import * as dotenv from 'dotenv'; import { sync as globSync } from 'glob'; -import { resolve } from 'path'; import { registrySetup } from './registrySetup';