Skip to content

feat(weekly-rotation): instrument plays, shares and notifications for usage analytics - #14605

Merged
dylanjeffers merged 5 commits into
mainfrom
feat/weekly-rotation-analytics
Sep 24, 2026
Merged

dylanjeffers merged 5 commits into
mainfrom
feat/weekly-rotation-analytics

Conversation

@dylanjeffers

@dylanjeffers dylanjeffers commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #14602 (fix/review-cleanup-sep). Merge that first; this PR's base will then retarget to main.

Instruments Weekly Rotation so reach, click-through, plays, shares, notification opens and retention can be measured in Amplitude. No existing events are renamed.

Events and properties

Common

  • PlaybackSource, FavoriteSource and RepostSource each gain WEEKLY_ROTATION = 'weekly rotation'.
  • Playback: Play gains weeklyRotationOwnerId. It is set on passive (auto-advance and previous) plays whose queue entry came from a mix, and is parsed from the WEEKLY_ROTATION_TRACKS:<ownerId> queue source in the shared playback saga, so it works on both web and mobile.
  • Weekly Rotation: Banner View, Banner Click, Page View and Play All gain period (e.g. 2026-38), isOwnMix and ownerUserId. Banner events are always isOwnMix: true, with the viewer as owner.
  • Weekly Rotation: Page View gains status (success | empty | error) and isSignedIn. It now fires once the fetch settles, so empty mixes and errors are counted. Signed-out visitors on /explore/weekly-rotation/:handle are counted too.
  • Modal Opened now fires with name: 'Share' whenever the share modal or drawer opens (from the shared share-modal saga, any content type). It carries source (the ShareSource), kind (e.g. weeklyRotation) and the entity id (userId for a mix).
  • Share gains an optional channel (copyLink | shareSheet).
  • Chat Entry Point gains kind (what was shared) when source: 'share'.
  • Notifications: Open Push Notification gains type and id from the push payload. This applies to every push type.
  • The period helper moved to @audius/common/utils (getWeeklyRotationPeriod, formatWeeklyRotationPeriod), along with getWeeklyRotationQueueSource and getWeeklyRotationOwnerIdFromQueueSource. The web SSR still imports it by path, and the file has no dependencies.

Web

  • The mix page's lineup plays and header play/pause use source: 'weekly rotation'. Before this they used 'playlist page' or 'track tile'.
  • Row favorite and repost buttons use 'weekly rotation'. TrackTableLineup takes favoriteSource and repostSource, and History keeps its default.
  • The share modal copy link records Share with channel: 'copyLink'. DM share records Chat Entry Point with kind.
  • Banner fix: the CTA button's click also bubbled to the card, so Banner Click was recorded (and navigation ran) twice. Only the card handles the click now.

Mobile

  • Mix screen tile taps now record Playback: Play and Playback: Pause with source: 'weekly rotation'. The header play button also records them. Before this, mobile lineup taps recorded no Playback events: TrackLineup accepted playbackSource but ignored it. It now records Playback events only when that prop is passed, and only this screen passes it, so other screens are unchanged.
  • Tile favorite and repost on the mix screen use 'weekly rotation'. TrackLineup and TrackTile take favoriteSource and repostSource, defaulting to TILE.
  • In the share drawer, weeklyRotation copy link records Share (channel: 'copyLink'), the share sheet records Share (channel: 'shareSheet'), and X records Share to Twitter. DM share records Chat Entry Point with kind.
  • A Weekly Rotation notification tile tap records Notifications: Clicked Tile with kind: 'WeeklyRotation', matching web.

Not covered

  • The overflow-menu save, repost and add-to-playlist actions still report overflow on both platforms. Adding a source there means threading it through the table, OverflowMenuButton and TrackMenu on web, and through the overflow drawer state on mobile. Playlist Add has no source property at all. Row and tile buttons are covered.

How to measure (Amplitude)

Metric Event and filter
Reach (impressions) Weekly Rotation: Banner View, uniques, grouped by surface and source
Banner CTR Funnel Weekly Rotation: Banner View → Weekly Rotation: Banner Click, same surface
Page views, own vs shared Weekly Rotation: Page View grouped by isOwnMix; filter status = success for non-empty views
Shared-link landings Weekly Rotation: Page View where isOwnMix = false, grouped by isSignedIn (signed-out = new-visitor landings)
Empty or broken mixes Weekly Rotation: Page View grouped by status
Plays per user from the mix Playback: Play where source = weekly rotation OR weeklyRotationOwnerId is set; formula TOTALS / UNIQUES. Split own vs shared by comparing weeklyRotationOwnerId to the user, or use Play All's isOwnMix
Play-all rate Funnel Page View → Weekly Rotation: Play All
Saves and reposts from the mix Favorite or Repost where source = weekly rotation
Share opens Modal Opened where name = Share and kind = weeklyRotation
Share rate by channel Share opens (above) as the denominator. Channels: Share where kind = weeklyRotation, grouped by channel (web copy link, mobile copy link or share sheet); Share to Twitter where kind = weeklyRotation; Chat Entry Point where source = share and kind = weeklyRotation
Push open rate Opens: Notifications: Open Push Notification where type = WeeklyRotation (id includes the weekly_rotation:<period>:<user> group). Sends come from the weekly rotation push processed log line added in AudiusProject/pedalboard#98 (sum mobileSent per groupId period)
Notification tile CTR Notifications: Clicked Tile where kind = WeeklyRotation
Week-over-week retention Retention on Weekly Rotation: Page View (or Play All) with weekly buckets, or group by period to line up with the Wednesday rollover

Testing

  • tsc passes for common, web and mobile. eslint passes on the touched files.
  • vitest: new tests for the common period and queue-source helpers and for the share-modal saga's Modal Opened. The web OG URL test and tracks-table tests pass.
  • Checked locally on the web dev server by loading /explore/weekly-rotation/dylan signed out and reading Amplitude's unsent-event queue:
    • Weekly Rotation: Page View was recorded with {isOwnMix: false, isSignedIn: false, ownerUserId: "18269", period: "2026-39", status: "success", trackCount: 30}.
    • Play recorded Weekly Rotation: Play All and Playback: Play with source: "weekly rotation". Pressing Next recorded Playback: Play with source: "passive", weeklyRotationOwnerId: "18269".
    • Opening the share modal recorded Modal Opened with {name: "Share", kind: "weeklyRotation", source: "page", userId: 18269}. Copy link recorded Share with channel: "copyLink".
  • Mobile jest does not run in this worktree: the react-native module mapping fails, which also happens on the base branch. No touched mobile file has tests.

🤖 Generated with Claude Code

dylanjeffers and others added 3 commits September 23, 2026 16:47
…play state

- Cap the Priority/General tab backfill at 5 pages per tab visit (web and
  mobile) so a near-empty tab doesn't page through the whole chat history.
- Raise the chat row swipe activation offsets to 30pt so diagonal vertical
  scrolls stay with the list.
- OTA banner: stop an in-flight poll from rescheduling after stop/restart.
- Weekly Rotation: don't treat a shared mix as your own while its handle
  is loading or not found, and key playback state per mix owner so your
  own mix and a shared one don't share the header play button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shorten comments to plain statements, fix stale api references in the
weekly rotation period helper, and align the weekly rotation notification
body with the push copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… usage analytics

- Add PlaybackSource/FavoriteSource/RepostSource WEEKLY_ROTATION and use
  them on the web page and mobile screen lineups and header play.
- Passive plays from a mix carry weeklyRotationOwnerId, derived from the
  queue source.
- Weekly Rotation banner, page view and play-all events carry period,
  isOwnMix and ownerUserId; page view adds status and isSignedIn and now
  fires for empty mixes and fetch errors.
- Share modal opens record Modal Opened (name Share) with kind and id.
  Mobile copy-link and X shares for weeklyRotation record Share and
  Share to Twitter; Share gains a channel property and Chat Entry Point
  a kind.
- Mobile notification tile tap records Notifications: Clicked Tile, and
  push opens record the payload type and id.
- Web banner CTA no longer fires the click event twice (the button
  click also bubbled to the card).
- Move the period helper and queue source into common.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 24, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 4cd6d77

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Base automatically changed from fix/review-cleanup-sep to main September 24, 2026 19:01
…analytics

# Conflicts:
#	packages/mobile/src/screens/weekly-rotation-screen/WeeklyRotationScreen.tsx
#	packages/web/src/pages/weekly-rotation-page/WeeklyRotationPage.tsx
#	packages/web/src/utils/weeklyRotationPeriod.test.ts
#	packages/web/src/utils/weeklyRotationPeriod.ts
…analytics

# Conflicts:
#	packages/web/src/components/weekly-rotation/WeeklyRotationBanner.tsx
@dylanjeffers
dylanjeffers merged commit 2113b9e into main Sep 24, 2026
18 checks passed
@dylanjeffers
dylanjeffers deleted the feat/weekly-rotation-analytics branch September 24, 2026 20:08
dylanjeffers added a commit that referenced this pull request Sep 24, 2026
…14606)

The Amplitude proxy (gain2.audius.co, metrics-worker) caps us at 1.95M
events a month, and it has been hitting that in about 4 days. This PR
removes most of the events the clients send: anything that's a
duplicate, automatic, from a bot, or not used by any recent chart.
What's left, apart from the core events, goes out from 10% of devices.

## Volume before (Amplitude stored totals, Aug 31 to Sep 4, 5 days,
project 265616)

| # | Event | Stored | Status after this PR |
|---|---|---:|---|
| 1 | Session Start | 118,654 | **removed**, replaced by a `client` user
property |
| 2 | Page View | 44,992 | kept (core; also used by sign-up charts) |
| 3 | Playback: Play | 38,170 | kept (core) |
| 4 | Search: Search | 23,636 | **removed** |
| 5 | Embed: Open Player | 23,015 | **removed** |
| 6 | Favorite | 20,708 | kept (core) |
| 7 | Buffering Time | 18,473 | **removed** |
| 8 | Repost | 17,618 | kept (core) |
| 9 | Explore: Section View | 11,997 | **removed** |
| 10 | Follow | 9,718 | kept (core) |
| 11 | Playback: Pause | 8,047 | kept (core) |
| 12 | Link Click | 7,340 | **removed** |
| 13 | session_end | 7,279 | **removed** (SDK automatic tracking off) |
| 14 | Share | 5,821 | kept (core) |
| 15 | Track Upload: Track Uploading | 4,094 | **removed** |
| 16 | Message Unfurl: Track | 3,966 | **removed** |
| 17 | [Amplitude] Form Started / Submitted | 3,798 | **removed** (SDK
automatic tracking off) |
| 18 | Create Account: Select Genre | 3,786 | kept (core, sign-up) |
| 19 | Feed: Change view | 3,298 | sampled at 10% (Chronological Feed
dashboard) |
| 20 | Comments: Load More Comments | 3,222 | **removed** |

Web plus embed is 314k of that, iOS 66k and Android 64k. Some events are
sent but never stored, so they don't show up above: `[Amplitude] Page
Viewed`, `session_start` (both deleted in the project) and `$identify`.

## What changed

### 1. SDK automatic events off, and bots skipped
- **Web `defaultTracking`:** it now names every option. Any option left
out defaults to on (`isTrackingEnabled` in analytics-client-common), so
`{ sessions: true }` had also been sending page views, form events and
file downloads. Attribution stays on.
- **Bots:** crawler and `navigator.webdriver` traffic gets no analytics
on web or embed. On Aug 1-3, one Singapore "Chrome 145" crawler sent
371k Session Starts from 347k new devices.
- **Identify dedupe:** identify only goes out when the traits change, on
web and mobile, and is resent weekly.

### 2. Session Start removed everywhere
The platform it carried (`Android Web`, `iOS Web`, `MacOS`, `Windows`,
`Linux`, `Desktop Web`, `iOS App`, `Android App`, `Embed`) is now a
**`client` user property**. It's set through identify only when it
changes for a device. Amplitude already computes sessions from session
ids.

### 3. Events removed (call site, enum entry and type)
The rule: an event is removed if it isn't core and isn't referenced by
an Amplitude chart, dashboard or cohort that was viewed or edited in the
last 12 months.

I listed all 642 charts, dashboards, cohorts and notebooks in project
265616, then read the definitions of the 33 charts and 7 dashboards used
since 2025-09-24. None of the 11 cohorts or 3 notebooks had been used in
that window.

- **Session and diagnostics:** Session Start, App Error, App Unexpected
Error, Buffering Time, Not Found Page, Chat Websocket Error, Modal
Opened/Closed
- **Search and navigation:** Search: Search, Tag Search, Result Select,
Tag Click, Link Click, Explore: Section View, Section Click, Trending:
Change view, Profile Page: Tab Click, Sort, the social and website link
clicks, Shown Artist Recommendations, Track Page: Play More By This
Artist, Visualizer: Open/Close
- **Embed:** Open Player, Player Error, Open modal
- **Comments (all interaction events):** create, update, delete, like,
report, mute, pin, reply, mention, timestamp and link clicks, load
more/new, sort, drawer open/close, overflow menus, notification toggles,
Comments History, Recent Comments
- **Chat:** Create Chat, Chat Blast CTA/Create/Message Viewed, Send
Message: Failure, Delete Chat, Set Chat Category, Block User, Change
Inbox Settings, Send Message Reaction, Message Unfurl: Track/Playlist,
Chat Entry Point (see note)
- **Playlists and library:** Playlist: Add To Playlist,
Open/Start/Complete Create, Make Public, Open Edit From Sidebar, Play;
Playlist Library: Reorder; Folder: Open/Submit/Cancel Edit
- **Tracks:** Track Download (all 6), Track Edit (all 3), Collection
Edit (both), Track Replace (all 3), Track Upload: Track Uploading, View
Track page, the follow and token gated upload variants; Follow/Token
Gated unlocks; Stem: Complete Upload, Delete; Delete; Remix: New Remix,
CoSign, CoSign Indicator, Hide; Artist Pick: Select Track
- **Remix contests:** Create, Update, Delete, Pick Winners Open,
Finalize Winners, View, Enter, View Submissions
- **Settings and account:** Change Theme, Resend Account Recovery,
Complete Change Password, Log Out, Deactivate Account (all 4), Developer
Apps (all 8), Authorized Apps (both), Manager Mode (all 5), Export
Private Key (all 5), Notifications: Toggle Setting, Browser Push
Notification, Account Health: Upload Cover Photo, Upload Profile
Picture, Download Desktop
- **Wallet and rewards:** Connect Wallet (all 5), Create User Bank
(both), Rewards Claim: Opened/Request/Success, Rewards Claim All:
Failure
- **Banners and prompts:** Banner Artist Coins/Yak Coin/Trading
Volume/TOS clicks, Rate CTA (all 3)
- **Mobile offline mode:** everything except Offline Play (the migration
and download lifecycle events)

The analytics-only `source` props on SearchTag, ExternalLink and
UserGeneratedText (`linkSource`) are removed too.

### 4. Sampling
- **Core allowlist:** `CORE_ANALYTICS_EVENTS` in `@audius/common`
(`models/AnalyticsSampling.ts`), plus the mobile share-channel events.
- **What's core:** Playback Play/Pause, Listen, Page View, and the
sign-up, sign-in, create-account and OAuth flows. Also purchase, buy,
withdraw, USDC, Stripe, coin swaps and Launchpad; the upload
open/start/complete/success/failure funnel; Share and share channels
(including the notification and first-upload Twitter shares); Favorite,
Repost and Follow and their undos; notification opens and clicks
(including push and comment notifications); chat sends (Send Message:
Success, Chat Blast: Message Sent); Report User: Chat (the only record
of an abuse report); Rewards Claim All request/success; Embed: Copy; and
all Weekly Rotation events.
- **Everything else:** sent only from devices where an FNV-1a hash of
the Amplitude device id falls in the bottom 10%. It carries `sampleRate:
0.1`, so counts scale back up by dividing by `sampleRate`. What's still
sampled after step 3 is only chart-backed: Play Queue (all 7), Feed:
Change view, Error Page, Account Health: Meter Full, Folder: Delete, the
Playlist Library folder moves, Offline Play, and Chat Entry Point.
- **Where it runs:** once, in the shared track path: web
`services/analytics/amplitude.ts` and mobile `services/analytics.ts`.
Embed now only sends core events.

## Why the proxy counts about 5.5x what Amplitude stores

- **Confirmed:** the proxy counted events that Amplitude drops without
billing: session_start and [Amplitude] Page Viewed. It also counted
`$identify` on every account load. Together that's roughly 50-70k a day.
- **Inferred:** the other ~330k a day. The proxy counts a batch before
forwarding it, so every SDK resend counts again (after 400/413/429/5xx
responses, network errors, and the localStorage queue being replayed on
the next load). Amplitude removes duplicates by `insert_id`, so it bills
only one copy. AudiusProject/metrics-worker#1 gives back events
Amplitude doesn't accept, and now also drops the removed event types
that older clients still send.

## Estimate against the 1.95M cap

| | Per day now | Per day after | Per month after |
|---|---:|---:|---:|
| Amplitude stored (billed) | ~100k | ~35k (core 35.0k + sampled 0.1k) |
**~1.05M** |
| Proxy count, if the gap is retries of rejected batches (fixed by
worker#1) | ~488k | ~34k events + ≤8k identify | **~1.3M** |
| Proxy count, if the gap is duplicates Amplitude accepts with a 200 |
~488k | ~34k × 4.8 + 8k | ~5.2M |

- **Billed volume ends up under 1.95M either way**, with about 45%
headroom. Kept core events stored 175k over the 5 days. The sampled
events stored 4.7k and will keep 10% of that.
- **The proxy count ends up under 1.95M only if** the unexplained gap is
retries of rejected batches. After deploying metrics-worker#1, compare
the counter against Amplitude's daily totals for a few days to find out.
- **Rollout timing:** web doesn't auto-deploy (Production gate), and
mobile needs an OTA dispatch. Until those ship, metrics-worker#1's
denylist is what stops old clients from spending the budget.

## Kept even though it's low value
- **Undo actions:** Unfavorite, Undo Repost and Unfollow. They're the
inverse of core actions and about 1k in 5 days.
- **Upload USDC-gated variants and USDC unlock events:** grouped with
purchase. Tiny volume.
- **Launchpad form-change events:** part of the coin flow and nearly
zero volume.
- **Chat Entry Point:** still fired from ShareModal and ShareDrawer,
which #14605 is changing. The enum and type stay so that merges cleanly.
It's sampled on the client and dropped by the proxy. Remove it after
#14605 lands.
- **A few enum entries and types stay** next to lines #14605 changes,
even though nothing fires them anymore: Modal Opened/Closed, Explore:
Section Click, Browser Push Notification, Chat Websocket Error, Message
Unfurl, Playlist: Play, Stem: Complete Upload, Export Private Key link.
They can be deleted after #14605 merges.

## Behavior changes to review
- **Sign-out:** Log Out used to run through the analytics callback. It's
now called directly on desktop web and in the mobile web SignOutModal,
still before the redirect.
- **Modal sagas:** `store/ui/modals/sagas.ts` now returns no sagas.
`trackModalOpened`/`trackModalClosed` still get dispatched and now do
nothing.
- **Wallet-connect errors:** the catch blocks in the mobile
wallet-connect sagas only sent an error event, so they're now empty.
- **Mobile Chat screen:** the blast "viewed" AsyncStorage bookkeeping
was only for the removed event, so it's gone.

## Tests
- **Typecheck:** `tsc` passes for common, web and mobile. The local sdk
and spl dist were rebuilt, because they were stale.
- **Lint:** eslint on all touched files has no errors. Two
`exhaustive-deps` warnings in the remix-contest host were already there.
- **Web vitest:** 33 files and 177 tests pass. That includes
`services/analytics/amplitude.test.ts`: automatic events off, client
property set once per device, core events always sent, about 10% of
devices kept for non-core events with `sampleRate`, identify dedupe, and
bots skipped.
- **Common vitest:** 172 of 179 pass. The 7 failures are in
`store/upload/selectors.test.ts`, which fails the same way on an
untouched checkout.
- **Embed jest:** passes.
- **Mobile jest:** can't run in this worktree, because react-native
can't be resolved from the copied node_modules.

🤖 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant