mirror of
https://github.com/vercel/next.js.git
synced 2026-09-20 02:25:18 +08:00
62084e3fbd
<img width="428" height="179" alt="Screenshot 2026-07-24 at 22 42 04" src="https://github.com/user-attachments/assets/38478940-d23e-4faa-bd88-5758190be158" /> [Flakiness metrics](https://app.datadoghq.com/ci/test/runs?query=test_level%3Atest%20%40git.repository.id%3A%22github.com%2Fvercel%2Fnext.js%22%20%40test.name%3A%22enabled%20features%20in%20trace%20should%20denormalize%20inherited%20enabled%20features%20during%20upload%22%20%40test.type%3A%22nextjs%22%20%40test.status%3A%28%22fail%22%20OR%20pass%29&agg_m=count&agg_m_source=base&agg_t=count&fromUser=true&index=citest&start=1784320894753&end=1784925694753&paused=false) The `render-path` span is recorded when a request's response closes, which is too late for any flush other than the one the dev server performs while shutting down. The parent `next dev` process escalates to SIGKILL 100ms after signalling the child, and on a machine running eight test files at once the child does not reliably get scheduled to run its cleanup within that window, so the span never reached the trace file and the upload assertions failed. This change raises the budget for the test through `NEXT_EXIT_TIMEOUT_MS`, which was added alongside that timeout in #67165 so that it can be increased when the child's exit work matters more than a fast exit. The same approach is already used in `test/e2e/filesystem-cache/warm-restart-task-stats.test.ts`, where the timeout would otherwise cut off a Rust `on_exit` handler before it writes its task statistics. Both test cases previously guarded their request with a check for the existence of the trace file, which the dev server creates on its own once the first compile finishes. When that happened before the first test body ran, neither case issued a request and the trace file contained no `compile-path` or `render-path` span at all. The request and the shutdown now happen once in `beforeAll`, and the fixed 500ms sleep that followed the shutdown is replaced by a `retry` that waits for the spans the assertions depend on. --- <sub>Stack created with <a href="https://github.com/github/gh-stack">GitHub Stacks CLI</a> • <a href="https://gh.io/stacks-feedback">Give Feedback 💬</a></sub>
165 lines
5.2 KiB
TypeScript
165 lines
5.2 KiB
TypeScript
import { nextTestSetup } from 'e2e-utils'
|
|
import { join } from 'path'
|
|
import { readFileSync } from 'fs'
|
|
import { createServer } from 'http'
|
|
import { spawn } from 'child_process'
|
|
import { retry } from 'next-test-utils'
|
|
import { parseTraceFile } from '../../../lib/parse-trace-file'
|
|
|
|
describe('enabled features in trace', () => {
|
|
const { next, isNextDev } = nextTestSetup({
|
|
files: __dirname,
|
|
startArgs: ['--no-server-fast-refresh'],
|
|
env: {
|
|
// Trace events are buffered in memory, and `render-path` is recorded when
|
|
// the response closes, too late for any flush other than the one the dev
|
|
// server performs while shutting down. The parent `next dev` process
|
|
// escalates to SIGKILL 100ms after signalling the child, and on a busy
|
|
// machine the child does not reliably get scheduled to run its cleanup
|
|
// within that window, so the span never reaches the trace file.
|
|
// `NEXT_EXIT_TIMEOUT_MS` raises that budget for cases like this one,
|
|
// where the flushed output matters more than how quickly the server
|
|
// exits.
|
|
NEXT_EXIT_TIMEOUT_MS: '30000',
|
|
},
|
|
})
|
|
|
|
if (!isNextDev) {
|
|
it('should be skipped in production', () => {})
|
|
return
|
|
}
|
|
|
|
let tracePath: string
|
|
|
|
beforeAll(async () => {
|
|
tracePath = join(next.testDir, '.next/dev/trace')
|
|
|
|
// Request a page so that the spans under test get recorded. The dev server
|
|
// creates the trace file on its own as soon as the first compile finishes,
|
|
// so its existence says nothing about whether this request has happened.
|
|
const $ = await next.render$('/')
|
|
const pageText = $('p').text()
|
|
if (pageText !== 'hello world') {
|
|
throw new Error(`Unexpected content rendered for "/": ${pageText}`)
|
|
}
|
|
|
|
// Shutting the server down flushes the buffered events to the trace file.
|
|
await next.stop('SIGTERM')
|
|
|
|
await retry(async () => {
|
|
const { eventsByName } = parseTraceFile(tracePath)
|
|
for (const name of ['start-dev-server', 'compile-path', 'render-path']) {
|
|
if (!eventsByName.has(name)) {
|
|
throw new Error(`The trace file has no "${name}" span`)
|
|
}
|
|
}
|
|
}, 5000)
|
|
})
|
|
|
|
it('should record enabled features on root span', async () => {
|
|
const { eventsByName } = parseTraceFile(tracePath)
|
|
|
|
// Verify start-dev-server span has feature tags
|
|
const [startDevServerEvent] = eventsByName.get('start-dev-server') ?? []
|
|
expect(startDevServerEvent).toBeDefined()
|
|
expect(startDevServerEvent?.tags).toBeDefined()
|
|
expect(
|
|
startDevServerEvent?.tags?.['feature.serverFastRefreshDisabled']
|
|
).toBe(true)
|
|
})
|
|
|
|
it('should denormalize inherited enabled features during upload', async () => {
|
|
const fakeServer = await createTestTraceUploadServer()
|
|
|
|
// Get trace ID from the trace file
|
|
const traceContent = readFileSync(tracePath, 'utf8')
|
|
const firstLine = traceContent.trim().split('\n')[0]
|
|
const firstEvents = JSON.parse(firstLine)
|
|
const traceId = firstEvents[0]?.traceId
|
|
expect(traceId).toBeDefined()
|
|
|
|
const uploaderPath = join(
|
|
__dirname,
|
|
'../../../../packages/next/dist/trace/trace-uploader.js'
|
|
)
|
|
const uploaderProcess = spawn('node', [
|
|
uploaderPath,
|
|
fakeServer.url,
|
|
'dev',
|
|
next.testDir,
|
|
'.next/dev',
|
|
'true',
|
|
traceId,
|
|
'test-anonymous-id',
|
|
'test-session-id',
|
|
])
|
|
|
|
await new Promise<void>((resolve, reject) => {
|
|
uploaderProcess.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve()
|
|
} else {
|
|
reject(new Error(`Uploader exited with code ${code}`))
|
|
}
|
|
})
|
|
uploaderProcess.on('error', reject)
|
|
})
|
|
|
|
const uploadedData = fakeServer.getUploadedData()
|
|
fakeServer.close()
|
|
|
|
// Verify uploaded data has inherited feature tags
|
|
expect(uploadedData).toBeDefined()
|
|
expect(uploadedData.traces).toHaveLength(1)
|
|
const traces = uploadedData.traces[0]
|
|
|
|
// Find compile-path and render-path events
|
|
const compilePathEvent = traces.find((e: any) => e.name === 'compile-path')
|
|
const renderPathEvent = traces.find((e: any) => e.name === 'render-path')
|
|
|
|
// Both should have inherited feature.serverFastRefreshDisabled from their parent
|
|
expect(compilePathEvent).toBeDefined()
|
|
expect(compilePathEvent.tags['feature.serverFastRefreshDisabled']).toBe(
|
|
true
|
|
)
|
|
|
|
expect(renderPathEvent).toBeDefined()
|
|
expect(renderPathEvent.tags['feature.serverFastRefreshDisabled']).toBe(true)
|
|
})
|
|
})
|
|
|
|
async function createTestTraceUploadServer(): Promise<{
|
|
url: string
|
|
getUploadedData: () => any
|
|
close: () => void
|
|
}> {
|
|
let uploadedData: any = null
|
|
|
|
const server = createServer((req, res) => {
|
|
let body = ''
|
|
req.on('data', (chunk) => {
|
|
body += chunk.toString()
|
|
})
|
|
req.on('end', () => {
|
|
uploadedData = JSON.parse(body)
|
|
res.writeHead(200, { 'Content-Type': 'application/json' })
|
|
res.end(JSON.stringify({ success: true }))
|
|
})
|
|
})
|
|
|
|
await new Promise<void>((resolve) => {
|
|
server.listen(0, () => resolve())
|
|
})
|
|
|
|
const address = server.address()
|
|
if (!address || typeof address === 'string') {
|
|
throw new Error('Server address is not available')
|
|
}
|
|
|
|
return {
|
|
url: `http://localhost:${address.port}`,
|
|
getUploadedData: () => uploadedData,
|
|
close: () => server.close(),
|
|
}
|
|
}
|