The documented meta was the THIN row -- the leftover second writer that amber-app just removed. It also never said that watchedS (elapsed watch clock) and positionS (where playback reached) are different numbers, which is exactly the distinction that made a never-started session readable. Records that aggregates over rows before 2026-08-07 double-count Android sessions, and gives the filter that excludes them.
4.3 KiB
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 |
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 |
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).
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).
watchedSis elapsed watch-clock time.positionSis where playback got to. They differ whenever someone seeks, rewatches, or stares at a spinner — a session that never started readswatchedS: 48, positionS: 0.sessionIdis on every row, and is what joins a summary to itsqa_sessionlines 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.
How Claude queries it
Superuser token (same as releases publishing), then filter/sort the collection:
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.