first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,106 @@
# `react-ui-metrics` — OpenTelemetry instruments for `@suaveplan/react-ui`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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 `<ReactUiProvider>` 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.