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
3 changes: 1 addition & 2 deletions packages/next/src/build/templates/app-page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1407,8 +1407,7 @@ export async function handler(
)
}
} catch (err) {
// if we aren't wrapped by base-server handle here
if (!activeSpan && !(err instanceof NoFallbackError)) {
if (!(err instanceof NoFallbackError)) {
await routeModule.onRequestError(
req,
err,
Expand Down
3 changes: 1 addition & 2 deletions packages/next/src/build/templates/app-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -466,8 +466,7 @@ export async function handler(
)
}
} catch (err) {
// if we aren't wrapped by base-server handle here
if (!activeSpan && !(err instanceof NoFallbackError)) {
if (!(err instanceof NoFallbackError)) {
await routeModule.onRequestError(req, err, {
routerKind: 'App Router',
routePath: normalizedSrcPage,
Expand Down
6 changes: 6 additions & 0 deletions test/e2e/on-request-error/otel/app/app-route/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { connection } from 'next/server'

export async function GET() {
await connection()
throw new Error('route-node-error')
}
11 changes: 11 additions & 0 deletions test/e2e/on-request-error/otel/app/layout.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Suspense } from 'react'

export default function Layout({ children }) {
return (
<html>
<body>
<Suspense>{children}</Suspense>
</body>
</html>
)
}
6 changes: 6 additions & 0 deletions test/e2e/on-request-error/otel/app/server-page/page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { connection } from 'next/server'

export default async function Page() {
await connection()
throw new Error('server-page-node-error')
}
40 changes: 40 additions & 0 deletions test/e2e/on-request-error/otel/app/write-log/route.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import fs from 'fs'
import fsp from 'fs/promises'
import path from 'path'

const dir = path.join(path.dirname(new URL(import.meta.url).pathname), '../..')
const logPath = path.join(dir, 'output-log.json')

export async function POST(req) {
let payloadString = ''
const decoder = new TextDecoder()
const reader = req.clone().body.getReader()
while (true) {
const { done, value } = await reader.read()
if (done) {
break
}

payloadString += decoder.decode(value)
}

const payload = JSON.parse(payloadString)

const json = fs.existsSync(logPath)
? JSON.parse(await fsp.readFile(logPath, 'utf8'))
: {}

if (!json[payload.message]) {
json[payload.message] = {
payload,
count: 1,
}
} else {
json[payload.message].count++
}

await fsp.writeFile(logPath, JSON.stringify(json, null, 2), 'utf8')

console.log(`[instrumentation] write-log:${payload.message}`)
return new Response(null, { status: 204 })
}
21 changes: 21 additions & 0 deletions test/e2e/on-request-error/otel/instrumentation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { registerOTel } from '@vercel/otel'

export const onRequestError = async (err, request, context) => {
await fetch(`http://localhost:${process.env.PORT}/write-log`, {
method: 'POST',
body: JSON.stringify({
message: err.message,
request,
context,
}),
headers: {
'Content-Type': 'application/json',
},
})
}

export function register() {
registerOTel({
serviceName: 'test',
})
}
83 changes: 83 additions & 0 deletions test/e2e/on-request-error/otel/otel.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { nextTestSetup } from 'e2e-utils'
import { retry } from 'next-test-utils'
import { getOutputLogJson } from '../_testing/utils'

describe('on-request-error - otel', () => {
const { next, skipped } = nextTestSetup({
files: __dirname,
skipDeployment: true,
packageJson: {
dependencies: {
'@vercel/otel': '^1.13.0',
},
},
})

if (skipped) {
return
}

const outputLogPath = 'output-log.json'

async function validateErrorRecord({
errorMessage,
url,
renderSource,
}: {
errorMessage: string
url: string
renderSource: string | undefined
}) {
// Assert the instrumentation is called
await retry(async () => {
const recordLogLines = next.cliOutput
.split('\n')
.filter((log) => log.includes('[instrumentation] write-log'))
expect(recordLogLines).toEqual(
expect.arrayContaining([expect.stringContaining(errorMessage)])
)
// TODO: remove custom duration in case we increase the default.
}, 5000)

const json = await getOutputLogJson(next, outputLogPath)
const record = json[errorMessage]

const { payload } = record
const { request } = payload

expect(request.path).toBe(url)
expect(record).toMatchObject({
count: 1,
payload: {
message: errorMessage,
request: { method: 'GET', headers: { accept: '*/*' } },
...(renderSource ? { context: { renderSource } } : undefined),
},
})
}

beforeAll(async () => {
await next.patchFile(outputLogPath, '{}')
})

describe('app router', () => {
it('should catch server component page error in node runtime', async () => {
await next.fetch('/server-page')
await validateErrorRecord({
errorMessage: 'server-page-node-error',
url: '/server-page',
renderSource: 'react-server-components',
})
})

it('should catch app routes error in node runtime', async () => {
await next.fetch('/app-route')

await validateErrorRecord({
errorMessage: 'route-node-error',
url: '/app-route',
renderSource: undefined,
})
})
})
})
Loading