Report a play when the track is done with, not at the halfway mark - #57
Merged
Conversation
…ay mark Every web play was landing with a completion ratio of almost exactly 0.5, whatever the listener actually heard. `usePlayTracking` delivered the play the moment listening crossed `min(duration / 2, 4 min)` and sent `completion_ratio` as measured *at that instant*. Nothing revised it afterwards, so the figure recorded when someone heard a track once through and when they heard half of it were the same number. Measured on the live database, 2026-08-01: **289 of 357 completed events sit in the 0.5–0.6 bucket**, while rows from the native client — which reports at the end — correctly read 0.95–1.00. Sample rows are exact: 166.0/331.4, 124.3/248.6, 120.6/241.0. This matters because of what the data is for. ADR-0004 exists so ADR-0005's recommender has a taste signal that can only accumulate in wall-clock time, and completion is that signal. A constant carries none of it, so the month of data `familiar` #53 is parked waiting for would have been a month of noise — and the error is invisible in aggregate, because 0.5 is a thoroughly plausible average. **Nothing changes about which plays count.** The early delivery only decided *when*: a play past 30s but short of the half mark was already reported on track change, by the same rule that now reports all of them. The Last.fm half/4-minute threshold turned out to gate nothing, which is why it is gone rather than moved. Two things that came with it: - **A page-hide flush**, because the early delivery was quietly buying durability: close the tab mid-track and the play still counted, since it had already been sent. Reporting at the end would have dropped that silently. Restored on `pagehide` and `visibilitychange`, with the ratio actually heard. Best effort and described as such — a browser tearing down a page owes the request nothing. - **The ratio is clamped.** Played time accumulates from the engine's clock while the duration is track metadata, so a looping or mis-tagged track could send 1.1. The server clamps as well, but agreeing with the native client — which has always clamped — beats leaving every stored figure a question about which end did it. Tests: the three that asserted mid-playback delivery now assert delivery on track change, and two were added — one pinning the ratio of a track heard nearly to the end, one covering the page-hide path and that it cannot double-report. 940 pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW
jeffcrouse
added a commit
that referenced
this pull request
Aug 3, 2026
…ruction (#80) Phase 1. Three decisions wait on ADR-0004's data — its own completion-ratio threshold, ADR-0005's RADIO weights, and #53 — and the data does not start where everyone assumed. Until #57 the web client delivered a play the moment listening crossed `min(duration / 2, 4 min)` and sent `completion_ratio` as measured at that instant, never revising it. Verified again on the live database on 2026-08-02, by day and context: **795 of 823 rows predate the fix**, and every context before 2026-08-01 shows completions averaging 0.42–0.50 — including `library`, because the web derives context from the queue source and sends `library` for a library queue exactly as the native app does. From 2026-08-01 the same context reads 0.972 and 1.000, with skips at ≤0.1. So the clock restarted on 2026-08-01, not 2026-07-27: a month of trustworthy data lands around 2026-09-01. `FEEDBACK_TRUSTWORTHY_SINCE` and `trustworthy_feedback_only` make the boundary one call away instead of a matter of discipline. A date, not a client filter, because `play_events` records no client and `context` does not stand in for one — the good native rows cannot be separated from the bad web rows beside them, and selecting on the ratio would be circular. Excluded rather than deleted, for a reason the investigation turned up: the damage is not limited to the ratio. A track abandoned at 55% was recorded as a completion at ~0.5 rather than as a skip, so `outcome` is unreliable before the cutoff and skips are *under*-counted. What survives is that `skipped` and `rejected` rows describe real abandonments. `ambient._negative_signal` counts exactly those two over a rolling 90-day window — so **the live recommender was never poisoned**, it has been running on a slightly weak negative signal that heals around 2026-10-30. ADR-0005 also gains the `Implementation:` block it never had, which is why it looked unbuilt in a survey while `RADIO` had been shipping for weeks. Two follow-ups recorded: a `client` column, cheap now and worthless retroactively, which is the only thing that would have made this separable; and that whether the web still reports at all is unverified — every post-cutoff row carries the context the native app hardcodes. Claude-Session: https://claude.ai/code/session_014P9p2fvFnyiywBxGkv4gfW Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every web play was landing with a completion ratio of almost exactly 0.5, whatever the listener actually heard.
usePlayTrackingdelivered the play the moment listening crossedmin(duration / 2, 4 min), sendingcompletion_ratioas measured at that instant. Nothing revised it afterwards — so the figure recorded for hearing a track once through and for hearing half of it was the same number.The evidence
Measured on the live database, 2026-08-01 (815 rows, first 2026-07-27):
289 of the 357 completed events sit in the 0.5–0.6 bucket. Sample rows are exact — 166.0/331.4, 124.3/248.6, 120.6/241.0, 146.3/292.5. Rows from the native client, which reports at the end, correctly read 0.95–1.00.
Why it matters more than it looks
ADR-0004 exists so that ADR-0005's recommender has a taste signal that can only accumulate in wall-clock time, and completion is that signal. A constant carries none of it.
familiar#53 is parked waiting for roughly a month ofplay_eventsbefore ADR-0010's cache budget can come from measured locality. That clock started 2026-07-27 and lands around 27 August — so without this, the month being waited for would have been a month of noise. The failure is invisible in aggregate precisely because 0.5 is such a plausible-looking average.What changes, and what does not
Which plays count does not change. The early delivery only decided when: a play past 30s but short of the half mark was already reported on track change, by the same rule that now reports all of them. The Last.fm half/4-minute threshold turned out to gate nothing, which is why it is deleted rather than moved.
Two things came with it:
pagehide/visibilitychange, with the ratio actually heard. Best effort, and documented as such: a browser tearing down a page owes the request nothing.1.1. The server clamps too, but agreeing with the native client — which has always clamped — beats leaving every stored figure a question about which end did it.Tests
The three that asserted mid-playback delivery now assert delivery on track change. Two added:
940 frontend tests pass; lint clean on the changed file.
Not addressed here
Existing rows keep their frozen ratios. They are distinguishable — web completions cluster at 0.5 with
contextofother/null, native ones at 0.95–1.00 withlibrary— so ADR-0005 can filter them, but it is worth deciding deliberately whether to exclude anything recorded before this lands.