Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { NextResponse } from 'next/server';

export const runtime = 'edge';
export const dynamic = 'force-dynamic';

export async function GET() {
return NextResponse.json({ message: 'Hello Edge Route Handler' });
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { NextResponse } from 'next/server';

export const dynamic = 'force-dynamic';

export async function GET() {
return NextResponse.json({ message: 'Hello Node Route Handler' });
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,6 @@ test('App router transactions should be attached to the pageload request span',
});

test('extracts HTTP request headers as span attributes', async ({ baseURL }) => {
test.skip(
process.env.TEST_ENV === 'prod-turbopack' || process.env.TEST_ENV === 'dev-turbopack',
'Incoming fetch request headers are not added as span attributes when Turbopack is enabled (addHeadersAsAttributes)',
);

const serverTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent?.transaction === 'GET /pageload-tracing';
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import test, { expect } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';

test('Should create a transaction for node route handlers', async ({ request }) => {
const routehandlerTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
console.log(transactionEvent?.transaction);
return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/node';
});

const response = await request.get('/route-handler/123/node', { headers: { 'x-charly': 'gomez' } });
expect(await response.json()).toStrictEqual({ message: 'Hello Node Route Handler' });

const routehandlerTransaction = await routehandlerTransactionPromise;

expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');

// This is flaking on dev mode
if (process.env.TEST_ENV !== 'development' && process.env.TEST_ENV !== 'dev-turbopack') {
expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez');
}
});

test('Should create a transaction for edge route handlers', async ({ request }) => {
// This test only works for webpack builds on non-async param extraction
// todo: check if we can set request headers for edge on sdkProcessingMetadata
test.skip();
const routehandlerTransactionPromise = waitForTransaction('nextjs-15', async transactionEvent => {
return transactionEvent?.transaction === 'GET /route-handler/[xoxo]/edge';
});

const response = await request.get('/route-handler/123/edge', { headers: { 'x-charly': 'gomez' } });
expect(await response.json()).toStrictEqual({ message: 'Hello Edge Route Handler' });

const routehandlerTransaction = await routehandlerTransactionPromise;

expect(routehandlerTransaction.contexts?.trace?.status).toBe('ok');
expect(routehandlerTransaction.contexts?.trace?.op).toBe('http.server');
expect(routehandlerTransaction.contexts?.trace?.data?.['http.request.header.x_charly']).toBe('gomez');
});
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import {
} from '@sentry/core';
import type { NextApiRequest } from 'next';
import type { AugmentedNextApiResponse, NextApiHandler } from '../types';
import { addHeadersAsAttributes } from '../utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from '../utils/responseEnd';
import { dropNextjsRootContext, escapeNextjsTracing } from '../utils/tracingUtils';

Expand Down Expand Up @@ -88,7 +87,6 @@ export function wrapApiHandlerWithSentry(apiHandler: NextApiHandler, parameteriz
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'route',
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.http.nextjs',
...addHeadersAsAttributes(normalizedRequest.headers || {}),
},
},
async span => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
import type { GenerationFunctionContext } from '../common/types';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
import { getSanitizedRequestUrl } from './utils/urls';
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
Expand Down Expand Up @@ -64,11 +63,6 @@ export function wrapGenerationFunctionWithSentry<F extends (...args: any[]) => a

const headersDict = headers ? winterCGHeadersToDict(headers) : undefined;

if (activeSpan) {
const rootSpan = getRootSpan(activeSpan);
addHeadersAsAttributes(headers, rootSpan);
}

let data: Record<string, unknown> | undefined = undefined;
if (getClient()?.getOptions().sendDefaultPii) {
const props: unknown = args[0];
Expand Down
6 changes: 0 additions & 6 deletions packages/nextjs/src/common/wrapMiddlewareWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
winterCGRequestToRequestData,
withIsolationScope,
} from '@sentry/core';
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import type { EdgeRouteHandler } from '../edge/types';

Expand Down Expand Up @@ -60,16 +59,13 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(

let spanName: string;
let spanSource: TransactionSource;
let headerAttributes: Record<string, string> = {};

if (req instanceof Request) {
isolationScope.setSDKProcessingMetadata({
normalizedRequest: winterCGRequestToRequestData(req),
});
spanName = `middleware ${req.method} ${new URL(req.url).pathname}`;
spanSource = 'url';

headerAttributes = addHeadersAsAttributes(req.headers);
} else {
spanName = 'middleware';
spanSource = 'component';
Expand All @@ -88,7 +84,6 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(
const rootSpan = getRootSpan(activeSpan);
if (rootSpan) {
setCapturedScopesOnSpan(rootSpan, currentScope, isolationScope);
rootSpan.setAttributes(headerAttributes);
}
}

Expand All @@ -99,7 +94,6 @@ export function wrapMiddlewareWithSentry<H extends EdgeRouteHandler>(
attributes: {
[SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: spanSource,
[SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.nextjs.wrap_middleware',
...headerAttributes,
},
},
() => {
Expand Down
6 changes: 0 additions & 6 deletions packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ import {
} from '@sentry/core';
import { isNotFoundNavigationError, isRedirectNavigationError } from './nextNavigationErrorUtils';
import type { RouteHandlerContext } from './types';
import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { flushSafelyWithTimeout } from './utils/responseEnd';
import { commonObjectToIsolationScope } from './utils/tracingUtils';

Expand All @@ -40,10 +39,6 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
const activeSpan = getActiveSpan();
const rootSpan = activeSpan ? getRootSpan(activeSpan) : undefined;

if (rootSpan && process.env.NEXT_RUNTIME !== 'edge') {
addHeadersAsAttributes(headers, rootSpan);
}

let edgeRuntimeIsolationScopeOverride: Scope | undefined;
if (rootSpan && process.env.NEXT_RUNTIME === 'edge') {
const isolationScope = commonObjectToIsolationScope(headers);
Expand All @@ -55,7 +50,6 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
rootSpan.updateName(`${method} ${parameterizedRoute}`);
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'route');
rootSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server');
addHeadersAsAttributes(headers, rootSpan);
}

return withIsolationScope(
Expand Down
6 changes: 0 additions & 6 deletions packages/nextjs/src/common/wrapServerComponentWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import { isNotFoundNavigationError, isRedirectNavigationError } from '../common/
import type { ServerComponentContext } from '../common/types';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import { TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL } from './span-attributes-with-logic-attached';
import { addHeadersAsAttributes } from './utils/addHeadersAsAttributes';
import { commonObjectToIsolationScope, commonObjectToPropagationContext } from './utils/tracingUtils';
import { getSanitizedRequestUrl } from './utils/urls';
import { maybeExtractSynchronousParamsAndSearchParams } from './utils/wrapperUtils';
Expand Down Expand Up @@ -62,11 +61,6 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>

const headersDict = context.headers ? winterCGHeadersToDict(context.headers) : undefined;

if (activeSpan) {
const rootSpan = getRootSpan(activeSpan);
addHeadersAsAttributes(context.headers, rootSpan);
}

let params: Record<string, string> | undefined = undefined;

if (getClient()?.getOptions().sendDefaultPii) {
Expand Down
10 changes: 10 additions & 0 deletions packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
applySdkMetadata,
getGlobalScope,
getIsolationScope,
getRootSpan,
GLOBAL_OBJ,
registerSpanErrorInstrumentation,
Expand All @@ -13,6 +14,7 @@ import {
} from '@sentry/core';
import type { VercelEdgeOptions } from '@sentry/vercel-edge';
import { getDefaultIntegrations, init as vercelEdgeInit } from '@sentry/vercel-edge';
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { isBuild } from '../common/utils/isBuild';
import { flushSafelyWithTimeout } from '../common/utils/responseEnd';
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';
Expand Down Expand Up @@ -59,6 +61,8 @@ export function init(options: VercelEdgeOptions = {}): void {

client?.on('spanStart', span => {
const spanAttributes = spanToJSON(span).data;
const rootSpan = getRootSpan(span);
const isRootSpan = span === rootSpan;

// Mark all spans generated by Next.js as 'auto'
if (spanAttributes?.['next.span_type'] !== undefined) {
Expand All @@ -70,6 +74,12 @@ export function init(options: VercelEdgeOptions = {}): void {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'http.server.middleware');
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, 'url');
}

if (isRootSpan) {
// todo: check if we can set request headers for edge on sdkProcessingMetadata
const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers;
addHeadersAsAttributes(headers, rootSpan);
}
});

// Use the preprocessEvent hook instead of an event processor, so that the users event processors receive the most
Expand Down
1 change: 0 additions & 1 deletion packages/nextjs/src/edge/wrapApiHandlerWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ export function wrapApiHandlerWithSentry<H extends EdgeRouteHandler>(
normalizedRequest: winterCGRequestToRequestData(req),
});
currentScope.setTransactionName(`${req.method} ${parameterizedRoute}`);

headerAttributes = addHeadersAsAttributes(req.headers);
} else {
currentScope.setTransactionName(`handler (${parameterizedRoute})`);
Expand Down
12 changes: 9 additions & 3 deletions packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
TRANSACTION_ATTR_SENTRY_TRACE_BACKFILL,
TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION,
} from '../common/span-attributes-with-logic-attached';
import { addHeadersAsAttributes } from '../common/utils/addHeadersAsAttributes';
import { isBuild } from '../common/utils/isBuild';
import { distDirRewriteFramesIntegration } from './distDirRewriteFramesIntegration';

Expand Down Expand Up @@ -163,13 +164,13 @@ export function init(options: NodeOptions): NodeClient | undefined {

client?.on('spanStart', span => {
const spanAttributes = spanToJSON(span).data;
const rootSpan = getRootSpan(span);
const isRootSpan = span === rootSpan;

// What we do in this glorious piece of code, is hoist any information about parameterized routes from spans emitted
// by Next.js via the `next.route` attribute, up to the transaction by setting the http.route attribute.
if (typeof spanAttributes?.['next.route'] === 'string') {
const rootSpan = getRootSpan(span);
const rootSpanAttributes = spanToJSON(rootSpan).data;

// Only hoist the http.route attribute if the transaction doesn't already have it
if (
// eslint-disable-next-line deprecation/deprecation
Expand All @@ -190,8 +191,13 @@ export function init(options: NodeOptions): NodeClient | undefined {
span.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, 'auto');
}

if (isRootSpan) {
const headers = getIsolationScope().getScopeData().sdkProcessingMetadata?.normalizedRequest?.headers;
addHeadersAsAttributes(headers, rootSpan);
}

// We want to fork the isolation scope for incoming requests
if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && span === getRootSpan(span)) {
if (spanAttributes?.['next.span_type'] === 'BaseServer.handleRequest' && isRootSpan) {
const scopes = getCapturedScopesOnSpan(span);

const isolationScope = (scopes.isolationScope || getIsolationScope()).clone();
Expand Down