From 418f566603803b8b35070bdc14b246f484c51701 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 11:49:04 +0200 Subject: [PATCH] =?UTF-8?q?Add=20client=5Flogs=20collection=20for=20backgr?= =?UTF-8?q?ound=20device=20diagnostics=20(feedback=202=20=C2=A74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The family runs Amber on hardware I can't debug directly, so the app batches a low-volume diagnostic stream here: uncaught errors, player error codes, per-session playback summaries, and the audio-delay a user dials in to fix desync (the "a track falls behind" signal). - Migration 1788000000_client_logs.js: owner-create-only, superuser-read-only (list/view/update/delete = null); fields kind/event/message/meta/appVersion/ platform/device/ts + created. - client_logs.pb.js: nightly cron trims rows older than 14 days. - docs/telemetry-contract.md: event catalogue, how I query it, and the privacy/redaction guarantees (host-only, never a token or full URL). Co-Authored-By: Claude Fable 5 --- docs/telemetry-contract.md | 55 +++++++++++++++++++++ pb_hooks/client_logs.pb.js | 14 ++++++ pb_migrations/1788000000_client_logs.js | 65 +++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 docs/telemetry-contract.md create mode 100644 pb_hooks/client_logs.pb.js create mode 100644 pb_migrations/1788000000_client_logs.js diff --git a/docs/telemetry-contract.md b/docs/telemetry-contract.md new file mode 100644 index 0000000..446d3a3 --- /dev/null +++ b/docs/telemetry-contract.md @@ -0,0 +1,55 @@ +# Client diagnostics (`client_logs`) + +Background diagnostics from prod devices (feedback 2 §4). The family runs Amber +on hardware that can't be debugged directly, so the app batches a low-volume +stream of events to the `client_logs` collection. Designed to be invisible on +the device (in-memory ring buffer, slow best-effort flush, never on a playback +thread) and safe (owner-create-only, superuser-read-only, redacted at source). + +## Collection `client_logs` +Migration `1788000000_client_logs.js`. Fields: `user` (relation), `kind`, +`event`, `message`, `meta` (json), `appVersion`, `platform`, `device`, `ts` +(device clock), `created` (server clock). + +Rules: `create` = owner (`user = @request.auth.id`); list/view/update/delete = +**null** (superusers only). Retention: `client_logs.pb.js` cron trims rows +older than 14 days nightly. + +## Event kinds +| kind | event | meta | +|-----------|---------------------|--------------------------------------------------| +| `error` | `uncaught` | `{library}` — Flutter framework errors | +| `player` | `exo_error` | `{host, anime}` — native player error | +| `player` | `av_delay_applied` | `{audioMs, host}` — user dialed in an audio offset (the "a track falls behind" signal) | +| `session` | `session_summary` | `{host, durationS, watchedS, stalls, audioDelayMs, anime}` on player close | + +`host` is only the stream's `scheme://host` — **never** a full signed URL or an +addon token (redacted client-side in `TelemetryService.redactUrl` before write). + +## How Claude queries it +Superuser token (same as releases publishing), then filter/sort the collection: + +```bash +TOKEN=$(...auth as superuser...) +# recent player errors + desync signals across the fleet, newest first +curl -s "$PB/api/collections/client_logs/records?perPage=100&sort=-created&filter=$(python3 -c ' +import urllib.parse;print(urllib.parse.quote("kind='"'"'player'"'"'"))')" \ + -H "Authorization: $TOKEN" | python3 -m json.tool +``` +Useful filters: `kind='player'` (errors + desync), `event='av_delay_applied'` +(who's fighting sync and by how much — the reported "track falls behind" bug), +`kind='session' && meta.stalls > 3` (hitchy playback). Group by `device` / +`platform` / `appVersion` to see which hardware struggles. + +## What it deliberately does NOT capture (yet) +True per-track A/V PTS drift needs native instrumentation on both players +(ExoPlayer exposes one clock; mpv would need `audio-pts`/`video-pts` sampling). +v1 uses **proxies**: buffering-stall counts and the manual audio-delay the user +applies to fix desync — which directly answers "is a track falling behind, on +which sources/devices, and by how much". Deeper PTS sampling is a follow-up if +the proxies point somewhere specific. + +## Privacy / kill switch +On by default; a per-device Settings toggle ("Diagnostika") disables it and +drops the pending buffer. Only sends while signed in. No addon credential or +full stream URL ever leaves the device. diff --git a/pb_hooks/client_logs.pb.js b/pb_hooks/client_logs.pb.js new file mode 100644 index 0000000..a83d480 --- /dev/null +++ b/pb_hooks/client_logs.pb.js @@ -0,0 +1,14 @@ +/// + +// Retention for the client_logs diagnostics (feedback 2 §4). These are for +// live debugging, not an archive — trim anything older than 14 days daily so +// the collection can't grow unbounded. Runs in PocketBase's cron scheduler. +cronAdd("trimClientLogs", "0 4 * * *", () => { + try { + const cutoff = new Date(Date.now() - 14 * 24 * 60 * 60 * 1000) + .toISOString().replace("T", " ").slice(0, 19) + ".000Z" + const stale = $app.findRecordsByFilter( + "client_logs", "created < {:c}", "created", 500, 0, { c: cutoff }) + for (const r of stale) $app.delete(r) + } catch (_) { /* nothing to trim / collection absent */ } +}) diff --git a/pb_migrations/1788000000_client_logs.js b/pb_migrations/1788000000_client_logs.js new file mode 100644 index 0000000..1992efa --- /dev/null +++ b/pb_migrations/1788000000_client_logs.js @@ -0,0 +1,65 @@ +/// + +// Background diagnostics from prod devices (feedback 2 §4). +// +// The family runs Amber on hardware I can't attach a debugger to, so the app +// batches a small stream of diagnostic events here: uncaught errors, player +// error codes, per-session playback summaries (host/resolution/stalls), and — +// the signal for the "a track falls behind" reports — the audio/subtitle delay +// the user dialed in to fix desync, plus stall counts. It's deliberately +// low-volume and best-effort so it never slows the device (see TelemetryService). +// +// Privacy/security: rows are owned by the authenticated user (create-only for +// them, no read/update/delete), and the client redacts addon tokens / stream +// URLs to their host before anything is written — never a credential or a full +// signed URL. Reading is superusers-only (that's how I query it). + +migrate((app) => { + const users = app.findCollectionByNameOrId("users") + + const logs = new Collection({ + type: "base", + name: "client_logs", + // Create-only for the owner; nobody but a superuser can read them back. + listRule: null, + viewRule: null, + createRule: "@request.auth.id != '' && user = @request.auth.id", + updateRule: null, + deleteRule: null, + fields: [ + { + type: "relation", + name: "user", + required: true, + collectionId: users.id, + cascadeDelete: true, + maxSelect: 1, + }, + // "error" | "player" | "session" | "info" — a coarse bucket for filtering. + { type: "text", name: "kind", required: true, max: 20 }, + // Short event slug, e.g. "uncaught", "exo_error", "session_summary", + // "av_delay_applied". + { type: "text", name: "event", required: true, max: 80 }, + // Human-ish message / error text (already redacted client-side). + { type: "text", name: "message", max: 4000 }, + // Structured payload (redacted): host, resolution, stalls, delayMs, etc. + { type: "json", name: "meta", maxSize: 20000 }, + // Device/app context so I can tell platforms apart without a debugger. + { type: "text", name: "appVersion", max: 40 }, + { type: "text", name: "platform", max: 40 }, + { type: "text", name: "device", max: 120 }, + // Client-set event time (device clock); `created` is the server clock. + { type: "date", name: "ts" }, + { type: "autodate", name: "created", onCreate: true }, + ], + indexes: [ + "CREATE INDEX `idx_client_logs_created` ON `client_logs` (`created`)", + "CREATE INDEX `idx_client_logs_kind` ON `client_logs` (`kind`)", + ], + }) + app.save(logs) +}, (app) => { + try { + app.delete(app.findCollectionByNameOrId("client_logs")) + } catch (_) { /* already gone */ } +}) -- 2.45.2