fix: restore native Android play-event recording (History was empty) #89

Merged
bvandeusen merged 2 commits from dev into main 2026-06-11 09:08:21 -04:00
Owner

Fixes listening History being empty/sparse on the native Android client.

Root cause (client)

Every native /api/events request omitted the type discriminator. The app's
Json is configured encodeDefaults=false, so a type left at its data-class
default (play_started, etc.) is never serialized. The server multiplexes on
type and returns 400 "unknown event type"; PlayEventsReporter swallows
the error and the play_started path has no offline fallback — so the play is
lost without a trace. Confirmed live in the server access log: POST /api/events → 400 on every play while reads return 200.

Net effect: all native Android play events have 400'd since the code was
written. History only ever populated from Flutter/web; as usage moved to the
native app, History went sparse (the 05-31 → 06-01 cliff).

653ed95f forces the discriminator onto the wire with
@EncodeDefault(Mode.ALWAYS) on each event request's type.

Server fix (history recovery)

b2c6f6f0autoClosePriorOpen previously hardcoded was_skipped=true when
closing an orphaned play, hiding even fully-listened tracks from History. It
now applies the same skip rule as RecordPlayEnded to the duration-capped
elapsed estimate, so a track that played long enough is recovered into History
instead of force-hidden. No skip_events row written (auto-close is an
ambiguous signal, not a deliberate skip).

Follow-ups (not in this PR)

  • One-time backfill of existing orphans wrongly marked skipped.
  • Reporter hardening: offline fallback for a failed play_started, onStop
    close-by-id (avoid orphan + duplicate), async-start race.

🤖 Generated with Claude Code

Fixes listening History being empty/sparse on the native Android client. ## Root cause (client) Every native `/api/events` request omitted the `type` discriminator. The app's Json is configured `encodeDefaults=false`, so a `type` left at its data-class default (`play_started`, etc.) is never serialized. The server multiplexes on `type` and returns **400 "unknown event type"**; `PlayEventsReporter` swallows the error and the `play_started` path has no offline fallback — so the play is lost without a trace. Confirmed live in the server access log: `POST /api/events → 400` on every play while reads return 200. Net effect: **all** native Android play events have 400'd since the code was written. History only ever populated from Flutter/web; as usage moved to the native app, History went sparse (the 05-31 → 06-01 cliff). `653ed95f` forces the discriminator onto the wire with `@EncodeDefault(Mode.ALWAYS)` on each event request's `type`. ## Server fix (history recovery) `b2c6f6f0` — `autoClosePriorOpen` previously hardcoded `was_skipped=true` when closing an orphaned play, hiding even fully-listened tracks from History. It now applies the same skip rule as `RecordPlayEnded` to the duration-capped elapsed estimate, so a track that played long enough is recovered into History instead of force-hidden. No `skip_events` row written (auto-close is an ambiguous signal, not a deliberate skip). ## Follow-ups (not in this PR) - One-time backfill of existing orphans wrongly marked skipped. - Reporter hardening: offline fallback for a failed `play_started`, `onStop` close-by-id (avoid orphan + duplicate), async-start race. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
bvandeusen added 2 commits 2026-06-11 09:08:14 -04:00
fix(server): apply skip rule on auto-close instead of force-hiding orphans
test-go / test (push) Successful in 35s
test-go / integration (push) Successful in 4m34s
b2c6f6f0e9
autoClosePriorOpen hardcoded was_skipped=true for every orphaned
play_event (a play_started whose play_ended never arrived, e.g. the
client backgrounded mid-track). That hid fully-listened tracks from
History — a play that sat open past its own length was capped to the
track duration (ratio ~1) yet still flagged skipped. Observed live:
History showed 3 plays for a day of listening because most rows were
auto-closed orphans marked skipped.

Now the auto-close applies the same skip rule as RecordPlayEnded to the
duration-capped elapsed estimate: ratio >= threshold OR elapsed >= the
duration floor -> a real play that lands in History; a genuine
quick-abandon still classifies as a skip. Still writes no skip_events
row, so the ambiguous auto-close never feeds the skip-ratio /
recommendation signal.

This is the server half. The client-side root cause (backgrounded
track transitions never closed, orphaning the rows in the first place)
is tracked separately.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
fix(android): always serialize event type so plays aren't dropped
android / Build + lint + test (push) Successful in 5m21s
653ed95f14
The native client's /api/events requests omitted the `type`
discriminator entirely. The app's Json is configured with
encodeDefaults=false (AppModule), so a `type` left at its data-class
default ("play_started" etc.) is never written to the wire. The server
multiplexes on `type` and returns 400 "unknown event type" for an
empty one, which PlayEventsReporter's catch swallows — and the
play_started path has no offline fallback, so the play is lost with no
trace.

Net effect: EVERY native Android play event (started/ended/skipped/
offline) has 400'd since this code was written. Listening History only
ever populated from the Flutter/web clients; as usage moved to the
native app, History went sparse. Confirmed live in the server access
log: POST /api/events -> 400 on every play, while reads 200.

Force the discriminator onto the wire with
@EncodeDefault(Mode.ALWAYS) on each request type's `type` field.
Surgical (vs flipping encodeDefaults globally), and idiomatic for a
constant-valued discriminator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
bvandeusen merged commit f167ddfbfb into main 2026-06-11 09:08:21 -04:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: bvandeusen/minstrel#89