Skip to content

Commit 60a0da0

Browse files
authored
chore(homepage): scaffold Hub Page Discovery Tabs A/B test (#29069)
## **Description** Scaffolds the LaunchDarkly A/B test for the Hub Page Navigational Discovery Tabs experiment ([MCU-589](https://consensyssoftware.atlassian.net/browse/TMCU-590)). This PR wires up the flag config, Redux selector, and analytics enrichment; **no UI behaviour is changed yet** Changes: - Added `HUB_PAGE_DISCOVERY_TABS_AB_KEY`, `HubPageDiscoveryTabsVariant` enum, `HUB_PAGE_DISCOVERY_TABS_VARIANTS`, and analytics mapping to `app/components/Views/Homepage/abTestConfig.ts` - Added `selectHubPageDiscoveryTabsABTest` selector to `app/selectors/featureFlagController/homepage/index.ts` — resolves `{ variantName, isActive }` from remote feature flags via `resolveABTestAssignment` - Registered `HUB_PAGE_DISCOVERY_TABS_AB_TEST_ANALYTICS_MAPPING` in `app/util/analytics/abTestAnalyticsRegistry.ts` so `Home Viewed` events are auto-enriched with `active_ab_tests` when the flag is active ## **Changelog** CHANGELOG entry:null ## **Related issues** Fixes: https://consensyssoftware.atlassian.net/browse/TMCU-590 ## **Manual testing steps** ```gherkin Feature: Hub Page Discovery Tabs A/B test flag Scenario: user opens the homepage with the flag active Given the LaunchDarkly flag coreMCU589AbtestHubPageDiscoveryTabs is enabled And the user is assigned to the treatment variant When user navigates to the Homepage Then the Home Viewed analytics event includes active_ab_tests: [{ key: 'coreMCU589AbtestHubPageDiscoveryTabs', value: 'treatment' }] Scenario: user opens the homepage with the flag inactive Given the LaunchDarkly flag coreMCU589AbtestHubPageDiscoveryTabs is off When user navigates to the Homepage Then the Home Viewed analytics event has no active_ab_tests entry for coreMCU589AbtestHubPageDiscoveryTabs ``` ## **Screenshots/Recordings** Feature flag logging correctly based on Launch Darkly configurations <img width="750" alt="Screenshot 2026-04-20 at 1 31 57 PM" src="https://github.com/user-attachments/assets/9755d531-a74f-412e-827f-3fc5572b73c9" /> ### **Before** `~` ### **After** `~` ## **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. #### 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](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/edit-v2/401401446401?draftShareId=9d77e1e1-4bdc-4be1-9ebb-ccd916988d93) to import wallets with many accounts and tokens - [ ] I've instrumented key operations with Sentry traces for production performance metrics - See [`trace()`](/app/util/trace.ts) for usage and [`addToken`](/app/components/Views/AddAsset/components/AddCustomToken/AddCustomToken.tsx#L274) for an example For performance guidelines and tooling, see the [Performance Guide](https://consensyssoftware.atlassian.net/wiki/spaces/TL1/pages/400085549067/Performance+Guide+for+Engineers). ## **Pre-merge reviewer checklist** - [x] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [x] 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. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: adds new A/B test constants/selector/analytics registration without changing UI behavior or core data flows; main risk is misconfigured flag/variant names affecting analytics enrichment. > > **Overview** > Adds scaffolding for the `coreMCU589AbtestHubPageDiscoveryTabs` LaunchDarkly A/B test, including variant definitions (`control`/`treatment`) and a config mapping indicating whether discovery tabs are enabled. > > Wires the experiment into state and analytics: introduces `selectHubPageDiscoveryTabsABTest` (via `resolveABTestAssignment`) and registers the test in `AB_TEST_ANALYTICS_MAPPINGS` so `Home Viewed` events are enriched with the experiment’s `active_ab_tests` entry when the assignment is active, with new unit tests covering selector resolution and analytics enrichment. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 0d37144. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent c47ffad commit 60a0da0

5 files changed

Lines changed: 108 additions & 2 deletions

File tree

app/components/Views/Homepage/abTestConfig.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,41 @@ import { EVENT_NAME } from '../../../core/Analytics/MetaMetrics.events';
22
import type { ABTestAnalyticsMapping } from '../../../util/analytics/abTestAnalytics.types';
33
import type { TransactionActiveAbTestEntry } from '../../../util/transactions/transaction-active-ab-test-attribution-registry';
44

5+
// ─── Hub Page Discovery Tabs ────────────────────────────────────────────────
6+
7+
/**
8+
* LaunchDarkly / remote flag key. Pattern: `{team}{TICKET}Abtest{Name}` — keep in
9+
* sync with the flag in LD (team `core`, ticket MCU-589).
10+
*/
11+
export const HUB_PAGE_DISCOVERY_TABS_AB_KEY =
12+
'coreMCU589AbtestHubPageDiscoveryTabs';
13+
14+
export enum HubPageDiscoveryTabsVariant {
15+
Control = 'control',
16+
Treatment = 'treatment',
17+
}
18+
19+
interface HubPageDiscoveryTabsVariantConfig {
20+
discoveryTabsEnabled: boolean;
21+
}
22+
23+
export const HUB_PAGE_DISCOVERY_TABS_VARIANTS: Record<
24+
HubPageDiscoveryTabsVariant,
25+
HubPageDiscoveryTabsVariantConfig
26+
> = {
27+
[HubPageDiscoveryTabsVariant.Control]: { discoveryTabsEnabled: false },
28+
[HubPageDiscoveryTabsVariant.Treatment]: { discoveryTabsEnabled: true },
29+
};
30+
31+
export const HUB_PAGE_DISCOVERY_TABS_AB_TEST_ANALYTICS_MAPPING: ABTestAnalyticsMapping =
32+
{
33+
flagKey: HUB_PAGE_DISCOVERY_TABS_AB_KEY,
34+
validVariants: Object.values(HubPageDiscoveryTabsVariant),
35+
eventNames: [EVENT_NAME.HOME_VIEWED],
36+
};
37+
38+
// ─── Trending Sections ───────────────────────────────────────────────────────
39+
540
/**
641
* LaunchDarkly / remote flag key. Pattern: `{team}{TICKET}Abtest{Name}` — keep in
742
* sync with the flag in LD (team `home`, ticket TMCU-470).

app/selectors/featureFlagController/homepage/index.test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { selectHomepageSectionsV1Enabled } from '.';
1+
import {
2+
selectHomepageSectionsV1Enabled,
3+
selectHubPageDiscoveryTabsABTest,
4+
} from '.';
25
// eslint-disable-next-line import-x/no-namespace
36
import * as remoteFeatureFlagModule from '../../../util/remoteFeatureFlag';
47

@@ -69,4 +72,39 @@ describe('Homepage Feature Flag Selectors', () => {
6972
expect(result).toBe(false);
7073
});
7174
});
75+
76+
describe('selectHubPageDiscoveryTabsABTest', () => {
77+
it('returns treatment assignment when flag is set to treatment string', () => {
78+
const result = selectHubPageDiscoveryTabsABTest.resultFunc({
79+
coreMCU589AbtestHubPageDiscoveryTabs: 'treatment',
80+
});
81+
expect(result).toEqual({ variantName: 'treatment', isActive: true });
82+
});
83+
84+
it('returns control assignment when flag is set to control string', () => {
85+
const result = selectHubPageDiscoveryTabsABTest.resultFunc({
86+
coreMCU589AbtestHubPageDiscoveryTabs: 'control',
87+
});
88+
expect(result).toEqual({ variantName: 'control', isActive: true });
89+
});
90+
91+
it('resolves controller object format ({ name }) for treatment', () => {
92+
const result = selectHubPageDiscoveryTabsABTest.resultFunc({
93+
coreMCU589AbtestHubPageDiscoveryTabs: { name: 'treatment' },
94+
});
95+
expect(result).toEqual({ variantName: 'treatment', isActive: true });
96+
});
97+
98+
it('falls back to control and isActive false when flag is missing', () => {
99+
const result = selectHubPageDiscoveryTabsABTest.resultFunc({});
100+
expect(result).toEqual({ variantName: 'control', isActive: false });
101+
});
102+
103+
it('falls back to control and isActive false when flag value is invalid', () => {
104+
const result = selectHubPageDiscoveryTabsABTest.resultFunc({
105+
coreMCU589AbtestHubPageDiscoveryTabs: 'unknown_variant',
106+
});
107+
expect(result).toEqual({ variantName: 'control', isActive: false });
108+
});
109+
});
72110
});

app/selectors/featureFlagController/homepage/index.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import {
44
validatedVersionGatedFeatureFlag,
55
VersionGatedFeatureFlag,
66
} from '../../../util/remoteFeatureFlag';
7+
import { resolveABTestAssignment } from '../../../util/abTest';
8+
import {
9+
HUB_PAGE_DISCOVERY_TABS_AB_KEY,
10+
HUB_PAGE_DISCOVERY_TABS_VARIANTS,
11+
} from '../../../components/Views/Homepage/abTestConfig';
712

813
const homepageSectionsV1Key = 'homepageSectionsV1';
914

@@ -16,3 +21,13 @@ export const selectHomepageSectionsV1Enabled = createSelector(
1621
return validatedVersionGatedFeatureFlag(remoteFlag) ?? false;
1722
},
1823
);
24+
25+
export const selectHubPageDiscoveryTabsABTest = createSelector(
26+
selectRemoteFeatureFlags,
27+
(remoteFeatureFlags) =>
28+
resolveABTestAssignment(
29+
remoteFeatureFlags,
30+
HUB_PAGE_DISCOVERY_TABS_AB_KEY,
31+
Object.keys(HUB_PAGE_DISCOVERY_TABS_VARIANTS),
32+
),
33+
);

app/util/analytics/abTestAnalyticsRegistry.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import type { ABTestAnalyticsMapping } from './abTestAnalytics.types';
22
import { CARD_BUTTON_BADGE_AB_TEST_ANALYTICS_MAPPING } from '../../components/UI/Card/components/CardButton/abTestConfig';
33
import { NUMPAD_QUICK_ACTIONS_AB_TEST_ANALYTICS_MAPPING } from '../../components/UI/Bridge/components/GaslessQuickPickOptions/abTestConfig';
44
import { TOKEN_SELECTOR_BALANCE_LAYOUT_AB_TEST_ANALYTICS_MAPPING } from '../../components/UI/Bridge/components/TokenSelectorItem.abTestConfig';
5-
import { HOMEPAGE_TRENDING_SECTIONS_AB_TEST_ANALYTICS_MAPPING } from '../../components/Views/Homepage/abTestConfig';
5+
import {
6+
HOMEPAGE_TRENDING_SECTIONS_AB_TEST_ANALYTICS_MAPPING,
7+
HUB_PAGE_DISCOVERY_TABS_AB_TEST_ANALYTICS_MAPPING,
8+
} from '../../components/Views/Homepage/abTestConfig';
69
import { STICKY_FOOTER_SWAP_LABEL_AB_TEST_ANALYTICS_MAPPING } from '../../components/UI/TokenDetails/components/abTestConfig';
710

811
export const AB_TEST_ANALYTICS_MAPPINGS: readonly ABTestAnalyticsMapping[] = [
@@ -15,6 +18,8 @@ export const AB_TEST_ANALYTICS_MAPPINGS: readonly ABTestAnalyticsMapping[] = [
1518

1619
// Homepage
1720
HOMEPAGE_TRENDING_SECTIONS_AB_TEST_ANALYTICS_MAPPING,
21+
HUB_PAGE_DISCOVERY_TABS_AB_TEST_ANALYTICS_MAPPING,
22+
1823
// Token Details
1924
STICKY_FOOTER_SWAP_LABEL_AB_TEST_ANALYTICS_MAPPING,
2025
];

app/util/analytics/enrichWithABTests.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,19 @@ describe('enrichWithABTests', () => {
120120
});
121121
});
122122

123+
it('enriches Home Viewed events with hub page discovery tabs assignment', () => {
124+
const event =
125+
AnalyticsEventBuilder.createEventBuilder('Home Viewed').build();
126+
127+
const result = enrichWithABTests(event, {
128+
coreMCU589AbtestHubPageDiscoveryTabs: 'treatment',
129+
});
130+
131+
expect(result.properties.active_ab_tests).toEqual([
132+
{ key: 'coreMCU589AbtestHubPageDiscoveryTabs', value: 'treatment' },
133+
]);
134+
});
135+
123136
it('leaves non-A/B properties and sensitive properties unchanged', () => {
124137
const event = AnalyticsEventBuilder.createEventBuilder('Card Button Viewed')
125138
.addProperties({

0 commit comments

Comments
 (0)