Repository navigation
feat(api): top listener cities for the artist dashboard - #1072
Merged
Merged
Conversation
GET /v1/users/:id/listener_cities?time=week|month|all returns the cities whose listeners played the artist's tracks the most. Auth: the artist or a manager. plays has no index by track, so a new indexer job rolls plays up into aggregate_daily_city_plays (per track, UTC day and city), skipping the owner's own plays and plays with no city. The job is checkpointed on plays.id and backfills retained plays 250k ids per run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dylanjeffers
added a commit
to AudiusProject/apps
that referenced
this pull request
Nov 12, 2026
Adds a Top Cities card to the web Artist Dashboard, under the stat tiles. It lists the 10 cities whose listeners played the artist's tracks the most, for the last 7 days, last 30 days (default) or all time, with a bar and play count for each. This matches the "top city" line in the weekly artist recap notification. Depends on AudiusProject/api#1072 (`GET /v1/users/:id/listener_cities`). That endpoint only answers the artist or their managers, so manager mode works. - Behind the new `dashboard_top_cities` flag: on in development, off in production - Only shown to accounts with tracks - `useListenerCities` tan-query hook in common - SDK: only `users.getListenerCities` and its two models, generated from the api PR's swagger. Unrelated drift from the generator was left out. Changeset included. - Copy on the card notes that locations come from the listener's internet connection and that VPNs and mobile networks can show a nearby city. Mobile is not included. Tested locally against the api branch with a seeded artist: the week/month/all switch, the artist's own plays and plays with no city left out, empty and error states. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5.5 <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.
Adds
GET /v1/users/:id/listener_cities?time=week|month|all&limit=10for the Artist Dashboard's new Top Cities card. It returns the cities whose listeners played the artist's tracks the most:city,region,country,play_count. Only the artist or one of their managers can call it (requireAuthForUserId).Counting matches the weekly artist recap: plays of the artist's current tracks, trimmed city, empty cities skipped, the artist's own plays skipped. Locations come from the listener's IP (MaxMind on the streaming node), so VPN and mobile carrier traffic can land in the wrong city.
Why a rollup table
playshas no index by track (onlycreated_at,slot,signatureand two user-first indexes), so a per-artist query would scan every play in the window, or the whole table forall.aggregate_monthly_playshas country but not city.So this adds
aggregate_daily_city_plays(track, UTC day, country, region, city, count), with the primary key leading onplay_item_id. The endpoint reads it throughtrack_owner_id_idx, then the primary key.A new indexer job,
CityPlaysJob, fills it every 15s fromplays, checkpointed onplays.idinindexing_checkpoints(same pattern ashourly_play_counts). Each run handles at most 250k play ids. On first deploy it backfills whateverplaysstill holds (prune keeps about 400 days), starting at the lowest remaining id. After that,allkeeps growing even as old plays are pruned.week= today plus the 6 days before (UTC),month= 30 daysNot measured
City coverage and backfill time in prod: there's no prod DB access from here. Worth checking
SELECT last_checkpoint FROM indexing_checkpoints WHERE tablename = 'aggregate_daily_city_plays'againstMAX(plays.id)after deploy.Tests
TestV1UsersListenerCities: week/month/all windows, limit, other artists' tracks excluded, manager access, 403 for other users, 400 for a badtimeTestCityPlaysJob: batching across runs, owner plays and empty or NULL cities skipped, trimming, day bucketingTestCityPlaysJob_SkipsPrunedIdsAlso ran it end to end with a local api and the web dashboard ((AudiusProject/apps#14770)).
🤖 Generated with Claude Code