fix(recommendation): Rediscover no longer ships a one-song playlist (#1246)
Confirmed against prod: exactly one track (17 plays, cold since May 21) met the c>=5 + 30d-cold bar, and three process defects turned that into a 1-track playlist instead of the locked placeholder. - ListRediscoverTracks: collapse the two-tier UNION into one blended pool. The old shallow-tier gate (WHERE NOT EXISTS deep) was all-or-nothing — one 6-month row suppressed the entire 30-day tier — and deep was a strict subset of shallow anyway. Eligibility drops to >=3 non-skip plays (on a weeks-old history the >=5-play tracks are precisely the ones still in rotation); ordering prefers >=6mo cold, then >=5 plays, then raw count. - Minimum viable mix floor for all five discovery mixes: below minLen (15; 5 for the album-coherent NewForYou/FirstListens) the variant is withheld so Home renders the 'listen more to unlock' placeholder instead of a mix that reads as built-wrong. - /api/events: clamp client-supplied 'at' to [user.created_at, now+5m]. Unbounded client clocks could write arbitrarily old plays and poison the 6-month ordering (prod data verified clean — no scrub needed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TsF3cNoKrqCYsU78cXC8U6
This commit is contained in:
+21
-1
@@ -31,6 +31,26 @@ type playStartedResponse struct {
|
||||
SessionID string `json:"session_id"`
|
||||
}
|
||||
|
||||
// eventFutureSkew is the tolerated client-clock drift into the future
|
||||
// before a timestamp is treated as bogus and replaced with now.
|
||||
const eventFutureSkew = 5 * time.Minute
|
||||
|
||||
// clampEventTime bounds a client-supplied event timestamp to sanity:
|
||||
// no earlier than the account's creation (offline replays can
|
||||
// legitimately be days old, but no play can predate the user) and no
|
||||
// later than now + a small skew allowance. Unbounded client clocks
|
||||
// previously let a skewed device write arbitrarily old plays, which
|
||||
// poisoned Rediscover's "not played in 6 months" ordering (#1246).
|
||||
func clampEventTime(at, userCreatedAt, now time.Time) time.Time {
|
||||
if at.Before(userCreatedAt) {
|
||||
return userCreatedAt
|
||||
}
|
||||
if at.After(now.Add(eventFutureSkew)) {
|
||||
return now
|
||||
}
|
||||
return at
|
||||
}
|
||||
|
||||
type okResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
}
|
||||
@@ -52,7 +72,7 @@ func (h *handlers) handleEvents(w http.ResponseWriter, r *http.Request) {
|
||||
writeErr(w, apierror.BadRequest("bad_request", "invalid `at` timestamp"))
|
||||
return
|
||||
}
|
||||
at = parsed
|
||||
at = clampEventTime(parsed.UTC(), user.CreatedAt.Time, time.Now().UTC())
|
||||
}
|
||||
clientID := ""
|
||||
if req.ClientID != nil {
|
||||
|
||||
Reference in New Issue
Block a user