next.js/test/development/app-dir/enabled-features-trace/enabled-features-trace.test.ts
enabled-features-trace.test.ts147 lines4.3 KB
import { nextTestSetup } from 'e2e-utils'
import { join } from 'path'
import { existsSync, readFileSync } from 'fs'
import { createServer } from 'http'
import { spawn } from 'child_process'
import { parseTraceFile } from '../../../lib/parse-trace-file'

describe('enabled features in trace', () => {
  const { next, isNextDev } = nextTestSetup({
    files: __dirname,
    startArgs: ['--no-server-fast-refresh'],
  })

  if (!isNextDev) {
    it('should be skipped in production', () => {})
    return
  }

  it('should record enabled features on root span', async () => {
    const tracePath = join(next.testDir, '.next/dev/trace')

    // Trigger page request to generate traces
    if (!existsSync(tracePath)) {
      const $ = await next.render$('/')
      expect($('p').text()).toBe('hello world')
      await next.stop('SIGTERM')
      await new Promise((resolve) => setTimeout(resolve, 500))
    }

    const traceStructure = parseTraceFile(tracePath)

    // Verify start-dev-server span has feature tags
    const startDevServerEvents =
      traceStructure.eventsByName.get('start-dev-server')
    expect(startDevServerEvents).toBeDefined()
    expect(startDevServerEvents!.length).toBeGreaterThan(0)

    const startDevServerEvent = startDevServerEvents![0]
    expect(startDevServerEvent.tags).toBeDefined()
    expect(startDevServerEvent.tags!['feature.serverFastRefreshDisabled']).toBe(
      true
    )
  })

  it('should denormalize inherited enabled features during upload', async () => {
    const tracePath = join(next.testDir, '.next/dev/trace')

    if (!existsSync(tracePath)) {
      const $ = await next.render$('/')
      expect($('p').text()).toBe('hello world')
      await next.stop('SIGTERM')
      await new Promise((resolve) => setTimeout(resolve, 500))
    }

    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

    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(),
  }
}
Quest for Codev2.0.0
/
SIGN IN