# `react-ui-metrics` — OpenTelemetry instruments for `@suaveplan/react-ui` _(no submodules)_ ## Purpose Owns every OpenTelemetry counter, histogram, and span emitted from the `@suaveplan/react-ui` chassis. The module wraps `metrics.getMeter()` / `trace.getTracer()` from `@opentelemetry/api` so consumers wiring an SDK get full observability with zero overhead when no SDK is registered (the default behaviour). The instruments mirror the telemetry contract documented in the design proposal — theme switches, pack lifecycle, locale-bundle loads, themed-variants resolution, toast notifications, and the `react_ui.provider.mount` span. ## Features - Lazy, cached instrument construction via `getReactUiMetrics()` so a single meter is shared by every production path - Discrete recorder helpers per signal — `recordThemeSwitch`, `recordPackRegister`, `recordPackUnregister`, `recordPackVersionMismatch`, `recordLocaleBundleLoad`, `recordThemedVariantsResolve`, `recordToastShown` - In-memory `state` ring exposed via `getReactUiMetricsState()` so `*.spec-conformance.test.ts` files can assert exact attribute payloads without standing up a full SDK pipeline - `startProviderMountSpan` / `startPackRegisterSpan` returning the OTel `Span` so call sites can `end()` once mount work completes - `resetReactUiMetricsState()` for test isolation between cases ## Usage: Basic example ```ts import { recordThemeSwitch } from "@suaveplan/react-ui/observability"; recordThemeSwitch({ from: "light", to: "dark", durationMs: 12.5 }); ``` ## Usage: Advanced example ```ts import { getReactUiMetrics, getReactUiMetricsState, resetReactUiMetricsState, startProviderMountSpan, } from "@suaveplan/react-ui/observability"; // On a `` mount path the chassis records the span // from React's effect: const span = startProviderMountSpan({ theme: "light", packCount: 2, locale: "en-GB", }); try { await loadLocaleBundle(); } finally { span.end(); } // In a test: resetReactUiMetricsState(); const meter = getReactUiMetrics(); expect(meter).toBeDefined(); expect(getReactUiMetricsState().themeSwitchTotal).toHaveLength(0); ``` ## API Reference - `getReactUiMetrics()` — returns the cached `{ counters, histograms, tracer }` bundle. Idempotent. - `recordThemeSwitch({ from, to, durationMs })` — increments `react_ui.theme.switch_total{from,to}` and records the matching histogram. - `recordPackRegister({ packName, packVersion })` — increments `react_ui.pack.register_total{pack.name, pack.version}`. - `recordPackUnregister({ packName })` — increments `react_ui.pack.unregister_total{pack.name}`. - `recordPackVersionMismatch({ packName, packVersion, reactUiVersion })` — increments `react_ui.pack.version_mismatch_total` with the three attributes. - `recordLocaleBundleLoad({ locale, outcome, durationMs })` — increments the load counter and records the duration histogram. - `recordThemedVariantsResolve({ component, durationMs })` — records the themed-variants cache-miss histogram. - `recordToastShown({ kind })` — increments `react_ui.toast.shown_total`. - `startProviderMountSpan({ theme, packCount, locale })` / `startPackRegisterSpan({ packName })` — open OTel spans; the caller owns `span.end()`. - `getReactUiMetricsState()` — snapshot of the in-memory ring. - `resetReactUiMetricsState()` — clears the ring and forces the cached instruments to rebuild on next access. ## Implementation notes The instruments are constructed lazily inside `getReactUiMetrics()` so no work happens at import time. The recorder helpers double-write through the OTel API (so a wired SDK collects samples) AND into a local state ring (so unit tests can assert payloads without SDK plumbing). The state ring lives in a module-level `const` and is cleared by `resetReactUiMetricsState()`; every test that exercises the recorders should call the reset in its `afterEach`. The tracer is fetched once inside `getReactUiMetrics()` rather than per-span so a single tracer identity flows through every span the package opens.