Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit 152b9d4

Browse files
feat(nextjs): Support node runtime on proxy files (#17995)
[Next 16 was released](https://github.com/vercel/next.js/releases/tag/v16.0.0) With that proxy files run per default on nodejs. This PR - Updates the tests to run on next 16 (non-beta) - Adds support for handling middleware transactions in the node part of the sdk
1 parent f75c3ed commit 152b9d4

File tree

4 files changed

+84
-36
lines changed

4 files changed

+84
-36
lines changed

‎dev-packages/e2e-tests/test-applications/nextjs-16/package.json‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
"test:build": "pnpm install && pnpm build",
1717
"test:build-webpack": "pnpm install && pnpm build-webpack",
1818
"test:build-canary": "pnpm install && pnpm add next@canary && pnpm build",
19+
"test:build-latest": "pnpm install && pnpm add next@latest && pnpm build",
20+
"test:build-latest-webpack": "pnpm install && pnpm add next@latest && pnpm build-webpack",
1921
"test:build-canary-webpack": "pnpm install && pnpm add next@canary && pnpm build-webpack",
2022
"test:assert": "pnpm test:prod && pnpm test:dev",
2123
"test:assert-webpack": "pnpm test:prod && pnpm test:dev-webpack"
@@ -25,7 +27,7 @@
2527
"@sentry/core": "latest || *",
2628
"ai": "^3.0.0",
2729
"import-in-the-middle": "^1",
28-
"next": "16.0.0-beta.0",
30+
"next": "16.0.0",
2931
"react": "19.1.0",
3032
"react-dom": "19.1.0",
3133
"require-in-the-middle": "^7",
@@ -50,6 +52,15 @@
5052
"build-command": "pnpm test:build-webpack",
5153
"label": "nextjs-16 (webpack)",
5254
"assert-command": "pnpm test:assert-webpack"
55+
},
56+
{
57+
"build-command": "pnpm test:build-latest-webpack",
58+
"label": "nextjs-16 (latest, webpack)",
59+
"assert-command": "pnpm test:assert-webpack"
60+
},
61+
{
62+
"build-command": "pnpm test:build-latest",
63+
"label": "nextjs-16 (latest, turbopack)"
5364
}
5465
],
5566
"optionalVariants": [

‎dev-packages/e2e-tests/test-applications/nextjs-16/tests/middleware.test.ts‎

Lines changed: 39 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { expect, test } from '@playwright/test';
22
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
3+
import { isDevMode } from './isDevMode';
34

45
test('Should create a transaction for middleware', async ({ request }) => {
56
const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
@@ -13,15 +14,16 @@ test('Should create a transaction for middleware', async ({ request }) => {
1314

1415
expect(middlewareTransaction.contexts?.trace?.status).toBe('ok');
1516
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
16-
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
17-
expect(middlewareTransaction.transaction_info?.source).toBe('url');
17+
expect(middlewareTransaction.contexts?.runtime?.name).toBe('node');
18+
expect(middlewareTransaction.transaction_info?.source).toBe('route');
1819

1920
// Assert that isolation scope works properly
2021
expect(middlewareTransaction.tags?.['my-isolated-tag']).toBe(true);
2122
expect(middlewareTransaction.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
2223
});
2324

2425
test('Faulty middlewares', async ({ request }) => {
26+
test.skip(isDevMode, 'Throwing crashes the dev server atm'); // https://github.com/vercel/next.js/issues/85261
2527
const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
2628
return transactionEvent?.transaction === 'middleware GET';
2729
});
@@ -36,27 +38,29 @@ test('Faulty middlewares', async ({ request }) => {
3638

3739
await test.step('should record transactions', async () => {
3840
const middlewareTransaction = await middlewareTransactionPromise;
39-
expect(middlewareTransaction.contexts?.trace?.status).toBe('unknown_error');
41+
expect(middlewareTransaction.contexts?.trace?.status).toBe('internal_error');
4042
expect(middlewareTransaction.contexts?.trace?.op).toBe('http.server.middleware');
41-
expect(middlewareTransaction.contexts?.runtime?.name).toBe('vercel-edge');
42-
expect(middlewareTransaction.transaction_info?.source).toBe('url');
43+
expect(middlewareTransaction.contexts?.runtime?.name).toBe('node');
44+
expect(middlewareTransaction.transaction_info?.source).toBe('route');
4345
});
4446

45-
await test.step('should record exceptions', async () => {
46-
const errorEvent = await errorEventPromise;
47-
48-
// Assert that isolation scope works properly
49-
expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
50-
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
51-
expect([
52-
'middleware GET', // non-otel webpack versions
53-
'/middleware', // middleware file
54-
'/proxy', // proxy file
55-
]).toContain(errorEvent.transaction);
56-
});
47+
// TODO: proxy errors currently not reported via onRequestError
48+
// await test.step('should record exceptions', async () => {
49+
// const errorEvent = await errorEventPromise;
50+
51+
// // Assert that isolation scope works properly
52+
// expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
53+
// expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
54+
// expect([
55+
// 'middleware GET', // non-otel webpack versions
56+
// '/middleware', // middleware file
57+
// '/proxy', // proxy file
58+
// ]).toContain(errorEvent.transaction);
59+
// });
5760
});
5861

5962
test('Should trace outgoing fetch requests inside middleware and create breadcrumbs for it', async ({ request }) => {
63+
test.skip(isDevMode, 'The fetch requests ends up in a separate tx in dev atm');
6064
const middlewareTransactionPromise = waitForTransaction('nextjs-16', async transactionEvent => {
6165
return (
6266
transactionEvent?.transaction === 'middleware GET' &&
@@ -74,18 +78,26 @@ test('Should trace outgoing fetch requests inside middleware and create breadcru
7478
expect.arrayContaining([
7579
{
7680
data: {
77-
'http.method': 'GET',
81+
'http.request.method': 'GET',
82+
'http.request.method_original': 'GET',
7883
'http.response.status_code': 200,
79-
type: 'fetch',
80-
url: 'http://localhost:3030/',
81-
'http.url': 'http://localhost:3030/',
82-
'server.address': 'localhost:3030',
84+
'network.peer.address': '::1',
85+
'network.peer.port': 3030,
86+
'otel.kind': 'CLIENT',
8387
'sentry.op': 'http.client',
84-
'sentry.origin': 'auto.http.wintercg_fetch',
88+
'sentry.origin': 'auto.http.otel.node_fetch',
89+
'server.address': 'localhost',
90+
'server.port': 3030,
91+
url: 'http://localhost:3030/',
92+
'url.full': 'http://localhost:3030/',
93+
'url.path': '/',
94+
'url.query': '',
95+
'url.scheme': 'http',
96+
'user_agent.original': 'node',
8597
},
8698
description: 'GET http://localhost:3030/',
8799
op: 'http.client',
88-
origin: 'auto.http.wintercg_fetch',
100+
origin: 'auto.http.otel.node_fetch',
89101
parent_span_id: expect.stringMatching(/[a-f0-9]{16}/),
90102
span_id: expect.stringMatching(/[a-f0-9]{16}/),
91103
start_timestamp: expect.any(Number),
@@ -95,11 +107,12 @@ test('Should trace outgoing fetch requests inside middleware and create breadcru
95107
},
96108
]),
97109
);
110+
98111
expect(middlewareTransaction.breadcrumbs).toEqual(
99112
expect.arrayContaining([
100113
{
101-
category: 'fetch',
102-
data: { method: 'GET', status_code: 200, url: 'http://localhost:3030/' },
114+
category: 'http',
115+
data: { 'http.method': 'GET', status_code: 200, url: 'http://localhost:3030/' },
103116
timestamp: expect.any(Number),
104117
type: 'http',
105118
},
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const ATTR_NEXT_SPAN_TYPE = 'next.span_type';
2+
export const ATTR_NEXT_SPAN_NAME = 'next.span_name';
3+
export const ATTR_NEXT_ROUTE = 'next.route';

‎packages/nextjs/src/server/index.ts‎

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { getScopesFromContext } from '@sentry/opentelemetry';
3131
import { DEBUG_BUILD } from '../common/debug-build';
3232
import { devErrorSymbolicationEventProcessor } from '../common/devErrorSymbolicationEventProcessor';
3333
import { getVercelEnv } from '../common/getVercelEnv';
34+
import { ATTR_NEXT_ROUTE, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../common/nextSpanAttributes';
3435
import {
3536
TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL,
3637
TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL,
@@ -169,25 +170,35 @@ export function init(options: NodeOptions): NodeClient | undefined {
169170

170171
// What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted
171172
// by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute.
172-
if (typeof spanAttributes?.['next.route'] === 'string') {
173+
if (typeof spanAttributes?.[ATTR_NEXT_ROUTE] === 'string') {
173174
const rootSpanAttributes = spanToJSON(rootSpan).data;
174175
// Only hoist the http.route attribute if the transaction doesn't already have it
175176
if (
176177
// eslint-disable-next-line deprecation/deprecation
177178
(rootSpanAttributes?.[ATTR_HTTP_REQUEST_METHOD] || rootSpanAttributes?.[SEMATTRS_HTTP_METHOD]) &&
178179
!rootSpanAttributes?.[ATTR_HTTP_ROUTE]
179180
) {
180-
const route = spanAttributes['next.route'].replace(/\/route$/, '');
181+
const route = spanAttributes[ATTR_NEXT_ROUTE].replace(/\/route$/, '');
181182
rootSpan.updateName(route);
182183
rootSpan.setAttribute(ATTR_HTTP_ROUTE, route);
183184
// Preserving the original attribute despite internally not depending on it
184-
rootSpan.setAttribute('next.route', route);
185+
rootSpan.setAttribute(ATTR_NEXT_ROUTE, route);
185186
}
186187
}
187188

189+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'Middleware.execute') {
190+
const middlewareName = spanAttributes[ATTR_NEXT_SPAN_NAME];
191+
if (typeof middlewareName === 'string') {
192+
rootSpan.updateName(middlewareName);
193+
rootSpan.setAttribute(ATTR_HTTP_ROUTE, middlewareName);
194+
rootSpan.setAttribute(ATTR_NEXT_SPAN_NAME, middlewareName);
195+
}
196+
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
197+
}
198+
188199
// We want to skip span data inference for any spans generated by Next.js. Reason being that Next.js emits spans
189200
// with patterns (e.g. http.server spans) that will produce confusing data.
190-
if (spanAttributes?.['next.span_type'] !== undefined) {
201+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] !== undefined) {
191202
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
192203
}
193204

@@ -197,7 +208,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
197208
}
198209

199210
// We want to fork the isolation scope for incoming requests
200-
if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && isRootSpan) {
211+
if (spanAttributes?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest' && isRootSpan) {
201212
const scopes = getCapturedScopesOnSpan(span);
202213

203214
const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
@@ -320,7 +331,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
320331
// Enhance route handler transactions
321332
if (
322333
event.type === 'transaction' &&
323-
event.contexts?.trace?.data?.['next.span_type'] === 'BaseServer.handleRequest'
334+
event.contexts?.trace?.data?.[ATTR_NEXT_SPAN_TYPE] === 'BaseServer.handleRequest'
324335
) {
325336
event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_OP] = 'http.server';
326337
event.contexts.trace.op = 'http.server';
@@ -333,21 +344,31 @@ export function init(options: NodeOptions): NodeClient | undefined {
333344
const method = event.contexts.trace.data[SEMATTRS_HTTP_METHOD];
334345
// eslint-disable-next-line deprecation/deprecation
335346
const target = event.contexts?.trace?.data?.[SEMATTRS_HTTP_TARGET];
336-
const route = event.contexts.trace.data[ATTR_HTTP_ROUTE] || event.contexts.trace.data['next.route'];
347+
const route = event.contexts.trace.data[ATTR_HTTP_ROUTE] || event.contexts.trace.data[ATTR_NEXT_ROUTE];
348+
const spanName = event.contexts.trace.data[ATTR_NEXT_SPAN_NAME];
337349

338-
if (typeof method === 'string' && typeof route === 'string') {
350+
if (typeof method === 'string' && typeof route === 'string'&&!route.startsWith('middleware')) {
339351
const cleanRoute = route.replace(/\/route$/, '');
340352
event.transaction = `${method} ${cleanRoute}`;
341353
event.contexts.trace.data[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE] = 'route';
342354
// Preserve next.route in case it did not get hoisted
343-
event.contexts.trace.data['next.route'] = cleanRoute;
355+
event.contexts.trace.data[ATTR_NEXT_ROUTE] = cleanRoute;
344356
}
345357

346358
// backfill transaction name for pages that would otherwise contain unparameterized routes
347359
if (event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL] && event.transaction !== 'GET /_app') {
348360
event.transaction = `${method} ${event.contexts.trace.data[TRANSACTION_ATTR_SENTRY_ROUTE_BACKFILL]}`;
349361
}
350362

363+
const middlewareMatch =
364+
typeof spanName === 'string' && spanName.match(/^middleware(GET|POST|PUT|DELETE|PATCH|HEAD|OPTIONS)/);
365+
366+
if (middlewareMatch) {
367+
const normalizedName = `middleware ${middlewareMatch[1]}`;
368+
event.transaction = normalizedName;
369+
event.contexts.trace.op = 'http.server.middleware';
370+
}
371+
351372
// Next.js overrides transaction names for page loads that throw an error
352373
// but we want to keep the original target name
353374
if (event.transaction === 'GET /_error' && target) {

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /