Skip to content

Commit 4e8553d

Browse files
chargomeclaude
andcommitted
test(e2e): Add nestjs-11-static E2E app
Copies `nestjs-11` to `nestjs-11-static`, which stays on `traceLifecycle: 'static'` and on the transaction-based specs, so the static trace lifecycle stays covered once the NestJS apps move to span streaming. Ref: #23801 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent ec1788c commit 4e8553d

25 files changed

Lines changed: 1637 additions & 0 deletions
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# compiled output
2+
/dist
3+
/node_modules
4+
/build
5+
6+
# Logs
7+
logs
8+
*.log
9+
npm-debug.log*
10+
pnpm-debug.log*
11+
yarn-debug.log*
12+
yarn-error.log*
13+
lerna-debug.log*
14+
15+
# OS
16+
.DS_Store
17+
18+
# Tests
19+
/coverage
20+
/.nyc_output
21+
22+
# IDEs and editors
23+
/.idea
24+
.project
25+
.classpath
26+
.c9/
27+
*.launch
28+
.settings/
29+
*.sublime-workspace
30+
31+
# IDE - VSCode
32+
.vscode/*
33+
!.vscode/settings.json
34+
!.vscode/tasks.json
35+
!.vscode/launch.json
36+
!.vscode/extensions.json
37+
38+
# dotenv environment variable files
39+
.env
40+
.env.development.local
41+
.env.test.local
42+
.env.production.local
43+
.env.local
44+
45+
# temp directory
46+
.temp
47+
.tmp
48+
49+
# Runtime data
50+
pids
51+
*.pid
52+
*.seed
53+
*.pid.lock
54+
55+
# Diagnostic reports (https://nodejs.org/api/report.html)
56+
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"$schema": "https://json.schemastore.org/nest-cli",
3+
"collection": "@nestjs/schematics",
4+
"sourceRoot": "src",
5+
"compilerOptions": {
6+
"deleteOutDir": true
7+
}
8+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "nestjs-11-static",
3+
"version": "0.0.1",
4+
"private": true,
5+
"scripts": {
6+
"build": "nest build",
7+
"start": "nest start",
8+
"start:dev": "nest start --watch",
9+
"start:debug": "nest start --debug --watch",
10+
"start:prod": "node dist/main",
11+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
12+
"test": "playwright test",
13+
"test:build": "pnpm install",
14+
"test:assert": "pnpm test"
15+
},
16+
"dependencies": {
17+
"@nestjs/common": "^11.0.0",
18+
"@nestjs/core": "^11.0.0",
19+
"@nestjs/microservices": "^11.0.0",
20+
"@nestjs/schedule": "^5.0.0",
21+
"@nestjs/platform-express": "^11.0.0",
22+
"@sentry/nestjs": "file:../../packed/sentry-nestjs-packed.tgz",
23+
"reflect-metadata": "^0.2.0",
24+
"rxjs": "^7.8.1"
25+
},
26+
"devDependencies": {
27+
"@playwright/test": "~1.56.0",
28+
"@sentry-internal/test-utils": "link:../../../test-utils",
29+
"@nestjs/cli": "^11.0.0",
30+
"@nestjs/schematics": "^11.0.0",
31+
"@nestjs/testing": "^11.0.0",
32+
"@types/express": "^4.17.17",
33+
"@types/node": "^18.19.1",
34+
"@types/supertest": "^6.0.0",
35+
"@typescript-eslint/eslint-plugin": "^6.0.0",
36+
"@typescript-eslint/parser": "^6.0.0",
37+
"eslint": "^8.42.0",
38+
"source-map-support": "^0.5.21",
39+
"supertest": "^6.3.3",
40+
"ts-loader": "^9.4.3",
41+
"tsconfig-paths": "^4.2.0",
42+
"typescript": "~5.0.0"
43+
},
44+
"pnpm": {
45+
"overrides": {
46+
"minimatch": "10.0.1"
47+
}
48+
},
49+
"volta": {
50+
"extends": "../../package.json"
51+
}
52+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Controller, Get, Param, ParseIntPipe, UseFilters, UseGuards, UseInterceptors } from '@nestjs/common';
2+
import { flush } from '@sentry/nestjs';
3+
import { AppService } from './app.service';
4+
import { AsyncInterceptor } from './async-example.interceptor';
5+
import { ExampleInterceptor1 } from './example-1.interceptor';
6+
import { ExampleInterceptor2 } from './example-2.interceptor';
7+
import { ExampleExceptionGlobalFilter } from './example-global-filter.exception';
8+
import { ExampleExceptionLocalFilter } from './example-local-filter.exception';
9+
import { ExampleLocalFilter } from './example-local.filter';
10+
import { ExampleGuard } from './example.guard';
11+
12+
@Controller()
13+
@UseFilters(ExampleLocalFilter)
14+
export class AppController {
15+
constructor(private readonly appService: AppService) {}
16+
17+
@Get('test-transaction')
18+
testTransaction() {
19+
return this.appService.testTransaction();
20+
}
21+
22+
@Get('test-middleware-instrumentation')
23+
testMiddlewareInstrumentation() {
24+
return this.appService.testSpan();
25+
}
26+
27+
@Get('test-guard-instrumentation')
28+
@UseGuards(ExampleGuard)
29+
testGuardInstrumentation() {
30+
return {};
31+
}
32+
33+
@Get('test-interceptor-instrumentation')
34+
@UseInterceptors(ExampleInterceptor1, ExampleInterceptor2)
35+
testInterceptorInstrumentation() {
36+
return this.appService.testSpan();
37+
}
38+
39+
@Get('test-async-interceptor-instrumentation')
40+
@UseInterceptors(AsyncInterceptor)
41+
testAsyncInterceptorInstrumentation() {
42+
return this.appService.testSpan();
43+
}
44+
45+
@Get('test-pipe-instrumentation/:id')
46+
testPipeInstrumentation(@Param('id', ParseIntPipe) id: number) {
47+
return { value: id };
48+
}
49+
50+
@Get('test-exception/:id')
51+
async testException(@Param('id') id: string) {
52+
return this.appService.testException(id);
53+
}
54+
55+
@Get('test-expected-400-exception/:id')
56+
async testExpected400Exception(@Param('id') id: string) {
57+
return this.appService.testExpected400Exception(id);
58+
}
59+
60+
@Get('test-expected-500-exception/:id')
61+
async testExpected500Exception(@Param('id') id: string) {
62+
return this.appService.testExpected500Exception(id);
63+
}
64+
65+
@Get('test-expected-rpc-exception/:id')
66+
async testExpectedRpcException(@Param('id') id: string) {
67+
return this.appService.testExpectedRpcException(id);
68+
}
69+
70+
@Get('test-span-decorator-async')
71+
async testSpanDecoratorAsync() {
72+
return { result: await this.appService.testSpanDecoratorAsync() };
73+
}
74+
75+
@Get('test-span-decorator-sync')
76+
async testSpanDecoratorSync() {
77+
return { result: await this.appService.testSpanDecoratorSync() };
78+
}
79+
80+
@Get('kill-test-cron/:job')
81+
async killTestCron(@Param('job') job: string) {
82+
this.appService.killTestCron(job);
83+
}
84+
85+
@Get('flush')
86+
async flush() {
87+
await flush();
88+
}
89+
90+
@Get('example-exception-global-filter')
91+
async exampleExceptionGlobalFilter() {
92+
throw new ExampleExceptionGlobalFilter();
93+
}
94+
95+
@Get('example-exception-local-filter')
96+
async exampleExceptionLocalFilter() {
97+
throw new ExampleExceptionLocalFilter();
98+
}
99+
100+
@Get('test-service-use')
101+
testServiceWithUseMethod() {
102+
return this.appService.use();
103+
}
104+
105+
@Get('test-service-transform')
106+
testServiceWithTransform() {
107+
return this.appService.transform();
108+
}
109+
110+
@Get('test-service-intercept')
111+
testServiceWithIntercept() {
112+
return this.appService.intercept();
113+
}
114+
115+
@Get('test-service-canActivate')
116+
testServiceWithCanActivate() {
117+
return this.appService.canActivate();
118+
}
119+
120+
@Get('test-function-name')
121+
testFunctionName() {
122+
return this.appService.getFunctionName();
123+
}
124+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import { MiddlewareConsumer, Module } from '@nestjs/common';
2+
import { APP_FILTER } from '@nestjs/core';
3+
import { ScheduleModule } from '@nestjs/schedule';
4+
import { SentryGlobalFilter, SentryModule } from '@sentry/nestjs/setup';
5+
import { AppController } from './app.controller';
6+
import { AppService } from './app.service';
7+
import { ExampleGlobalFilter } from './example-global.filter';
8+
import { ExampleMiddleware } from './example.middleware';
9+
10+
@Module({
11+
imports: [SentryModule.forRoot(), ScheduleModule.forRoot()],
12+
controllers: [AppController],
13+
providers: [
14+
AppService,
15+
{
16+
provide: APP_FILTER,
17+
useClass: SentryGlobalFilter,
18+
},
19+
{
20+
provide: APP_FILTER,
21+
useClass: ExampleGlobalFilter,
22+
},
23+
],
24+
})
25+
export class AppModule {
26+
configure(consumer: MiddlewareConsumer): void {
27+
consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation');
28+
}
29+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
2+
import { RpcException } from '@nestjs/microservices';
3+
import { Cron, SchedulerRegistry } from '@nestjs/schedule';
4+
import * as Sentry from '@sentry/nestjs';
5+
import { SentryCron, SentryTraced } from '@sentry/nestjs';
6+
7+
const monitorConfig = {
8+
schedule: {
9+
type: 'crontab',
10+
value: '* * * * *',
11+
},
12+
} as const;
13+
14+
@Injectable()
15+
export class AppService {
16+
constructor(private schedulerRegistry: SchedulerRegistry) {}
17+
18+
testTransaction() {
19+
Sentry.startSpan({ name: 'test-span' }, () => {
20+
Sentry.startSpan({ name: 'child-span' }, () => {});
21+
});
22+
}
23+
24+
testSpan() {
25+
// span that should not be a child span of the middleware span
26+
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
27+
}
28+
29+
testException(id: string) {
30+
throw new Error(`This is an exception with id ${id}`);
31+
}
32+
33+
testExpected400Exception(id: string) {
34+
throw new HttpException(`This is an expected 400 exception with id ${id}`, HttpStatus.BAD_REQUEST);
35+
}
36+
37+
testExpected500Exception(id: string) {
38+
throw new HttpException(`This is an expected 500 exception with id ${id}`, HttpStatus.INTERNAL_SERVER_ERROR);
39+
}
40+
41+
testExpectedRpcException(id: string) {
42+
throw new RpcException(`This is an expected RPC exception with id ${id}`);
43+
}
44+
45+
@SentryTraced('wait and return a string')
46+
async wait() {
47+
await new Promise(resolve => setTimeout(resolve, 500));
48+
return 'test';
49+
}
50+
51+
async testSpanDecoratorAsync() {
52+
return await this.wait();
53+
}
54+
55+
@SentryTraced('return a string')
56+
getString(): { result: string } {
57+
return { result: 'test' };
58+
}
59+
60+
@SentryTraced('return the function name')
61+
getFunctionName(): { result: string } {
62+
return { result: this.getFunctionName.name };
63+
}
64+
65+
async testSpanDecoratorSync() {
66+
const returned = this.getString();
67+
// Will fail if getString() is async, because returned will be a Promise<>
68+
return returned.result;
69+
}
70+
71+
/*
72+
Actual cron schedule differs from schedule defined in config because Sentry
73+
only supports minute granularity, but we don't want to wait (worst case) a
74+
full minute for the tests to finish.
75+
*/
76+
@Cron('*/5 * * * * *', { name: 'test-cron-job' })
77+
@SentryCron('test-cron-slug', monitorConfig)
78+
async testCron() {
79+
console.log('Test cron!');
80+
}
81+
82+
/*
83+
Actual cron schedule differs from schedule defined in config because Sentry
84+
only supports minute granularity, but we don't want to wait (worst case) a
85+
full minute for the tests to finish.
86+
*/
87+
@Cron('*/5 * * * * *', { name: 'test-cron-error' })
88+
@SentryCron('test-cron-error-slug', monitorConfig)
89+
async testCronError() {
90+
throw new Error('Test error from cron job');
91+
}
92+
93+
async killTestCron(job: string) {
94+
this.schedulerRegistry.deleteCronJob(job);
95+
}
96+
97+
use() {
98+
console.log('Test use!');
99+
}
100+
101+
transform() {
102+
console.log('Test transform!');
103+
}
104+
105+
intercept() {
106+
console.log('Test intercept!');
107+
}
108+
109+
canActivate() {
110+
console.log('Test canActivate!');
111+
}
112+
}

0 commit comments

Comments
 (0)