feat(gateway): unsupported-engine telemetry beacon + Grafana counter
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 19s
CI / ui (pull_request) Successful in 1m5s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m40s
The index.html boot guard now fires a fire-and-forget beacon (POST /telemetry/unsupported)
when it turns a client away on the unsupported-engine screen, so the owner can see — on the
Users dashboard beside "app opens" — how many real clients hit it and on which engines.
- Client: navigator.sendBeacon (fetch fallback) with a localStorage dedup keyed by app version
+ reason + Chromium, so a user reopening the app is one report, not ten.
- Gateway: a new unauthenticated POST /telemetry/unsupported handler (the client never booted,
so it carries no session), per-IP public-limited and body-capped, mirroring the export-download
route. It folds the beacon into the OTel counter unsupported_engine_total {reason =
no_bigint/no_proxy/boot_error/other, chromium}, with reason allow-listed and the Chromium major
reduced to a bounded range (normalizeUnsupported) so a spoofed beacon cannot inflate the metric
cardinality; the full user agent is logged, not labelled.
- Caddy: /telemetry/* added to the @gateway matcher (else it falls to the landing catch-all).
- Grafana: two panels on the Scrabble — Users dashboard (by reason, by Chromium major).
- Docs: ARCHITECTURE.md §11.
Tests: recordUnsupportedEngine (metric split), normalizeUnsupported (cardinality bounding), and
the handler route end to end (204 / 405 / counter). go build+vet+test and gofmt clean; ui
check/build/e2e green; the client beacon + dedup verified against a forced hard-gate (BigInt
removed) — one POST, deduped on reopen, correct reason/chromium/version labels.
This commit is contained in:
+34
-3
@@ -97,9 +97,40 @@
|
||||
}
|
||||
|
||||
var shown = false;
|
||||
function show(reason) {
|
||||
// Fire-and-forget beacon so the gateway can count who hits this screen (Grafana). Deduped
|
||||
// in localStorage by app version + reason + Chromium, so a user reopening the app ten times
|
||||
// is one report. sendBeacon (Chrome 39+, present on the engines that reach here) with a
|
||||
// fetch fallback; both are best-effort and never block or throw.
|
||||
function beacon(code) {
|
||||
try {
|
||||
var cm = /Chrom(?:e|ium)\/(\d+)/.exec(ua);
|
||||
var chromium = cm ? cm[1] : '';
|
||||
var sig = VERSION + '|' + code + '|' + chromium;
|
||||
try {
|
||||
if (localStorage.getItem('scrabble_unsupp') === sig) return;
|
||||
} catch (e) {}
|
||||
var payload = JSON.stringify({ reason: code, chromium: chromium, version: VERSION, ua: ua });
|
||||
var sent = false;
|
||||
try {
|
||||
if (nav.sendBeacon) sent = nav.sendBeacon('/telemetry/unsupported', new Blob([payload], { type: 'application/json' }));
|
||||
} catch (e) {}
|
||||
if (!sent && typeof fetch === 'function') {
|
||||
try {
|
||||
fetch('/telemetry/unsupported', { method: 'POST', body: payload, headers: { 'Content-Type': 'application/json' }, keepalive: true }).catch(function () {});
|
||||
sent = true;
|
||||
} catch (e) {}
|
||||
}
|
||||
if (sent) {
|
||||
try {
|
||||
localStorage.setItem('scrabble_unsupp', sig);
|
||||
} catch (e) {}
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
function show(reason, code) {
|
||||
if (shown) return;
|
||||
shown = true;
|
||||
beacon(code);
|
||||
var root = el('div', 'position:fixed;left:0;top:0;right:0;bottom:0;z-index:2147483647;overflow:auto;' +
|
||||
'-webkit-overflow-scrolling:touch;background:#0f1115;color:#e8eaed;box-sizing:border-box;padding:24px;' +
|
||||
'font:16px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Arial,sans-serif;');
|
||||
@@ -179,7 +210,7 @@
|
||||
|
||||
// 1 + 2: the gate.
|
||||
if (hardMissing.length) {
|
||||
show((RU ? 'нет: ' : 'missing: ') + hardMissing.join(', '));
|
||||
show((RU ? 'нет: ' : 'missing: ') + hardMissing.join(', '), hardMissing.indexOf('BigInt') >= 0 ? 'no_bigint' : 'no_proxy');
|
||||
} else if (softMissing) {
|
||||
document.write('<script src="polyfills.js"><\/script>');
|
||||
}
|
||||
@@ -201,7 +232,7 @@
|
||||
note(r && (r.stack || r.message) ? r.stack || r.message : String(r));
|
||||
});
|
||||
setTimeout(function () {
|
||||
if (!window.__booted && bootErr && !shown) show((RU ? 'ошибка запуска: ' : 'boot error: ') + bootErr);
|
||||
if (!window.__booted && bootErr && !shown) show((RU ? 'ошибка запуска: ' : 'boot error: ') + bootErr, 'boot_error');
|
||||
}, 8000);
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user