Repository navigation
Inject default Vercel tracing for Next.js builds without an instrumentation file - #148
kakadiadarpan wants to merge 26 commits into
Conversation
modifyConfig writes .vercel/otel and appends injectInstrumentation only on a production build that can use it. The inject is skipped when the project already has an instrumentation file, Next.js does not read the field, @opentelemetry/api is missing or outside >=1.9.0 <2.0.0, or the image is missing a package. A failure is logged and does not fail the build. VERCEL_OTEL_PACKAGES_DIR is a placeholder until the build image names the directory that contains @vercel/otel and @opentelemetry/api-logs.
The tests cover the copied tree, createRequire resolution, a project instrumentation file, a missing image package, an incompatible OpenTelemetry API, and preserved injectInstrumentation entries.
…I below 1.10 The build image sets VERCEL_OTEL_PACKAGES_DIR on every build, so it only locates the packages now. The API sets VERCEL_DEFAULT_TRACING_ENABLED=1 for eligible projects. OpenTelemetry JS SDK 2.x caps the API below 1.10.
@vercel/otel 2.1.3 only uses API features that 1.6.0 has, so Next's vendored copy is enough. Lower the floor for app copies to 1.6.0.
|
@kakadiadarpan is attempting to deploy a commit to the vtest314-next-adapter-e2e-tests Team on Vercel. A member of the Team first needs to authorize it. |
🦋 Changeset detectedLatest commit: 3ff6296 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Pass VERCEL_PROJECT_NAME as serviceName so spans don't all report "app". OTEL_SERVICE_NAME still wins, and a missing value falls back to "app".
It required a next/dist internal. The adapter doesn't gate other config on the Next.js version: older versions ignore injectInstrumentation.
Use build-utils' debug() so customer build logs don't show a skip line, for example on every build of a project with its own instrumentation file.
Match the other blocks in modifyConfig. injectDefaultTracing now only does the filesystem checks and the copy.
Next.js always sets pageExtensions, defaulting to tsx, ts, jsx and js.
The accepted range is >=1.6.0 <1.10.0, so compare the major and minor numbers instead of parsing full semver.
Next.js now chains injected instrumentation before the project's own file, and Turbopack finds that file in parent folders up to the tracing root. Check the same folders so a monorepo root instrumentation file isn't chained with default tracing.
webpack parses .js as CommonJS when the app's package.json sets "type": "commonjs", and .vercel/otel/ has no package.json of its own, so the wrapper's import failed to parse and the build failed. .mjs is always ESM.
|
Your Vercel team vtest314-next-adapter-e2e-tests is not permitted to deploy from this git repository. Contact an administrator to add github organization nextjs as a Protected Git Scope in vtest314-next-adapter-e2e-tests on Vercel. Once added, commit again to see your changes. Learn more: https://vercel.com/docs/security/protected-git-scopes |
Next.js compiles instrumentation for both runtimes, so the static import put @vercel/otel's edge build in every Edge function (about +60 KB gzipped each). Default tracing targets Node.js, and NEXT_RUNTIME is inlined per runtime, so the dynamic import behind it drops out of Edge bundles.
Copy @opentelemetry/api from the build image next to @vercel/otel, so webpack always resolves it from .vercel/otel, and skip only when the project root or Next.js resolves an API that a 1.9 provider can't reach: 1.10 or newer, 2.x, or a prerelease.
|
Fixed the pnpm hidden-API case in 800c462 and a8a24e6:
Checked with linux/amd64, pnpm 10.5.2, both bundlers, and
|
vercel/next.js#100658 renamed the config option to match instrumentationClientInject.
lucleray
left a comment
There was a problem hiding this comment.
looks good, thanks! left a few notes inline, i think the duplicate fetch spans one is worth fixing before GA 👍
### What?
Adds an `instrumentationInject?: string[]` config option. Next.js
compiles one module that runs every entry's `register` and
`onRequestError`, then the project's own `instrumentation.{ts,js}` if it
has one. This works like `instrumentationClientInject` does for the
client.
```ts
// set by a deployment adapter in modifyConfig, or a config wrapper like withSentry
config.instrumentationInject ??= []
config.instrumentationInject.push('./path/to/instrumentation.js')
```
```text
instrumentation slot
→ each instrumentationInject entry, in array order
→ then the project's instrumentation file, if there is one
```
### Why?
Hosting platforms want to give every app default instrumentation, such
as OpenTelemetry tracing. Today that means asking users to add an
`instrumentation.ts` and a dependency to their project. This hook lets
an adapter or a config wrapper supply the module at build time instead.
The user's files, `package.json` and lockfile stay untouched, and
Next.js installs nothing.
### How?
- Anyone can set the field: a deployment adapter in `modifyConfig`, or a
config wrapper such as `withSentry`. Whoever sets it decides whether to
inject when the project already has its own instrumentation file.
- Both bundlers generate the same CommonJS module: it requires every
entry from the project root, then the project's instrumentation file,
and passes them to `combineInstrumentation`
(`next/dist/lib/combine-instrumentation`).
- webpack compiles a stub entry through
`next-instrumentation-inject-loader`. Turbopack uses a virtual
`instrumentation.inject.cjs` at the project root, so a `type: module`
package.json can't change how it parses.
- `register` hooks run in array order. `onRequestError` hooks run
concurrently, so a slow or hanging handler can't stop the project's own
from running. A hook that throws, or a module with top-level await that
fails to load, doesn't stop the others. The error is rethrown after all
have run, as an `AggregateError` when more than one fails, so Next.js
surfaces it as it does for a single file. A module without top-level
await that throws while loading stops startup, as a single broken file
does today.
- Modules that use top-level `await` come back from `require()` as
promises. `combineInstrumentation` awaits each one, and marks them
handled up front so a later rejection isn't reported as unhandled while
an earlier hook runs.
- Entries can be package names, project-relative paths, or absolute
paths. The bundler resolves them from the project root, as it would an
import. Package entries keep their name, so `serverExternalPackages` and
the built-in externals list apply, and an external entry is still
included in the server trace. Absolute paths are rewritten to
project-relative ones for Turbopack, which resolves a leading `/` from
the project root.
- `instrumentationInject` defaults to `[]`, like
`instrumentationClientInject`, so `modifyConfig` can push to it.
- To find the project file, webpack tries each `pageExtensions`
extension, in config order, at the project root and then in `src/`.
Turbopack uses its usual `instrumentation` lookup, which also checks
parent folders up to `outputFileTracingRoot` and tries longer extensions
first. With more than one candidate file, the two can pick different
ones.
- An entry that doesn't resolve fails the build, so whoever sets the
field should check it first.
### Tests
`test/e2e/app-dir/inject-instrumentation` covers five cases with webpack
and Turbopack:
1. With two injected entries and no project file, both `register()`
calls run, in order.
2. With a project instrumentation file, both injected `register()` calls
run, then the project's.
3. With `pageExtensions: ['page.ts', …]`, the project's
`instrumentation.page.ts` is chained.
4. With no inject and no file, there's no instrumentation.
5. Package entries: one listed in `serverExternalPackages` stays out of
the Node server bundles and appears in `instrumentation.js.nft.json`,
and a bundled one is compiled in. The bundle check reads build output,
so it's gated to `!deploy`, and the trace check to `start`.
The fixtures cover a relative and an absolute entry, an injected entry
and a project file that both use top-level `await`, and an injected
module that imports a sibling file.
`lib/combine-instrumentation.test.ts` covers `register` order, error
isolation, `onRequestError` arguments, concurrent `onRequestError`
handlers (a hanging one doesn't block the rest), promise modules, and
that a later rejection isn't left unhandled.
### Related
nextjs/adapter-vercel#148
<!-- NEXT_JS_LLM -->
<!-- Thanks for opening a PR! Your contribution is much appreciated.
To make sure your PR is handled as smoothly as possible we request that
you follow the checklist sections below.
Choose the right checklist for the change(s) that you're making:
## For Contributors
We encourage you to use AI to assist you in researching, creating, and
reviewing changes. However, you must review and deeply understand the
contributions you are making. For this reason, **pull request
descriptions from external contributors must be written by a human**.
### Improving Documentation
- Run `pnpm prettier-fix` to fix formatting issues before opening the
PR.
- Read the Docs Contribution Guide to ensure your contribution follows
the docs guidelines:
https://nextjs.org/docs/community/contribution-guide
### Fixing a bug
- Related issues linked using `fixes #number`
- Tests added. See:
https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md
### Adding a feature
- Implements an existing feature request or RFC. Make sure the feature
request has been accepted for implementation before opening a PR. (A
discussion must be opened, see
https://github.com/vercel/next.js/discussions/new?category=ideas)
- Related issues/discussions are linked using `fixes #number`
- e2e tests added
(https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs)
- Documentation added
- Telemetry added. In case of a feature if it's used or not.
- Errors have a helpful link attached, see
https://github.com/vercel/next.js/blob/canary/contributing.md
### Signed commits
- This repository requires verified commit signatures on protected
branches.
- If this pull request is blocked for unsigned commits, re-sign the
commits and force-push the branch.
- A `Signed-off-by` line in the commit message is not enough.
## For Maintainers
- Minimal description (aim for explaining to someone not on the team to
understand the PR)
- When linking to a Slack thread, you might want to share details of the
conclusion
- Link both the Linear (Fixes NEXT-xxx) and the GitHub issues
- Add review comments if necessary to explain to the reviewer the logic
behind a change
### What?
### Why?
### How?
Closes NEXT-
Fixes #
-->
---------
Co-authored-by: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com>
When Next.js can't resolve the API from its own folder, webpack aliases every import to Next's compiled 1.6.0, so the copied 1.9.0 isn't used there.
A config wrapper that injects its own instrumentation may register its own provider. Chaining default tracing after it would register a second one, the same reason the inject skips when the project has its own file.
Only @vercel/otel, @opentelemetry/api-logs and @opentelemetry/api are copied. A future image with a package that needs another runtime dependency would pass the resolve check, then fail next build on the missing import.
Next.js keeps the register() promise and every request awaits it, and combineInstrumentation rethrows. If the import or registerOTel ever threw, every request on the instance would fail, for a customer who never opted in. The wrapper now catches and warns.
webpack runs modifyConfig in each build worker, so several processes copied the ~8 MB tree into the same folder at once. With 8 parallel runs, 7 skipped the inject on EEXIST and the tree was left a file short. Each run now copies into its own temp folder and renames it into place, and a stamp of the image path and package versions lets later runs reuse the tree.
The runtime traces outbound fetches unless a function sets shouldDisableAutomaticFetchInstrumentation, which the CLI only does when package.json lists OpenTelemetry packages. The injected @vercel/otel also instruments fetch, so each fetch was traced twice. onBuildComplete now sets the flag on Node.js functions and Node middleware when the build compiled the inject. Next.js versions that ignore instrumentationInject compile no instrumentation entry, so they keep the runtime's fetch spans.
Problem
New Next.js projects on Vercel get no traces unless they add
instrumentation.tsand install@vercel/otel. Default tracing has to work without touching the project'spackage.jsonor lockfile, and without downloading anything per build.Solution
In
modifyConfig, duringnext build, the adapter writes a wrapper into the project and points Next's newinstrumentationInjectat it.Each build worker copies the tree into its own temp folder and renames it into place, so parallel webpack workers never read a half-copied tree. A stamp of the image path and package versions lets later workers reuse it.
The files have to sit inside the project. Turbopack can't read outside the project root, and it rejects symlinks that point out of it.
It injects only when every check passes. A miss skips silently (the reason is logged with
VERCEL_BUILDER_DEBUG=1), and errors never fail the build.Spans use
VERCEL_PROJECT_NAMEas the service name. A customer'sOTEL_SERVICE_NAMEstill wins.There's no Next.js version check: older versions ignore
instrumentationInject.@vercel/otelinstruments fetch, soonBuildCompletesetsshouldDisableAutomaticFetchInstrumentationon Node.js functions and Node middleware when the build compiled the inject (the config has the entry and.next/server/instrumentation.jsexists). Otherwise every fetch would be traced twice. A Next.js version that ignoresinstrumentationInjectcompiles no instrumentation entry, so it keeps the runtime's fetch spans.@vercel/otelregisters through its own pinned@opentelemetry/api1.9, except on webpack when Next.js finds no API at all: webpack then aliases every import to Next's compiled 1.6.0, which has the API surface@vercel/oteluses. Every 1.x copy of the API shares one global slot and accepts a provider registered by the same or a newer minor version. So the app's copy, Next's vendored 1.6.0, or a copy only Next.js can see (pnpm hoists a dependency's API intonode_modules/.pnpm/node_modules) all read the same provider. Webpack always finds the API next to@vercel/otel, so the build no longer fails when Next.js finds a copy the project root can't. The inject skips when either copy is 1.10 or newer, because those readers reject a 1.9 provider. It also skips for 2.x, which uses a different slot, and for prereleases, which only accept an exact match. Nested API copies in the image, such as the 1.10.0 that yarn 1 puts underapi-logs, are left out of the copy. The SDK 2.x bundled in@vercel/otelcaps the API below 1.10.Related PR(s)
instrumentationInject