mirror of
https://github.com/hanzoai/next.js.git
synced 2026-10-30 14:46:38 +00:00
Node.js Middleware was inheriting the wrong parent span (and behaved differently compared to Edge runtime Middleware) `Middleware.execute` and `BaseServer.handleRequest` should be sibling spans (depending on context, both are root spans). Previously `BaseServer.handleRequest` had a child span `Middleware.execute` Depends on https://github.com/vercel/next.js/pull/95357 to make the direct entrypoint test pass
2106 lines
77 KiB
TypeScript
2106 lines
77 KiB
TypeScript
import { FileRef, isNextDev, isNextStart, nextTestSetup } from 'e2e-utils'
|
|
import { retry } from 'next-test-utils'
|
|
import { NEXT_RSC_UNION_QUERY } from 'next/dist/client/components/app-router-headers'
|
|
import path from 'path'
|
|
|
|
import { SavedSpan } from './constants'
|
|
import { type Collector, connectCollector } from './collector'
|
|
|
|
const EXTERNAL = {
|
|
traceId: 'ee75cd9e534ff5e9ed78b4a0c706f0f2',
|
|
spanId: '0f6a325411bdc432',
|
|
} as const
|
|
|
|
const COLLECTOR_PORT = 9001
|
|
|
|
function setup({ useDirectEntrypointHandler, useNodeMiddleware }) {
|
|
let collector: Collector
|
|
|
|
function getCollector(): Collector {
|
|
return collector
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
collector = await connectCollector({ port: COLLECTOR_PORT })
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await collector.shutdown()
|
|
})
|
|
|
|
let next = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
dependencies: require('./package.json').dependencies,
|
|
...(!useDirectEntrypointHandler
|
|
? {
|
|
env: {
|
|
TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT),
|
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
},
|
|
}
|
|
: {
|
|
startCommand: 'pnpm start-entrypoint',
|
|
packageJson: {
|
|
scripts: {
|
|
'start-entrypoint':
|
|
'pnpm tsx custom-entrypoint-server.ts --without-parent-span',
|
|
},
|
|
},
|
|
serverReadyPattern: /- Local:/,
|
|
env: {
|
|
TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT),
|
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
NODE_ENV: 'production',
|
|
},
|
|
}),
|
|
overrideFiles: useNodeMiddleware
|
|
? {
|
|
'middleware.ts': new FileRef(
|
|
path.join(__dirname, 'middleware-node.ts')
|
|
),
|
|
}
|
|
: undefined,
|
|
})
|
|
return { next, getCollector }
|
|
}
|
|
|
|
describe.each(
|
|
[
|
|
{ name: 'default' },
|
|
isNextStart && {
|
|
name: 'direct entrypoints',
|
|
useDirectEntrypointHandler: true,
|
|
},
|
|
].filter(Boolean)
|
|
)('opentelemetry - $name', ({ useDirectEntrypointHandler }) => {
|
|
const {
|
|
next: { next, skipped, isNextDev },
|
|
getCollector,
|
|
} = setup({
|
|
useDirectEntrypointHandler,
|
|
useNodeMiddleware: false,
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
// Edge runtime is currently not implemented in custom-entrypoint-server.ts
|
|
const itEdge = useDirectEntrypointHandler ? it.skip : it
|
|
|
|
for (const env of [
|
|
{
|
|
name: 'root context',
|
|
fetchInit: undefined,
|
|
span: {
|
|
traceId: '[trace-id]',
|
|
rootParentId: undefined,
|
|
},
|
|
},
|
|
{
|
|
name: 'incoming context propagation',
|
|
fetchInit: {
|
|
headers: {
|
|
traceparent: `00-${EXTERNAL.traceId}-${EXTERNAL.spanId}-01`,
|
|
},
|
|
},
|
|
span: {
|
|
traceId: EXTERNAL.traceId,
|
|
rootParentId: EXTERNAL.spanId,
|
|
},
|
|
},
|
|
]) {
|
|
;(process.env.__NEXT_CACHE_COMPONENTS ? describe.skip : describe)(
|
|
env.name,
|
|
() => {
|
|
describe('app router', () => {
|
|
it('should handle RSC with fetch', async () => {
|
|
await next.fetch('/app/param/rsc-fetch', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /app/[param]/rsc-fetch',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/app/[param]/rsc-fetch',
|
|
'http.status_code': 200,
|
|
'http.target': '/app/param/rsc-fetch',
|
|
'next.route': '/app/[param]/rsc-fetch',
|
|
'next.rsc': false,
|
|
'next.span_name': 'GET /app/[param]/rsc-fetch',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'render route (app) /app/[param]/rsc-fetch',
|
|
attributes: {
|
|
'next.route': '/app/[param]/rsc-fetch',
|
|
'next.span_name':
|
|
'render route (app) /app/[param]/rsc-fetch',
|
|
'next.span_type': 'AppRender.getBodyResult',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'build component tree',
|
|
attributes: {
|
|
'next.span_name': 'build component tree',
|
|
'next.span_type':
|
|
'NextNodeServer.createComponentTree',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '__PAGE__',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '[param]',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'fetch GET https://example.vercel.sh/',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.url': 'https://example.vercel.sh/',
|
|
'net.peer.name': 'example.vercel.sh',
|
|
'next.span_name':
|
|
'fetch GET https://example.vercel.sh/',
|
|
'next.span_type': 'AppRender.fetch',
|
|
},
|
|
kind: 2,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/layout',
|
|
attributes: {
|
|
'next.page': '/app/[param]/layout',
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/layout',
|
|
'next.span_type': 'ResolveMetadata.generateMetadata',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/rsc-fetch/page',
|
|
attributes: {
|
|
'next.page': '/app/[param]/rsc-fetch/page',
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/rsc-fetch/page',
|
|
'next.span_type': 'ResolveMetadata.generateMetadata',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
attributes: {
|
|
'next.clientComponentLoadCount': isNextDev ? 8 : 7,
|
|
'next.span_type':
|
|
'NextNodeServer.clientComponentLoading',
|
|
},
|
|
kind: 0,
|
|
name: 'NextNodeServer.clientComponentLoading',
|
|
status: {
|
|
code: 0,
|
|
},
|
|
},
|
|
{
|
|
name: 'start response',
|
|
attributes: {
|
|
'next.span_name': 'start response',
|
|
'next.span_type': 'NextNodeServer.startResponse',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/app/[param]/rsc-fetch',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should propagate custom context without span', async () => {
|
|
await next.fetch('/app/param/rsc-fetch', {
|
|
...env.fetchInit,
|
|
headers: { ...env.fetchInit?.headers, 'x-custom': 'custom1' },
|
|
})
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /app/[param]/rsc-fetch',
|
|
attributes: {
|
|
custom: 'custom1',
|
|
},
|
|
},
|
|
])
|
|
})
|
|
|
|
itEdge('should handle RSC with fetch on edge', async () => {
|
|
await next.fetch('/app/param/rsc-fetch/edge', env.fetchInit)
|
|
|
|
await expectTrace(
|
|
getCollector(),
|
|
[
|
|
{
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
runtime: 'edge',
|
|
name: 'GET /app/[param]/rsc-fetch/edge',
|
|
kind: 1,
|
|
attributes: {
|
|
'next.span_name': 'GET /app/[param]/rsc-fetch/edge',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
'http.method': 'GET',
|
|
'http.target': '/app/param/rsc-fetch/edge?param=param',
|
|
'http.status_code': 200,
|
|
'next.route': '/app/[param]/rsc-fetch/edge',
|
|
'http.route': '/app/[param]/rsc-fetch/edge',
|
|
},
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'render route (app) /app/[param]/rsc-fetch/edge',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name':
|
|
'render route (app) /app/[param]/rsc-fetch/edge',
|
|
'next.span_type': 'AppRender.getBodyResult',
|
|
'next.route': '/app/[param]/rsc-fetch/edge',
|
|
},
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'build component tree',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name': 'build component tree',
|
|
'next.span_type':
|
|
'NextNodeServer.createComponentTree',
|
|
},
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'resolve segment modules',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
'next.segment': '__PAGE__',
|
|
},
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'resolve segment modules',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
'next.segment': '[param]',
|
|
},
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'fetch GET https://example.vercel.sh/',
|
|
kind: 2,
|
|
attributes: {
|
|
'next.span_name':
|
|
'fetch GET https://example.vercel.sh/',
|
|
'next.span_type': 'AppRender.fetch',
|
|
'http.url': 'https://example.vercel.sh/',
|
|
'http.method': 'GET',
|
|
'net.peer.name': 'example.vercel.sh',
|
|
},
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/layout',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/layout',
|
|
'next.span_type':
|
|
'ResolveMetadata.generateMetadata',
|
|
'next.page': '/app/[param]/layout',
|
|
},
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/rsc-fetch/edge/page',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/rsc-fetch/edge/page',
|
|
'next.span_type':
|
|
'ResolveMetadata.generateMetadata',
|
|
'next.page': '/app/[param]/rsc-fetch/edge/page',
|
|
},
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
true
|
|
)
|
|
})
|
|
|
|
it('should handle RSC with fetch in RSC mode', async () => {
|
|
await next.fetch(`/app/param/rsc-fetch?${NEXT_RSC_UNION_QUERY}`, {
|
|
...env.fetchInit,
|
|
headers: {
|
|
...env.fetchInit?.headers,
|
|
Rsc: '1',
|
|
},
|
|
})
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
runtime: 'nodejs',
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'RSC GET /app/[param]/rsc-fetch',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/app/[param]/rsc-fetch',
|
|
'http.status_code': 200,
|
|
'http.target': `/app/param/rsc-fetch?${NEXT_RSC_UNION_QUERY}`,
|
|
'next.route': '/app/[param]/rsc-fetch',
|
|
'next.rsc': true,
|
|
'next.span_name': 'RSC GET /app/[param]/rsc-fetch',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should handle route handlers in app router', async () => {
|
|
await next.fetch('/api/app/param/data', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /api/app/[param]/data',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/api/app/[param]/data',
|
|
'http.status_code': 200,
|
|
'http.target': '/api/app/param/data',
|
|
'next.route': '/api/app/[param]/data',
|
|
'next.span_name': 'GET /api/app/[param]/data',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'executing api route (app) /api/app/[param]/data',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/data',
|
|
'next.span_name':
|
|
'executing api route (app) /api/app/[param]/data',
|
|
'next.span_type': 'AppRouteRouteHandlers.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/data',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
{
|
|
name: 'start response',
|
|
attributes: {
|
|
'next.span_name': 'start response',
|
|
'next.span_type': 'NextNodeServer.startResponse',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should record accurate status code for non-200 route handler responses', async () => {
|
|
await next.fetch('/api/app/param/status', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /api/app/[param]/status',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/api/app/[param]/status',
|
|
'http.status_code': 418,
|
|
'http.target': '/api/app/param/status',
|
|
'next.route': '/api/app/[param]/status',
|
|
'next.span_name': 'GET /api/app/[param]/status',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'executing api route (app) /api/app/[param]/status',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/status',
|
|
'next.span_name':
|
|
'executing api route (app) /api/app/[param]/status',
|
|
'next.span_type': 'AppRouteRouteHandlers.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/status',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
{
|
|
name: 'start response',
|
|
attributes: {
|
|
'next.span_name': 'start response',
|
|
'next.span_type': 'NextNodeServer.startResponse',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should record status code for failing handler', async () => {
|
|
await next.fetch('/api/app/param/error', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /api/app/[param]/error',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/api/app/[param]/error',
|
|
'http.status_code': 500,
|
|
'http.target': '/api/app/param/error',
|
|
'next.route': '/api/app/[param]/error',
|
|
'next.span_name': 'GET /api/app/[param]/error',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 2 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'executing api route (app) /api/app/[param]/error',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/error',
|
|
'next.span_name':
|
|
'executing api route (app) /api/app/[param]/error',
|
|
'next.span_type': 'AppRouteRouteHandlers.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 2, message: 'foobar' },
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/error',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
itEdge(
|
|
'should handle route handlers in app router on edge',
|
|
async () => {
|
|
await next.fetch('/api/app/param/data/edge', env.fetchInit)
|
|
|
|
await expectTrace(
|
|
getCollector(),
|
|
[
|
|
{
|
|
runtime: 'edge',
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'executing api route (app) /api/app/[param]/data/edge',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/data/edge',
|
|
'next.span_name':
|
|
'executing api route (app) /api/app/[param]/data/edge',
|
|
'next.span_type': 'AppRouteRouteHandlers.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
true
|
|
)
|
|
}
|
|
)
|
|
|
|
itEdge('should handle failing handler on edge', async () => {
|
|
await next.fetch('/api/app/param/error/edge', env.fetchInit)
|
|
|
|
await expectTrace(
|
|
getCollector(),
|
|
[
|
|
{
|
|
runtime: 'edge',
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'executing api route (app) /api/app/[param]/error/edge',
|
|
attributes: {
|
|
'next.route': '/api/app/[param]/error/edge',
|
|
'next.span_name':
|
|
'executing api route (app) /api/app/[param]/error/edge',
|
|
'next.span_type': 'AppRouteRouteHandlers.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 2 },
|
|
},
|
|
],
|
|
true
|
|
)
|
|
})
|
|
|
|
it('should handle error in RSC', async () => {
|
|
await next.fetch(
|
|
'/app/param/rsc-fetch/error?status=error',
|
|
env.fetchInit
|
|
)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /app/[param]/rsc-fetch/error',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/app/[param]/rsc-fetch/error',
|
|
'http.status_code': 500,
|
|
'http.target': '/app/param/rsc-fetch/error?status=error',
|
|
'next.route': '/app/[param]/rsc-fetch/error',
|
|
'next.rsc': false,
|
|
'next.span_name': 'GET /app/[param]/rsc-fetch/error',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
'error.type': '500',
|
|
},
|
|
kind: 1,
|
|
status: { code: 2 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'render route (app) /app/[param]/rsc-fetch/error',
|
|
attributes: {
|
|
'next.route': '/app/[param]/rsc-fetch/error',
|
|
'next.span_name':
|
|
'render route (app) /app/[param]/rsc-fetch/error',
|
|
'next.span_type': 'AppRender.getBodyResult',
|
|
},
|
|
kind: 0,
|
|
status: { code: 2 },
|
|
spans: [
|
|
{
|
|
name: 'build component tree',
|
|
attributes: {
|
|
'next.span_name': 'build component tree',
|
|
'next.span_type':
|
|
'NextNodeServer.createComponentTree',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '__PAGE__',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '[param]',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/layout',
|
|
attributes: {
|
|
'next.page': '/app/[param]/layout',
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/layout',
|
|
'next.span_type': 'ResolveMetadata.generateMetadata',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/layout',
|
|
attributes: {
|
|
'next.page': '/app/[param]/layout',
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/layout',
|
|
'next.span_type': 'ResolveMetadata.generateMetadata',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
attributes: {
|
|
'next.clientComponentLoadCount': isNextDev ? 12 : 10,
|
|
'next.span_type':
|
|
'NextNodeServer.clientComponentLoading',
|
|
},
|
|
kind: 0,
|
|
name: 'NextNodeServer.clientComponentLoading',
|
|
status: {
|
|
code: 0,
|
|
},
|
|
},
|
|
{
|
|
name: 'start response',
|
|
attributes: {
|
|
'next.span_name': 'start response',
|
|
'next.span_type': 'NextNodeServer.startResponse',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/app/[param]/rsc-fetch/error',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should handle error inside Suspense boundary', async () => {
|
|
await next.fetch('/app/param/loading/error', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /app/[param]/loading/error',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/app/[param]/loading/error',
|
|
// The response starts streaming before the error,
|
|
// so HTTP status is 200 (unlike synchronous errors which get 500)
|
|
'http.status_code': 200,
|
|
'http.target': '/app/param/loading/error',
|
|
'next.route': '/app/[param]/loading/error',
|
|
'next.rsc': false,
|
|
'next.span_name': 'GET /app/[param]/loading/error',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'render route (app) /app/[param]/loading/error',
|
|
attributes: {
|
|
'next.route': '/app/[param]/loading/error',
|
|
'next.span_name':
|
|
'render route (app) /app/[param]/loading/error',
|
|
'next.span_type': 'AppRender.getBodyResult',
|
|
'error.type': 'Error',
|
|
},
|
|
kind: 0,
|
|
// The render span should have error status because an error
|
|
// was thrown inside a Suspense boundary during streaming
|
|
status: {
|
|
code: 2,
|
|
message: 'Error inside Suspense boundary',
|
|
},
|
|
spans: [
|
|
{
|
|
name: 'build component tree',
|
|
attributes: {
|
|
'next.span_name': 'build component tree',
|
|
'next.span_type':
|
|
'NextNodeServer.createComponentTree',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '__PAGE__',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '[param]',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/layout',
|
|
attributes: {
|
|
'next.page': '/app/[param]/layout',
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/layout',
|
|
'next.span_type': 'ResolveMetadata.generateMetadata',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
attributes: {
|
|
'next.clientComponentLoadCount': isNextDev ? 9 : 8,
|
|
'next.span_type':
|
|
'NextNodeServer.clientComponentLoading',
|
|
},
|
|
kind: 0,
|
|
name: 'NextNodeServer.clientComponentLoading',
|
|
status: {
|
|
code: 0,
|
|
},
|
|
},
|
|
{
|
|
name: 'start response',
|
|
attributes: {
|
|
'next.span_name': 'start response',
|
|
'next.span_type': 'NextNodeServer.startResponse',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/app/[param]/loading/error',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('pages', () => {
|
|
it('should handle getServerSideProps', async () => {
|
|
await next.fetch('/pages/param/getServerSideProps', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /pages/[param]/getServerSideProps',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/pages/[param]/getServerSideProps',
|
|
'http.status_code': 200,
|
|
'http.target': '/pages/param/getServerSideProps',
|
|
'next.route': '/pages/[param]/getServerSideProps',
|
|
'next.span_name': 'GET /pages/[param]/getServerSideProps',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'getServerSideProps /pages/[param]/getServerSideProps',
|
|
attributes: {
|
|
'next.route': '/pages/[param]/getServerSideProps',
|
|
'next.span_name':
|
|
'getServerSideProps /pages/[param]/getServerSideProps',
|
|
'next.span_type': 'Render.getServerSideProps',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'render route (pages) /pages/[param]/getServerSideProps',
|
|
attributes: {
|
|
'next.route': '/pages/[param]/getServerSideProps',
|
|
'next.span_name':
|
|
'render route (pages) /pages/[param]/getServerSideProps',
|
|
'next.span_type': 'Render.renderDocument',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/pages/[param]/getServerSideProps',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
it("should handle getStaticProps when fallback: 'blocking'", async () => {
|
|
const v = env.span.rootParentId ? '2' : ''
|
|
await next.fetch(`/pages/param/getStaticProps${v}`, env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: `GET /pages/[param]/getStaticProps${v}`,
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': `/pages/[param]/getStaticProps${v}`,
|
|
'http.status_code': 200,
|
|
'http.target': `/pages/param/getStaticProps${v}`,
|
|
'next.route': `/pages/[param]/getStaticProps${v}`,
|
|
'next.span_name': `GET /pages/[param]/getStaticProps${v}`,
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: `getStaticProps /pages/[param]/getStaticProps${v}`,
|
|
attributes: {
|
|
'next.route': `/pages/[param]/getStaticProps${v}`,
|
|
'next.span_name': `getStaticProps /pages/[param]/getStaticProps${v}`,
|
|
'next.span_type': 'Render.getStaticProps',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: `render route (pages) /pages/[param]/getStaticProps${v}`,
|
|
attributes: {
|
|
'next.route': `/pages/[param]/getStaticProps${v}`,
|
|
'next.span_name': `render route (pages) /pages/[param]/getStaticProps${v}`,
|
|
'next.span_type': 'Render.renderDocument',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': `/pages/[param]/getStaticProps${v}`,
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
itEdge('should handle getServerSideProps on edge', async () => {
|
|
await next.fetch(
|
|
'/pages/param/edge/getServerSideProps',
|
|
env.fetchInit
|
|
)
|
|
|
|
await expectTrace(
|
|
getCollector(),
|
|
[
|
|
{
|
|
runtime: 'edge',
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'GET /pages/[param]/edge/getServerSideProps',
|
|
kind: 1,
|
|
attributes: {
|
|
'next.span_name':
|
|
'GET /pages/[param]/edge/getServerSideProps',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
'http.method': 'GET',
|
|
'http.target':
|
|
'/pages/param/edge/getServerSideProps?param=param',
|
|
'http.status_code': 200,
|
|
'next.route': '/pages/[param]/edge/getServerSideProps',
|
|
'http.route': '/pages/[param]/edge/getServerSideProps',
|
|
},
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'getServerSideProps /pages/[param]/edge/getServerSideProps',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name':
|
|
'getServerSideProps /pages/[param]/edge/getServerSideProps',
|
|
'next.span_type': 'Render.getServerSideProps',
|
|
'next.route': '/pages/[param]/edge/getServerSideProps',
|
|
},
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'render route (pages) /pages/[param]/edge/getServerSideProps',
|
|
kind: 0,
|
|
attributes: {
|
|
'next.span_name':
|
|
'render route (pages) /pages/[param]/edge/getServerSideProps',
|
|
'next.span_type': 'Render.renderDocument',
|
|
'next.route': '/pages/[param]/edge/getServerSideProps',
|
|
},
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
true
|
|
)
|
|
})
|
|
|
|
it('should handle getServerSideProps exceptions', async () => {
|
|
await next.fetch(
|
|
'/pages/param/getServerSidePropsError',
|
|
env.fetchInit
|
|
)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /pages/[param]/getServerSidePropsError',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/pages/[param]/getServerSidePropsError',
|
|
'http.target': '/pages/param/getServerSidePropsError',
|
|
'next.route': '/pages/[param]/getServerSidePropsError',
|
|
'next.span_name':
|
|
'GET /pages/[param]/getServerSidePropsError',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
...(useDirectEntrypointHandler
|
|
? {
|
|
// With direct entrypoints, this 500 error has to be handled by whatever is
|
|
// invoking the handler. And that same invoker is then also responsible for
|
|
// setting OTEL correctly.
|
|
'error.type': 'Error',
|
|
'http.status_code': 200,
|
|
}
|
|
: {
|
|
'error.type': '500',
|
|
'http.status_code': 500,
|
|
}),
|
|
},
|
|
kind: 1,
|
|
status: { code: 2 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'getServerSideProps /pages/[param]/getServerSidePropsError',
|
|
attributes: {
|
|
'next.route': '/pages/[param]/getServerSidePropsError',
|
|
'next.span_name':
|
|
'getServerSideProps /pages/[param]/getServerSidePropsError',
|
|
'next.span_type': 'Render.getServerSideProps',
|
|
'error.type': 'Error',
|
|
},
|
|
kind: 0,
|
|
status: {
|
|
code: 2,
|
|
message: 'ServerSideProps error',
|
|
},
|
|
events: [
|
|
{
|
|
name: 'exception',
|
|
attributes: {
|
|
'exception.type': 'Error',
|
|
'exception.message': 'ServerSideProps error',
|
|
},
|
|
},
|
|
],
|
|
},
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'render route (pages) /_error',
|
|
attributes: {
|
|
'next.route': '/_error',
|
|
'next.span_name': 'render route (pages) /_error',
|
|
'next.span_type': 'Render.renderDocument',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/_error',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
...(isNextDev || useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/500',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/500',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route':
|
|
'/pages/[param]/getServerSidePropsError',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should handle getServerSideProps returning notFound', async () => {
|
|
await next.fetch(
|
|
'/pages/param/getServerSidePropsNotFound',
|
|
env.fetchInit
|
|
)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /pages/[param]/getServerSidePropsNotFound',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/pages/[param]/getServerSidePropsNotFound',
|
|
'http.status_code': 404,
|
|
'http.target': '/pages/param/getServerSidePropsNotFound',
|
|
'next.route': '/pages/[param]/getServerSidePropsNotFound',
|
|
'next.span_name':
|
|
'GET /pages/[param]/getServerSidePropsNotFound',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'getServerSideProps /pages/[param]/getServerSidePropsNotFound',
|
|
attributes: {
|
|
'next.route': '/pages/[param]/getServerSidePropsNotFound',
|
|
'next.span_name':
|
|
'getServerSideProps /pages/[param]/getServerSidePropsNotFound',
|
|
'next.span_type': 'Render.getServerSideProps',
|
|
},
|
|
kind: 0,
|
|
status: {
|
|
code: 0,
|
|
},
|
|
},
|
|
...(isNextDev
|
|
? [
|
|
{
|
|
name: 'render route (app) /_not-found',
|
|
attributes: {
|
|
'next.route': '/_not-found',
|
|
'next.span_name': 'render route (app) /_not-found',
|
|
'next.span_type': 'AppRender.getBodyResult',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]
|
|
: []),
|
|
...(useDirectEntrypointHandler
|
|
? []
|
|
: [
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/_not-found',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route':
|
|
'/pages/[param]/getServerSidePropsNotFound',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
it('should handle api routes in pages', async () => {
|
|
await next.fetch('/api/pages/param/basic', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /api/pages/[param]/basic',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/api/pages/[param]/basic',
|
|
'http.status_code': 200,
|
|
'http.target': '/api/pages/param/basic',
|
|
'next.route': '/api/pages/[param]/basic',
|
|
'next.span_name': 'GET /api/pages/[param]/basic',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'executing api route (pages) /api/pages/[param]/basic',
|
|
attributes: {
|
|
'next.span_name':
|
|
'executing api route (pages) /api/pages/[param]/basic',
|
|
'next.span_type': 'Node.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
itEdge('should handle api routes in pages on edge', async () => {
|
|
await next.fetch('/api/pages/param/edge', env.fetchInit)
|
|
|
|
await expectTrace(
|
|
getCollector(),
|
|
[
|
|
{
|
|
runtime: 'edge',
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'executing api route (pages) /api/pages/[param]/edge',
|
|
attributes: {
|
|
'next.span_name':
|
|
'executing api route (pages) /api/pages/[param]/edge',
|
|
'next.span_type': 'Node.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
true
|
|
)
|
|
})
|
|
|
|
it('should handle failing api routes in pages', async () => {
|
|
await next.fetch('/api/pages/param/error', env.fetchInit)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /api/pages/[param]/error',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/api/pages/[param]/error',
|
|
'http.status_code': 500,
|
|
'http.target': '/api/pages/param/error',
|
|
'next.route': '/api/pages/[param]/error',
|
|
'next.span_name': 'GET /api/pages/[param]/error',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 2 },
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
spans: [
|
|
{
|
|
name: 'executing api route (pages) /api/pages/[param]/error',
|
|
attributes: {
|
|
'next.span_name':
|
|
'executing api route (pages) /api/pages/[param]/error',
|
|
'next.span_type': 'Node.runHandler',
|
|
},
|
|
kind: 0,
|
|
// TODO this difference is odd
|
|
status: { code: isNextDev ? 2 : 0 },
|
|
},
|
|
...(isNextDev
|
|
? [
|
|
{
|
|
name: 'render route (pages) /_error',
|
|
attributes: {
|
|
'next.route': '/_error',
|
|
'next.span_name': 'render route (pages) /_error',
|
|
'next.span_type': 'Render.renderDocument',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/_error',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type':
|
|
'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
]
|
|
: []),
|
|
],
|
|
},
|
|
])
|
|
})
|
|
|
|
itEdge(
|
|
'should handle failing api routes in pages on edge',
|
|
async () => {
|
|
await next.fetch('/api/pages/param/error-edge', env.fetchInit)
|
|
|
|
await expectTrace(
|
|
getCollector(),
|
|
[
|
|
{
|
|
runtime: 'edge',
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'executing api route (pages) /api/pages/[param]/error-edge',
|
|
attributes: {
|
|
'next.span_name':
|
|
'executing api route (pages) /api/pages/[param]/error-edge',
|
|
'next.span_type': 'Node.runHandler',
|
|
},
|
|
kind: 0,
|
|
status: { code: 2 },
|
|
},
|
|
],
|
|
true
|
|
)
|
|
}
|
|
)
|
|
})
|
|
}
|
|
)
|
|
}
|
|
})
|
|
|
|
describe.each(
|
|
[
|
|
{ name: 'default', useDirectEntrypointHandler: false },
|
|
isNextStart && {
|
|
name: 'direct entrypoints',
|
|
useDirectEntrypointHandler: true,
|
|
},
|
|
].filter(Boolean)
|
|
)('opentelemetry - middleware $name', ({ useDirectEntrypointHandler }) => {
|
|
describe.each(['edge', 'nodejs'])('%s runtime', (runtime) => {
|
|
const {
|
|
next: { next, skipped },
|
|
getCollector,
|
|
} = setup({
|
|
useDirectEntrypointHandler,
|
|
useNodeMiddleware: runtime === 'nodejs',
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
if (useDirectEntrypointHandler && runtime === 'edge') {
|
|
it.skip('direct entrypoint handler is not implemented for edge runtime', () => {})
|
|
return
|
|
}
|
|
|
|
for (const env of [
|
|
{
|
|
name: 'root context',
|
|
fetchInit: undefined,
|
|
span: {
|
|
traceId: '[trace-id]',
|
|
rootParentId: undefined,
|
|
},
|
|
},
|
|
{
|
|
name: 'incoming context propagation',
|
|
fetchInit: {
|
|
headers: {
|
|
traceparent: `00-${EXTERNAL.traceId}-${EXTERNAL.spanId}-01`,
|
|
},
|
|
},
|
|
span: {
|
|
traceId: EXTERNAL.traceId,
|
|
rootParentId: EXTERNAL.spanId,
|
|
},
|
|
},
|
|
]) {
|
|
;(process.env.__NEXT_CACHE_COMPONENTS ? describe.skip : describe)(
|
|
env.name,
|
|
() => {
|
|
it('should trace middleware', async () => {
|
|
await next.fetch('/behind-middleware', env.fetchInit)
|
|
let expected = [
|
|
{
|
|
runtime: runtime,
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'middleware GET',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.target': '/behind-middleware',
|
|
'next.span_name': 'middleware GET',
|
|
'next.span_type': 'Middleware.execute',
|
|
},
|
|
status: { code: 0 },
|
|
spans: [],
|
|
},
|
|
{
|
|
runtime: 'nodejs',
|
|
traceId: env.span.traceId,
|
|
parentId: env.span.rootParentId,
|
|
name: 'GET /behind-middleware',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/behind-middleware',
|
|
'http.status_code': 200,
|
|
'http.target': '/behind-middleware',
|
|
'next.route': '/behind-middleware',
|
|
'next.span_name': 'GET /behind-middleware',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
},
|
|
]
|
|
if (runtime === 'nodejs') {
|
|
// TODO unclear why this is reversed for Node.js runtime
|
|
expected.reverse()
|
|
}
|
|
await expectTrace(getCollector(), expected)
|
|
})
|
|
}
|
|
)
|
|
}
|
|
})
|
|
})
|
|
;(process.env.__NEXT_CACHE_COMPONENTS ? describe.skip : describe)(
|
|
'opentelemetry NEXT_OTEL_VERBOSE=1',
|
|
() => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
dependencies: require('./package.json').dependencies,
|
|
env: {
|
|
TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT),
|
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
NEXT_OTEL_VERBOSE: '1',
|
|
},
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
let collector: Collector | undefined
|
|
|
|
beforeEach(async () => {
|
|
collector = await connectCollector({ port: COLLECTOR_PORT })
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await collector?.shutdown()
|
|
collector = undefined
|
|
})
|
|
|
|
// Regression for https://github.com/vercel/otel/issues/107.
|
|
it('all spans (including verbose) inherit traceId from incoming traceparent header', async () => {
|
|
const pathname = '/app/param/rsc-fetch'
|
|
await next.fetch(pathname, {
|
|
headers: {
|
|
traceparent: `00-${EXTERNAL.traceId}-${EXTERNAL.spanId}-01`,
|
|
},
|
|
})
|
|
|
|
let spans: SavedSpan[] = []
|
|
await retry(async () => {
|
|
const all = collector?.getSpans() ?? []
|
|
const root = all.find(
|
|
(s) =>
|
|
s.attributes?.['next.span_type'] === 'BaseServer.handleRequest' &&
|
|
s.attributes?.['http.target'] === pathname
|
|
)
|
|
expect(root).toBeDefined()
|
|
expect(root!.traceId).toBe(EXTERNAL.traceId)
|
|
|
|
spans = all.filter((s) => s.traceId === root!.traceId)
|
|
expect(spans.length).toBeGreaterThan(1)
|
|
|
|
const verbose = spans.find(
|
|
(s) =>
|
|
s.attributes?.['next.span_type'] === 'NextServer.getRequestHandler'
|
|
)
|
|
expect(verbose).toBeDefined()
|
|
expect(verbose!.traceId).toBe(EXTERNAL.traceId)
|
|
const parentSpanId = verbose!.parentId
|
|
expect(parentSpanId).toBe(EXTERNAL.spanId)
|
|
})
|
|
|
|
for (const span of spans) {
|
|
expect(span.traceId).toBe(EXTERNAL.traceId)
|
|
}
|
|
})
|
|
}
|
|
)
|
|
|
|
describe('opentelemetry with disabled fetch tracing', () => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
dependencies: require('./package.json').dependencies,
|
|
env: {
|
|
NEXT_OTEL_FETCH_DISABLED: '1',
|
|
TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT),
|
|
},
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
let collector: Collector
|
|
|
|
function getCollector(): Collector {
|
|
return collector
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
collector = await connectCollector({ port: COLLECTOR_PORT })
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await collector.shutdown()
|
|
await new Promise((r) => setTimeout(r, 1000))
|
|
})
|
|
;(process.env.__NEXT_CACHE_COMPONENTS ? describe.skip : describe)(
|
|
'root context',
|
|
() => {
|
|
describe('app router with disabled fetch', () => {
|
|
it('should handle RSC with disabled fetch', async () => {
|
|
await next.fetch('/app/param/rsc-fetch')
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'GET /app/[param]/rsc-fetch',
|
|
traceId: '[trace-id]',
|
|
parentId: undefined,
|
|
spans: [
|
|
{
|
|
name: 'render route (app) /app/[param]/rsc-fetch',
|
|
spans: [
|
|
{
|
|
name: 'build component tree',
|
|
spans: [
|
|
{
|
|
name: 'resolve segment modules',
|
|
},
|
|
{
|
|
name: 'resolve segment modules',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/layout',
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/rsc-fetch/page',
|
|
},
|
|
{
|
|
name: 'NextNodeServer.clientComponentLoading',
|
|
},
|
|
{
|
|
name: 'start response',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'resolve page components',
|
|
},
|
|
],
|
|
},
|
|
])
|
|
})
|
|
})
|
|
}
|
|
)
|
|
})
|
|
|
|
describe('opentelemetry with custom server', () => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
dependencies: require('./package.json').dependencies,
|
|
startCommand: 'pnpm start',
|
|
packageJson: {
|
|
scripts: {
|
|
start: 'pnpm tsx custom-server.ts',
|
|
},
|
|
},
|
|
serverReadyPattern: /- Local:/,
|
|
env: {
|
|
TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT),
|
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
NODE_ENV: isNextDev ? 'development' : 'production',
|
|
},
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
let collector: Collector
|
|
|
|
function getCollector(): Collector {
|
|
return collector
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
collector = await connectCollector({ port: COLLECTOR_PORT })
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await collector.shutdown()
|
|
})
|
|
|
|
it('should set attributes correctly on handleRequest span', async () => {
|
|
await next.fetch('/app/param/rsc-fetch')
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: 'custom-server-request',
|
|
traceId: '[trace-id]',
|
|
parentId: undefined,
|
|
spans: [
|
|
{
|
|
name: 'GET /app/[param]/rsc-fetch',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.route': '/app/[param]/rsc-fetch',
|
|
'http.status_code': 200,
|
|
'http.target': '/app/param/rsc-fetch',
|
|
'next.route': '/app/[param]/rsc-fetch',
|
|
'next.rsc': false,
|
|
'next.span_name': 'GET /app/[param]/rsc-fetch',
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
},
|
|
kind: 1,
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'render route (app) /app/[param]/rsc-fetch',
|
|
attributes: {
|
|
'next.route': '/app/[param]/rsc-fetch',
|
|
'next.span_name': 'render route (app) /app/[param]/rsc-fetch',
|
|
'next.span_type': 'AppRender.getBodyResult',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'build component tree',
|
|
attributes: {
|
|
'next.span_name': 'build component tree',
|
|
'next.span_type': 'NextNodeServer.createComponentTree',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
spans: [
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '__PAGE__',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'resolve segment modules',
|
|
attributes: {
|
|
'next.segment': '[param]',
|
|
'next.span_name': 'resolve segment modules',
|
|
'next.span_type':
|
|
'NextNodeServer.getLayoutOrPageModule',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'fetch GET https://example.vercel.sh/',
|
|
attributes: {
|
|
'http.method': 'GET',
|
|
'http.url': 'https://example.vercel.sh/',
|
|
'net.peer.name': 'example.vercel.sh',
|
|
'next.span_name': 'fetch GET https://example.vercel.sh/',
|
|
'next.span_type': 'AppRender.fetch',
|
|
},
|
|
kind: 2,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/layout',
|
|
attributes: {
|
|
'next.page': '/app/[param]/layout',
|
|
'next.span_name': 'generateMetadata /app/[param]/layout',
|
|
'next.span_type': 'ResolveMetadata.generateMetadata',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
name: 'generateMetadata /app/[param]/rsc-fetch/page',
|
|
attributes: {
|
|
'next.page': '/app/[param]/rsc-fetch/page',
|
|
'next.span_name':
|
|
'generateMetadata /app/[param]/rsc-fetch/page',
|
|
'next.span_type': 'ResolveMetadata.generateMetadata',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
{
|
|
attributes: {
|
|
'next.clientComponentLoadCount': isNextDev ? 8 : 7,
|
|
'next.span_type': 'NextNodeServer.clientComponentLoading',
|
|
},
|
|
kind: 0,
|
|
name: 'NextNodeServer.clientComponentLoading',
|
|
status: {
|
|
code: 0,
|
|
},
|
|
},
|
|
{
|
|
name: 'start response',
|
|
attributes: {
|
|
'next.span_name': 'start response',
|
|
'next.span_type': 'NextNodeServer.startResponse',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
{
|
|
name: 'resolve page components',
|
|
attributes: {
|
|
'next.route': '/app/[param]/rsc-fetch',
|
|
'next.span_name': 'resolve page components',
|
|
'next.span_type': 'NextNodeServer.findPageComponents',
|
|
},
|
|
kind: 0,
|
|
status: { code: 0 },
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
])
|
|
})
|
|
})
|
|
|
|
if (isNextStart) {
|
|
describe('opentelemetry with direct entrypoint handler', () => {
|
|
const { next, skipped } = nextTestSetup({
|
|
files: __dirname,
|
|
skipDeployment: true,
|
|
dependencies: require('./package.json').dependencies,
|
|
startCommand: 'pnpm start-entrypoint',
|
|
packageJson: {
|
|
scripts: {
|
|
'start-entrypoint': 'pnpm tsx custom-entrypoint-server.ts',
|
|
},
|
|
},
|
|
serverReadyPattern: /- Local:/,
|
|
env: {
|
|
TEST_OTEL_COLLECTOR_PORT: String(COLLECTOR_PORT),
|
|
NEXT_TELEMETRY_DISABLED: '1',
|
|
NODE_ENV: 'production',
|
|
},
|
|
})
|
|
|
|
if (skipped) {
|
|
return
|
|
}
|
|
|
|
let collector: Collector
|
|
|
|
function getCollector(): Collector {
|
|
return collector
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
collector = await connectCollector({ port: COLLECTOR_PORT })
|
|
})
|
|
|
|
afterEach(async () => {
|
|
await collector.shutdown()
|
|
})
|
|
|
|
const directEntrypointCases = [
|
|
{ pathname: '/app/param/rsc-fetch', route: '/app/[param]/rsc-fetch' },
|
|
{ pathname: '/api/app/param/data', route: '/api/app/[param]/data' },
|
|
{
|
|
pathname: '/pages/param/getServerSideProps',
|
|
route: '/pages/[param]/getServerSideProps',
|
|
},
|
|
{
|
|
pathname: '/api/pages/param/basic',
|
|
route: '/api/pages/[param]/basic',
|
|
},
|
|
] as const
|
|
|
|
describe.each(directEntrypointCases)(
|
|
'direct entrypoint $pathname',
|
|
({ pathname, route }) => {
|
|
it(`should add route names to handleRequest and parent spans for direct entrypoint ${pathname}`, async () => {
|
|
const response = await next.fetch(pathname)
|
|
expect(response.status).toBe(200)
|
|
|
|
await retry(
|
|
async () => {
|
|
const spans = collector.getSpans()
|
|
const handleRequestSpan = spans.find((span) => {
|
|
if (
|
|
span.attributes?.['next.span_type'] !==
|
|
'BaseServer.handleRequest'
|
|
) {
|
|
return false
|
|
}
|
|
const target = span.attributes?.['http.target'] as
|
|
| string
|
|
| undefined
|
|
return Boolean(target && target.includes(pathname))
|
|
})
|
|
|
|
expect(handleRequestSpan).toBeDefined()
|
|
expect(handleRequestSpan!.name).toBe(`GET ${route}`)
|
|
expect(handleRequestSpan!.attributes?.['http.target']).toContain(
|
|
pathname
|
|
)
|
|
expect(handleRequestSpan!.attributes?.['next.route']).toBe(route)
|
|
expect(handleRequestSpan!.attributes?.['http.route']).toBe(route)
|
|
expect(handleRequestSpan!.attributes?.['next.span_name']).toBe(
|
|
`GET ${route}`
|
|
)
|
|
|
|
const parentSpan = spans.find(
|
|
(span) =>
|
|
span.traceId === handleRequestSpan!.traceId &&
|
|
!span.parentId &&
|
|
!span.attributes?.['next.span_type'] &&
|
|
span.name === handleRequestSpan!.name
|
|
)
|
|
expect(parentSpan).toBeDefined()
|
|
expect(parentSpan!.name).toBe(`GET ${route}`)
|
|
},
|
|
30_000,
|
|
1_000,
|
|
`direct entrypoint span route naming ${pathname}`
|
|
)
|
|
})
|
|
|
|
it(`should propagate incoming context without next-server wrapper for direct entrypoint ${pathname}`, async () => {
|
|
const response = await next.fetch(pathname, {
|
|
headers: {
|
|
traceparent: `00-${EXTERNAL.traceId}-${EXTERNAL.spanId}-01`,
|
|
},
|
|
})
|
|
expect(response.status).toBe(200)
|
|
|
|
await expectTrace(getCollector(), [
|
|
{
|
|
name: `GET ${route}`,
|
|
traceId: EXTERNAL.traceId,
|
|
parentId: EXTERNAL.spanId,
|
|
attributes: {
|
|
'http.target': pathname,
|
|
'next.span_type': 'BaseServer.handleRequest',
|
|
'http.route': route,
|
|
'next.route': route,
|
|
},
|
|
},
|
|
])
|
|
})
|
|
}
|
|
)
|
|
})
|
|
}
|
|
|
|
type HierSavedSpan = SavedSpan & { spans?: HierSavedSpan[] }
|
|
type SpanMatch = Omit<Partial<HierSavedSpan>, 'spans'> & { spans?: SpanMatch[] }
|
|
|
|
async function expectTrace(
|
|
collector: Collector,
|
|
match: SpanMatch[],
|
|
edgeOnly?: boolean
|
|
) {
|
|
// Extract expected http.target values from the match to filter out extra spans
|
|
// that may be generated in production mode (e.g., RSC prefetch requests)
|
|
const expectedTargets = new Set(
|
|
match
|
|
.map((m) => m.attributes?.['http.target'] as string | undefined)
|
|
.filter(Boolean)
|
|
)
|
|
|
|
await retry(async () => {
|
|
const traces = collector.getSpans()
|
|
|
|
const tree: HierSavedSpan[] = []
|
|
const spansForTree: HierSavedSpan[] = traces.map((span) => ({
|
|
...span,
|
|
spans: [],
|
|
}))
|
|
for (const span of spansForTree) {
|
|
// for edge runtime with `next start` the nodejs runtime spans
|
|
// will not be updated as the sandbox can't update spans across
|
|
// node -> edge boundary, these spans also are not present when
|
|
// deployed as next-server is not invoking edge functions there
|
|
if (span.runtime === 'nodejs' && edgeOnly) {
|
|
continue
|
|
}
|
|
|
|
const parent =
|
|
!span.parentId || span.parentId === EXTERNAL.spanId
|
|
? null
|
|
: spansForTree.find((s) => s.id === span.parentId)
|
|
if (parent) {
|
|
parent.spans.push(span)
|
|
} else {
|
|
tree.push(span)
|
|
}
|
|
}
|
|
for (const span of spansForTree) {
|
|
delete span.duration
|
|
delete span.timestamp
|
|
|
|
span.traceId =
|
|
span.traceId === EXTERNAL.traceId ? span.traceId : '[trace-id]'
|
|
span.parentId = span.parentId || undefined
|
|
|
|
span.spans.sort((a, b) => {
|
|
const nameDiff = a.name.localeCompare(b.name)
|
|
if (nameDiff !== 0) {
|
|
return nameDiff
|
|
}
|
|
const segmentDiff =
|
|
(a.attributes?.['next.segment'] ?? '').localeCompare(
|
|
b.attributes?.['next.segment'] ?? ''
|
|
) ?? 0
|
|
if (segmentDiff !== 0) {
|
|
return segmentDiff
|
|
}
|
|
return (
|
|
(a.attributes?.['next.route'] ?? '').localeCompare(
|
|
b.attributes?.['next.route'] ?? ''
|
|
) ?? 0
|
|
)
|
|
})
|
|
}
|
|
|
|
// Filter root spans to only those matching expected http.target values
|
|
// This prevents flakiness from extra spans in prod mode (RSC prefetch, etc.)
|
|
const filteredTree =
|
|
expectedTargets.size > 0
|
|
? tree.filter((span) => {
|
|
const target = span.attributes?.['http.target'] as
|
|
| string
|
|
| undefined
|
|
return target && expectedTargets.has(target)
|
|
})
|
|
: tree
|
|
|
|
filteredTree.sort((a, b) => {
|
|
const runtimeDiff = (a.runtime ?? '').localeCompare(b.runtime ?? '')
|
|
if (runtimeDiff !== 0) {
|
|
return runtimeDiff
|
|
}
|
|
return a.name.localeCompare(b.name)
|
|
})
|
|
|
|
expect(filteredTree).toMatchObject(match)
|
|
})
|
|
}
|