Skip to content

Commit 311fafe

Browse files
piehlukasholzer
andauthoredMar 20, 2024
feat: start moving to SugaredTracer and collect spans manually to be able to create 'server-timing' header when debugging (#358)
* chore: wrap blob.get with individual trace, add more attributes * feat: add server-timing header to response when x-nf-debug-logging is enabled * chore: remove tmp extra attributes * chore: remove dev logs * fix: lint * test: update request-context tests * chore: convert to using SugaredTracer methods * chore: fix lint, remove more unneded manual error setting on spans * fix: let's not await await * Apply suggestions from code review Co-authored-by: Lukas Holzer <lukas.holzer@typeflow.cc> * chore: format with prettier * fix: rename types --------- Co-authored-by: Lukas Holzer <lukas.holzer@typeflow.cc> Co-authored-by: pieh <pieh@users.noreply.github.com>
1 parent 037b695 commit 311fafe

11 files changed

+260
-170
lines changed
 

‎.eslintrc.cjs

+17
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,23 @@ module.exports = {
3333
},
3434
overrides: [
3535
...overrides,
36+
{
37+
files: ['src/run/**'],
38+
rules: {
39+
'no-restricted-imports': [
40+
'error',
41+
{
42+
paths: [
43+
{
44+
name: '@opentelemetry/api',
45+
importNames: ['trace'],
46+
message: 'Please use `getTracer()` from `./handlers/tracer.cjs` instead',
47+
},
48+
],
49+
},
50+
],
51+
},
52+
},
3653
{
3754
files: ['src/run/handlers/**'],
3855
rules: {

‎src/build/templates/handler-monorepo.tmpl.js

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import tracing, { trace } from '{{cwd}}/.netlify/dist/run/handlers/tracing.js'
1+
import {
2+
createRequestContext,
3+
runWithRequestContext,
4+
} from '{{cwd}}/.netlify/dist/run/handlers/request-context.cjs'
5+
import { getTracer } from '{{cwd}}/.netlify/dist/run/handlers/tracer.cjs'
6+
import tracing from '{{cwd}}/.netlify/dist/run/handlers/tracing.js'
27

38
process.chdir('{{cwd}}')
49

@@ -8,10 +13,11 @@ export default async function (req, context) {
813
tracing.start()
914
}
1015

11-
/** @type {import('@opentelemetry/api').Tracer} */
12-
const tracer = trace.getTracer('Next.js Runtime')
13-
return tracer.startActiveSpan('Next.js Server Handler', async (span) => {
14-
try {
16+
const requestContext = createRequestContext(req.headers.get('x-nf-debug-logging'))
17+
const tracer = getTracer()
18+
19+
const handlerResponse = await runWithRequestContext(requestContext, () => {
20+
return tracer.withActiveSpan('Next.js Server Handler', async (span) => {
1521
span.setAttributes({
1622
'account.id': context.account.id,
1723
'deploy.id': context.deploy.id,
@@ -31,16 +37,14 @@ export default async function (req, context) {
3137
'http.status_code': response.status,
3238
})
3339
return response
34-
} catch (error) {
35-
span.recordException(error)
36-
if (error instanceof Error) {
37-
span.addEvent({ name: error.name, message: error.message })
38-
}
39-
throw error
40-
} finally {
41-
span.end()
42-
}
40+
})
4341
})
42+
43+
if (requestContext.serverTiming) {
44+
handlerResponse.headers.set('server-timing', requestContext.serverTiming)
45+
}
46+
47+
return handlerResponse
4448
}
4549

4650
export const config = {

‎src/build/templates/handler.tmpl.js

+18-14
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,21 @@
1+
import {
2+
createRequestContext,
3+
runWithRequestContext,
4+
} from './.netlify/dist/run/handlers/request-context.cjs'
15
import serverHandler from './.netlify/dist/run/handlers/server.js'
2-
import tracing, { trace } from './.netlify/dist/run/handlers/tracing.js'
6+
import { getTracer } from './.netlify/dist/run/handlers/tracer.cjs'
7+
import tracing from './.netlify/dist/run/handlers/tracing.js'
38

49
export default async function handler(req, context) {
510
if (process.env.NETLIFY_OTLP_TRACE_EXPORTER_URL) {
611
tracing.start()
712
}
813

9-
/** @type {import('@opentelemetry/api').Tracer} */
10-
const tracer = trace.getTracer('Next.js Runtime')
11-
return tracer.startActiveSpan('Next.js Server Handler', async (span) => {
12-
try {
14+
const requestContext = createRequestContext(req.headers.get('x-nf-debug-logging'))
15+
const tracer = getTracer()
16+
17+
const handlerResponse = await runWithRequestContext(requestContext, () => {
18+
return tracer.withActiveSpan('Next.js Server Handler', async (span) => {
1319
span.setAttributes({
1420
'account.id': context.account.id,
1521
'deploy.id': context.deploy.id,
@@ -25,16 +31,14 @@ export default async function handler(req, context) {
2531
'http.status_code': response.status,
2632
})
2733
return response
28-
} catch (error) {
29-
span.recordException(error)
30-
if (error instanceof Error) {
31-
span.addEvent({ name: error.name, message: error.message })
32-
}
33-
throw error
34-
} finally {
35-
span.end()
36-
}
34+
})
3735
})
36+
37+
if (requestContext.serverTiming) {
38+
handlerResponse.headers.set('server-timing', requestContext.serverTiming)
39+
}
40+
41+
return handlerResponse
3842
}
3943

4044
export const config = {

‎src/run/handlers/cache.cts

+55-78
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { Buffer } from 'node:buffer'
55

66
import { getDeployStore, Store } from '@netlify/blobs'
77
import { purgeCache } from '@netlify/functions'
8-
import { trace, type Span, SpanStatusCode } from '@opentelemetry/api'
8+
import { type Span } from '@opentelemetry/api'
99
import { NEXT_CACHE_TAGS_HEADER } from 'next/dist/lib/constants.js'
1010
import type {
1111
CacheHandler,
@@ -15,6 +15,7 @@ import type {
1515
} from 'next/dist/server/lib/incremental-cache/index.js'
1616

1717
import { getRequestContext } from './request-context.cjs'
18+
import { getTracer } from './tracer.cjs'
1819

1920
type TagManifest = { revalidatedAt: number }
2021

@@ -26,7 +27,7 @@ export class NetlifyCacheHandler implements CacheHandler {
2627
options: CacheHandlerContext
2728
revalidatedTags: string[]
2829
blobStore: Store
29-
tracer = trace.getTracer('Netlify Cache Handler')
30+
tracer = getTracer()
3031
tagManifestsFetchedFromBlobStoreInCurrentRequest: TagManifestBlobCache
3132

3233
constructor(options: CacheHandlerContext) {
@@ -90,77 +91,67 @@ export class NetlifyCacheHandler implements CacheHandler {
9091
}
9192

9293
async get(...args: Parameters<CacheHandler['get']>): ReturnType<CacheHandler['get']> {
93-
return this.tracer.startActiveSpan('get cache key', async (span) => {
94-
try {
95-
const [key, ctx = {}] = args
96-
console.debug(`[NetlifyCacheHandler.get]: ${key}`)
97-
98-
const blobKey = await this.encodeBlobKey(key)
99-
span.setAttributes({ key, blobKey })
100-
const blob = (await this.blobStore.get(blobKey, {
101-
type: 'json',
102-
})) as CacheHandlerValue | null
94+
return this.tracer.withActiveSpan('get cache key', async (span) => {
95+
const [key, ctx = {}] = args
96+
console.debug(`[NetlifyCacheHandler.get]: ${key}`)
10397

104-
// if blob is null then we don't have a cache entry
105-
if (!blob) {
106-
span.addEvent('Cache miss', { key, blobKey })
107-
return null
108-
}
98+
const blobKey = await this.encodeBlobKey(key)
99+
span.setAttributes({ key, blobKey })
109100

110-
const staleByTags = await this.checkCacheEntryStaleByTags(blob, ctx.tags, ctx.softTags)
101+
const blob = (await this.tracer.withActiveSpan('blobStore.get', async (blobGetSpan) => {
102+
blobGetSpan.setAttributes({ key, blobKey })
103+
return await this.blobStore.get(blobKey, {
104+
type: 'json',
105+
})
106+
})) as CacheHandlerValue | null
111107

112-
if (staleByTags) {
113-
span.addEvent('Stale', { staleByTags })
114-
return null
115-
}
108+
// if blob is null then we don't have a cache entry
109+
if (!blob) {
110+
span.addEvent('Cache miss', { key, blobKey })
111+
return null
112+
}
116113

117-
this.captureResponseCacheLastModified(blob, key, span)
114+
const staleByTags = await this.checkCacheEntryStaleByTags(blob, ctx.tags, ctx.softTags)
118115

119-
switch (blob.value?.kind) {
120-
case 'FETCH':
121-
span.addEvent('FETCH', { lastModified: blob.lastModified, revalidate: ctx.revalidate })
122-
return {
123-
lastModified: blob.lastModified,
124-
value: blob.value,
125-
}
126-
127-
case 'ROUTE':
128-
span.addEvent('ROUTE', { lastModified: blob.lastModified, status: blob.value.status })
129-
return {
130-
lastModified: blob.lastModified,
131-
value: {
132-
...blob.value,
133-
body: Buffer.from(blob.value.body as unknown as string, 'base64'),
134-
},
135-
}
136-
case 'PAGE':
137-
span.addEvent('PAGE', { lastModified: blob.lastModified })
138-
return {
139-
lastModified: blob.lastModified,
140-
value: blob.value,
141-
}
142-
default:
143-
span.recordException(new Error(`Unknown cache entry kind: ${blob.value?.kind}`))
144-
// TODO: system level logging not implemented
145-
}
116+
if (staleByTags) {
117+
span.addEvent('Stale', { staleByTags })
146118
return null
147-
} catch (error) {
148-
if (error instanceof Error) {
149-
span.recordException(error)
150-
}
151-
span.setStatus({
152-
code: SpanStatusCode.ERROR,
153-
message: error instanceof Error ? error.message : String(error),
154-
})
155-
throw error
156-
} finally {
157-
span.end()
158119
}
120+
121+
this.captureResponseCacheLastModified(blob, key, span)
122+
123+
switch (blob.value?.kind) {
124+
case 'FETCH':
125+
span.addEvent('FETCH', { lastModified: blob.lastModified, revalidate: ctx.revalidate })
126+
return {
127+
lastModified: blob.lastModified,
128+
value: blob.value,
129+
}
130+
131+
case 'ROUTE':
132+
span.addEvent('ROUTE', { lastModified: blob.lastModified, status: blob.value.status })
133+
return {
134+
lastModified: blob.lastModified,
135+
value: {
136+
...blob.value,
137+
body: Buffer.from(blob.value.body as unknown as string, 'base64'),
138+
},
139+
}
140+
case 'PAGE':
141+
span.addEvent('PAGE', { lastModified: blob.lastModified })
142+
return {
143+
lastModified: blob.lastModified,
144+
value: blob.value,
145+
}
146+
default:
147+
span.recordException(new Error(`Unknown cache entry kind: ${blob.value?.kind}`))
148+
}
149+
return null
159150
})
160151
}
161152

162153
async set(...args: Parameters<IncrementalCache['set']>) {
163-
return this.tracer.startActiveSpan('set cache key', async (span) => {
154+
return this.tracer.withActiveSpan('set cache key', async (span) => {
164155
const [key, data] = args
165156
const blobKey = await this.encodeBlobKey(key)
166157
const lastModified = Date.now()
@@ -189,7 +180,6 @@ export class NetlifyCacheHandler implements CacheHandler {
189180
})
190181
}
191182
}
192-
span.end()
193183
})
194184
}
195185

@@ -264,22 +254,9 @@ export class NetlifyCacheHandler implements CacheHandler {
264254

265255
if (!tagManifestPromise) {
266256
tagManifestPromise = this.encodeBlobKey(tag).then((blobKey) => {
267-
return this.tracer.startActiveSpan(`get tag manifest`, async (span) => {
257+
return this.tracer.withActiveSpan(`get tag manifest`, async (span) => {
268258
span.setAttributes({ tag, blobKey })
269-
try {
270-
return await this.blobStore.get(blobKey, { type: 'json' })
271-
} catch (error) {
272-
if (error instanceof Error) {
273-
span.recordException(error)
274-
}
275-
span.setStatus({
276-
code: SpanStatusCode.ERROR,
277-
message: error instanceof Error ? error.message : String(error),
278-
})
279-
throw error
280-
} finally {
281-
span.end()
282-
}
259+
return this.blobStore.get(blobKey, { type: 'json' })
283260
})
284261
})
285262

‎src/run/handlers/request-context.cts

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
import { AsyncLocalStorage } from 'node:async_hooks'
22

33
export type RequestContext = {
4+
debug: boolean
45
responseCacheGetLastModified?: number
56
responseCacheKey?: string
67
usedFsRead?: boolean
78
didPagesRouterOnDemandRevalidate?: boolean
9+
serverTiming?: string
810
}
911

1012
type RequestContextAsyncLocalStorage = AsyncLocalStorage<RequestContext>
1113

12-
export function createRequestContext(): RequestContext {
13-
return {}
14+
export function createRequestContext(debug = false): RequestContext {
15+
return {
16+
debug,
17+
}
1418
}
1519

1620
const REQUEST_CONTEXT_GLOBAL_KEY = Symbol.for('nf-request-context-async-local-storage')

‎src/run/handlers/server.ts

+15-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { OutgoingHttpHeaders } from 'http'
22

33
import { toComputeResponse, toReqRes, ComputeJsOutgoingMessage } from '@fastly/http-compute-js'
4-
import { SpanStatusCode, trace } from '@opentelemetry/api'
54
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
65
import type { WorkerRequestHandler } from 'next/dist/server/lib/types.js'
76

@@ -16,7 +15,8 @@ import {
1615
import { nextResponseProxy } from '../revalidate.js'
1716
import { logger } from '../systemlog.js'
1817

19-
import { createRequestContext, runWithRequestContext } from './request-context.cjs'
18+
import { createRequestContext, getRequestContext } from './request-context.cjs'
19+
import { getTracer } from './tracer.cjs'
2020

2121
let nextHandler: WorkerRequestHandler, nextConfig: NextConfigComplete, tagsManifest: TagsManifest
2222

@@ -44,10 +44,10 @@ const disableFaultyTransferEncodingHandling = (res: ComputeJsOutgoingMessage) =>
4444
}
4545

4646
export default async (request: Request) => {
47-
const tracer = trace.getTracer('Next.js Runtime')
47+
const tracer = getTracer()
4848

4949
if (!nextHandler) {
50-
await tracer.startActiveSpan('initialize next server', async (span) => {
50+
await tracer.withActiveSpan('initialize next server', async (span) => {
5151
// set the server config
5252
const { getRunConfig, setRunConfig } = await import('../config.js')
5353
nextConfig = await getRunConfig()
@@ -72,35 +72,26 @@ export default async (request: Request) => {
7272
dir: process.cwd(),
7373
isDev: false,
7474
})
75-
span.end()
7675
})
7776
}
7877

79-
return await tracer.startActiveSpan('generate response', async (span) => {
78+
return await tracer.withActiveSpan('generate response', async (span) => {
8079
const { req, res } = toReqRes(request)
8180

82-
const requestContext = createRequestContext()
83-
8481
disableFaultyTransferEncodingHandling(res as unknown as ComputeJsOutgoingMessage)
8582

83+
const requestContext = getRequestContext() ?? createRequestContext()
84+
8685
const resProxy = nextResponseProxy(res, requestContext)
8786

8887
// We don't await this here, because it won't resolve until the response is finished.
89-
const nextHandlerPromise = runWithRequestContext(requestContext, () =>
90-
nextHandler(req, resProxy).catch((error) => {
91-
logger.withError(error).error('next handler error')
92-
console.error(error)
93-
resProxy.statusCode = 500
94-
span.recordException(error)
95-
span.setAttribute('http.status_code', 500)
96-
span.setStatus({
97-
code: SpanStatusCode.ERROR,
98-
message: error instanceof Error ? error.message : String(error),
99-
})
100-
span.end()
101-
resProxy.end('Internal Server Error')
102-
}),
103-
)
88+
const nextHandlerPromise = nextHandler(req, resProxy).catch((error) => {
89+
logger.withError(error).error('next handler error')
90+
console.error(error)
91+
resProxy.statusCode = 500
92+
span.setAttribute('http.status_code', 500)
93+
resProxy.end('Internal Server Error')
94+
})
10495

10596
// Contrary to the docs, this resolves when the headers are available, not when the stream closes.
10697
// See https://github.com/fastly/http-compute-js/blob/main/src/http-compute-js/http-server.ts#L168-L173
@@ -125,17 +116,14 @@ export default async (request: Request) => {
125116
// TODO: Remove once a fix has been rolled out.
126117
if ((response.status > 300 && response.status < 400) || response.status >= 500) {
127118
const body = await response.text()
128-
span.end()
129119
return new Response(body || null, response)
130120
}
131121

132122
const keepOpenUntilNextFullyRendered = new TransformStream({
133123
flush() {
134124
// it's important to keep the stream open until the next handler has finished,
135125
// or otherwise the cache revalidates might not go through
136-
return nextHandlerPromise.then(() => {
137-
span.end()
138-
})
126+
return nextHandlerPromise
139127
},
140128
})
141129

‎src/run/handlers/tracer.cts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
// Here we need to actually import `trace` from @opentelemetry/api to add extra wrappers
2+
// other places should import `getTracer` from this module
3+
// eslint-disable-next-line no-restricted-imports
4+
import { trace, Tracer, Span } from '@opentelemetry/api'
5+
import { SugaredTracer, wrapTracer } from '@opentelemetry/api/experimental'
6+
7+
import { RequestContext, getRequestContext } from './request-context.cjs'
8+
9+
const spanMeta = new WeakMap<Span, { start: number; name: string }>()
10+
const spanCounter = new WeakMap<RequestContext, number>()
11+
12+
function spanHook(span: Span): Span {
13+
const originalEnd = span.end.bind(span)
14+
15+
span.end = (endTime) => {
16+
originalEnd(endTime)
17+
18+
const meta = spanMeta.get(span)
19+
if (meta) {
20+
const requestContext = getRequestContext()
21+
if (requestContext?.debug) {
22+
const duration = (typeof endTime === 'number' ? endTime : performance.now()) - meta.start
23+
24+
const serverTiming = requestContext.serverTiming ?? ''
25+
const currentRequestSpanCounter = spanCounter.get(requestContext) ?? 1
26+
27+
requestContext.serverTiming = `${serverTiming}${serverTiming.length === 0 ? '' : ', '}s${currentRequestSpanCounter};dur=${duration};desc="${meta.name}"`
28+
29+
spanCounter.set(requestContext, currentRequestSpanCounter + 1)
30+
}
31+
}
32+
33+
spanMeta.delete(span)
34+
}
35+
36+
return span
37+
}
38+
39+
// startSpan and startActiveSpan don't automatically handle span ending and error handling
40+
// so this typing just tries to enforce not using those methods in our code
41+
// we should be using withActiveSpan (and optionally withSpan) instead
42+
export type RuntimeTracer = Omit<SugaredTracer, 'startSpan' | 'startActiveSpan'>
43+
44+
let tracer: RuntimeTracer | undefined
45+
46+
export function getTracer(): RuntimeTracer {
47+
if (!tracer) {
48+
const baseTracer = trace.getTracer('Next.js Runtime')
49+
50+
// we add hooks to capture span start and end events to be able to add server-timings
51+
// while preserving OTEL api
52+
const startSpan = baseTracer.startSpan.bind(baseTracer)
53+
baseTracer.startSpan = (
54+
...args: Parameters<Tracer['startSpan']>
55+
): ReturnType<Tracer['startSpan']> => {
56+
const span = startSpan(...args)
57+
spanMeta.set(span, { start: performance.now(), name: args[0] })
58+
return spanHook(span)
59+
}
60+
61+
const startActiveSpan = baseTracer.startActiveSpan.bind(baseTracer)
62+
63+
// @ts-expect-error Target signature provides too few arguments. Expected 4 or more, but got 2.
64+
baseTracer.startActiveSpan = (
65+
...args: Parameters<Tracer['startActiveSpan']>
66+
): ReturnType<Tracer['startActiveSpan']> => {
67+
const [name, ...restOfArgs] = args
68+
69+
const augmentedArgs = restOfArgs.map((arg) => {
70+
// callback might be 2nd, 3rd or 4th argument depending on used signature
71+
// only callback can be a function so target that and keep rest arguments as-is
72+
if (typeof arg === 'function') {
73+
return (span: Span) => {
74+
spanMeta.set(span, { start: performance.now(), name: args[0] })
75+
spanHook(span)
76+
return arg(span)
77+
}
78+
}
79+
80+
return arg
81+
}) as typeof restOfArgs
82+
83+
return startActiveSpan(name, ...augmentedArgs)
84+
}
85+
86+
// finally use SugaredTracer
87+
tracer = wrapTracer(baseTracer)
88+
}
89+
90+
return tracer
91+
}

‎src/run/handlers/tracing.ts

-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ const sdk = new NodeSDK({
2323
),
2424
})
2525
export default sdk
26-
export { trace } from '@opentelemetry/api'
2726

2827
// gracefully shut down the SDK on process exit
2928
process.on('SIGTERM', () => {

‎src/run/headers.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { getDeployStore } from '@netlify/blobs'
2-
import type { Span, Tracer } from '@opentelemetry/api'
2+
import type { Span } from '@opentelemetry/api'
33
import type { NextConfigComplete } from 'next/dist/server/config-shared.js'
44

55
import { encodeBlobKey } from '../shared/blobkey.js'
66

77
import type { TagsManifest } from './config.js'
88
import type { RequestContext } from './handlers/request-context.cjs'
9+
import type { RuntimeTracer } from './handlers/tracer.cjs'
910

1011
interface NetlifyVaryValues {
1112
headers: string[]
@@ -95,7 +96,7 @@ export const adjustDateHeader = async ({
9596
headers: Headers
9697
request: Request
9798
span: Span
98-
tracer: Tracer
99+
tracer: RuntimeTracer
99100
requestContext: RequestContext
100101
}) => {
101102
const cacheState = headers.get('x-nextjs-cache')
@@ -132,7 +133,7 @@ export const adjustDateHeader = async ({
132133
const blobStore = getDeployStore({ fetch: fetchBeforeNextPatchedIt, consistency: 'strong' })
133134

134135
// TODO: use metadata for this
135-
lastModified = await tracer.startActiveSpan(
136+
lastModified = await tracer.withActiveSpan(
136137
'get cache to calculate date header',
137138
async (getBlobForDateSpan) => {
138139
getBlobForDateSpan.setAttributes({
@@ -142,7 +143,6 @@ export const adjustDateHeader = async ({
142143
const blob = (await blobStore.get(blobKey, { type: 'json' })) ?? {}
143144

144145
getBlobForDateSpan.addEvent(blob ? 'Cache hit' : 'Cache miss')
145-
getBlobForDateSpan.end()
146146
return blob.lastModified
147147
},
148148
)

‎src/run/next.cts

+12-10
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,22 @@ import fs from 'fs/promises'
22
import { relative, resolve } from 'path'
33

44
import { getDeployStore } from '@netlify/blobs'
5-
import { trace } from '@opentelemetry/api'
65
// @ts-expect-error no types installed
76
import { patchFs } from 'fs-monkey'
8-
import type { getRequestHandlers } from 'next/dist/server/lib/start-server.js'
7+
import type { getRequestHandlers as GetRequestHandlersSignature } from 'next/dist/server/lib/start-server.js'
98

109
import { getRequestContext } from './handlers/request-context.cjs'
10+
import { getTracer } from './handlers/tracer.cjs'
1111

1212
type FS = typeof import('fs')
1313

1414
const fetchBeforeNextPatchedIt = globalThis.fetch
1515

16-
export async function getMockedRequestHandlers(...args: Parameters<typeof getRequestHandlers>) {
17-
const tracer = trace.getTracer('Next.js Runtime')
18-
return tracer.startActiveSpan('mocked request handler', async (span) => {
16+
export async function getMockedRequestHandlers(
17+
...args: Parameters<typeof GetRequestHandlersSignature>
18+
) {
19+
const tracer = getTracer()
20+
return tracer.withActiveSpan('mocked request handler', async () => {
1921
const ofs = { ...fs }
2022

2123
const { encodeBlobKey } = await import('../shared/blobkey.js')
@@ -55,11 +57,11 @@ export async function getMockedRequestHandlers(...args: Parameters<typeof getReq
5557
require('fs').promises,
5658
)
5759

58-
const importSpan = tracer.startSpan('import next server')
59-
// eslint-disable-next-line no-shadow
60-
const { getRequestHandlers } = await import('next/dist/server/lib/start-server.js')
61-
importSpan.end()
62-
span.end()
60+
const { getRequestHandlers } = await tracer.withActiveSpan(
61+
'import next server',
62+
async () => import('next/dist/server/lib/start-server.js'),
63+
)
64+
6365
return getRequestHandlers(...args)
6466
})
6567
}

‎tests/integration/request-context.test.ts

+24-20
Original file line numberDiff line numberDiff line change
@@ -132,22 +132,24 @@ describe('request-context does NOT leak between concurrent requests', () => {
132132
})
133133

134134
// fastCall finished completely so it should have acquired request context
135-
expect(getRequestContextSpy).toHaveBeenCalledTimes(1)
136-
expect(getRequestContextSpy).toHaveNthReturnedWith(1, {
137-
responseCacheGetLastModified: new Date(mockedDateForRevalidateAutomatic).getTime(),
138-
responseCacheKey: '/static/revalidate-automatic',
139-
})
135+
expect(getRequestContextSpy).toHaveLastReturnedWith(
136+
expect.objectContaining({
137+
responseCacheGetLastModified: new Date(mockedDateForRevalidateAutomatic).getTime(),
138+
responseCacheKey: '/static/revalidate-automatic',
139+
}),
140+
)
140141

141142
// second request finished - now we can unpause the first one
142143
unpauseSlowCall()
143144

144145
const slowCall = await slowCallPromise
145146
// slowCall finished completely so it should have acquired request context
146-
expect(getRequestContextSpy).toHaveBeenCalledTimes(2)
147-
expect(getRequestContextSpy).toHaveNthReturnedWith(2, {
148-
responseCacheGetLastModified: new Date(mockedDateForRevalidateSlow).getTime(),
149-
responseCacheKey: '/static/revalidate-slow',
150-
})
147+
expect(getRequestContextSpy).toHaveLastReturnedWith(
148+
expect.objectContaining({
149+
responseCacheGetLastModified: new Date(mockedDateForRevalidateSlow).getTime(),
150+
responseCacheKey: '/static/revalidate-slow',
151+
}),
152+
)
151153

152154
expect(slowCall.headers['date']).toBe(mockedDateForRevalidateSlow)
153155
expect(fastCall.headers['date']).toBe(mockedDateForRevalidateAutomatic)
@@ -205,22 +207,24 @@ describe('request-context does NOT leak between concurrent requests', () => {
205207
})
206208

207209
// fastCall finished completely so it should have acquired request context
208-
expect(getRequestContextSpy).toHaveBeenCalledTimes(1)
209-
expect(getRequestContextSpy).toHaveNthReturnedWith(1, {
210-
responseCacheGetLastModified: new Date(mockedDateForStaticFetch1).getTime(),
211-
responseCacheKey: '/static-fetch/1',
212-
})
210+
expect(getRequestContextSpy).toHaveLastReturnedWith(
211+
expect.objectContaining({
212+
responseCacheGetLastModified: new Date(mockedDateForStaticFetch1).getTime(),
213+
responseCacheKey: '/static-fetch/1',
214+
}),
215+
)
213216

214217
// second request finished - now we can unpause the first one
215218
unpauseSlowCall()
216219

217220
const slowCall = await slowCallPromise
218221
// slowCall finished completely so it should have acquired request context
219-
expect(getRequestContextSpy).toHaveBeenCalledTimes(2)
220-
expect(getRequestContextSpy).toHaveNthReturnedWith(2, {
221-
responseCacheGetLastModified: new Date(mockedDateForStaticFetch2).getTime(),
222-
responseCacheKey: '/static-fetch/2',
223-
})
222+
expect(getRequestContextSpy).toHaveLastReturnedWith(
223+
expect.objectContaining({
224+
responseCacheGetLastModified: new Date(mockedDateForStaticFetch2).getTime(),
225+
responseCacheKey: '/static-fetch/2',
226+
}),
227+
)
224228

225229
expect(slowCall.headers['date']).toBe(mockedDateForStaticFetch2)
226230
expect(fastCall.headers['date']).toBe(mockedDateForStaticFetch1)

0 commit comments

Comments
 (0)
Please sign in to comment.