Skip to content

Recording tracer

RecordingTracer is the test counterpart to OtelTracerAdapter. Instead of exporting spans, it stores them in memory — tests assert on the recorded spans.

import { TestKit, RecordingTracer, TracingExtensionId } from 'actor-ts';
const tracer = new RecordingTracer();
const tk = TestKit.create();
tk.system.extension(TracingExtensionId).configure({ tracer });
// ... run the actor ...
ref.tell({ kind: 'process' });
await probe.expectMsg(...);
// Assert on recorded spans:
const spans = tracer.recordedSpans();
expect(spans).toHaveLength(1);
expect(spans[0].name).toBe('actor.receive');
expect(spans[0].attributes['actor.path']).toContain('worker');

Each RecordedSpan:

interface RecordedSpan {
name: string;
context: SpanContext;
parent?: SpanContext;
attributes: Record<string, AttributeValue>;
events: Array<{ name: string; attrs: ... }>;
status: { status: 'ok' | 'error'; message?: string };
startTimeMs: number;
endTimeMs: number;
ended: boolean;
kind: SpanKind;
}

You see what attributes were set, when the span started + ended, its kind, its parent (for verifying causality chains), and its final status.

class RecordingTracer implements Tracer {
// ... full Tracer interface ...
recordedSpans(): RecordedSpan[]; // all spans, including unended
finishedSpans(): RecordedSpan[]; // spans where end() was called
reset(): void; // clear recorded state
}

recordedSpans returns every span the tracer was asked to create. finishedSpans filters to those that ended — useful to detect “started a span and never ended it” bugs.

”Did the framework auto-span this actor’s message?"

Section titled “”Did the framework auto-span this actor’s message?"”
ref.tell({ kind: 'work' });
await probe.expectMsg(...);
const spans = tracer.finishedSpans();
const receive = spans.find(s => s.name === 'actor.receive' && s.attributes['actor.path']?.includes('worker'));
expect(receive).toBeDefined();
expect(receive!.status.status).toBe('ok');

"Did my custom span fire with the right attributes?"

Section titled “"Did my custom span fire with the right attributes?"”
const orderSpan = tracer.finishedSpans().find(s => s.name === 'place-order');
expect(orderSpan).toBeDefined();
expect(orderSpan!.attributes['order.id']).toBe('o-1');
expect(orderSpan!.attributes['order.amount']).toBe(42);
const spans = tracer.finishedSpans();
const parent = spans.find(s => s.name === 'http-request');
const child = spans.find(s => s.name === 'db-query');
expect(child!.parent?.traceId).toBe(parent!.context.traceId);

Useful for verifying trace causality — parent context correctly propagated to child spans.

beforeEach(() => tracer.reset());

Without reset, recorded spans accumulate across tests. Reset on each test to keep assertions on the current test’s spans only.

Recording is cheap — appending to an array. But spans accumulate in memory. For long test runs, periodically reset to avoid memory growth.

  • Tracer API — the interface this implements.
  • OTel adapter — the production alternative.
  • Actor tracing — what the framework auto-spans, observable via this tracer.
  • TestKit — pair with the recording tracer for assertion-friendly tests.

The RecordingTracer API reference covers the full surface.