# `metrics.ts` — detector-chain OTel instruments _(no submodules)_ ## Purpose `metrics.ts` owns the OTel surface for the entire `@suaveplan/i18n-detector` package. It exposes lazily-built instruments (`tracer`, `matchTotal`, `durationMs`, `detectedTotal`, `chainExhaustedTotal`) plus an in-memory state ring so spec-conformance tests can assert on exact attribute payloads without standing up a full `MeterProvider` + `InMemoryMetricExporter` pipeline. All instruments are no-ops when no `MeterProvider` / `TracerProvider` is registered (the default), so consumers pay zero overhead unless they wire up the SDK. The cached singleton is reset between tests via `resetDetectorMetricsCache()`. ## Features - `getDetectorMetrics()` — lazily builds the tracer, counters, and histograms; caches the result. - `recordDetectorMatch(detector, locale)` — increments the aggregate `i18n.detector.match_total{detector}` counter AND the per-locale `i18n.detector.detected_total{detector,locale}` counter, mirroring both events into the in-memory state ring. - `recordDetectorDuration(detector, durationMs)` — records `i18n.detector.duration_ms{detector}` and pushes the sample into state. - `recordChainExhausted()` — increments `i18n.detector.chain.exhausted_total` on every sentinel return. - `getDetectorMetricsState()` / `resetDetectorMetricsState()` — test-only helpers for asserting + clearing the in-memory ring. - `resetDetectorMetricsCache()` — drops the singleton so a fresh meter can be built between tests. ## Usage: Basic Example ```ts import { getDetectorMetrics } from "@suaveplan/i18n-detector"; const m = getDetectorMetrics(); m.matchTotal.add(1, { detector: "cookie" }); ``` ## Usage: Advanced Example ```ts import { getDetectorMetricsState, recordDetectorMatch, resetDetectorMetricsState, } from "@suaveplan/i18n-detector"; beforeEach(() => resetDetectorMetricsState()); it("increments detected_total once per match", () => { recordDetectorMatch("cookie", "fr-FR"); const state = getDetectorMetricsState(); expect(state.detectedTotal).toEqual([ { attributes: { detector: "cookie", locale: "fr-FR" } }, ]); }); ``` ## API Reference - `getDetectorMetrics()` — returns the cached `CachedInstruments` object (`tracer`, `matchTotal`, `durationMs`, `detectedTotal`, `chainExhaustedTotal`). - `recordDetectorMatch(detector, locale)` — emits both aggregate and labelled counters. - `recordDetectorDuration(detector, ms)` — emits the duration histogram. - `recordChainExhausted()` — emits the chain-exhausted counter. - `getDetectorMetricsState()` / `resetDetectorMetricsState()` / `resetDetectorMetricsCache()` — test plumbing. - `DetectorMetricSample` / `DetectorMetricsState` — in-memory ring shape. ## Implementation Notes - The cached singleton has process-wide lifetime by design — metrics emitters are not per-chain because the chain is a thin orchestrator and detector names are globally unique. - `state.detectedTotal` carries `{detector, locale}` attributes; `state.matchTotal` carries `{detector}` only. Tests that need per-locale assertions read `detectedTotal`; tests that only care about which detector won read `matchTotal`. - All sample arrays are unbounded — `resetDetectorMetricsState` is the safety valve. Production code should never call the state-inspection helpers (they exist for spec conformance).