Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PropsWithChildren } from 'react';

export const dynamic = 'force-dynamic';

export default function Layout({ children }: PropsWithChildren<{}>) {
return (
<div>
<p>DynamicLayout</p>
{children}
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
export const dynamic = 'force-dynamic';

export default async function Page() {
return (
<div>
<p>Dynamic Page</p>
</div>
);
}

export async function generateMetadata() {
return {
title: 'I am dynamic page generated metadata',
};
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForTransaction } from '@sentry-internal/test-utils';
import { isNext13 } from './nextjsVersion';

test('Will create a transaction with spans for every server component and metadata generation functions when visiting a page', async ({
page,
Expand All @@ -14,8 +15,49 @@ test('Will create a transaction with spans for every server component and metada
return span.description;
});

expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout)/nested-layout)');
expect(spanDescriptions).toContainEqual('Layout Server Component (/(nested-layout))');
expect(spanDescriptions).toContainEqual('Page Server Component (/(nested-layout)/nested-layout)');
expect(spanDescriptions).toContainEqual('render route (app) /nested-layout');
expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/page');
expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout)');

// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans
if (!isNext13) {
expect(spanDescriptions).toContainEqual('resolve page components');
expect(spanDescriptions).toContainEqual('build component tree');
expect(spanDescriptions).toContainEqual('resolve root layout server component');
expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"');
expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"');
expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout"');
expect(spanDescriptions).toContainEqual('start response');
}
});

test('Will create a transaction with spans for every server component and metadata generation functions when visiting a dynamic page', async ({
page,
}) => {
const serverTransactionEventPromise = waitForTransaction('nextjs-app-dir', async transactionEvent => {
console.log(transactionEvent?.transaction);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Debug console.log left in test code

A console.log(transactionEvent?.transaction) statement appears to have been left in the test code, likely from debugging. While this is in test code, it will produce unnecessary output during test runs.

Fix in Cursor Fix in Web

return transactionEvent?.transaction === 'GET /nested-layout/[dynamic]';
});

await page.goto('/nested-layout/123');

const spanDescriptions = (await serverTransactionEventPromise).spans?.map(span => {
return span.description;
});

expect(spanDescriptions).toContainEqual('render route (app) /nested-layout/[dynamic]');
expect(spanDescriptions).toContainEqual('generateMetadata /(nested-layout)/nested-layout/[dynamic]/page');
expect(spanDescriptions).toContainEqual('Page.generateMetadata (/(nested-layout)/nested-layout/[dynamic])');

// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following spans
if (!isNext13) {
expect(spanDescriptions).toContainEqual('resolve page components');
expect(spanDescriptions).toContainEqual('build component tree');
expect(spanDescriptions).toContainEqual('resolve root layout server component');
expect(spanDescriptions).toContainEqual('resolve layout server component "(nested-layout)"');
expect(spanDescriptions).toContainEqual('resolve layout server component "nested-layout"');
expect(spanDescriptions).toContainEqual('resolve layout server component "[dynamic]"');
expect(spanDescriptions).toContainEqual('resolve page server component "/nested-layout/[dynamic]"');
expect(spanDescriptions).toContainEqual('start response');
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const packageJson = require('../package.json');
const nextjsVersion = packageJson.dependencies.next;
const nextjsMajor = Number(nextjsVersion.split('.')[0]);

export const isNext13 = !isNaN(nextjsMajor) && nextjsMajor === 13;
export const nextjsMajorVersion = nextjsMajor;
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { expect, test } from '@playwright/test';
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
import { isNext13 } from './nextjsVersion';

test('Sends a transaction for a request to app router', async ({ page }) => {
const serverComponentTransactionPromise = waitForTransaction('nextjs-app-dir', transactionEvent => {
Expand Down Expand Up @@ -70,19 +71,29 @@ test('Should set a "not_found" status on a server component span when notFound()

const transactionEvent = await serverComponentTransactionPromise;

// Transaction should have status ok, because the http status is ok, but the server component span should be not_found
// Transaction should have status ok, because the http status is ok, but the render component span should be not_found
expect(transactionEvent.contexts?.trace?.status).toBe('ok');
expect(transactionEvent.spans).toContainEqual(
expect.objectContaining({
description: 'Page Server Component (/server-component/not-found)',
op: 'function.nextjs',
description: 'render route (app) /server-component/not-found',
status: 'not_found',
data: expect.objectContaining({
'sentry.nextjs.ssr.function.type': 'Page',
'sentry.nextjs.ssr.function.route': '/server-component/not-found',
}),
}),
);

// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following span
if (!isNext13) {
// Page server component span should have the right name and attributes
expect(transactionEvent.spans).toContainEqual(
expect.objectContaining({
description: 'resolve page server component "/server-component/not-found"',
op: 'function.nextjs',
data: expect.objectContaining({
'sentry.nextjs.ssr.function.type': 'Page',
'sentry.nextjs.ssr.function.route': '/server-component/not-found',
}),
}),
);
}
});

test('Should capture an error and transaction for a app router page', async ({ page }) => {
Expand All @@ -102,20 +113,30 @@ test('Should capture an error and transaction for a app router page', async ({ p
// Error event should have the right transaction name
expect(errorEvent.transaction).toBe(`Page Server Component (/server-component/faulty)`);

// Transaction should have status ok, because the http status is ok, but the server component span should be internal_error
// Transaction should have status ok, because the http status is ok, but the render component span should be internal_error
expect(transactionEvent.contexts?.trace?.status).toBe('ok');
expect(transactionEvent.spans).toContainEqual(
expect.objectContaining({
description: 'Page Server Component (/server-component/faulty)',
op: 'function.nextjs',
description: 'render route (app) /server-component/faulty',
status: 'internal_error',
data: expect.objectContaining({
'sentry.nextjs.ssr.function.type': 'Page',
'sentry.nextjs.ssr.function.route': '/server-component/faulty',
}),
}),
);

// Next.js 13 has limited OTEL support for server components, so we don't expect to see the following span
if (!isNext13) {
// The page server component span should have the right name and attributes
expect(transactionEvent.spans).toContainEqual(
expect.objectContaining({
description: 'resolve page server component "/server-component/faulty"',
op: 'function.nextjs',
data: expect.objectContaining({
'sentry.nextjs.ssr.function.type': 'Page',
'sentry.nextjs.ssr.function.route': '/server-component/faulty',
}),
}),
);
}

expect(errorEvent.tags?.['my-isolated-tag']).toBe(true);
expect(errorEvent.tags?.['my-global-scope-isolated-tag']).not.toBeDefined();
expect(transactionEvent.tags?.['my-isolated-tag']).toBe(true);
Expand Down
2 changes: 2 additions & 0 deletions packages/nextjs/src/common/nextSpanAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export const ATTR_NEXT_SPAN_TYPE = 'next.span_type';
export const ATTR_NEXT_SPAN_NAME = 'next.span_name';
export const ATTR_NEXT_ROUTE = 'next.route';
export const ATTR_NEXT_SPAN_DESCRIPTION = 'next.span_description';
export const ATTR_NEXT_SEGMENT = 'next.segment';
75 changes: 73 additions & 2 deletions packages/nextjs/src/common/utils/tracingUtils.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
import type { PropagationContext } from '@sentry/core';
import { debug, getActiveSpan, getRootSpan, GLOBAL_OBJ, Scope, spanToJSON, startNewTrace } from '@sentry/core';
import { ATTR_HTTP_ROUTE } from '@opentelemetry/semantic-conventions';
import type { PropagationContext, Span, SpanAttributes } from '@sentry/core';
import {
debug,
getActiveSpan,
getRootSpan,
GLOBAL_OBJ,
Scope,
SEMANTIC_ATTRIBUTE_SENTRY_OP,
spanToJSON,
startNewTrace,
} from '@sentry/core';
import { DEBUG_BUILD } from '../debug-build';
import { ATTR_NEXT_SEGMENT, ATTR_NEXT_SPAN_NAME, ATTR_NEXT_SPAN_TYPE } from '../nextSpanAttributes';
import { TRANSACTION_ATTR_SHOULD_DROP_TRANSACTION } from '../span-attributes-with-logic-attached';

const commonPropagationContextMap = new WeakMap<object, PropagationContext>();

const PAGE_SEGMENT = '__PAGE__';

/**
* Takes a shared (garbage collectable) object between resources, e.g. a headers object shared between Next.js server components and returns a common propagation context.
*
Expand Down Expand Up @@ -108,3 +121,61 @@ export function dropNextjsRootContext(): void {
}
}
}

/**
* Checks if the span is a resolve segment span.
* @param spanAttributes The attributes of the span to check.
* @returns True if the span is a resolve segment span, false otherwise.
*/
export function isResolveSegmentSpan(spanAttributes: SpanAttributes): boolean {
return (
spanAttributes[ATTR_NEXT_SPAN_TYPE] === 'NextNodeServer.getLayoutOrPageModule' &&
spanAttributes[ATTR_NEXT_SPAN_NAME] === 'resolve segment modules' &&
typeof spanAttributes[ATTR_NEXT_SEGMENT] === 'string'
);
}

/**
* Returns the enhanced name for a resolve segment span.
* @param segment The segment of the resolve segment span.
* @param route The route of the resolve segment span.
* @returns The enhanced name for the resolve segment span.
*/
export function getEnhancedResolveSegmentSpanName({ segment, route }: { segment: string; route: string }): string {
if (segment === PAGE_SEGMENT) {
return `resolve page server component "${route}"`;
}

if (segment === '') {
return 'resolve root layout server component';
}

return `resolve layout server component "${segment}"`;
}

/**
* Maybe enhances the span name for a resolve segment span.
* If the span is not a resolve segment span, this function does nothing.
* @param activeSpan The active span.
* @param spanAttributes The attributes of the span to check.
* @param rootSpanAttributes The attributes of the according root span.
*/
export function maybeEnhanceServerComponentSpanName(
activeSpan: Span,
spanAttributes: SpanAttributes,
rootSpanAttributes: SpanAttributes,
): void {
if (!isResolveSegmentSpan(spanAttributes)) {
return;
}

const segment = spanAttributes[ATTR_NEXT_SEGMENT] as string;
const route = rootSpanAttributes[ATTR_HTTP_ROUTE];
const enhancedName = getEnhancedResolveSegmentSpanName({ segment, route: typeof route === 'string' ? route : '' });
activeSpan.updateName(enhancedName);
activeSpan.setAttributes({
'sentry.nextjs.ssr.function.type': segment === PAGE_SEGMENT ? 'Page' : 'Layout',
'sentry.nextjs.ssr.function.route': route,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Inconsistent type handling for route span attribute

The route variable from rootSpanAttributes[ATTR_HTTP_ROUTE] can be undefined or a non-string type. On line 174, this is properly handled with typeof route === 'string' ? route : '' for the span name. However, on line 178, route is passed directly to setAttributes as 'sentry.nextjs.ssr.function.route': route, which could set the attribute to undefined or an unexpected type instead of being omitted or defaulted consistently.

Fix in Cursor Fix in Web

});
activeSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_OP, 'function.nextjs');
}
Loading
Loading