Skip to content

feat: persist deeplink attribution with MMKV#29542

Merged
grvgoel81 merged 18 commits into
mainfrom
feat/TO-717-persistent-marketing-attribution
May 12, 2026
Merged

feat: persist deeplink attribution with MMKV#29542
grvgoel81 merged 18 commits into
mainfrom
feat/TO-717-persistent-marketing-attribution

Conversation

@grvgoel81

@grvgoel81 grvgoel81 commented Apr 30, 2026

Copy link
Copy Markdown
Contributor

Description

Mobile only kept deep-link UTM / attribution_id in memory (via AppStateEventListener / processAttribution). After the first foreground pass the deeplink is often cleared, and onboarding → Wallet Setup Completed usually happens in a later session, so acquisition data was lost and could not be attached to conversion analytics.

This PR adds a dedicated Redux slice for attribution with redux-persist + MMKV (separate from root filesystem persist). It exposes saveAttribution, clearAttribution, and expireAttributionIfStale (default 7-day TTL). Data is written when:

  • AppStateEventListener.processAppStateChange runs after processAttribution() (still gated by dataCollectionForMarketing inside processAttribution), and
  • handleDeeplink sees a URI with acquisition params while marketing consent is on (covers cold open / session split).

Privacy: No persistence when marketing consent is off; persisted data is cleared when the user opts out of marketing data collection and when onboarding is cleared (CLEAR_ONBOARDING). Root persist blacklists attribution so it is not double-stored with the nested MMKV persist.

Includes unit tests for the slice (save / clear / expire), marketing sagas, deeplink handler, app state listener, and persistConfig blacklist expectations

Jira: https://consensyssoftware.atlassian.net/browse/TO-717

Changelog

CHANGELOG entry: null

Related issues

Fixes:

Manual testing steps

Feature: Persist deep-link attribution (marketing consent on)

  Scenario: User opens app from acquisition deeplink and survives process restart
    Given marketing data collection for analytics is enabled
    And the app handles a deeplink URL that includes utm_source (and optionally attribution_id/other utm_* params)

    When the user fully terminates the app and launches MetaMask again without using the same deeplink

    Then persisted Redux state under the attribution slice still contains the saved acquisition fields (and capturedAt) until TTL or opt-out

  Scenario: User disables marketing consent after attribution was saved
    Given marketing data collection for analytics was enabled
    And attribution had been persisted from a prior deeplink or foreground attribution capture

    When the user turns off marketing data collection for analytics in settings

    Then persisted attribution is cleared (no acquisition payload remains in the attribution slice)

Screenshots/Recordings

Before

After

Screenshot 2026-05-04 at 2 02 56 PM

Pre-merge author checklist

Performance checks (if applicable)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

Pre-merge reviewer checklist

  • I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed).
  • I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots.

Note

Medium Risk
Introduces new persisted attribution state and wires it into deeplink/app-state flows plus migrations; mistakes could incorrectly retain or clear marketing acquisition data and impact analytics/privacy expectations.

Overview
Adds a new persisted attribution Redux slice to store acquisition fields (utm_*, attribution_id) with a 7-day TTL, deduping, and explicit saveAttribution/clearAttribution/expireAttributionIfStale actions.

Attribution is now saved from both the legacy deeplink entrypoint (when security.dataCollectionForMarketing is true) and from AppStateEventListener after processAttribution, and the listener now clears currentDeeplink after processing to avoid re-saving on subsequent resumes.

On app start, persisted attribution is cleared if marketing consent isn’t explicitly granted, otherwise stale records are expired; new sagas also clear attribution on marketing opt-out and CLEAR_ONBOARDING. A migration (136) imports any legacy MMKV-stored attribution into the root persisted state and removes the old key, and selectors/tests are added to validate the new behavior.

Reviewed by Cursor Bugbot for commit 4e3ac25. Bugbot is set up for automated code reviews on this repo. Configure here.

@grvgoel81 grvgoel81 self-assigned this Apr 30, 2026
@grvgoel81 grvgoel81 requested a review from a team as a code owner April 30, 2026 07:03
@metamaskbotv2 metamaskbotv2 Bot added the team-onboarding Onboarding team label Apr 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes.

Comment thread app/core/AppStateEventListener.ts
Comment thread app/selectors/attribution/index.ts
@grvgoel81 grvgoel81 marked this pull request as draft April 30, 2026 08:11
@codecov-commenter

codecov-commenter commented Apr 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.09524% with 23 lines in your changes missing coverage. Please review.
✅ Project coverage is 82.15%. Comparing base (4e11714) to head (1dc19ac).
⚠️ Report is 34 commits behind head on main.

Files with missing lines Patch % Lines
app/core/redux/slices/attributionFromSources.ts 70.00% 6 Missing and 9 partials ⚠️
app/store/attributionPersistStorage.ts 37.50% 5 Missing ⚠️
app/core/redux/slices/attribution.ts 92.00% 0 Missing and 2 partials ⚠️
app/core/AppStateEventListener.ts 80.00% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main   #29542      +/-   ##
==========================================
- Coverage   82.16%   82.15%   -0.02%     
==========================================
  Files        5176     5184       +8     
  Lines      137275   137548     +273     
  Branches    31024    31115      +91     
==========================================
+ Hits       112795   112999     +204     
- Misses      16840    16888      +48     
- Partials     7640     7661      +21     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@grvgoel81

grvgoel81 commented May 4, 2026

Copy link
Copy Markdown
Contributor Author

There is a PR #29641 to skip the known failing test. Once it’s merged, I’ll update my PR.

@grvgoel81 grvgoel81 marked this pull request as ready for review May 4, 2026 07:23

@Cal-L Cal-L left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

left a comment

Comment thread app/store/attributionPersistStorage.ts Outdated
Cal-L
Cal-L previously approved these changes May 8, 2026

@Cal-L Cal-L left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

platform lgtm

@grvgoel81 grvgoel81 added force-builds Forces e2e native builds to trigger and removed force-builds Forces e2e native builds to trigger labels May 9, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 4e3ac25. Configure here.

Comment thread app/core/AppStateEventListener.ts
@github-actions github-actions Bot added size-XL and removed size-L labels May 11, 2026
@github-actions

Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeWalletPlatform
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 82%
click to see 🤖 AI reasoning details

E2E Test Selection:
The PR introduces a marketing attribution persistence system: a new Redux slice (attribution.ts), attribution extraction utilities (attributionFromSources.ts), a Redux migration (136), new sagas, and modifications to AppStateEventListener and handleDeeplink.

Key impacts:

  1. AppStateEventListener.ts now saves attribution to Redux and resets currentDeeplink to null after each background→active cycle (one-shot pattern). This affects the APP_OPENED analytics event flow.
  2. handleDeeplink.ts now saves attribution to Redux when dataCollectionForMarketing is true.
  3. Store initialization now clears or expires attribution based on marketing consent on every app start.
  4. Migration 136 migrates legacy MMKV attribution data to root Redux persist.
  5. processAttribution.tsx now handles both attributionId and attribution_id (snake_case fallback).

SmokeWalletPlatform is selected because:

  • It contains the wallet analytics tests (tests/smoke/wallet/analytics/new-wallet.spec.ts and import-wallet.spec.ts) that directly test analytics event tracking during wallet creation and import flows — exactly the flows affected by AppStateEventListener changes.
  • The AppStateEventListener one-shot deeplink reset could affect any analytics event tracking on app open.
  • The store initialization changes (attribution clear/expire) run on every app start and could affect test state.

No other tags are warranted: the changes are purely attribution/analytics infrastructure with no impact on confirmations, swaps, accounts, network management, snaps, browser, or other feature areas.

Performance Test Selection:
The changes are focused on marketing attribution data persistence (Redux slice, sagas, migration). These are lightweight operations (Redux dispatch, MMKV read/write during migration) that run infrequently and do not affect UI rendering, list performance, asset loading, or critical user flow performance. No performance tests are warranted.

View GitHub Actions results

@sonarqubecloud

Copy link
Copy Markdown

@grvgoel81 grvgoel81 enabled auto-merge May 11, 2026 07:46
@grvgoel81 grvgoel81 added this pull request to the merge queue May 12, 2026
try {
if (uri && typeof uri === 'string') {
AppStateEventProcessor.setCurrentDeeplink(uri, source);
if (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do we have a prettier issue here?

@github-merge-queue github-merge-queue Bot removed this pull request from the merge queue due to failed status checks May 12, 2026
@grvgoel81 grvgoel81 added this pull request to the merge queue May 12, 2026
Merged via the queue into main with commit a591a53 May 12, 2026
101 of 105 checks passed
@grvgoel81 grvgoel81 deleted the feat/TO-717-persistent-marketing-attribution branch May 12, 2026 19:15
@github-actions github-actions Bot locked and limited conversation to collaborators May 12, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.78.0 Issue or pull request that will be included in release 7.78.0 label May 12, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

force-builds Forces e2e native builds to trigger release-7.78.0 Issue or pull request that will be included in release 7.78.0 size-XL team-onboarding Onboarding team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants