Skip to content

feat(api): top listener cities for the artist dashboard - #1072

Merged
dylanjeffers merged 2 commits into
mainfrom
feat/listener-cities
Nov 12, 2026
Merged

dylanjeffers merged 2 commits into
mainfrom
feat/listener-cities

Conversation

@dylanjeffers

@dylanjeffers dylanjeffers commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Adds GET /v1/users/:id/listener_cities?time=week|month|all&limit=10 for 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

plays has no index by track (only created_at, slot, signature and two user-first indexes), so a per-artist query would scan every play in the window, or the whole table for all. aggregate_monthly_plays has country but not city.

So this adds aggregate_daily_city_plays (track, UTC day, country, region, city, count), with the primary key leading on play_item_id. The endpoint reads it through track_owner_id_idx, then the primary key.

A new indexer job, CityPlaysJob, fills it every 15s from plays, checkpointed on plays.id in indexing_checkpoints (same pattern as hourly_play_counts). Each run handles at most 250k play ids. On first deploy it backfills whatever plays still holds (prune keeps about 400 days), starting at the lowest remaining id. After that, all keeps growing even as old plays are pruned.

  • week = today plus the 6 days before (UTC), month = 30 days
  • The migration only creates the table, so it is quick. The backfill runs in the job.

Not 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' against MAX(plays.id) after deploy.

Tests

  • TestV1UsersListenerCities: week/month/all windows, limit, other artists' tracks excluded, manager access, 403 for other users, 400 for a bad time
  • TestCityPlaysJob: batching across runs, owner plays and empty or NULL cities skipped, trimming, day bucketing
  • TestCityPlaysJob_SkipsPrunedIds

Also ran it end to end with a local api and the web dashboard ((AudiusProject/apps#14770)).

🤖 Generated with Claude Code

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
dylanjeffers merged commit cad8826 into main Nov 12, 2026
@dylanjeffers
dylanjeffers deleted the feat/listener-cities branch November 12, 2026 00:44
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant