Skip to content

feat(notifications): log weekly rotation push sends - #98

Merged
dylanjeffers merged 6 commits into
mainfrom
feat/weekly-rotation-push-logs
Sep 24, 2026
Merged

dylanjeffers merged 6 commits into
mainfrom
feat/weekly-rotation-push-logs

Conversation

@dylanjeffers

@dylanjeffers dylanjeffers commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #97 (chore/review-cleanup).

Adds one structured logger.info line per processed Weekly Rotation notification (msg: 'weekly rotation push processed') so sends per period can be counted from logs:

  • notificationType: 'weekly_rotation', groupId (weekly_rotation:<YYYY-WW>:<user>), userId
  • browserPushEnabled, browserSent (web push subscriptions delivered)
  • mobileDevices, mobileSent, mobileDisabled (endpoints SNS reported disabled)
  • skippedReason: no_mobile_devices or abusive when no mobile push is attempted

The client side is instrumented in AudiusProject/apps#14605: Notifications: Open Push Notification now carries type: 'WeeklyRotation' and the push id. Push open rate is opens in Amplitude divided by the sum of mobileSent for that period's groupId prefix.

Note: sendPushNotification swallows non-endpoint errors, so mobileSent counts sends that did not come back as a disabled endpoint.

Testing

  • tsc --noEmit passes, and eslint and prettier pass on the changed file.
  • The jest suite needs the discovery and identity template DBs (audius-compose test run notifications), which I did not run locally. Behavior is unchanged apart from the log line.

🤖 Generated with Claude Code

dylanjeffers and others added 5 commits September 23, 2026 16:43
…t devices

The browser push was behind shouldSendPushNotification, which requires a
mobile device, so web-only listeners never got it. Send it first, then gate
only the mobile push. Removes the devices.length check that could never hit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A title containing >, < or & broke the <url|title> link markup in the
digest. Escape them per Slack's mrkdwn rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Shorten comments added with the trending digest, General inbox and weekly
rotation changes, and use a plain hyphen in the digest's track lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Logs browser and mobile send counts, disabled endpoints and the skip
reason for each processed weekly rotation notification so sends per
period can be counted from logs.

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

# Conflicts:
#	apps/notifications/src/processNotifications/mappers/weeklyRotation.ts
@dylanjeffers
dylanjeffers merged commit 1f2033c into main Sep 24, 2026
2 checks passed
dylanjeffers added a commit to AudiusProject/apps that referenced this pull request Sep 24, 2026
… usage analytics (#14605)

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](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