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") +}