vitest-dev/vitest · error · Error

Failed to import custom OpenTelemetry SDK script

Error message

Failed to import custom OpenTelemetry SDK script (${options.sdkPath}): ${cause.message}

What it means

When a custom sdkPath is supplied in tracing options, Vitest dynamically imports that module to obtain an OpenTelemetry SDK with a default export exposing a shutdown method. If that import fails for any reason, the .catch at traces.ts:67-69 rethrows an error that embeds the underlying cause's message so the failure is attributable.

Solutions

  1. Reproduce the import standalone to see the real cause: node --input-type=module -e "import('<sdkPath>').then(console.log).catch(console.error)".
  2. Fix the syntax/runtime error reported in cause.message.
  3. Use an absolute path or a path resolvable from the Vitest process root.
  4. Drop sdkPath if you do not need a custom SDK; rely on the default OpenTelemetry setup.

Example fix

// before
experimentalTraces: { enabled: true, sdkPath: './otel.ts' }  // wrong path

// after
experimentalTraces: { enabled: true, sdkPath: './tracing/otel.ts' }
Defensive patterns

Strategy: try-catch

Validate before calling

async function sdkModuleLoads(p) {
  if (!p) return true
  try {
    const mod = await import(/* @vite-ignore */ p)
    return mod && typeof mod.default?.shutdown === 'function'
  } catch {
    return false
  }
}
// gate the option:
if (config.experimentalTraces?.sdkPath && !(await sdkModuleLoads(config.experimentalTraces.sdkPath))) {
  delete config.experimentalTraces.sdkPath
}

Type guard

function isOtelSdk(mod: unknown): mod is { default: { shutdown: () => Promise<void>; forceFlush?: () => Promise<void> } } {
  return !!mod && typeof mod === 'object'
    && typeof (mod as any).default?.shutdown === 'function'
}

Try / catch

try {
  await traces.waitInit()
} catch (e) {
  if (e instanceof Error && /Failed to import custom OpenTelemetry SDK/.test(e.message)) {
    console.warn('Custom OTel SDK not loaded; continuing without it:', e.message)
  } else {
    throw e
  }
}

Prevention

When it happens

Trigger: Setting experimentalTraces with an sdkPath that cannot be imported: the path does not resolve, the module throws during evaluation, it uses incompatible module syntax (CJS in an ESM dynamic-import context), or it references a missing dependency.

Common situations: Wrong relative path (not relative to project root); typo in the option; SDK file uses CommonJS only; SDK depends on a package not installed; path correct in dev but broken in CI due to a different CWD.

Related errors


AI-assisted analysis of vitest-dev/vitest@1fa9837ec2 (2026-08-11). Data as JSON: /api/errors/eba25364fc4bd3d2. Report an issue: GitHub.

Appendix: source

Thrown at packages/vitest/src/utils/traces.ts:68

  #initRecorded = false

  constructor(options: TracesOptions) {
    if (options.enabled) {
      const apiInit = import('@opentelemetry/api').then((api) => {
        const otel = {
          tracer: api.trace.getTracer(options.tracerName || 'vitest'),
          context: api.context,
          propagation: api.propagation,
          trace: api.trace,
          SpanKind: api.SpanKind,
          SpanStatusCode: api.SpanStatusCode,
        }
        this.#otel = otel
      }).catch(() => {
        throw new Error(`"@opentelemetry/api" is not installed locally. Make sure you have setup OpenTelemetry instrumentation: https://vitest.dev/guide/open-telemetry`)
      })
      const sdkInit = (options.sdkPath ? import(/* @vite-ignore */ options.sdkPath!) : Promise.resolve()).catch((cause) => {
        throw new Error(`Failed to import custom OpenTelemetry SDK script (${options.sdkPath}): ${cause.message}`)
      })
      this.#init = Promise.all([sdkInit, apiInit]).then(([sdk]) => {
        if (sdk != null) {
          if (sdk.default != null && typeof sdk.default === 'object' && typeof sdk.default.shutdown === 'function') {
            this.#sdk = sdk.default
          }
          else if (options.watchMode !== true && process.env.VITEST_MODE !== 'watch') {
            console.warn(`OpenTelemetry instrumentation module (${options.sdkPath}) does not have a default export with a "shutdown" method. Vitest won't be able to ensure that all traces are processed in time. Try running Vitest in watch mode instead.`)
          }
        }
      }).finally(() => {
        this.#initEndTime = performance.now()
        this.#init = null
      })
    }
  }

  public isEnabled(): boolean {

View on GitHub (pinned to 1fa9837ec2)