Route push notifications by deep link, record opens for attribution - #2517
Merged
Merged
Conversation
The backend now stamps every push with the campaign that sent it (utm_source / utm_medium=push / utm_campaign) and the deep link it points at. Read both on tap. Two things follow. The open is tracked as push_notification_opened, and the campaign is written into the attribution store — which is the half that answers "did the nudge move spend", because every event is enriched from that store, so a deposit or first payment later in the session now carries the push's utm_source exactly as one after an email CTA carries the email's. Before this, utm_medium in Amplitude had sixteen values and `push` was not one of them. Routing then follows the link's path, mapped to the canonical path.* Href rather than handed to the router as a string: /card is a redirect shim, the Fuse vault and the referral sheet are routes plus a param. This gives the eleven lifecycle types a destination for the first time — they had no branch here at all and every one of them opened home, while the email saying the same thing pointed at /card, /savings or /activity. Spend and 3DS pushes still resolve first on transactionId, which no static link can carry, and the type switch stays as the fallback for pushes sent before links existed — a notification can sit in the tray for days. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7vZF3WSp2uZtQP8TvhTPc
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
Code reviewNo issues found. Checked for bugs and CLAUDE.md compliance. |
routeForLink read the pathname of a push's deep link without checking where the link pointed, while redirectSystemPath — the app's other entry point for an outside URL choosing an in-app screen — checks the host against KNOWN_HOSTS. Reported by the Sentry review bot on #2517. The exposure is small: sending a push needs our FCM credentials, and the paths resolve through a closed map to hardcoded screens, so a foreign link could at most pick which of our own screens the tap opened. But the link is data from outside the app, the allowlist already existed for exactly this decision, and the day a link is built from something a user supplied is not the day to start looking for the control point. KNOWN_HOSTS moves to constants/deeplink.ts and both call sites import it, rather than a second copy that can drift — the copy that falls behind being the one that stops trusting a host we do serve. Also screen the scheme. `javascript://app.solid.xyz/card` parses with a hostname that passes the allowlist; nothing here would execute it, but it did not come from our backend, and redirectSystemPath draws the same line with path.startsWith('http'). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01C7vZF3WSp2uZtQP8TvhTPc
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.
Summary
This change enables push notifications to be routed by deep links stamped by the backend, rather than solely by notification type. It also adds analytics tracking for push opens and updates the attribution store to credit sessions to the campaign that triggered the notification.
Key Changes
Deep link routing: Added
routeForLink()function that maps backend-provided URLs to in-app screens, with aROUTE_BY_LINK_PATHlookup table. This allows the backend to control where pushes navigate without app changes.Routing priority: Restructured
getNotificationRoute()to prioritize transaction-specific pushes (spend and 3DS) first, then follow deep links, then fall back to legacy type-based routing for older notifications.Attribution tracking: Added
recordOpen()function that:PUSH_NOTIFICATION_OPENEDevent with campaign metadata (utm_source,utm_medium,utm_campaign)Enhanced notification data types: Extended
NotificationDatatype to includeutm_source,utm_medium,utm_campaign, andlinkfields from backend push payloads.Comprehensive test coverage: Added 176 lines of tests covering:
/rewards?referral=openAnalytics event: Added
PUSH_NOTIFICATION_OPENEDtracking event with detailed documentation about how push attribution flows through the session.Notable Implementation Details
routeForLink()function returnsundefinedfor unrecognized links rather than throwing, allowing graceful fallback to type-based routing.linkfield and are resolved before link routing to ensure they always open the specific transaction.updateAttribution()directly rather thancaptureFromDeepLink()to preserve first-touch attribution while recording the last-touch push campaign.recordOpen()usesconsole.warn()to ensure telemetry failures don't block navigation.__testingobject for unit test access to routing functions.https://claude.ai/code/session_01C7vZF3WSp2uZtQP8TvhTPc