Skip to content

Commit 93036f7

Browse files
authoredMar 26, 2024
feat: add build time o11ty to the runtime (#372)
* feat: instrument the build time aspect of the new runtime as well * chore: improve traces * chore: PR feedback * chore: fix linting * chore: remove recursive span as it creates a lot of spans
1 parent e176f9c commit 93036f7

File tree

7 files changed

+2640
-533
lines changed

7 files changed

+2640
-533
lines changed
 

‎package-lock.json

+2,289-233
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
"@netlify/functions": "^2.5.1",
5353
"@netlify/serverless-functions-api": "^1.10.1",
5454
"@netlify/zip-it-and-ship-it": "^9.30.0",
55-
"@opentelemetry/api": "^1.7.0",
55+
"@opentelemetry/api": "^1.8.0",
5656
"@opentelemetry/exporter-trace-otlp-http": "^0.49.0",
5757
"@opentelemetry/resources": "^1.21.0",
5858
"@opentelemetry/sdk-node": "^0.49.0",

‎src/build/content/prerendered.ts

+59-53
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { existsSync } from 'node:fs'
22
import { mkdir, readFile, writeFile } from 'node:fs/promises'
33
import { join } from 'node:path'
44

5+
import { trace } from '@opentelemetry/api'
6+
import { wrapTracer } from '@opentelemetry/api/experimental'
57
import { glob } from 'fast-glob'
68
import type { CacheHandlerValue } from 'next/dist/server/lib/incremental-cache/index.js'
79
import type { IncrementalCacheValue } from 'next/dist/server/response-cache/types.js'
@@ -14,6 +16,8 @@ type CachedPageValue = Extract<IncrementalCacheValue, { kind: 'PAGE' }>
1416
type CachedRouteValue = Extract<IncrementalCacheValue, { kind: 'ROUTE' }>
1517
type CachedFetchValue = Extract<IncrementalCacheValue, { kind: 'FETCH' }>
1618

19+
const tracer = wrapTracer(trace.getTracer('Next runtime'))
20+
1721
/**
1822
* Write a cache entry to the blob upload directory.
1923
*/
@@ -79,61 +83,63 @@ const buildFetchCacheValue = async (path: string): Promise<CachedFetchValue> =>
7983
* Upload prerendered content to the blob store
8084
*/
8185
export const copyPrerenderedContent = async (ctx: PluginContext): Promise<void> => {
82-
try {
83-
// ensure the blob directory exists
84-
await mkdir(ctx.blobDir, { recursive: true })
85-
// read prerendered content and build JSON key/values for the blob store
86-
const manifest = await ctx.getPrerenderManifest()
87-
88-
const limitConcurrentPrerenderContentHandling = pLimit(10)
89-
90-
await Promise.all(
91-
Object.entries(manifest.routes).map(
92-
([route, meta]): Promise<void> =>
93-
limitConcurrentPrerenderContentHandling(async () => {
94-
const lastModified = meta.initialRevalidateSeconds
95-
? Date.now() - 31536000000
96-
: Date.now()
97-
const key = routeToFilePath(route)
98-
let value: IncrementalCacheValue
99-
switch (true) {
100-
// Parallel route default layout has no prerendered page
101-
case meta.dataRoute?.endsWith('/default.rsc') &&
102-
!existsSync(join(ctx.publishDir, 'server/app', `${key}.html`)):
103-
return
104-
case meta.dataRoute?.endsWith('.json'):
105-
if (manifest.notFoundRoutes.includes(route)) {
106-
// if pages router returns 'notFound: true', build won't produce html and json files
86+
return tracer.withActiveSpan('copyPrerenderedContent', async () => {
87+
try {
88+
// ensure the blob directory exists
89+
await mkdir(ctx.blobDir, { recursive: true })
90+
// read prerendered content and build JSON key/values for the blob store
91+
const manifest = await ctx.getPrerenderManifest()
92+
93+
const limitConcurrentPrerenderContentHandling = pLimit(10)
94+
95+
await Promise.all(
96+
Object.entries(manifest.routes).map(
97+
([route, meta]): Promise<void> =>
98+
limitConcurrentPrerenderContentHandling(async () => {
99+
const lastModified = meta.initialRevalidateSeconds
100+
? Date.now() - 31536000000
101+
: Date.now()
102+
const key = routeToFilePath(route)
103+
let value: IncrementalCacheValue
104+
switch (true) {
105+
// Parallel route default layout has no prerendered page
106+
case meta.dataRoute?.endsWith('/default.rsc') &&
107+
!existsSync(join(ctx.publishDir, 'server/app', `${key}.html`)):
107108
return
108-
}
109-
value = await buildPagesCacheValue(join(ctx.publishDir, 'server/pages', key))
110-
break
111-
case meta.dataRoute?.endsWith('.rsc'):
112-
value = await buildAppCacheValue(join(ctx.publishDir, 'server/app', key))
113-
break
114-
case meta.dataRoute === null:
115-
value = await buildRouteCacheValue(join(ctx.publishDir, 'server/app', key))
116-
break
117-
default:
118-
throw new Error(`Unrecognized content: ${route}`)
119-
}
120-
121-
await writeCacheEntry(key, value, lastModified, ctx)
122-
}),
123-
),
124-
)
125-
126-
// app router 404 pages are not in the prerender manifest
127-
// so we need to check for them manually
128-
if (existsSync(join(ctx.publishDir, `server/app/_not-found.html`))) {
129-
const lastModified = Date.now()
130-
const key = '/404'
131-
const value = await buildAppCacheValue(join(ctx.publishDir, 'server/app/_not-found'))
132-
await writeCacheEntry(key, value, lastModified, ctx)
109+
case meta.dataRoute?.endsWith('.json'):
110+
if (manifest.notFoundRoutes.includes(route)) {
111+
// if pages router returns 'notFound: true', build won't produce html and json files
112+
return
113+
}
114+
value = await buildPagesCacheValue(join(ctx.publishDir, 'server/pages', key))
115+
break
116+
case meta.dataRoute?.endsWith('.rsc'):
117+
value = await buildAppCacheValue(join(ctx.publishDir, 'server/app', key))
118+
break
119+
case meta.dataRoute === null:
120+
value = await buildRouteCacheValue(join(ctx.publishDir, 'server/app', key))
121+
break
122+
default:
123+
throw new Error(`Unrecognized content: ${route}`)
124+
}
125+
126+
await writeCacheEntry(key, value, lastModified, ctx)
127+
}),
128+
),
129+
)
130+
131+
// app router 404 pages are not in the prerender manifest
132+
// so we need to check for them manually
133+
if (existsSync(join(ctx.publishDir, `server/app/_not-found.html`))) {
134+
const lastModified = Date.now()
135+
const key = '/404'
136+
const value = await buildAppCacheValue(join(ctx.publishDir, 'server/app/_not-found'))
137+
await writeCacheEntry(key, value, lastModified, ctx)
138+
}
139+
} catch (error) {
140+
ctx.failBuild('Failed assembling prerendered content for upload', error)
133141
}
134-
} catch (error) {
135-
ctx.failBuild('Failed assembling prerendered content for upload', error)
136-
}
142+
})
137143
}
138144

139145
/**

‎src/build/content/server.ts

+135-127
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,17 @@ import { createRequire } from 'node:module'
1414
import { dirname, join, resolve, sep } from 'node:path'
1515
import { sep as posixSep, join as posixJoin } from 'node:path/posix'
1616

17+
import { trace } from '@opentelemetry/api'
18+
import { wrapTracer } from '@opentelemetry/api/experimental'
1719
import glob from 'fast-glob'
1820
import { prerelease, lt as semverLowerThan, lte as semverLowerThanOrEqual } from 'semver'
1921

2022
import { RUN_CONFIG } from '../../run/constants.js'
2123
import { PluginContext } from '../plugin-context.js'
2224
import { verifyNextVersion } from '../verification.js'
2325

26+
const tracer = wrapTracer(trace.getTracer('Next runtime'))
27+
2428
const toPosixPath = (path: string) => path.split(sep).join(posixSep)
2529

2630
function isError(error: unknown): error is NodeJS.ErrnoException {
@@ -31,86 +35,88 @@ function isError(error: unknown): error is NodeJS.ErrnoException {
3135
* Copy App/Pages Router Javascript needed by the server handler
3236
*/
3337
export const copyNextServerCode = async (ctx: PluginContext): Promise<void> => {
34-
// update the dist directory inside the required-server-files.json to work with
35-
// nx monorepos and other setups where the dist directory is modified
36-
const reqServerFilesPath = join(
37-
ctx.standaloneRootDir,
38-
ctx.relativeAppDir,
39-
ctx.requiredServerFiles.config.distDir,
40-
'required-server-files.json',
41-
)
42-
try {
43-
await access(reqServerFilesPath)
44-
} catch (error) {
45-
if (isError(error) && error.code === 'ENOENT') {
46-
// this error at this point is problem in runtime and not user configuration
47-
ctx.failBuild(
48-
`Failed creating server handler. required-server-files.json file not found at expected location "${reqServerFilesPath}". Your repository setup is currently not yet supported.`,
49-
)
50-
} else {
51-
throw error
38+
await tracer.withActiveSpan('copyNextServerCode', async () => {
39+
// update the dist directory inside the required-server-files.json to work with
40+
// nx monorepos and other setups where the dist directory is modified
41+
const reqServerFilesPath = join(
42+
ctx.standaloneRootDir,
43+
ctx.relativeAppDir,
44+
ctx.requiredServerFiles.config.distDir,
45+
'required-server-files.json',
46+
)
47+
try {
48+
await access(reqServerFilesPath)
49+
} catch (error) {
50+
if (isError(error) && error.code === 'ENOENT') {
51+
// this error at this point is problem in runtime and not user configuration
52+
ctx.failBuild(
53+
`Failed creating server handler. required-server-files.json file not found at expected location "${reqServerFilesPath}". Your repository setup is currently not yet supported.`,
54+
)
55+
} else {
56+
throw error
57+
}
5258
}
53-
}
54-
const reqServerFiles = JSON.parse(await readFile(reqServerFilesPath, 'utf-8'))
55-
56-
// if the resolved dist folder does not match the distDir of the required-server-files.json
57-
// this means the path got altered by a plugin like nx and contained ../../ parts so we have to reset it
58-
// to point to the correct lambda destination
59-
if (
60-
toPosixPath(ctx.distDir).replace(new RegExp(`^${ctx.relativeAppDir}/?`), '') !==
61-
reqServerFiles.config.distDir
62-
) {
63-
// set the distDir to the latest path portion of the publish dir
64-
reqServerFiles.config.distDir = ctx.nextDistDir
65-
await writeFile(reqServerFilesPath, JSON.stringify(reqServerFiles))
66-
}
59+
const reqServerFiles = JSON.parse(await readFile(reqServerFilesPath, 'utf-8'))
6760

68-
// ensure the directory exists before writing to it
69-
await mkdir(ctx.serverHandlerDir, { recursive: true })
70-
// write our run-config.json to the root dir so that we can easily get the runtime config of the required-server-files.json
71-
// without the need to know about the monorepo or distDir configuration upfront.
72-
await writeFile(
73-
join(ctx.serverHandlerDir, RUN_CONFIG),
74-
JSON.stringify(reqServerFiles.config),
75-
'utf-8',
76-
)
61+
// if the resolved dist folder does not match the distDir of the required-server-files.json
62+
// this means the path got altered by a plugin like nx and contained ../../ parts so we have to reset it
63+
// to point to the correct lambda destination
64+
if (
65+
toPosixPath(ctx.distDir).replace(new RegExp(`^${ctx.relativeAppDir}/?`), '') !==
66+
reqServerFiles.config.distDir
67+
) {
68+
// set the distDir to the latest path portion of the publish dir
69+
reqServerFiles.config.distDir = ctx.nextDistDir
70+
await writeFile(reqServerFilesPath, JSON.stringify(reqServerFiles))
71+
}
7772

78-
const srcDir = join(ctx.standaloneDir, ctx.nextDistDir)
79-
// if the distDir got resolved and altered use the nextDistDir instead
80-
const nextFolder =
81-
toPosixPath(ctx.distDir) === toPosixPath(ctx.buildConfig.distDir)
82-
? ctx.distDir
83-
: ctx.nextDistDir
84-
const destDir = join(ctx.serverHandlerDir, nextFolder)
85-
86-
const paths = await glob(
87-
[`*`, `server/*`, `server/chunks/*`, `server/edge-chunks/*`, `server/+(app|pages)/**/*.js`],
88-
{
89-
cwd: srcDir,
90-
extglob: true,
91-
},
92-
)
73+
// ensure the directory exists before writing to it
74+
await mkdir(ctx.serverHandlerDir, { recursive: true })
75+
// write our run-config.json to the root dir so that we can easily get the runtime config of the required-server-files.json
76+
// without the need to know about the monorepo or distDir configuration upfront.
77+
await writeFile(
78+
join(ctx.serverHandlerDir, RUN_CONFIG),
79+
JSON.stringify(reqServerFiles.config),
80+
'utf-8',
81+
)
9382

94-
await Promise.all(
95-
paths.map(async (path: string) => {
96-
const srcPath = join(srcDir, path)
97-
const destPath = join(destDir, path)
83+
const srcDir = join(ctx.standaloneDir, ctx.nextDistDir)
84+
// if the distDir got resolved and altered use the nextDistDir instead
85+
const nextFolder =
86+
toPosixPath(ctx.distDir) === toPosixPath(ctx.buildConfig.distDir)
87+
? ctx.distDir
88+
: ctx.nextDistDir
89+
const destDir = join(ctx.serverHandlerDir, nextFolder)
90+
91+
const paths = await glob(
92+
[`*`, `server/*`, `server/chunks/*`, `server/edge-chunks/*`, `server/+(app|pages)/**/*.js`],
93+
{
94+
cwd: srcDir,
95+
extglob: true,
96+
},
97+
)
9898

99-
// If this is the middleware manifest file, replace it with an empty
100-
// manifest to avoid running middleware again in the server handler.
101-
if (path === 'server/middleware-manifest.json') {
102-
try {
103-
await replaceMiddlewareManifest(srcPath, destPath)
104-
} catch (error) {
105-
throw new Error('Could not patch middleware manifest file', { cause: error })
106-
}
99+
await Promise.all(
100+
paths.map(async (path: string) => {
101+
const srcPath = join(srcDir, path)
102+
const destPath = join(destDir, path)
103+
104+
// If this is the middleware manifest file, replace it with an empty
105+
// manifest to avoid running middleware again in the server handler.
106+
if (path === 'server/middleware-manifest.json') {
107+
try {
108+
await replaceMiddlewareManifest(srcPath, destPath)
109+
} catch (error) {
110+
throw new Error('Could not patch middleware manifest file', { cause: error })
111+
}
107112

108-
return
109-
}
113+
return
114+
}
110115

111-
await cp(srcPath, destPath, { recursive: true, force: true })
112-
}),
113-
)
116+
await cp(srcPath, destPath, { recursive: true, force: true })
117+
}),
118+
)
119+
})
114120
}
115121

116122
/**
@@ -238,69 +244,71 @@ async function patchNextModules(
238244
}
239245

240246
export const copyNextDependencies = async (ctx: PluginContext): Promise<void> => {
241-
const entries = await readdir(ctx.standaloneDir)
242-
const promises: Promise<void>[] = entries.map(async (entry) => {
243-
// copy all except the package.json and distDir (.next) folder as this is handled in a separate function
244-
// this will include the node_modules folder as well
245-
if (entry === 'package.json' || entry === ctx.nextDistDir) {
246-
return
247-
}
248-
const src = join(ctx.standaloneDir, entry)
249-
const dest = join(ctx.serverHandlerDir, entry)
250-
await cp(src, dest, { recursive: true, verbatimSymlinks: true, force: true })
247+
await tracer.withActiveSpan('copyNextDependencies', async () => {
248+
const entries = await readdir(ctx.standaloneDir)
249+
const promises: Promise<void>[] = entries.map(async (entry) => {
250+
// copy all except the package.json and distDir (.next) folder as this is handled in a separate function
251+
// this will include the node_modules folder as well
252+
if (entry === 'package.json' || entry === ctx.nextDistDir) {
253+
return
254+
}
255+
const src = join(ctx.standaloneDir, entry)
256+
const dest = join(ctx.serverHandlerDir, entry)
257+
await cp(src, dest, { recursive: true, verbatimSymlinks: true, force: true })
251258

252-
if (entry === 'node_modules') {
253-
await recreateNodeModuleSymlinks(ctx.resolveFromSiteDir('node_modules'), dest)
259+
if (entry === 'node_modules') {
260+
await recreateNodeModuleSymlinks(ctx.resolveFromSiteDir('node_modules'), dest)
261+
}
262+
})
263+
264+
// inside a monorepo there is a root `node_modules` folder that contains all the dependencies
265+
const rootSrcDir = join(ctx.standaloneRootDir, 'node_modules')
266+
const rootDestDir = join(ctx.serverHandlerRootDir, 'node_modules')
267+
268+
// use the node_modules tree from the process.cwd() and not the one from the standalone output
269+
// as the standalone node_modules are already wrongly assembled by Next.js.
270+
// see: https://github.com/vercel/next.js/issues/50072
271+
if (existsSync(rootSrcDir) && ctx.standaloneRootDir !== ctx.standaloneDir) {
272+
promises.push(
273+
cp(rootSrcDir, rootDestDir, { recursive: true, verbatimSymlinks: true }).then(() =>
274+
recreateNodeModuleSymlinks(resolve('node_modules'), rootDestDir),
275+
),
276+
)
254277
}
255-
})
256278

257-
// inside a monorepo there is a root `node_modules` folder that contains all the dependencies
258-
const rootSrcDir = join(ctx.standaloneRootDir, 'node_modules')
259-
const rootDestDir = join(ctx.serverHandlerRootDir, 'node_modules')
260-
261-
// use the node_modules tree from the process.cwd() and not the one from the standalone output
262-
// as the standalone node_modules are already wrongly assembled by Next.js.
263-
// see: https://github.com/vercel/next.js/issues/50072
264-
if (existsSync(rootSrcDir) && ctx.standaloneRootDir !== ctx.standaloneDir) {
265-
promises.push(
266-
cp(rootSrcDir, rootDestDir, { recursive: true, verbatimSymlinks: true }).then(() =>
267-
recreateNodeModuleSymlinks(resolve('node_modules'), rootDestDir),
268-
),
269-
)
270-
}
271-
272-
await Promise.all(promises)
279+
await Promise.all(promises)
273280

274-
// detect if it might lead to a runtime issue and throw an error upfront on build time instead of silently failing during runtime
275-
const serverHandlerRequire = createRequire(posixJoin(ctx.serverHandlerDir, ':internal:'))
281+
// detect if it might lead to a runtime issue and throw an error upfront on build time instead of silently failing during runtime
282+
const serverHandlerRequire = createRequire(posixJoin(ctx.serverHandlerDir, ':internal:'))
276283

277-
let nextVersion: string | undefined
278-
try {
279-
const { version } = serverHandlerRequire('next/package.json')
280-
if (version) {
281-
nextVersion = version as string
284+
let nextVersion: string | undefined
285+
try {
286+
const { version } = serverHandlerRequire('next/package.json')
287+
if (version) {
288+
nextVersion = version as string
289+
}
290+
} catch {
291+
// failed to resolve package.json - currently this is resolvable in all known next versions, but if next implements
292+
// exports map it still might be a problem in the future, so we are not breaking here
282293
}
283-
} catch {
284-
// failed to resolve package.json - currently this is resolvable in all known next versions, but if next implements
285-
// exports map it still might be a problem in the future, so we are not breaking here
286-
}
287294

288-
if (nextVersion) {
289-
verifyNextVersion(ctx, nextVersion)
295+
if (nextVersion) {
296+
verifyNextVersion(ctx, nextVersion)
290297

291-
await patchNextModules(ctx, nextVersion, serverHandlerRequire.resolve)
292-
}
298+
await patchNextModules(ctx, nextVersion, serverHandlerRequire.resolve)
299+
}
293300

294-
try {
295-
const nextEntryAbsolutePath = serverHandlerRequire.resolve('next')
296-
const nextRequire = createRequire(nextEntryAbsolutePath)
297-
nextRequire.resolve('styled-jsx')
298-
} catch {
299-
throw new Error(
300-
'node_modules are not installed correctly, if you are using pnpm please set the public hoist pattern to: `public-hoist-pattern[]=*`.\n' +
301-
'Refer to your docs for more details: https://docs.netlify.com/integrations/frameworks/next-js/overview/#pnpm-support',
302-
)
303-
}
301+
try {
302+
const nextEntryAbsolutePath = serverHandlerRequire.resolve('next')
303+
const nextRequire = createRequire(nextEntryAbsolutePath)
304+
nextRequire.resolve('styled-jsx')
305+
} catch {
306+
throw new Error(
307+
'node_modules are not installed correctly, if you are using pnpm please set the public hoist pattern to: `public-hoist-pattern[]=*`.\n' +
308+
'Refer to your docs for more details: https://docs.netlify.com/integrations/frameworks/next-js/overview/#pnpm-support',
309+
)
310+
}
311+
})
304312
}
305313

306314
export const writeTagsManifest = async (ctx: PluginContext): Promise<void> => {

‎src/build/content/static.ts

+52-41
Original file line numberDiff line numberDiff line change
@@ -2,68 +2,79 @@ import { existsSync } from 'node:fs'
22
import { cp, mkdir, rename, rm } from 'node:fs/promises'
33
import { basename, join } from 'node:path'
44

5+
import { trace } from '@opentelemetry/api'
6+
import { wrapTracer } from '@opentelemetry/api/experimental'
57
import glob from 'fast-glob'
68

79
import { encodeBlobKey } from '../../shared/blobkey.js'
810
import { PluginContext } from '../plugin-context.js'
911

12+
const tracer = wrapTracer(trace.getTracer('Next runtime'))
13+
1014
/**
1115
* Assemble the static content for being uploaded to the blob storage
1216
*/
1317
export const copyStaticContent = async (ctx: PluginContext): Promise<void> => {
14-
const srcDir = join(ctx.publishDir, 'server/pages')
15-
const destDir = ctx.blobDir
18+
return tracer.withActiveSpan('copyStaticContent', async () => {
19+
const srcDir = join(ctx.publishDir, 'server/pages')
20+
const destDir = ctx.blobDir
1621

17-
const paths = await glob('**/*.+(html|json)', {
18-
cwd: srcDir,
19-
extglob: true,
20-
})
22+
const paths = await glob('**/*.+(html|json)', {
23+
cwd: srcDir,
24+
extglob: true,
25+
})
2126

22-
try {
23-
await Promise.all(
24-
paths
25-
.filter((path) => !paths.includes(`${path.slice(0, -5)}.json`))
26-
.map(async (path): Promise<void> => {
27-
await cp(join(srcDir, path), join(destDir, await encodeBlobKey(path)), {
28-
recursive: true,
29-
force: true,
30-
})
31-
}),
32-
)
33-
} catch (error) {
34-
ctx.failBuild('Failed assembling static pages for upload', error)
35-
}
27+
try {
28+
await Promise.all(
29+
paths
30+
.filter((path) => !paths.includes(`${path.slice(0, -5)}.json`))
31+
.map(async (path): Promise<void> => {
32+
await cp(join(srcDir, path), join(destDir, await encodeBlobKey(path)), {
33+
recursive: true,
34+
force: true,
35+
})
36+
}),
37+
)
38+
} catch (error) {
39+
ctx.failBuild('Failed assembling static pages for upload', error)
40+
}
41+
})
3642
}
3743

3844
/**
3945
* Copy static content to the static dir so it is uploaded to the CDN
4046
*/
4147
export const copyStaticAssets = async (ctx: PluginContext): Promise<void> => {
42-
try {
43-
await rm(ctx.staticDir, { recursive: true, force: true })
44-
const { basePath } = await ctx.getRoutesManifest()
45-
if (existsSync(ctx.resolveFromSiteDir('public'))) {
46-
await cp(ctx.resolveFromSiteDir('public'), join(ctx.staticDir, basePath), {
47-
recursive: true,
48-
})
49-
}
50-
if (existsSync(join(ctx.publishDir, 'static'))) {
51-
await cp(join(ctx.publishDir, 'static'), join(ctx.staticDir, basePath, '_next/static'), {
52-
recursive: true,
53-
})
48+
return tracer.withActiveSpan('copyStaticAssets', async (span): Promise<void> => {
49+
try {
50+
await rm(ctx.staticDir, { recursive: true, force: true })
51+
const { basePath } = await ctx.getRoutesManifest()
52+
if (existsSync(ctx.resolveFromSiteDir('public'))) {
53+
await cp(ctx.resolveFromSiteDir('public'), join(ctx.staticDir, basePath), {
54+
recursive: true,
55+
})
56+
}
57+
if (existsSync(join(ctx.publishDir, 'static'))) {
58+
await cp(join(ctx.publishDir, 'static'), join(ctx.staticDir, basePath, '_next/static'), {
59+
recursive: true,
60+
})
61+
}
62+
} catch (error) {
63+
span.end()
64+
ctx.failBuild('Failed copying static assets', error)
5465
}
55-
} catch (error) {
56-
ctx.failBuild('Failed copying static assets', error)
57-
}
66+
})
5867
}
5968

6069
export const copyStaticExport = async (ctx: PluginContext): Promise<void> => {
61-
try {
62-
await rm(ctx.staticDir, { recursive: true, force: true })
63-
await cp(ctx.resolveFromSiteDir('out'), ctx.staticDir, { recursive: true })
64-
} catch (error) {
65-
ctx.failBuild('Failed copying static export', error)
66-
}
70+
tracer.withActiveSpan('copyStaticExport', async () => {
71+
try {
72+
await rm(ctx.staticDir, { recursive: true, force: true })
73+
await cp(ctx.resolveFromSiteDir('out'), ctx.staticDir, { recursive: true })
74+
} catch (error) {
75+
ctx.failBuild('Failed copying static export', error)
76+
}
77+
})
6778
}
6879

6980
/**

‎src/build/functions/server.ts

+56-47
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { cp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'
22
import { join, relative } from 'node:path'
33
import { join as posixJoin } from 'node:path/posix'
44

5+
import { trace } from '@opentelemetry/api'
6+
import { wrapTracer } from '@opentelemetry/api/experimental'
57
import { glob } from 'fast-glob'
68

79
import {
@@ -12,45 +14,50 @@ import {
1214
} from '../content/server.js'
1315
import { PluginContext, SERVER_HANDLER_NAME } from '../plugin-context.js'
1416

17+
const tracer = wrapTracer(trace.getTracer('Next runtime'))
18+
1519
/** Copies the runtime dist folder to the lambda */
1620
const copyHandlerDependencies = async (ctx: PluginContext) => {
17-
const promises: Promise<void>[] = []
18-
const { included_files: includedFiles = [] } = ctx.netlifyConfig.functions?.['*'] || {}
19-
// if the user specified some files to include in the lambda
20-
// we need to copy them to the functions-internal folder
21-
if (includedFiles.length !== 0) {
22-
const resolvedFiles = await Promise.all(
23-
includedFiles.map((globPattern) => glob(globPattern, { cwd: process.cwd() })),
24-
)
25-
for (const filePath of resolvedFiles.flat()) {
26-
promises.push(
27-
cp(
28-
join(process.cwd(), filePath),
29-
// the serverHandlerDir is aware of the dist dir.
30-
// The distDir must not be the package path therefore we need to rely on the
31-
// serverHandlerDir instead of the serverHandlerRootDir
32-
// therefore we need to remove the package path from the filePath
33-
join(ctx.serverHandlerDir, relative(ctx.relativeAppDir, filePath)),
34-
{
35-
recursive: true,
36-
force: true,
37-
},
38-
),
21+
await tracer.withActiveSpan('copyHandlerDependencies', async (span) => {
22+
const promises: Promise<void>[] = []
23+
const { included_files: includedFiles = [] } = ctx.netlifyConfig.functions?.['*'] || {}
24+
// if the user specified some files to include in the lambda
25+
// we need to copy them to the functions-internal folder
26+
span.setAttribute('next.includedFiles', includedFiles.join(','))
27+
if (includedFiles.length !== 0) {
28+
const resolvedFiles = await Promise.all(
29+
includedFiles.map((globPattern) => glob(globPattern, { cwd: process.cwd() })),
3930
)
31+
for (const filePath of resolvedFiles.flat()) {
32+
promises.push(
33+
cp(
34+
join(process.cwd(), filePath),
35+
// the serverHandlerDir is aware of the dist dir.
36+
// The distDir must not be the package path therefore we need to rely on the
37+
// serverHandlerDir instead of the serverHandlerRootDir
38+
// therefore we need to remove the package path from the filePath
39+
join(ctx.serverHandlerDir, relative(ctx.relativeAppDir, filePath)),
40+
{
41+
recursive: true,
42+
force: true,
43+
},
44+
),
45+
)
46+
}
4047
}
41-
}
4248

43-
const fileList = await glob('dist/**/*', { cwd: ctx.pluginDir })
49+
const fileList = await glob('dist/**/*', { cwd: ctx.pluginDir })
4450

45-
for (const filePath of fileList) {
46-
promises.push(
47-
cp(join(ctx.pluginDir, filePath), join(ctx.serverHandlerDir, '.netlify', filePath), {
48-
recursive: true,
49-
force: true,
50-
}),
51-
)
52-
}
53-
await Promise.all(promises)
51+
for (const filePath of fileList) {
52+
promises.push(
53+
cp(join(ctx.pluginDir, filePath), join(ctx.serverHandlerDir, '.netlify', filePath), {
54+
recursive: true,
55+
force: true,
56+
}),
57+
)
58+
}
59+
await Promise.all(promises)
60+
})
5461
}
5562

5663
const writeHandlerManifest = async (ctx: PluginContext) => {
@@ -104,18 +111,20 @@ const writeHandlerFile = async (ctx: PluginContext) => {
104111
* Create a Netlify function to run the Next.js server
105112
*/
106113
export const createServerHandler = async (ctx: PluginContext) => {
107-
await rm(ctx.serverFunctionsDir, { recursive: true, force: true })
108-
await mkdir(join(ctx.serverHandlerDir, '.netlify'), { recursive: true })
109-
110-
await Promise.all([
111-
copyNextServerCode(ctx),
112-
copyNextDependencies(ctx),
113-
writeTagsManifest(ctx),
114-
copyHandlerDependencies(ctx),
115-
writeHandlerManifest(ctx),
116-
writePackageMetadata(ctx),
117-
writeHandlerFile(ctx),
118-
])
119-
120-
await verifyHandlerDirStructure(ctx)
114+
await tracer.withActiveSpan('createServerHandler', async () => {
115+
await rm(ctx.serverFunctionsDir, { recursive: true, force: true })
116+
await mkdir(join(ctx.serverHandlerDir, '.netlify'), { recursive: true })
117+
118+
await Promise.all([
119+
copyNextServerCode(ctx),
120+
copyNextDependencies(ctx),
121+
writeTagsManifest(ctx),
122+
copyHandlerDependencies(ctx),
123+
writeHandlerManifest(ctx),
124+
writePackageMetadata(ctx),
125+
writeHandlerFile(ctx),
126+
])
127+
128+
await verifyHandlerDirStructure(ctx)
129+
})
121130
}

‎src/index.ts

+48-31
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import type { NetlifyPluginOptions } from '@netlify/build'
2+
import { trace } from '@opentelemetry/api'
3+
import { wrapTracer } from '@opentelemetry/api/experimental'
24

35
import { restoreBuildCache, saveBuildCache } from './build/cache.js'
46
import { copyPrerenderedContent } from './build/content/prerendered.js'
@@ -15,51 +17,66 @@ import { setImageConfig } from './build/image-cdn.js'
1517
import { PluginContext } from './build/plugin-context.js'
1618
import { verifyBuildConfig, verifyPublishDir } from './build/verification.js'
1719

20+
const tracer = wrapTracer(trace.getTracer('Next.js runtime'))
21+
1822
export const onPreBuild = async (options: NetlifyPluginOptions) => {
19-
// Enable Next.js standalone mode at build time
20-
process.env.NEXT_PRIVATE_STANDALONE = 'true'
21-
if (!options.constants.IS_LOCAL) {
22-
await restoreBuildCache(new PluginContext(options))
23-
}
23+
await tracer.withActiveSpan('onPreBuild', async () => {
24+
// Enable Next.js standalone mode at build time
25+
process.env.NEXT_PRIVATE_STANDALONE = 'true'
26+
if (!options.constants.IS_LOCAL) {
27+
await restoreBuildCache(new PluginContext(options))
28+
}
29+
})
2430
}
2531

2632
export const onBuild = async (options: NetlifyPluginOptions) => {
27-
const ctx = new PluginContext(options)
28-
verifyPublishDir(ctx)
29-
verifyBuildConfig(ctx)
33+
await tracer.withActiveSpan('onBuild', async (span) => {
34+
const ctx = new PluginContext(options)
35+
36+
verifyPublishDir(ctx)
37+
verifyBuildConfig(ctx)
38+
39+
span.setAttribute('next.buildConfig', JSON.stringify(ctx.buildConfig))
3040

31-
// only save the build cache if not run via the CLI
32-
if (!options.constants.IS_LOCAL) {
33-
await saveBuildCache(ctx)
34-
}
41+
// only save the build cache if not run via the CLI
42+
if (!options.constants.IS_LOCAL) {
43+
await saveBuildCache(ctx)
44+
}
3545

36-
// static exports only need to be uploaded to the CDN
37-
if (ctx.buildConfig.output === 'export') {
38-
return copyStaticExport(ctx)
39-
}
46+
// static exports only need to be uploaded to the CDN
47+
if (ctx.buildConfig.output === 'export') {
48+
return copyStaticExport(ctx)
49+
}
4050

41-
await Promise.all([
42-
copyStaticAssets(ctx),
43-
copyStaticContent(ctx),
44-
copyPrerenderedContent(ctx),
45-
createServerHandler(ctx),
46-
createEdgeHandlers(ctx),
47-
setImageConfig(ctx),
48-
])
51+
await Promise.all([
52+
copyStaticAssets(ctx),
53+
copyStaticContent(ctx),
54+
copyPrerenderedContent(ctx),
55+
createServerHandler(ctx),
56+
createEdgeHandlers(ctx),
57+
setImageConfig(ctx),
58+
])
59+
})
4960
}
5061

5162
export const onPostBuild = async (options: NetlifyPluginOptions) => {
52-
await publishStaticDir(new PluginContext(options))
63+
await tracer.withActiveSpan('onPostBuild', async () => {
64+
await publishStaticDir(new PluginContext(options))
65+
})
5366
}
5467

5568
export const onSuccess = async () => {
56-
const prewarm = [process.env.DEPLOY_URL, process.env.DEPLOY_PRIME_URL, process.env.URL].filter(
57-
// If running locally then the deploy ID is a placeholder value. Filtering for `https://0--` removes it.
58-
(url?: string): url is string => Boolean(url && !url.startsWith('https://0--')),
59-
)
60-
await Promise.allSettled(prewarm.map((url) => fetch(url)))
69+
await tracer.withActiveSpan('onSuccess', async () => {
70+
const prewarm = [process.env.DEPLOY_URL, process.env.DEPLOY_PRIME_URL, process.env.URL].filter(
71+
// If running locally then the deploy ID is a placeholder value. Filtering for `https://0--` removes it.
72+
(url?: string): url is string => Boolean(url && !url.startsWith('https://0--')),
73+
)
74+
await Promise.allSettled(prewarm.map((url) => fetch(url)))
75+
})
6176
}
6277

6378
export const onEnd = async (options: NetlifyPluginOptions) => {
64-
await unpublishStaticDir(new PluginContext(options))
79+
await tracer.withActiveSpan('onEnd', async () => {
80+
await unpublishStaticDir(new PluginContext(options))
81+
})
6582
}

0 commit comments

Comments
 (0)
Please sign in to comment.