Skip to content

Commit cfafa71

Browse files
committed
Merge branch 'develop' into lms/sync-master-develop
2 parents f609d05 + e3af1ce commit cfafa71

35 files changed

+803
-30
lines changed

dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.controller.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export class AppController {
1010
return this.appService.testTransaction();
1111
}
1212

13+
@Get('test-middleware-instrumentation')
14+
testMiddlewareInstrumentation() {
15+
return this.appService.testMiddleware();
16+
}
17+
1318
@Get('test-exception/:id')
1419
async testException(@Param('id') id: string) {
1520
return this.appService.testException(id);
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1-
import { Module } from '@nestjs/common';
1+
import { MiddlewareConsumer, Module } from '@nestjs/common';
22
import { ScheduleModule } from '@nestjs/schedule';
33
import { SentryModule } from '@sentry/nestjs/setup';
44
import { AppController } from './app.controller';
55
import { AppService } from './app.service';
6+
import { ExampleMiddleware } from './example.middleware';
67

78
@Module({
89
imports: [SentryModule.forRoot(), ScheduleModule.forRoot()],
910
controllers: [AppController],
1011
providers: [AppService],
1112
})
12-
export class AppModule {}
13+
export class AppModule {
14+
configure(consumer: MiddlewareConsumer): void {
15+
consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation');
16+
}
17+
}

dev-packages/e2e-tests/test-applications/nestjs-basic/src/app.service.ts

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export class AppService {
2121
});
2222
}
2323

24+
testMiddleware() {
25+
// span that should not be a child span of the middleware span
26+
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
27+
}
28+
2429
testException(id: string) {
2530
throw new Error(`This is an exception with id ${id}`);
2631
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable, NestMiddleware } from '@nestjs/common';
2+
import * as Sentry from '@sentry/nestjs';
3+
import { NextFunction, Request, Response } from 'express';
4+
5+
@Injectable()
6+
export class ExampleMiddleware implements NestMiddleware {
7+
use(req: Request, res: Response, next: NextFunction) {
8+
// span that should be a child span of the middleware span
9+
Sentry.startSpan({ name: 'test-middleware-span' }, () => {});
10+
next();
11+
}
12+
}

dev-packages/e2e-tests/test-applications/nestjs-basic/tests/transactions.test.ts

+79
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,82 @@ test('Sends an API route transaction', async ({ baseURL }) => {
121121
}),
122122
);
123123
});
124+
125+
test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({
126+
baseURL,
127+
}) => {
128+
const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
129+
return (
130+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
131+
transactionEvent?.transaction === 'GET /test-middleware-instrumentation'
132+
);
133+
});
134+
135+
await fetch(`${baseURL}/test-middleware-instrumentation`);
136+
137+
const transactionEvent = await pageloadTransactionEventPromise;
138+
139+
expect(transactionEvent).toEqual(
140+
expect.objectContaining({
141+
spans: expect.arrayContaining([
142+
{
143+
span_id: expect.any(String),
144+
trace_id: expect.any(String),
145+
data: {
146+
'sentry.op': 'middleware.nestjs',
147+
'sentry.origin': 'auto.middleware.nestjs',
148+
},
149+
description: 'ExampleMiddleware',
150+
parent_span_id: expect.any(String),
151+
start_timestamp: expect.any(Number),
152+
timestamp: expect.any(Number),
153+
status: 'ok',
154+
op: 'middleware.nestjs',
155+
origin: 'auto.middleware.nestjs',
156+
},
157+
]),
158+
}),
159+
);
160+
161+
const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware');
162+
const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id;
163+
164+
expect(transactionEvent).toEqual(
165+
expect.objectContaining({
166+
spans: expect.arrayContaining([
167+
{
168+
span_id: expect.any(String),
169+
trace_id: expect.any(String),
170+
data: expect.any(Object),
171+
description: 'test-controller-span',
172+
parent_span_id: expect.any(String),
173+
start_timestamp: expect.any(Number),
174+
timestamp: expect.any(Number),
175+
status: 'ok',
176+
origin: 'manual',
177+
},
178+
{
179+
span_id: expect.any(String),
180+
trace_id: expect.any(String),
181+
data: expect.any(Object),
182+
description: 'test-middleware-span',
183+
parent_span_id: expect.any(String),
184+
start_timestamp: expect.any(Number),
185+
timestamp: expect.any(Number),
186+
status: 'ok',
187+
origin: 'manual',
188+
},
189+
]),
190+
}),
191+
);
192+
193+
// verify correct span parent-child relationships
194+
const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span');
195+
const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
196+
197+
// 'ExampleMiddleware' is the parent of 'test-middleware-span'
198+
expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId);
199+
200+
// 'ExampleMiddleware' is NOT the parent of 'test-controller-span'
201+
expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId);
202+
});

dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.controller.ts

+5
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ export class AppController {
1010
return this.appService.testTransaction();
1111
}
1212

13+
@Get('test-middleware-instrumentation')
14+
testMiddlewareInstrumentation() {
15+
return this.appService.testMiddleware();
16+
}
17+
1318
@Get('test-exception/:id')
1419
async testException(@Param('id') id: string) {
1520
return this.appService.testException(id);
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
1-
import { Module } from '@nestjs/common';
1+
import { MiddlewareConsumer, Module } from '@nestjs/common';
22
import { ScheduleModule } from '@nestjs/schedule';
33
import { AppController } from './app.controller';
44
import { AppService } from './app.service';
5+
import { ExampleMiddleware } from './example.middleware';
56

67
@Module({
78
imports: [ScheduleModule.forRoot()],
89
controllers: [AppController],
910
providers: [AppService],
1011
})
11-
export class AppModule {}
12+
export class AppModule {
13+
configure(consumer: MiddlewareConsumer): void {
14+
consumer.apply(ExampleMiddleware).forRoutes('test-middleware-instrumentation');
15+
}
16+
}

dev-packages/e2e-tests/test-applications/node-nestjs-basic/src/app.service.ts

+5
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export class AppService {
2121
});
2222
}
2323

24+
testMiddleware() {
25+
// span that should not be a child span of the middleware span
26+
Sentry.startSpan({ name: 'test-controller-span' }, () => {});
27+
}
28+
2429
testException(id: string) {
2530
throw new Error(`This is an exception with id ${id}`);
2631
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Injectable, NestMiddleware } from '@nestjs/common';
2+
import * as Sentry from '@sentry/nestjs';
3+
import { NextFunction, Request, Response } from 'express';
4+
5+
@Injectable()
6+
export class ExampleMiddleware implements NestMiddleware {
7+
use(req: Request, res: Response, next: NextFunction) {
8+
// span that should be a child span of the middleware span
9+
Sentry.startSpan({ name: 'test-middleware-span' }, () => {});
10+
next();
11+
}
12+
}

dev-packages/e2e-tests/test-applications/node-nestjs-basic/tests/transactions.test.ts

+79
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,82 @@ test('Sends an API route transaction', async ({ baseURL }) => {
121121
}),
122122
);
123123
});
124+
125+
test('API route transaction includes nest middleware span. Spans created in and after middleware are nested correctly', async ({
126+
baseURL,
127+
}) => {
128+
const pageloadTransactionEventPromise = waitForTransaction('nestjs', transactionEvent => {
129+
return (
130+
transactionEvent?.contexts?.trace?.op === 'http.server' &&
131+
transactionEvent?.transaction === 'GET /test-middleware-instrumentation'
132+
);
133+
});
134+
135+
await fetch(`${baseURL}/test-middleware-instrumentation`);
136+
137+
const transactionEvent = await pageloadTransactionEventPromise;
138+
139+
expect(transactionEvent).toEqual(
140+
expect.objectContaining({
141+
spans: expect.arrayContaining([
142+
{
143+
span_id: expect.any(String),
144+
trace_id: expect.any(String),
145+
data: {
146+
'sentry.op': 'middleware.nestjs',
147+
'sentry.origin': 'auto.middleware.nestjs',
148+
},
149+
description: 'ExampleMiddleware',
150+
parent_span_id: expect.any(String),
151+
start_timestamp: expect.any(Number),
152+
timestamp: expect.any(Number),
153+
status: 'ok',
154+
op: 'middleware.nestjs',
155+
origin: 'auto.middleware.nestjs',
156+
},
157+
]),
158+
}),
159+
);
160+
161+
const exampleMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'ExampleMiddleware');
162+
const exampleMiddlewareSpanId = exampleMiddlewareSpan?.span_id;
163+
164+
expect(transactionEvent).toEqual(
165+
expect.objectContaining({
166+
spans: expect.arrayContaining([
167+
{
168+
span_id: expect.any(String),
169+
trace_id: expect.any(String),
170+
data: expect.any(Object),
171+
description: 'test-controller-span',
172+
parent_span_id: expect.any(String),
173+
start_timestamp: expect.any(Number),
174+
timestamp: expect.any(Number),
175+
status: 'ok',
176+
origin: 'manual',
177+
},
178+
{
179+
span_id: expect.any(String),
180+
trace_id: expect.any(String),
181+
data: expect.any(Object),
182+
description: 'test-middleware-span',
183+
parent_span_id: expect.any(String),
184+
start_timestamp: expect.any(Number),
185+
timestamp: expect.any(Number),
186+
status: 'ok',
187+
origin: 'manual',
188+
},
189+
]),
190+
}),
191+
);
192+
193+
// verify correct span parent-child relationships
194+
const testMiddlewareSpan = transactionEvent.spans.find(span => span.description === 'test-middleware-span');
195+
const testControllerSpan = transactionEvent.spans.find(span => span.description === 'test-controller-span');
196+
197+
// 'ExampleMiddleware' is the parent of 'test-middleware-span'
198+
expect(testMiddlewareSpan.parent_span_id).toBe(exampleMiddlewareSpanId);
199+
200+
// 'ExampleMiddleware' is NOT the parent of 'test-controller-span'
201+
expect(testControllerSpan.parent_span_id).not.toBe(exampleMiddlewareSpanId);
202+
});

dev-packages/e2e-tests/test-applications/solidstart/package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"This is currently not an issue outside of our repo. See: https://github.com/nksaraf/vinxi/issues/177"
1212
],
1313
"preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev",
14+
"start": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start",
1415
"test:prod": "TEST_ENV=production playwright test",
1516
"test:build": "pnpm install && npx playwright install && pnpm build",
1617
"test:assert": "pnpm test:prod"
@@ -31,7 +32,7 @@
3132
"jsdom": "^24.0.0",
3233
"solid-js": "1.8.17",
3334
"typescript": "^5.4.5",
34-
"vinxi": "^0.3.12",
35+
"vinxi": "^0.4.0",
3536
"vite": "^5.2.8",
3637
"vite-plugin-solid": "^2.10.2",
3738
"vitest": "^1.5.0"

dev-packages/e2e-tests/test-applications/solidstart/src/entry-client.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Sentry.init({
1212
tunnel: 'http://localhost:3031/', // proxy server
1313
// Performance Monitoring
1414
tracesSampleRate: 1.0, // Capture 100% of the transactions
15+
debug: !!import.meta.env.DEBUG,
1516
});
1617

1718
mount(() => <StartClient />, document.getElementById('app')!);

dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.mjs

+1
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ Sentry.init({
55
environment: 'qa', // dynamic sampling bias to keep transactions
66
tracesSampleRate: 1.0, // Capture 100% of the transactions
77
tunnel: 'http://localhost:3031/', // proxy server
8+
debug: !!process.env.DEBUG,
89
});

dev-packages/e2e-tests/test-applications/solidstart/src/routes/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ export default function Home() {
1111
<li>
1212
<A href="/client-error">Client error</A>
1313
</li>
14+
<li>
15+
<A href="/server-error">Server error</A>
16+
</li>
1417
<li>
1518
<A id="navLink" href="/users/5">
1619
User 5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { withServerActionInstrumentation } from '@sentry/solidstart';
2+
import { createAsync } from '@solidjs/router';
3+
4+
const getPrefecture = async () => {
5+
'use server';
6+
return await withServerActionInstrumentation('getPrefecture', () => {
7+
throw new Error('Error thrown from Solid Start E2E test app server route');
8+
9+
return { prefecture: 'Kanagawa' };
10+
});
11+
};
12+
13+
export default function ServerErrorPage() {
14+
const data = createAsync(() => getPrefecture());
15+
16+
return <div>Prefecture: {data()?.prefecture}</div>;
17+
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1-
import { useParams } from '@solidjs/router';
1+
import { withServerActionInstrumentation } from '@sentry/solidstart';
2+
import { createAsync, useParams } from '@solidjs/router';
23

4+
const getPrefecture = async () => {
5+
'use server';
6+
return await withServerActionInstrumentation('getPrefecture', () => {
7+
return { prefecture: 'Ehime' };
8+
});
9+
};
310
export default function User() {
411
const params = useParams();
5-
return <div>User ID: {params.id}</div>;
12+
const userData = createAsync(() => getPrefecture());
13+
14+
return (
15+
<div>
16+
User ID: {params.id}
17+
<br />
18+
Prefecture: {userData()?.prefecture}
19+
</div>
20+
);
621
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError } from '@sentry-internal/test-utils';
3+
4+
test.describe('server-side errors', () => {
5+
test('captures server action error', async ({ page }) => {
6+
const errorEventPromise = waitForError('solidstart', errorEvent => {
7+
return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route';
8+
});
9+
10+
await page.goto(`/server-error`);
11+
12+
const error = await errorEventPromise;
13+
14+
expect(error.tags).toMatchObject({ runtime: 'node' });
15+
expect(error).toMatchObject({
16+
exception: {
17+
values: [
18+
{
19+
type: 'Error',
20+
value: 'Error thrown from Solid Start E2E test app server route',
21+
mechanism: {
22+
type: 'solidstart',
23+
handled: false,
24+
},
25+
},
26+
],
27+
},
28+
transaction: 'GET /server-error',
29+
});
30+
});
31+
});

0 commit comments

Comments
 (0)