2026-07-23 09:49:04 +00:00
|
|
|
# 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) |
|
2026-08-07 10:40:49 +00:00
|
|
|
| `session` | `session_summary` | one row per playback — see below. **Written only by `SessionFeedbackService._write`.** |
|
|
|
|
|
| `qa` | `qa_session` | `{sid, lines[], truncated?}` — the QA decision log for one playback (issue #92), off by default |
|
2026-07-23 09:49:04 +00:00
|
|
|
|
|
|
|
|
`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).
|
|
|
|
|
|
2026-08-07 10:40:49 +00:00
|
|
|
### `session_summary`, and the two numbers that are not the same
|
|
|
|
|
|
|
|
|
|
`{sessionId, title, probeKey, host, durationS, watchedS, positionS, stalls,
|
|
|
|
|
droppedFrames, renderedFrames, maxConsecutiveDropped, problemAtS[], promptOutcome,
|
|
|
|
|
rating?}` plus the player's provenance fields (`forcedAudio`, `sideloadedSubs`,
|
|
|
|
|
`resumed`, `seeks`, `seekStormMax`, codec/height/bitrate).
|
|
|
|
|
|
|
|
|
|
- **`watchedS` is elapsed watch-clock time. `positionS` is where playback got to.**
|
|
|
|
|
They differ whenever someone seeks, rewatches, or stares at a spinner — a session
|
|
|
|
|
that never started reads `watchedS: 48, positionS: 0`.
|
|
|
|
|
- **`sessionId` is on every row**, and is what joins a summary to its `qa_session`
|
|
|
|
|
lines and to the amber-api health record.
|
|
|
|
|
|
|
|
|
|
**Before 2026-08-07 this was written twice per Android playback**, by the native
|
|
|
|
|
player *and* by the feedback service, and the two disagreed: the extra row had no
|
|
|
|
|
`sessionId`, `title` or `probeKey`, and put the *position* in `watchedS`. In one
|
|
|
|
|
14-day window that was 542 rows of which only 294 were real. **Any aggregate over
|
|
|
|
|
this collection computed before that date counts Android sessions twice**, and
|
|
|
|
|
rows older than the fix still carry the duplicate. `sessionId:isset = true` is the
|
|
|
|
|
filter that excludes them.
|
|
|
|
|
|
2026-07-23 09:49:04 +00:00
|
|
|
## 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.
|