How to Fix: Remove `@opentelemetry/api` from edge bundle when telemetry is disabled
Edge bundles should not pay for telemetry code they never execute. This issue appears when @opentelemetry/api is still pulled into the edge runtime bundle even though telemetry is disabled, causing unnecessary bundle size and confusing dependency graphs in tools such as next/bundle-analyzer.
Table of Contents
Understanding the Root Cause
The bug is typically caused by a static import path that references telemetry-related code from a module that is still reachable by the edge compiler. Even when telemetry is disabled at runtime, bundlers like webpack or Turbopack analyze the module graph ahead of time. If a file imported by an edge entrypoint contains a top-level import of @opentelemetry/api, that package can still be included in the final edge bundle.
In other words, the problem is not whether telemetry actually runs. The problem is whether the bundler sees a compile-time dependency on the telemetry package.
This usually happens in one of these patterns:
- A shared utility imports telemetry helpers at the top level, and that utility is reused by edge code.
- A conditional like
if (telemetryEnabled)guards execution, but the import itself remains static. - A barrel export re-exports telemetry code, making the dependency visible even when only non-telemetry functions are needed.
- A server-oriented module is accidentally reused in edge code, dragging in packages that should stay out of the edge runtime.
Because edge bundles target a constrained runtime, every unnecessary dependency matters. Removing @opentelemetry/api from disabled telemetry paths improves bundle size, reduces noise in analyzer output, and keeps the edge runtime dependency graph clean.
Step-by-Step Solution
The fix is to make telemetry dependencies non-reachable from edge code when telemetry is disabled. The safest pattern is to isolate telemetry into a separate module and load it only when needed.
1. Identify the static import chain
Use bundle-analyzer to confirm where @opentelemetry/api enters the graph. Look for an import path from edge runtime files into shared telemetry helpers.
// Problem pattern: static import in code reachable by edge bundles
import { trace } from '@opentelemetry/api'
export function trackSpan(name: string) {
return trace.getTracer('app').startSpan(name)
}
Even if this function is never called when telemetry is disabled, the package is still visible to the bundler.
2. Move telemetry code into a dedicated module
Create a file whose only responsibility is loading telemetry-specific logic.
// telemetry-runtime.ts
import { trace } from '@opentelemetry/api'
export function trackSpan(name: string) {
return trace.getTracer('app').startSpan(name)
}
This keeps the OpenTelemetry dependency isolated instead of mixing it into general-purpose edge-safe utilities.
3. Replace static imports with conditional dynamic loading
In code that may run in edge contexts, avoid importing the telemetry module at the top level. Load it only when telemetry is explicitly enabled.
// edge-safe-telemetry.ts
export async function trackSpanIfEnabled(name: string, telemetryEnabled: boolean) {
if (!telemetryEnabled) {
return null
}
const mod = await import('./telemetry-runtime')
return mod.trackSpan(name)
}
This pattern prevents @opentelemetry/api from being part of the default edge path when telemetry is off.
4. Keep edge-safe fallbacks dependency-free
If a caller expects a telemetry function to exist, provide a no-op fallback that does not import any telemetry package.
// telemetry.ts
export type SpanHandle = {
end: () => void
} | null
export async function startSpan(name: string, telemetryEnabled: boolean): Promise<SpanHandle> {
if (!telemetryEnabled) {
return {
end() {}
}
}
const { trackSpan } = await import('./telemetry-runtime')
return trackSpan(name)
}
This preserves the caller API while keeping the disabled path clean.
5. Avoid barrel exports that re-expose telemetry modules
A common mistake is exporting both edge-safe utilities and telemetry helpers from the same index file.
// Avoid this in shared index files
export * from './edge-safe-utils'
export * from './telemetry-runtime'
Instead, keep telemetry exports in a separate entrypoint so edge code cannot accidentally pull them in.
// Better
export * from './edge-safe-utils'
And import telemetry code only from server-specific or explicitly conditional paths.
6. Guard against server-only imports crossing into edge code
If the issue is inside framework or application internals, ensure server-only modules are not referenced by files used in middleware, route handlers configured for edge runtime, or other edge-executed entrypoints.
// Example split
// instrumentation.server.ts
import { trace } from '@opentelemetry/api'
export function createServerTracer() {
return trace.getTracer('server')
}
// instrumentation.edge.ts
export function createEdgeTracer() {
return null
}
Then resolve the correct implementation based on runtime boundaries rather than sharing a single import-heavy module.
7. Verify the fix with bundle analysis
Rebuild and rerun the analyzer. The edge bundle should no longer include @opentelemetry/api when telemetry is disabled.
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
reactStrictMode: true,
})
# Example verification flow
ANALYZE=true next build
If the package still appears, inspect whether another shared module still statically references telemetry.
8. Framework-level patch strategy
If you are fixing this inside a framework package such as create-next-app or related internals, the principle is the same:
- Remove top-level imports of @opentelemetry/api from files reachable by edge bundles.
- Split telemetry logic into a separate module.
- Use conditional dynamic import only when telemetry is enabled.
- Keep disabled telemetry paths as pure no-ops.
This is the most reliable way to let the bundler tree-shake or entirely exclude the dependency from edge output.
Common Edge Cases
- Dead code that still bundles: A runtime condition does not guarantee elimination. If the import is static, it may still be bundled.
- Re-exports from index files: Barrel files often reintroduce the dependency graph even after refactoring the main caller.
- Mixed runtime modules: A single helper used by both Node.js and edge can accidentally carry server-only dependencies into edge bundles.
- Analyzer confusion from transitive imports: The package may not be imported directly by edge code but can arrive through a shared tracing, logging, or instrumentation helper.
- Build-time constants not inlined: If telemetry flags are not statically known to the bundler, it may preserve both branches.
- Test passing but bundle still wrong: Functional tests only prove telemetry is disabled at runtime; they do not prove the package was removed from the compiled edge artifact.
FAQ
Why does @opentelemetry/api appear in the bundle if telemetry is disabled?
Because the bundler follows import statements, not just runtime behavior. If an edge-reachable module statically imports telemetry code, the package can still be included even when execution never reaches it.
Will a simple if (process.env.X) check fix it?
Not always. That only helps if the bundler can fully evaluate the condition at build time and safely remove the import path. A separate module with dynamic import is usually more reliable.
What is the safest pattern for edge-compatible telemetry?
Keep edge-facing modules dependency-light, use no-op fallbacks when telemetry is off, and isolate OpenTelemetry imports inside server-only or explicitly conditional modules.
By restructuring the import graph instead of only changing runtime checks, you ensure disabled telemetry stays truly absent from the edge bundle, which is the real fix behind this GitHub issue.