feat(telemetry): local move-preview adoption metrics (Phase 4)
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 15s
CI / ui (pull_request) Successful in 57s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m17s

Measure uptake of the client-side local move-preview accelerator (§5) so
adoption can be watched before defaulting it on: app cold starts, dictionary
loads by result (fetched / cache_hit / miss) and move previews by path
(local / network — the backend load shed).

A small best-effort client beacon (POST /metrics/local-eval, session-gated)
batches counter deltas and posts them on a 60s timer and when the app is
backgrounded — never on the gameplay path: the in-app counters are plain
in-memory increments, only the periodic flush touches the network and it is
fire-and-forget. The gateway folds each batch into three OTel counters
(local_eval_cold_start_total, local_eval_dict_load_total,
local_eval_preview_total), clamped against a spoofed inflation.

- gateway: counters + recordLocalEval + session-gated /metrics/local-eval handler
- ui: localeval-metrics accumulator/beacon; hooks in the dict loader, in
  Game.recompute and in bootstrap (skipped under the mock harness)
- caddy: route /metrics/* to the gateway
- docs: ARCHITECTURE §11; Grafana "Scrabble — Users" dashboard panels
This commit is contained in:
Ilia Denisov
2026-07-01 23:31:49 +02:00
parent 5f87b3fa43
commit 2e8fa83814
12 changed files with 254 additions and 3 deletions
+4
View File
@@ -8,6 +8,7 @@
import { Dawg } from './dawg';
import { dictKey, idbGetDawg, idbPutDawg, idbDelDawg, requestPersist } from './store';
import { gateway } from '../gateway';
import { noteDictFetched, noteDictCacheHit, noteDictMiss as noteDictMissMetric } from '../localeval-metrics';
import type { Variant } from '../model';
// Loaded readers by key. A dictionary is immutable, so an instance is reused for
@@ -31,6 +32,7 @@ let dictDisabled = false;
*/
export function noteDictMiss(): void {
if (!dictDisabled && ++misses >= MISS_LIMIT) dictDisabled = true;
noteDictMissMetric();
}
/** dictLoadingDisabled reports whether the bad-connection breaker has tripped for this
@@ -73,6 +75,7 @@ async function load(variant: Variant, version: string, key: string, signal?: Abo
if (cached) {
const reader = new Dawg(new Uint8Array(cached));
instances.set(key, reader);
noteDictCacheHit();
return reader;
}
} catch {
@@ -101,6 +104,7 @@ async function load(variant: Variant, version: string, key: string, signal?: Abo
return null; // not a dawg — do not cache it
}
instances.set(key, reader);
noteDictFetched();
void idbPutDawg(key, buf);
void requestPersist();
return reader;