From 9868e5369393cf2e13dcffb5581da108c6acb53a Mon Sep 17 00:00:00 2001 From: Dylan Jeffers Date: Mon, 24 Aug 2026 13:46:54 -0700 Subject: [PATCH] fix(api): read the live trending rows in Discover Weekly The endpoint shipped in #1025 returned an empty mix for every user in production. Two bugs compounded. track_trending_scores holds two populations: rows carrying a genre, which the trending job keeps current (median track age ~4 days), and rows with a null/empty genre, which are stale -- in production those resolve to tracks five to six years old. The candidate CTEs matched only the null-genre rows, so every candidate was ancient, and the 365-day age cutoff in the filter stage then dropped all of them. GET /tracks/trending and /tracks/trending/underground read the live rows by omitting the genre filter entirely; this now does the same. The age cutoff stays and is no longer load-bearing, since the live pool tops out around twelve days old. Every existing test seeded score rows without a genre, which is why none of them caught this. Adds one that seeds a genre-carrying row -- the shape that actually reaches the query in production. Note: /users/:id/feed/for-you has the same null-genre predicate on its trending and underground candidate sources, so it is very likely feeding on the same stale rows. Not touched here. Co-Authored-By: Claude Opus 5 --- api/v1_users_discover_weekly.go | 11 ++++++-- api/v1_users_discover_weekly_test.go | 41 ++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 2 deletions(-) diff --git a/api/v1_users_discover_weekly.go b/api/v1_users_discover_weekly.go index c8763957..359b9a79 100644 --- a/api/v1_users_discover_weekly.go +++ b/api/v1_users_discover_weekly.go @@ -238,13 +238,21 @@ func (app *ApiServer) getDiscoverWeeklyTrackIds( GROUP BY owner_id ), -- Source 1: weekly trending tracks. + -- + -- No genre predicate, deliberately. track_trending_scores holds two + -- populations: rows carrying a genre, which are the live list the trending + -- job refreshes (median track age ~4 days), and rows with a null/empty + -- genre, which are stale -- in production those resolve to tracks five to + -- six years old. GET /tracks/trending and /tracks/trending/underground read + -- the live rows by omitting the genre filter, so this does the same. + -- Matching on a null-or-empty genre instead reads the stale population and, + -- combined with the age cutoff below, returns nothing at all. cand_trending AS ( SELECT tts.track_id, 'trending'::text AS source FROM track_trending_scores tts WHERE tts.type = 'TRACKS' AND tts.version = 'pnagD' AND tts.time_range = 'week' - AND (tts.genre IS NULL OR tts.genre = '') ORDER BY tts.score DESC, tts.track_id DESC LIMIT 400 ), @@ -258,7 +266,6 @@ func (app *ApiServer) getDiscoverWeeklyTrackIds( WHERE tts.type = 'TRACKS' AND tts.version = 'pnagD' AND tts.time_range = 'week' - AND (tts.genre IS NULL OR tts.genre = '') AND au.follower_count < 1500 AND au.following_count < 1500 ORDER BY tts.score DESC, tts.track_id DESC diff --git a/api/v1_users_discover_weekly_test.go b/api/v1_users_discover_weekly_test.go index 45ef52ef..e2dc4bdf 100644 --- a/api/v1_users_discover_weekly_test.go +++ b/api/v1_users_discover_weekly_test.go @@ -360,3 +360,44 @@ func TestV1UsersDiscoverWeeklyRequiresValidUserId(t *testing.T) { status, _ := testGet(t, app, "/v1/users/not-a-real-id/discover-weekly", &resp) assert.Equal(t, 400, status) } + +// Regression for the bug that shipped in #1025 and returned an empty mix for +// every user in production. +// +// track_trending_scores holds two populations: rows carrying a genre, which +// the trending job keeps current, and rows with a null/empty genre, which are +// stale and resolve to tracks five to six years old. The original query +// matched only the null-genre rows, so every candidate then failed the +// 365-day age cutoff and the mix came back empty. +// +// Every other test here seeds score rows without a genre, so none of them +// could catch it. This one seeds a genre-carrying row specifically -- the +// shape that actually reaches the query in production. +func TestV1UsersDiscoverWeeklyReadsGenreCarryingTrendingRows(t *testing.T) { + app := emptyTestApp(t) + + fixtures := database.FixtureMap{ + "users": []map[string]any{ + {"user_id": 1, "handle": "me", "handle_lc": "me", "wallet": "0x0000000000000000000000000000000000000001"}, + {"user_id": 2, "handle": "artist", "handle_lc": "artist", "wallet": "0x0000000000000000000000000000000000000002"}, + }, + "aggregate_user": []map[string]any{ + {"user_id": 1, "follower_count": 0, "following_count": 0}, + {"user_id": 2, "follower_count": 5000, "following_count": 10}, + }, + "tracks": []map[string]any{{"track_id": 200, "owner_id": 2, "title": "genred track", "genre": "Rock"}}, + "aggregate_track": []map[string]any{{"track_id": 200, "save_count": 100, "repost_count": 50}}, + "track_trending_scores": []map[string]any{ + {"track_id": 200, "score": 1_000_000_000, "time_range": "week", "genre": "Rock"}, + }, + } + database.Seed(app.pool.Replicas[0], fixtures) + + var resp struct { + Data []dbv1.Track + } + status, _ := testGet(t, app, "/v1/users/7eP5n/discover-weekly", &resp) + assert.Equal(t, 200, status) + assert.Len(t, resp.Data, 1, + "a score row carrying a genre must still be a candidate") +}