fix(core): make @opentelemetry/api dynamic import opaque to bundlers#1947
fix(core): make @opentelemetry/api dynamic import opaque to bundlers#1947VaguelySerious wants to merge 1 commit intomainfrom
Conversation
`@opentelemetry/api` is an optional peer dependency loaded via a
try/catch'd `await import('@opentelemetry/api')`. Bundlers that
statically follow `import('…')` strings reject it as unresolvable when
the consumer hasn't installed it — SvelteKit's Rollup pipeline turns
this into a fatal build error:
[vite]: Rollup failed to resolve import "@opentelemetry/api" from
"src/routes/.well-known/workflow/v1/flow/__step_registrations.js"
Construct the specifier at runtime so the static analyzer can't resolve
it. The `globalThis`-cached symbol pattern used by `get-world-lazy.ts`
works for the same reason. Runtime semantics are unchanged: present →
loaded, absent → caught and tracing disabled.
Other build systems (Next.js, Nitro, Astro, Hono, Nuxt) tolerate the
unresolvable specifier today, so they were unaffected — but the same
pattern fails for any bundler that treats unresolved static specifiers
as fatal, and depending on transitive resolution to provide the peer
is brittle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: 8fcd10a The changes in this PR will be included in the next version bump. This PR includes changesets to release 18 packages
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 |
🧪 E2E Test Results✅ All tests passed Summary
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
✅ 📋 Other
|
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Nitro fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
❌ Some benchmark jobs failed:
Check the workflow run for details. |
| const specifier = ['@opentelemetry', 'api'].join('/'); | ||
| try { | ||
| return await import('@opentelemetry/api'); | ||
| return (await import(specifier)) as typeof api; |
There was a problem hiding this comment.
Should we mark this as external instead?
Summary
`@opentelemetry/api` is declared as an optional peer dependency on `@workflow/core` and loaded via:
```ts
try {
return await import('@opentelemetry/api');
} catch {
runtimeLogger.info('OpenTelemetry not available, tracing will be disabled');
return null;
}
```
Bundlers that statically follow `import('…')` strings reject the unresolvable specifier when the consumer hasn't installed the peer. SvelteKit's Rollup pipeline turns this into a fatal build error:
```
[vite]: Rollup failed to resolve import "@opentelemetry/api" from
"src/routes/.well-known/workflow/v1/flow/__step_registrations.js"
```
(Reproduced cleanly against `workflow-tarballs-n5b4yau0p`. Same error on the SvelteKit example in `workflow-examples`.)
Fix
Construct the specifier at runtime so static analyzers can't follow it — the same pattern `get-world-lazy.ts` uses for `./world.js`:
```ts
const specifier = ['@opentelemetry', 'api'].join('/');
return (await import(specifier)) as typeof api;
```
Runtime behavior is unchanged: present at runtime → loaded, absent → caught and tracing is disabled.
Why other bundlers haven't tripped on this
Next.js / Turbopack / esbuild (Nitro, Astro, Hono, Nuxt) currently tolerate the unresolvable specifier — either they downgrade unresolved `import()` to a runtime warning, or some transitive (`ai`, `@vercel/otel`) installs `@opentelemetry/api` so the resolution actually succeeds. SvelteKit's Rollup config is the strict case.
Relying on transitive resolution to satisfy the optional peer is brittle, so making the import opaque is the proper fix.
Test plan
🤖 Generated with Claude Code