From 7df1453a3d4e2b5712343eec45ed8fc9675ffaa5 Mon Sep 17 00:00:00 2001 From: Gaurav Goel Date: Tue, 24 Mar 2026 13:17:56 +0000 Subject: [PATCH] chore(runway): cherry-pick feat: add metrics opt In event (#27846) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** * Add Metrics Opt In event in Onboarding, Optinmetrics and MetaMetricsAndDataCollectionSection screen ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** ```gherkin Feature: METRICS_OPT_IN analytics on user opt-in Scenario: User opts in from onboarding MetaMetrics screen Given the user is on the onboarding MetaMetrics / data collection screen with basic usage enabled by default When the user continues without turning off basic usage Then the app completes onboarding as before and analytics pipelines receive a "Metrics Opt In" event with onboarding location and expected properties in addition to "Analytics Preference Selected" Scenario: User enables MetaMetrics from Settings Given the user is logged in and MetaMetrics is currently off When the user opens Settings > Security & privacy and turns the MetaMetrics switch on Then the app opts in successfully and emits "Metrics Opt In" with settings location and updated_after_onboarding before the preference-selected event Scenario: User enables marketing which requires MetaMetrics Given MetaMetrics is off and marketing data collection is off When the user turns marketing data collection on (which enables MetaMetrics) Then MetaMetrics turns on and "Metrics Opt In" is recorded before the subsequent preference events ``` ## **Screenshots/Recordings** ### **Before** ### **After** Screenshot 2026-03-24 at 3 35 19 PM Screenshot 2026-03-24 at 3 36 30 PM Screenshot 2026-03-24 at 3 40 10 PM ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] I've documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I've applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **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] > **Low Risk** > Low risk analytics-only change that adds an additional tracking call when users enable metrics (onboarding, social login, and settings). Main risk is event ordering/duplication affecting downstream dashboards rather than app behavior. > > **Overview** > Adds a new `MetaMetricsEvents.METRICS_OPT_IN` event and emits it whenever users enable metrics, including the onboarding opt-in screen (`location: onboarding_metametrics`), social login onboarding flow (`location: onboarding_social_login`), and the settings MetaMetrics toggle (`location: settings` / `onboarding_default_settings`). > > Updates tests to assert the new opt-in event is sent (and in settings/onboarding cases is sent *before* `ANALYTICS_PREFERENCE_SELECTED`), including verifying `updated_after_onboarding` and optional `account_type` properties. > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 9968f73a4a81ca8b53ccb4dcfc581560fc4652bd. Configure [here](https://cursor.com/dashboard?tab=bugbot). --- app/components/UI/OptinMetrics/index.test.tsx | 30 +++++++++++ app/components/UI/OptinMetrics/index.tsx | 28 ++++++----- .../Views/Onboarding/index.test.tsx | 8 +++ app/components/Views/Onboarding/index.tsx | 13 ++++- ...taMetricsAndDataCollectionSection.test.tsx | 50 +++++++++++++++++-- .../MetaMetricsAndDataCollectionSection.tsx | 11 ++++ app/core/Analytics/MetaMetrics.events.ts | 2 + 7 files changed, 124 insertions(+), 18 deletions(-) diff --git a/app/components/UI/OptinMetrics/index.test.tsx b/app/components/UI/OptinMetrics/index.test.tsx index f40b25cf32ca..a8c5373eadce 100644 --- a/app/components/UI/OptinMetrics/index.test.tsx +++ b/app/components/UI/OptinMetrics/index.test.tsx @@ -146,6 +146,16 @@ describe('OptinMetrics', () => { await waitFor(() => { expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( 1, + expect.objectContaining({ + name: MetaMetricsEvents.METRICS_OPT_IN.category, + properties: expect.objectContaining({ + location: 'onboarding_metametrics', + updated_after_onboarding: false, + }), + }), + ); + expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ name: 'Analytics Preference Selected', properties: expect.objectContaining({ @@ -177,6 +187,16 @@ describe('OptinMetrics', () => { await waitFor(() => { expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( 1, + expect.objectContaining({ + name: MetaMetricsEvents.METRICS_OPT_IN.category, + properties: expect.objectContaining({ + location: 'onboarding_metametrics', + updated_after_onboarding: false, + }), + }), + ); + expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ name: 'Analytics Preference Selected', properties: expect.objectContaining({ @@ -212,6 +232,16 @@ describe('OptinMetrics', () => { ); await waitFor(() => { + expect(mockAnalytics.trackEvent).toHaveBeenCalledWith( + expect.objectContaining({ + name: MetaMetricsEvents.METRICS_OPT_IN.category, + properties: expect.objectContaining({ + location: 'onboarding_metametrics', + updated_after_onboarding: false, + account_type: AccountType.Imported, + }), + }), + ); expect(mockAnalytics.trackEvent).toHaveBeenCalledWith( expect.objectContaining({ name: 'Analytics Preference Selected', diff --git a/app/components/UI/OptinMetrics/index.tsx b/app/components/UI/OptinMetrics/index.tsx index 63871a546dae..7485dff5b08b 100644 --- a/app/components/UI/OptinMetrics/index.tsx +++ b/app/components/UI/OptinMetrics/index.tsx @@ -168,19 +168,21 @@ const OptinMetrics = () => { dispatch(setDataCollectionForMarketing(isMarketingChecked)); - // Track opt-out event if user opted out of metrics - if (!isBasicUsageChecked) { - metrics.trackEvent( - metrics - .createEventBuilder(MetaMetricsEvents.METRICS_OPT_OUT) - .addProperties({ - updated_after_onboarding: false, - location: 'onboarding_metametrics', - ...(accountType && { account_type: accountType }), - }) - .build(), - ); - } + // Track opt-in / opt-out for metrics + metrics.trackEvent( + metrics + .createEventBuilder( + isBasicUsageChecked + ? MetaMetricsEvents.METRICS_OPT_IN + : MetaMetricsEvents.METRICS_OPT_OUT, + ) + .addProperties({ + updated_after_onboarding: false, + location: 'onboarding_metametrics', + ...(accountType && { account_type: accountType }), + }) + .build(), + ); metrics.trackEvent( metrics diff --git a/app/components/Views/Onboarding/index.test.tsx b/app/components/Views/Onboarding/index.test.tsx index 4608707035d5..771c308e15be 100644 --- a/app/components/Views/Onboarding/index.test.tsx +++ b/app/components/Views/Onboarding/index.test.tsx @@ -75,6 +75,7 @@ import { captureException } from '@sentry/react-native'; import Logger from '../../../util/Logger'; import { MIGRATION_ERROR_HAPPENED } from '../../../constants/storage'; import { AccountType } from '../../../constants/onboarding'; +import { MetaMetricsEvents } from '../../../core/Analytics'; // Mock netinfo - using existing mock jest.mock('@react-native-community/netinfo'); @@ -1990,6 +1991,13 @@ describe('Onboarding', () => { await waitFor(() => { expect(mockAnalytics.optIn).toHaveBeenCalled(); + expect( + mockCreateEventBuilder.mock.calls.some( + (call) => + (call[0] as { category: string }).category === + MetaMetricsEvents.METRICS_OPT_IN.category, + ), + ).toBe(true); }); }); }); diff --git a/app/components/Views/Onboarding/index.tsx b/app/components/Views/Onboarding/index.tsx index 353227bd74ec..f2d715f0a34b 100644 --- a/app/components/Views/Onboarding/index.tsx +++ b/app/components/Views/Onboarding/index.tsx @@ -733,6 +733,18 @@ const Onboarding = () => { discardBufferedTraces(); await setupSentry(); + const accountType = getSocialAccountType(provider, !createWallet); + metrics.trackEvent( + metrics + .createEventBuilder(MetaMetricsEvents.METRICS_OPT_IN) + .addProperties({ + updated_after_onboarding: false, + location: 'onboarding_social_login', + account_type: accountType, + }) + .build(), + ); + // use new trace instead of buffered trace for social login onboardingTraceCtx.current = trace({ name: TraceName.OnboardingJourneyOverall, @@ -740,7 +752,6 @@ const Onboarding = () => { tags: getTraceTags(store.getState()), }); - const accountType = getSocialAccountType(provider, !createWallet); if (createWallet) { track(MetaMetricsEvents.WALLET_SETUP_STARTED, { account_type: accountType, diff --git a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.test.tsx b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.test.tsx index c5ebee60e6d8..545f466e3544 100644 --- a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.test.tsx +++ b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.test.tsx @@ -377,7 +377,18 @@ describe('MetaMetricsAndDataCollectionSection', () => { deviceProp: 'Device value', userProp: 'User value', }); - expect(mockAnalytics.trackEvent).toHaveBeenCalledWith( + expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + name: MetaMetricsEvents.METRICS_OPT_IN.category, + properties: expect.objectContaining({ + updated_after_onboarding: true, + location: 'settings', + }), + }), + ); + expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ name: MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED.category, properties: expect.objectContaining({ @@ -407,7 +418,18 @@ describe('MetaMetricsAndDataCollectionSection', () => { fireEvent(metaMetricsSwitch, 'valueChange', true); await waitFor(() => { - expect(mockAnalytics.trackEvent).toHaveBeenCalledWith( + expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + name: MetaMetricsEvents.METRICS_OPT_IN.category, + properties: expect.objectContaining({ + updated_after_onboarding: true, + location: 'onboarding_default_settings', + }), + }), + ); + expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ name: MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED.category, properties: expect.objectContaining({ @@ -467,6 +489,16 @@ describe('MetaMetricsAndDataCollectionSection', () => { fireEvent(metaMetricsSwitch, 'valueChange', true); await waitFor(() => { + expect(mockAnalytics.trackEvent).toHaveBeenCalledWith( + expect.objectContaining({ + name: MetaMetricsEvents.METRICS_OPT_IN.category, + properties: expect.objectContaining({ + updated_after_onboarding: true, + location: 'settings', + account_type: AccountType.MetamaskGoogle, + }), + }), + ); expect(mockAnalytics.trackEvent).toHaveBeenCalledWith( expect.objectContaining({ name: MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED.category, @@ -808,6 +840,16 @@ describe('MetaMetricsAndDataCollectionSection', () => { }); expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( 1, + expect.objectContaining({ + name: MetaMetricsEvents.METRICS_OPT_IN.category, + properties: expect.objectContaining({ + location: 'settings', + updated_after_onboarding: true, + }), + }), + ); + expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ name: MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED.category, properties: expect.objectContaining({ @@ -827,8 +869,8 @@ describe('MetaMetricsAndDataCollectionSection', () => { }, ); expect(mockAnalytics.trackEvent).toHaveBeenNthCalledWith( - // if MetaMetrics is initially disabled, trackEvent is called twice and this is 2nd call - !metaMetricsInitiallyEnabled ? 2 : 1, + // if MetaMetrics is initially disabled, marketing consent is the 3rd trackEvent + !metaMetricsInitiallyEnabled ? 3 : 1, expect.objectContaining({ name: MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED.category, properties: expect.objectContaining({ diff --git a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.tsx b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.tsx index c2940f9408b2..59debfafdfba 100644 --- a/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.tsx +++ b/app/components/Views/Settings/SecuritySettings/Sections/MetaMetricsAndDataCollectionSection/MetaMetricsAndDataCollectionSection.tsx @@ -114,6 +114,17 @@ const MetaMetricsAndDataCollectionSection: React.FC< setAnalyticsEnabled(true); analytics.identify(consolidatedTraits); + analytics.trackEvent( + AnalyticsEventBuilder.createEventBuilder( + MetaMetricsEvents.METRICS_OPT_IN, + ) + .addProperties({ + updated_after_onboarding: true, + location: analyticsLocation, + ...(accountType && { account_type: accountType }), + }) + .build(), + ); analytics.trackEvent( AnalyticsEventBuilder.createEventBuilder( MetaMetricsEvents.ANALYTICS_PREFERENCE_SELECTED, diff --git a/app/core/Analytics/MetaMetrics.events.ts b/app/core/Analytics/MetaMetrics.events.ts index 063eb7bc6213..3e0d1d23446e 100644 --- a/app/core/Analytics/MetaMetrics.events.ts +++ b/app/core/Analytics/MetaMetrics.events.ts @@ -124,6 +124,7 @@ enum EVENT_NAME { // Analytics ANALYTICS_PREFERENCE_SELECTED = 'Analytics Preference Selected', + METRICS_OPT_IN = 'Metrics Opt In', METRICS_OPT_OUT = 'Metrics Opt Out', ANALYTICS_REQUEST_DATA_DELETION = 'Delete MetaMetrics Data Request Submitted', EXPERIMENT_VIEWED = 'Experiment Viewed', @@ -829,6 +830,7 @@ const events = { ANALYTICS_PREFERENCE_SELECTED: generateOpt( EVENT_NAME.ANALYTICS_PREFERENCE_SELECTED, ), + METRICS_OPT_IN: generateOpt(EVENT_NAME.METRICS_OPT_IN), METRICS_OPT_OUT: generateOpt(EVENT_NAME.METRICS_OPT_OUT), ANALYTICS_REQUEST_DATA_DELETION: generateOpt( EVENT_NAME.ANALYTICS_REQUEST_DATA_DELETION,