Files
2026-05-27 23:29:53 +01:00

3.7 KiB

Context

Every genesis package is required to ship @opentelemetry/api instrumentation (counters, histograms, spans) on its public entrypoints, with a no-op fallback when no SDK is registered (openspec/specs/testing/spec.md "Observability" requirement; CLAUDE.md Quality gates). The audit at /mnt/s/web/genesis/docs/superpowers/specs/2026-04-25-test-suite-audit.md §1 and §4 records two related defects: there is no fixture that exercises the export path of that instrumentation, and the test runner itself emits no telemetry — so flaky-test triage has nothing to correlate against. This change addresses both at once because the runner-side requirement is meaningless without a collector to receive its export, and the collector fixture is hard to justify without a real producer driving it.

Goals / Non-Goals

Goals:

  • A ServiceFixture plugin for otel/opentelemetry-collector-contrib whose lifecycle, env-bridge, and health contract match the conventions established by the existing testing-integration-* plugins (CLAUDE.md package list).
  • A runner reporter at @suaveplan/testing/reporters/otel that wraps each it/test in a span, records assertion failures as span events, and serialises the full span tree of failed tests to disk + Garage S3.
  • Deterministic span ids and timestamps under test (the reporter accepts Clock and IdGenerator from @suaveplan/types so OTel-of-OTel-tests is itself reproducible).
  • Zero behaviour change when OTEL_EXPORTER_OTLP_ENDPOINT is unset.

Non-Goals:

  • Replacing the per-package @opentelemetry/api instrumentation. The reporter wraps tests, not source code.
  • Shipping a Jaeger / Tempo / Honeycomb backend — the collector fixture supports logging, memory, jaeger, and file exporters; downstream backend choice is a consumer concern.
  • Sampling. Every test span is exported; tail-sampling is the consumer's job in their own collector config.

Decisions

  • Memory exporter is the assertion path. The getSpans({ filter }) helper queries the collector's in-memory exporter rather than parsing logging exporter stdout — assertion-grade reliability is required.
  • Trace sink is opt-in. The reporter serialises failure traces unconditionally to playwright-report/traces/<spec>.json (zero infra), but only uploads when TESTING_TRACE_SINK_URL is set. This avoids surprise S3 traffic from local runs.
  • Reporter ships in @suaveplan/testing (not in the new plugin) so that runner-side OTel works without taking the testcontainers dep — testing-infrastructure rule (CLAUDE.md rule 8) forbids @suaveplan/* runtime deps in @suaveplan/testing but not external @opentelemetry/* packages, which are already permitted.
  • Config synthesis vs configFile. Most consumers want "OTLP HTTP receiver + memory exporter"; the plugin synthesises that case from the typed config object. Power users pass configFile and the plugin mounts it verbatim — no template language.
  • Image pin. Tag 0.<minor> initially (matches existing plugin practice); tasks.md tracks the digest-pin migration as a follow-up so this change does not block on a registry lookup.

Risks / Trade-offs

  • Collector startup latency can dominate fast unit-test suites. Mitigated by skipIfUnavailable() falling through when the fixture cannot start (consistent with other testing-integration-* plugins) and by recommending nightly-only export in CI.
  • Memory-exporter retention is bounded; long suites can drop early spans. The plugin documents the bound and exposes a flushSpans() method that pages through the memory exporter at suite teardown.
  • Failure-trace upload introduces a network dependency on red runs. Mitigated by best-effort upload (failures logged, not thrown) and by the env-var gate.