# OpenSpec Change: enterprise-events-otel - **Change ID**: enterprise-events-otel - **Package**: `@suaveplan/events-otel` - **Category**: universal - **Target path**: `universal/events/events-otel` - **Status**: PROPOSED - **Date**: 2026-04-24 - **Reference spec**: `/mnt/s/web/suaveplan/enterprise/events-otel.md` --- ## Summary Deliver `@suaveplan/events-otel` on the genesis branch from scratch at enterprise quality. The package is an opt-in OpenTelemetry tracing companion for `@suaveplan/events`: it wraps each dispatch in a span, propagates W3C Trace Context across synchronous and async subscriber invocations, records event-level attributes on each span, and correctly marks listener exceptions as span errors. Attaches via the public `@suaveplan/events` interceptor API. Sampling-aware: honours the parent sampling decision and supports rule-based head sampling. --- ## Current-state audit (genesis) The package does not exist on genesis yet — fresh build. No prior source, no test results, no coverage data. --- ## Available workspace deps on genesis | Package | Role | |---|---| | `@suaveplan/events` (`workspace:^`) | Event bus and interceptor API | | `@suaveplan/error` (`workspace:^`) | Base class for `EventsOtelError`; `defineCodes` for `EVENTS_OTEL.*` codes | | `@suaveplan/types` (`workspace:^`) | `LoggerLike`, `Clock` injection interfaces | | `zod` (`catalog:forms`) | Zod schemas for all public option types | | `@opentelemetry/api` (`catalog:observability`) | `Tracer`, `Span`, `SpanStatusCode`, W3C context propagation | | `tslib` (`catalog:build`) | Runtime helpers | Dev deps: `vitest`, `@vitest/coverage-istanbul`, `fast-check` (all `catalog:testing`). --- ## Public API surface ### Setup ```ts // Attaches OTel tracing companion to an existing EventBus function attachEventTracing(bus: EventBusLike, options?: EventTracingOptions): EventTracingHandle interface EventTracingOptions { tracer?: Tracer // default: global tracer from @opentelemetry/api propagator?: TextMapPropagator // default: W3CTraceContextPropagator eventFilter?: (name: string) => boolean samplingRule?: SamplingRule clock?: Clock logger?: LoggerLike } interface EventTracingHandle { detach(): void } ``` ### Span attribute convention ```ts // All span attribute keys as stable constants const SPAN_ATTRIBUTES = { EVENT_NAME: "suaveplan.event.name", EVENT_ID: "suaveplan.event.id", EVENT_CATEGORY: "suaveplan.event.category", LISTENER_ID: "suaveplan.listener.id", BUS_ID: "suaveplan.bus.id", } as const // Span names const SPAN_NAMES = { DISPATCH: "events.dispatch", LISTENER: "events.listener", } as const ``` ### Sampling rule ```ts type SamplingRule = | { type: "always" } | { type: "never" } | { type: "rate"; rate: number } // 0.0–1.0 | { type: "event-name"; names: string[] } // sample only named events ``` ### Trace context envelope (for transport interop) ```ts // Serialize current trace context into a plain object for transport function serializeTraceContext(): Record // Restore trace context from a serialized envelope function deserializeTraceContext(headers: Record): Con```text ### Error handling ```ts class EventsOtelError extends SuaveplanError type EventsOtelErrorCode = | "EVENTS_OTEL_ATTACH_FAILED" | "EVENTS_OTEL_ALREADY_ATTACHED" | "EVENTS_OTEL_PROPAGATION_FAILED" function registerEventsOtelErrorCodes(): void ``` ### Zod schemas ```ts const EventTracingOptionsSchema: ZodSchema const SamplingRuleSchema: ZodSchema ``` --- ## New additions for genesis ### 1. W3C Trace Context propagation across async subscribers The interceptor captures the active `Context` at dispatch time. Each listener invocation runs with that context restored as the active context, so any nested OTel operations inside listeners are correctly parented to the dispatch span. ### 2. Listener exception → span error If a listener throws, the dispatch span's status is set to `SpanStatusCode.ERROR` with the exception recorded via `span.recordException(err)`. The error is re-thrown after recording. ### 3. Sampling-aware dispatch When `samplingRule` is `"rate"` or `"event-name"`, spans are only created for the matching subset. The companion checks the parent sampling decision first; if the parent is sampled, the rule is bypassed to maintain trace completeness. ### 4. `serializeTraceContext` / `deserializeTraceContext` These helpers allow `@suaveplan/events-transport` to inject and extract trace context from transport envelopes, enabling cross-context trace propagation without a direct package dependency on OTel internals. ### 5. Property-based tests (fast-check) - Parent-child relationship: every listener span is a child of its dispatch span - Context propagation under reentrancy: nested dispatches inside listeners produce correct span trees - Sampling rate accuracy: at sampling rate 0.5, sampled span count is within 5% of 50% over 10k dispatches --- ## File layout ```text universal/events/events-otel/ package.json tsconfig.json vitest.config.ts src/ index.ts error-codes.ts constants/ index.ts core/ attach-event-tracing/ attach-event-tracing.ts attach-event-tracing.md attach-event-tracing.types.ts attach-event-tracing.test.ts index.ts propagation/ trace-context/ trace-context.ts trace-context.md trace-context.test.ts index.ts sampling/ sampling-rule/ sampling-rule.ts sampling-rule.md sampling-rule.types.ts sampling-rule.test.ts index.ts tests/ integration.test.ts property.test.ts ``` --- ## Impact **Downstream packages**: `@suaveplan/events-transport` uses `serializeTraceContext`/`deserializeTraceContext` for cross-context propagation. Any application using `@suaveplan/events` with production tracing can add this companion without code changes to publishers or subscribers. **Workspace dep prerequisites**: `@suaveplan/events`, `@suaveplan/error`, `@suaveplan/types` must be published to Verdaccio. **Breaking changes**: none (new package).