n8n-io/n8n · error · Error

Telemetry tracer must implement startSpan() and startActiveS

Error message

Telemetry tracer must implement startSpan() and startActiveSpan().

What it means

The Telemetry builder accepts a custom tracer via .tracer(), but it must be structurally compatible with the OpenTelemetry Tracer interface. The runtime duck-types the object via Reflect.get and requires both `startSpan` and `startActiveSpan` to be functions before it can wrap it with @ai-sdk/otel's LegacyOpenTelemetry integration. Any object missing either method is rejected at build time rather than silently producing an uninstrumented agent.

Source

Thrown at packages/@n8n/agents/src/sdk/telemetry.ts:136

			return wrapped;
		},
	});
}

function isOpenTelemetryTracer(value: unknown): value is Tracer {
	return (
		value !== null &&
		typeof value === 'object' &&
		typeof Reflect.get(value, 'startSpan') === 'function' &&
		typeof Reflect.get(value, 'startActiveSpan') === 'function'
	);
}

async function createAiSdkOpenTelemetryIntegrationFactory(
	tracer: OpaqueTracer,
): Promise<(metadata: Record<string, AttributeValue> | undefined) => AiSdkTelemetry> {
	if (!isOpenTelemetryTracer(tracer)) {
		throw new Error('Telemetry tracer must implement startSpan() and startActiveSpan().');
	}

	const { LegacyOpenTelemetry } = await import('@ai-sdk/otel');
	return (metadata) =>
		new LegacyOpenTelemetry({ tracer: createMetadataEnrichedTracer(tracer, metadata) });
}

/**
 * Create an OTel tracer + provider by dynamically importing OTel packages.
 * This keeps OTel as a true optional peer dependency — the packages are only
 * loaded when .otlpEndpoint() is actually called.
 */
async function createOtlpTracer(endpoint: string): Promise<{
	tracer: OpaqueTracer;
	provider: OpaqueTracerProvider;
}> {
	const { NodeTracerProvider } = (await import('@opentelemetry/sdk-trace-node')) as {
		NodeTracerProvider: new (config?: {

View on GitHub (pinned to 5ac6606e81)

Solutions

  1. Pass the Tracer, not the TracerProvider — call provider.getTracer('my-name') and pass that result to .tracer().
  2. If you only have an OTLP endpoint URL, drop .tracer() and use .otlpEndpoint(url) instead; the builder will construct a compliant tracer for you.
  3. Verify the object structurally: ensure both obj.startSpan and obj.startActiveSpan are functions before calling .tracer(obj).
  4. In tests, build a minimal tracer stub that implements both startSpan() and startActiveSpan() returning no-op span objects.

Example fix

// before
const provider = new NodeTracerProvider();
new Telemetry().tracer(provider).build(); // wrong: provider, not tracer

// after
const provider = new NodeTracerProvider();
const tracer = provider.getTracer('@n8n/agents');
new Telemetry().tracer(tracer).build();
Defensive patterns

Strategy: type-guard

Validate before calling

import type { Telemetry } from '@n8n/agents';

function isOpenTelemetryTracer(value: unknown): boolean {
  return (
    value !== null &&
    typeof value === 'object' &&
    typeof Reflect.get(value as object, 'startSpan') === 'function' &&
    typeof Reflect.get(value as object, 'startActiveSpan') === 'function'
  );
}

if (!isOpenTelemetryTracer(myTracer)) {
  throw new Error('Expected an OTel Tracer with startSpan/startActiveSpan');
}
new Telemetry().tracer(myTracer);

Type guard

function isOpenTelemetryTracer(value: unknown): value is { startSpan: Function; startActiveSpan: Function } {
  return (
    value !== null &&
    typeof value === 'object' &&
    typeof Reflect.get(value as object, 'startSpan') === 'function' &&
    typeof Reflect.get(value as object, 'startActiveSpan') === 'function'
  );
}

Try / catch

try {
  const built = await new Telemetry().tracer(candidate).build();
} catch (err) {
  if (err instanceof Error && err.message.includes('startSpan() and startActiveSpan()')) {
    // fall back to OTLP endpoint auto-construction
    telemetry = new Telemetry().otlpEndpoint(process.env.OTLP_ENDPOINT!);
  } else throw err;
}

Prevention

When it happens

Trigger: Calling new Telemetry().tracer(someObject).build() (or passing the builder to an Agent which lazy-builds) where someObject lacks startSpan or startActiveSpan. Commonly hit by passing a TracerProvider instead of the Tracer returned by provider.getTracer(...), by passing a NoopTracer stub, or by passing a mock tracer in tests that only implements one method.

Common situations: Confusing the OTel TracerProvider with the Tracer (the provider's getTracer() returns the tracer); wiring a LangSmith or custom observability shim that exposes a different span API; version drift where @opentelemetry/api's Tracer interface changed; passing a partial mock during unit testing.

Related errors


AI-assisted analysis of n8n-io/n8n@5ac6606e81 (2026-08-12). Data as JSON: /api/errors/5034530a50200dae. Report an issue: GitHub.