Protocol 15: one shared session log on the server, chat never logged, Mark a problem

Every app streams its redacted diagnostic lines (server time, batched
every 10 s, gzip, buffered while offline) to POST /l/; the server merges
them with its own into session-YYYY-MM-DD.log in time order, tagged by
who. Opt-out in Settings > About; Mark a problem (Ctrl+Shift+B, the
player menus, About) writes a marker with an optional note. Copy
diagnostics can include the server's session log. Chat text never
reaches any log.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
foxyzandClaude Opus 5.5 committed 2026-10-12 19:41:06 +03:00
1 parent 4ab37c55cf
commit 7487b4deb1
29 files changed
+1380 -47

No files matched your search

+47 -3
View File
@@ -1517,9 +1517,14 @@ dedicated server (`package:logging` loggers named by area: `relay`,
`--config`, else `logs` next to the binary (systemd:
`$STATE_DIRECTORY/logs`), else `~/.cache/watch-together/logs`; it
prints the folder at start. Both also print every line to stdout.
The session log (below) sits next to them: the hosting app's server
writes it in the app's logs folder (not for a solo session), the
dedicated one in its log folder.
- **Files** (`LogFiles`): one per day, the next `.1.log`, `.2.log` once a
file passes 10 MiB; files older than 7 days, then the oldest beyond
100 MiB in all, are deleted whenever one is opened. A line is
100 MiB in all (the app's or server's files and the session log's
together, the oldest of either first, never the other writer's file of
today), are deleted whenever one is opened. A line is
`2026-10-12 18:44:27.202 LEVEL logger: message`, the error and stack
trace on the lines below. Writes are synchronous appends (nothing lost
in a crash). Lines from before the folder is known (the app finds it
@@ -1527,7 +1532,7 @@ dedicated server (`package:logging` loggers named by area: `relay`,
- **Redaction** (`redactLog`): every line, in one place
(`formatLogLine`), before console or file: URL query strings cut to
their parameter names (signed CDN links, Jellyfin `api_key`),
`user:pass@` in URLs, member tokens in `/r /m /j /a /i` paths, and the
`user:pass@` in URLs, member tokens in `/r /m /j /a /i /l` paths, and the
values of passwords, secrets, tokens, API keys, cookies,
`Authorization` / `X-Emby-*` headers (`name=value`, `"name":"value"`,
`Name="value"`, `Name: value`) and `Bearer` credentials. Code logs
@@ -1554,12 +1559,51 @@ dedicated server (`package:logging` loggers named by area: `relay`,
source (media opened and how long it took, channel up and down,
reconnects, upstream refusals with the header names sent, re-resolves
of expired format URLs).
- **Never logged**: chat text (the server relays it without a log line,
the app does not log it), and no user-typed free text other than a
problem mark's note. Names, media titles and links (query cut) are.
- **The session log** (`packages/server/lib/src/session_log.dart`,
`packages/core/lib/src/diag_share.dart`): one shared log per server so
a problem can be read afterwards from every side. While in a session
the app (`DiagUploader`, owned by the coordinator) keeps its own
records (not its hosted server's `server.*`, which the server logs
itself), redacted, and sends them every 10 s from the session's 1 s
ticker and when it leaves (before its token goes, at most 1.5 s), as
gzip batches to `POST /l/<sid>/<token>` (PROTOCOL
"Diagnostics"), timestamps moved to the server's clock with the ping
offset (`ClockSync`), the app version in every batch. Until it can
send (reconnecting, no clock sample yet) it keeps up to 2 MiB, the
oldest dropped and counted. The server (`SessionLog`) takes every
member's lines and its own records, holds them 15 s and writes them in
server-time order to `session-YYYY-MM-DD.log` (day and time on the
server's clock; a line later than that, a reconnect's backlog, goes in
with its own time, a little out of order). A line is
`HH:MM:SS.mmm [who] logger: message` (`LEVEL logger` from WARNING up),
`who` = `name (id)` or `server`, continuation lines indented; every
line is redacted again on the server. The server notes once per member
`Bob (m2) shares diagnostics (<version>)` or `Bob (m2) doesn't share
diagnostics`, and `N lines dropped` when the app's buffer overflowed.
Limits and rate caps: PROTOCOL "Diagnostics"; a refused batch stays in
the app's buffer for the next try.
- **Opting out**: Settings → About → "Share diagnostics with the server"
(default on). Off: no lines are kept or sent; one batch per connection
says so (`share:false`). The local log is unchanged.
- **Mark a problem**: Ctrl+Shift+B (`AppKeys`), the player's ⚙ card and
Settings → About open a small card with an optional note (200
characters, one line); the mark goes at once with the lines waiting,
opted out or not, and the server writes `[who] === PROBLEM marked by
<name>: <note> ===` at its time. The app's own log gets
`=== PROBLEM marked: <note> ===` (logger `problem`, never sent). Out
of a session only the app's log gets it. The toast says which.
- **Getting it**: Settings → About → "Copy diagnostics" puts the app and
OS versions, mpv/ffmpeg and yt-dlp versions, whether yt-dlp has
cookies, and the last 6 hours of the log (up to 4 MiB) on the
clipboard, already redacted, and saves the same text as
`diagnostics-<time>.txt` in the logs folder (kept 7 days); "Open logs
folder" opens it.
folder" opens it. In a session it first asks whether to add the
server's session log of the same 6 hours (`GET /l/...`). Any member
may read the session log, not only the host: friends trust each other,
and it is redacted and holds no chat.
## Health reporting
+14 -1
View File
@@ -145,7 +145,20 @@ on Linux (on Windows under `%APPDATA%`; Settings → About → "Open logs
folder" finds it anywhere), with the hosted server's lines
in the same file; a dedicated server prints its folder at start
(`--log-dir`). Users send theirs with Settings → About → "Copy
diagnostics". For a "can't play" report, grep both sides around the
diagnostics".
Start with the session log when there is one: `session-YYYY-MM-DD.log`
in the server's folder (the hosting app's logs folder, or the dedicated
server's `--log-dir`), every member's lines and the server's merged in
server-clock order, each tagged `[name (id)]` or `[server]`. Any member
gets it with "Copy diagnostics" in a session. Find
`=== PROBLEM marked by` first (a member pressed Ctrl+Shift+B), then read
the lines around it from every side; `grep '\[Bob (m2)\]'` follows one
member. `[server] diag:` lines say who shares (with their app version)
and who doesn't, so a missing side is explained. Times are the server's
clock; a member's local log (`app-*.log`) is on its own clock.
For a "can't play" report, grep both sides around the
time: `swap by` (what went up and how), `relay channel` (when the
source dialled in), `media:` (each viewer's requests: status and time to
headers), `mpv:` (`cannot open the media` and ffmpeg's lines), `yt-dlp:`
+43 -3
View File
@@ -1,4 +1,4 @@
# Protocol (proto = 14)
# Protocol (proto = 15)
Wire format only. Behavior (gate, clock, timeouts) is in
[ARCHITECTURE.md](ARCHITECTURE.md). Source of truth:
@@ -26,12 +26,14 @@ with different `proto` refuse each other at `hello` (`proto mismatch`).
"Jellyfin media" below). `/r/<sid>/<token>/...` is the relay of a
member's media (see "Relay" below). `GET /a/<sid>/<token>/<hash>` serves
a member's picture (see "Avatars" below), `GET /i/<sid>/<token>/<hash>`
the current media's art (see "Media art" below).
the current media's art (see "Media art" below). `POST` and `GET
/l/<sid>/<token>` send and read the session log (see "Diagnostics"
below).
## Client to server
```
{"t":"hello","proto":14,"secret":"","user":{"name":"","avatar":""},
{"t":"hello","proto":15,"secret":"","user":{"name":"","avatar":""},
"media":{"name":"","size":0,"duration":0,"url":null},"hostKey":"...",
"resume":"<token>"}
{"t":"ready","ready":true,"source":"manual"} source: manual | auto
@@ -467,3 +469,41 @@ state "queue":[<item>,...], "queuePlay":"q3"
- `state.queue` (omitted when empty) and `state.queuePlay` (omitted when
none) ride in every state, `welcome.state` included. The first item's
`media.relay` shows its warm relay (see "Relay").
## Diagnostics (proto 15)
Each app sends its log lines to the server, which merges them with its
own into the session log (behavior, file and format: ARCHITECTURE.md,
"Logging").
```
POST /l/<sid>/<token> content-type application/gzip, body = gzip of:
{"v":"Starsync 1.4.0 on linux","share":false,
"lines":[{"t":1767225600123,"l":"WARNING","n":"mpv","m":"..."}],
"dropped":12,"marks":[{"t":1767225600500,"note":"..."}]}
GET /l/<sid>/<token>?hours=6
```
- `POST`: one batch. Every field is optional. `v`: the app and version,
noted once per member (and when it changes). `share:false` (omitted
when true): the member does not share; the server notes it once and
drops any `lines`. `lines[]`: `t` epoch ms on the server's clock (the
client converts with its `pong` offset), `l` level name, `n` logger
(`[A-Za-z0-9_.-]`, up to 40, else `app`), `m` the message, already
redacted, an error and stack trace on the lines below (cut to 8192
characters). `dropped`: lines the client dropped since its last batch
(its buffer was full). `marks[]`: "Mark a problem" at `t` with an
optional `note` (one line, cut to 200 characters); it goes in even
with `share:false`.
- Limits: a body of at most 256 KiB, unpacking to at most 1 MiB (else
413); not gzip of a JSON object: 400. Per member and minute at most 30
posts and 4 MiB of lines, else 429 with `retry-after`: the client keeps
the batch and sends it later. Unknown token: 401. Success: 204, also
on a server that keeps no session log (the batch is dropped).
- `GET`: the session log of the last `hours` (1 to 168, default 6; at
most 8 MiB, the newest kept), lines the server still holds for
ordering included, as gzip of UTF-8 text (`application/gzip`). Any
member may read it (it is redacted and holds no chat). 401 for an
unknown token, 404 on a server that keeps no session log.
- Clients send every 10 s and when they leave; each batch carries at
most 256 KiB of lines.