+ );
+}
+
+export async function generateMetadata() {
+ return {
+ title: 'I am dynamic page generated metadata',
+ };
+}
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
index 3d2f29358d54..1db93072310f 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/connected-servercomponent-trace.test.ts
@@ -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,
@@ -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);
+ 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');
+ }
});
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/nextjsVersion.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/nextjsVersion.ts
new file mode 100644
index 000000000000..1ca2c43302e4
--- /dev/null
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/nextjsVersion.ts
@@ -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;
diff --git a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
index eedb702715de..3386324d650c 100644
--- a/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
+++ b/dev-packages/e2e-tests/test-applications/nextjs-app-dir/tests/server-components.test.ts
@@ -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 => {
@@ -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 }) => {
@@ -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);
diff --git a/packages/nextjs/src/common/nextSpanAttributes.ts b/packages/nextjs/src/common/nextSpanAttributes.ts
index 8b9f4a9d1374..2f412d6ce7db 100644
--- a/packages/nextjs/src/common/nextSpanAttributes.ts
+++ b/packages/nextjs/src/common/nextSpanAttributes.ts
@@ -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';
diff --git a/packages/nextjs/src/common/utils/tracingUtils.ts b/packages/nextjs/src/common/utils/tracingUtils.ts
index bda3049fbc78..efa3ac4fdbf6 100644
--- a/packages/nextjs/src/common/utils/tracingUtils.ts
+++ b/packages/nextjs/src/common/utils/tracingUtils.ts
@@ -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