Skip to content

Commit b2e711a

Browse files
authored
test: add dynamic routes and suspense test case for onRequestError (#67848)
### What Add tests cases for dynamic routes and suspense page rendering case coverage for onRequestError. Mostly for checking the `routePath` where we can display the proper original route path Closes NDX-22 Closes NDX-79
1 parent 5c9706e commit b2e711a

File tree

13 files changed

+272
-21
lines changed

13 files changed

+272
-21
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export async function getOutputLogJson(next, outputLogPath) {
2+
if (!(await next.hasFile(outputLogPath))) {
3+
return {}
4+
}
5+
const content = await next.readFile(outputLogPath)
6+
return JSON.parse(content)
7+
}

test/e2e/on-request-error/basic/basic.test.ts

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { nextTestSetup } from 'e2e-utils'
22
import { retry } from 'next-test-utils'
3+
import { getOutputLogJson } from '../_testing/utils'
34

45
describe('on-request-error - basic', () => {
56
const { next, skipped } = nextTestSetup({
@@ -16,14 +17,6 @@ describe('on-request-error - basic', () => {
1617

1718
const outputLogPath = 'output-log.json'
1819

19-
async function getOutputLogJson() {
20-
if (!(await next.hasFile(outputLogPath))) {
21-
return {}
22-
}
23-
const content = await next.readFile(outputLogPath)
24-
return JSON.parse(content)
25-
}
26-
2720
async function validateErrorRecord({
2821
errorMessage,
2922
url,
@@ -37,14 +30,15 @@ describe('on-request-error - basic', () => {
3730
}) {
3831
// Assert the instrumentation is called
3932
await retry(async () => {
40-
const recordLogs = next.cliOutput
33+
const recordLogLines = next.cliOutput
4134
.split('\n')
4235
.filter((log) => log.includes('[instrumentation] write-log'))
43-
const expectedLog = recordLogs.find((log) => log.includes(errorMessage))
44-
expect(expectedLog).toBeDefined()
36+
expect(recordLogLines).toEqual(
37+
expect.arrayContaining([expect.stringContaining(errorMessage)])
38+
)
4539
}, 5000)
4640

47-
const json = await getOutputLogJson()
41+
const json = await getOutputLogJson(next, outputLogPath)
4842
const record = json[errorMessage]
4943

5044
const { payload } = record
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export default function Page() {
2+
throw new Error('server-dynamic-page-node-error')
3+
}
4+
5+
export const dynamic = 'force-dynamic'
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Suspense } from 'react'
2+
3+
export default function Page() {
4+
return (
5+
<Suspense>
6+
<Inner />
7+
</Suspense>
8+
)
9+
}
10+
11+
function Inner() {
12+
if (typeof window === 'undefined') {
13+
throw new Error('server-suspense-page-node-error')
14+
}
15+
return 'inner'
16+
}
17+
18+
export const dynamic = 'force-dynamic'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export function GET() {
2+
throw new Error('server-dynamic-route-node-error')
3+
}
4+
5+
export const dynamic = 'force-dynamic'
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export const metadata = {
2+
title: 'Next.js',
3+
description: 'Generated by Next.js',
4+
}
5+
6+
export default function RootLayout({ children }) {
7+
return (
8+
<html lang="en">
9+
<body>{children}</body>
10+
</html>
11+
)
12+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import fs from 'fs'
2+
import fsp from 'fs/promises'
3+
import path from 'path'
4+
5+
const dir = path.join(path.dirname(new URL(import.meta.url).pathname), '../..')
6+
const logPath = path.join(dir, 'output-log.json')
7+
8+
export async function POST(req) {
9+
let payloadString = ''
10+
const decoder = new TextDecoder()
11+
const reader = req.clone().body.getReader()
12+
while (true) {
13+
const { done, value } = await reader.read()
14+
if (done) {
15+
break
16+
}
17+
18+
payloadString += decoder.decode(value)
19+
}
20+
21+
const payload = JSON.parse(payloadString)
22+
23+
const json = fs.existsSync(logPath)
24+
? JSON.parse(await fsp.readFile(logPath, 'utf8'))
25+
: {}
26+
27+
if (!json[payload.message]) {
28+
json[payload.message] = {
29+
payload,
30+
count: 1,
31+
}
32+
} else {
33+
json[payload.message].count++
34+
}
35+
36+
await fsp.writeFile(logPath, JSON.stringify(json, null, 2), 'utf8')
37+
38+
console.log(`[instrumentation] write-log:${payload.message}`)
39+
return new Response(null, { status: 204 })
40+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import { nextTestSetup } from 'e2e-utils'
2+
import { retry } from 'next-test-utils'
3+
import { getOutputLogJson } from '../_testing/utils'
4+
5+
describe('on-request-error - dynamic-routes', () => {
6+
const { next, skipped } = nextTestSetup({
7+
files: __dirname,
8+
skipDeployment: true,
9+
env: {
10+
__NEXT_EXPERIMENTAL_INSTRUMENTATION: '1',
11+
},
12+
})
13+
14+
if (skipped) {
15+
return
16+
}
17+
18+
const outputLogPath = 'output-log.json'
19+
20+
async function getErrorRecord({ errorMessage }: { errorMessage: string }) {
21+
// Assert the instrumentation is called
22+
await retry(async () => {
23+
const recordLogLines = next.cliOutput
24+
.split('\n')
25+
.filter((log) => log.includes('[instrumentation] write-log'))
26+
expect(recordLogLines).toEqual(
27+
expect.arrayContaining([expect.stringContaining(errorMessage)])
28+
)
29+
}, 5000)
30+
31+
const json = await getOutputLogJson(next, outputLogPath)
32+
const record = json[errorMessage]
33+
34+
return record
35+
}
36+
37+
beforeAll(async () => {
38+
await next.patchFile(outputLogPath, '{}')
39+
})
40+
41+
describe('app router', () => {
42+
it('should catch app router dynamic page error with search params', async () => {
43+
await next.fetch('/app-page/dynamic/123?apple=dope')
44+
const record = await getErrorRecord({
45+
errorMessage: 'server-dynamic-page-node-error',
46+
})
47+
expect(record).toMatchObject({
48+
payload: {
49+
message: 'server-dynamic-page-node-error',
50+
request: {
51+
url: '/app-page/dynamic/123?apple=dope',
52+
},
53+
context: {
54+
routerKind: 'App Router',
55+
routeType: 'render',
56+
routePath: '/app-page/dynamic/[id]',
57+
},
58+
},
59+
})
60+
})
61+
62+
it('should catch app router dynamic routes error with search params', async () => {
63+
await next.fetch('/app-route/dynamic/123?apple=dope')
64+
const record = await getErrorRecord({
65+
errorMessage: 'server-dynamic-route-node-error',
66+
})
67+
expect(record).toMatchObject({
68+
payload: {
69+
message: 'server-dynamic-route-node-error',
70+
request: {
71+
url: '/app-route/dynamic/123?apple=dope',
72+
},
73+
context: {
74+
routerKind: 'App Router',
75+
routeType: 'route',
76+
routePath: '/app-route/dynamic/[id]',
77+
},
78+
},
79+
})
80+
})
81+
82+
it('should catch suspense rendering page error in node runtime', async () => {
83+
await next.fetch('/app-page/suspense')
84+
const record = await getErrorRecord({
85+
errorMessage: 'server-suspense-page-node-error',
86+
})
87+
88+
expect(record).toMatchObject({
89+
payload: {
90+
message: 'server-suspense-page-node-error',
91+
request: {
92+
url: '/app-page/suspense',
93+
},
94+
context: {
95+
routerKind: 'App Router',
96+
routeType: 'render',
97+
routePath: '/app-page/suspense',
98+
},
99+
},
100+
})
101+
})
102+
})
103+
104+
describe('pages router', () => {
105+
it('should catch pages router dynamic page error with search params', async () => {
106+
await next.fetch('/pages-page/dynamic/123?apple=dope')
107+
const record = await getErrorRecord({
108+
errorMessage: 'pages-page-node-error',
109+
})
110+
111+
expect(record).toMatchObject({
112+
payload: {
113+
message: 'pages-page-node-error',
114+
request: {
115+
url: '/pages-page/dynamic/123?apple=dope',
116+
},
117+
context: {
118+
routerKind: 'Pages Router',
119+
routeType: 'render',
120+
routePath: '/pages-page/dynamic/[id]',
121+
},
122+
},
123+
})
124+
})
125+
126+
it('should catch pages router dynamic API route error with search params', async () => {
127+
await next.fetch('/api/dynamic/123?apple=dope')
128+
const record = await getErrorRecord({
129+
errorMessage: 'pages-api-node-error',
130+
})
131+
132+
expect(record).toMatchObject({
133+
payload: {
134+
message: 'pages-api-node-error',
135+
request: {
136+
url: '/api/dynamic/123?apple=dope',
137+
},
138+
context: {
139+
routerKind: 'Pages Router',
140+
routeType: 'route',
141+
routePath: '/api/dynamic/[id]',
142+
},
143+
},
144+
})
145+
})
146+
})
147+
})
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
export function onRequestError(err, request, context) {
2+
fetch(`http://localhost:${process.env.PORT}/write-log`, {
3+
method: 'POST',
4+
body: JSON.stringify({
5+
message: err.message,
6+
request,
7+
context,
8+
}),
9+
headers: {
10+
'Content-Type': 'application/json',
11+
},
12+
})
13+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module.exports = {
2+
experimental: {
3+
instrumentationHook: true,
4+
},
5+
}

0 commit comments

Comments
 (0)