diff --git a/packages/remote-feature-flag-controller/CHANGELOG.md b/packages/remote-feature-flag-controller/CHANGELOG.md index dde66df33b4..9ac1a8ff9fd 100644 --- a/packages/remote-feature-flag-controller/CHANGELOG.md +++ b/packages/remote-feature-flag-controller/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **BREAKING:** Add required `getCanonicalProfileId` constructor option to `RemoteFeatureFlagController` for threshold flag segmentation ([#9325](https://github.com/MetaMask/core/pull/9325)) + - By default, canonical profile ID is used, but MetaMetrics ID can be used when the flag name is present in `metaMetricsFlags`, typically for scenarios when canonical profile ID is unavailable. +- Add optional `metaMetricsFlags` constructor option to `RemoteFeatureFlagController` to segment flags by MetaMetrics ID ([#9325](https://github.com/MetaMask/core/pull/9325)) + - Flags with names present in `metaMetricsFlags` are segmented by MetaMetrics ID; all others segment by canonical profile ID. - Add optional `defaultFeatureFlags` constructor option to `RemoteFeatureFlagController` for client-side defaults as the lowest-precedence layer under processed remote flags and local overrides ([#9747](https://github.com/MetaMask/core/pull/9747)) ### Changed diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts index af20d48bab9..5df6a8c4416 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.test.ts @@ -42,11 +42,17 @@ const MOCK_FLAGS_WITH_THRESHOLD = { scope: { type: 'threshold', value: 0.5 }, value: 'valueB', }, - { name: 'groupC', scope: { type: 'threshold', value: 1 }, value: 'valueC' }, + { + name: 'groupC', + scope: { type: 'threshold', value: 1 }, + value: 'valueC', + }, ], }; const MOCK_METRICS_ID = 'f9e8d7c6-b5a4-4210-9876-543210fedcba'; +const MOCK_CANONICAL_ID = + '0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420'; const MOCK_BASE_VERSION = '13.10.0'; /** @@ -57,6 +63,8 @@ const MOCK_BASE_VERSION = '13.10.0'; * @param options.clientConfigApiService - The client config API service instance * @param options.disabled - Whether the controller should start disabled * @param options.getMetaMetricsId - Returns metaMetricsId + * @param options.getCanonicalProfileId - Returns canonicalProfileId + * @param options.metaMetricsFlags - Names of feature flags that should use MetaMetrics ID * @param options.clientVersion - The client version string * @param options.prevClientVersion - The previous client version string * @param options.defaultFeatureFlags - Client-side default feature flags @@ -68,6 +76,8 @@ function createController( clientConfigApiService: AbstractClientConfigApiService; disabled: boolean; getMetaMetricsId: () => string; + getCanonicalProfileId: () => string; + metaMetricsFlags: readonly string[]; clientVersion: string; prevClientVersion: string; defaultFeatureFlags: FeatureFlags; @@ -83,6 +93,10 @@ function createController( getMetaMetricsId: options.getMetaMetricsId ?? ((): typeof MOCK_METRICS_ID => MOCK_METRICS_ID), + getCanonicalProfileId: + options.getCanonicalProfileId ?? + ((): typeof MOCK_CANONICAL_ID => MOCK_CANONICAL_ID), + metaMetricsFlags: options.metaMetricsFlags, clientVersion: options.clientVersion ?? MOCK_BASE_VERSION, prevClientVersion: options.prevClientVersion, defaultFeatureFlags: options.defaultFeatureFlags, @@ -474,6 +488,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlagForThreshold'], }); await messenger.call( 'RemoteFeatureFlagController:updateRemoteFeatureFlags', @@ -509,6 +524,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger.call( @@ -575,6 +591,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger.call( 'RemoteFeatureFlagController:updateRemoteFeatureFlags', @@ -619,6 +636,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['featureA', 'featureB'], }); // Act @@ -651,6 +669,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); // Act @@ -687,6 +706,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['mixedArray'], }); // Act @@ -728,6 +748,7 @@ describe('RemoteFeatureFlagController', () => { createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger1.call( 'RemoteFeatureFlagController:updateRemoteFeatureFlags', @@ -738,6 +759,7 @@ describe('RemoteFeatureFlagController', () => { createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger2.call( 'RemoteFeatureFlagController:updateRemoteFeatureFlags', @@ -752,10 +774,74 @@ describe('RemoteFeatureFlagController', () => { testFlag: 'control', }); }); + + it('uses getCanonicalProfileId for threshold flags absent from metaMetricsFlags', async () => { + const mockFlags = { + canonicalThresholdFlag: [ + { + name: 'groupA', + scope: { type: 'threshold', value: 0.5 }, + value: 'canonicalA', + }, + { + name: 'groupB', + scope: { type: 'threshold', value: 1.0 }, + value: 'canonicalB', + }, + ], + }; + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: mockFlags, + }); + const { controller, messenger } = createController({ + clientConfigApiService, + getMetaMetricsId: () => '', + getCanonicalProfileId: () => MOCK_CANONICAL_ID, + }); + + await messenger.call( + 'RemoteFeatureFlagController:updateRemoteFeatureFlags', + ); + + expect(controller.state.remoteFeatureFlags.canonicalThresholdFlag).toBe( + 'canonicalB', + ); + expect(controller.state.thresholdCache).toStrictEqual({ + [`${MOCK_CANONICAL_ID}:canonicalThresholdFlag`]: expect.any(Number), + }); + }); + + it('preserves threshold arrays when canonical profile id is empty', async () => { + const mockFlags = { + canonicalThresholdFlag: [ + { + name: 'groupA', + scope: { type: 'threshold', value: 1.0 }, + value: 'canonicalA', + }, + ], + }; + const clientConfigApiService = buildClientConfigApiService({ + remoteFeatureFlags: mockFlags, + }); + const { controller, messenger } = createController({ + clientConfigApiService, + getMetaMetricsId: () => MOCK_METRICS_ID, + getCanonicalProfileId: () => '', + }); + + await messenger.call( + 'RemoteFeatureFlagController:updateRemoteFeatureFlags', + ); + + expect( + controller.state.remoteFeatureFlags.canonicalThresholdFlag, + ).toStrictEqual(mockFlags.canonicalThresholdFlag); + }); }); describe('metaMetricsIds explicit targeting', () => { - const MOCK_FLAGS_WITH_EXPLICIT_IDS = { + const MOCK_FLAGS_WITH_EXPLICIT_IDS: FeatureFlags = { testFlag: [ { name: 'qaGroup', @@ -783,6 +869,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger.call( @@ -796,7 +883,7 @@ describe('RemoteFeatureFlagController', () => { }); it('first entry with a matching metaMetricsId wins when multiple entries match', async () => { - const mockFlags = { + const mockFlags: FeatureFlags = { testFlag: [ { name: 'first', @@ -818,6 +905,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger.call( @@ -831,7 +919,7 @@ describe('RemoteFeatureFlagController', () => { }); it('falls back to hash-based threshold when no entry matches the metaMetricsId', async () => { - const mockFlags = { + const mockFlags: FeatureFlags = { testFlag: [ { name: 'qaGroup', @@ -857,6 +945,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger.call( @@ -871,7 +960,7 @@ describe('RemoteFeatureFlagController', () => { }); it('ignores entries with a malformed metaMetricsIds (non-array) and falls back to hash-based threshold', async () => { - const mockFlags = { + const mockFlags: FeatureFlags = { testFlag: [ { name: 'badGroup', @@ -897,13 +986,14 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger.call( 'RemoteFeatureFlagController:updateRemoteFeatureFlags', ); - // Malformed entry ignored; hash-based selects groupA + // Malformed entry ignored; hash of MOCK_METRICS_ID + 'testFlag' selects groupA expect(controller.state.remoteFeatureFlags.testFlag).toBe('valueA'); expect(controller.state.featureFlagThresholdGroups).toStrictEqual({ testFlag: 'groupA', @@ -911,7 +1001,7 @@ describe('RemoteFeatureFlagController', () => { }); it('ignores non-string items within metaMetricsIds when matching', async () => { - const mockFlags = { + const mockFlags: FeatureFlags = { testFlag: [ { name: 'badGroup', @@ -946,7 +1036,7 @@ describe('RemoteFeatureFlagController', () => { }); it('normalizes metaMetricsId with trim and toLowerCase before matching', async () => { - const mockFlags = { + const mockFlags: FeatureFlags = { testFlag: [ { name: 'qaGroup', @@ -1000,7 +1090,7 @@ describe('RemoteFeatureFlagController', () => { }); it('still populates the threshold cache for hash-based fallback when no explicit ID matches', async () => { - const mockFlags = { + const mockFlags: FeatureFlags = { testFlag: [ { name: 'qaGroup', @@ -1021,6 +1111,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], }); await messenger.call( @@ -1054,9 +1145,12 @@ describe('RemoteFeatureFlagController', () => { const clientConfigApiService = buildClientConfigApiService({ remoteFeatureFlags: MOCK_FLAGS_WITH_EXPLICIT_IDS, }); + // This flag segments by MetaMetrics ID, so an unavailable ID leaves the + // threshold array unprocessed. const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => '', + metaMetricsFlags: ['testFlag'], }); await messenger.call( @@ -1091,7 +1185,7 @@ describe('RemoteFeatureFlagController', () => { }); it('supports ThresholdVersion.DirectValue entries with explicit-ID matching', async () => { - const mockFlags = { + const mockFlags: FeatureFlags = { testFlag: [ { thresholdName: 'qaGroup', @@ -1424,6 +1518,7 @@ describe('RemoteFeatureFlagController', () => { clientConfigApiService: mockApiService, clientVersion: '13.1.5', // Qualifies for 13.1.0 version but not 13.2.0 getMetaMetricsId: () => MOCK_METRICS_ID, // This generates threshold > 0.7 + metaMetricsFlags: ['multiVersionABFlag'], }); await messenger.call( @@ -2110,6 +2205,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['flagA', 'flagB'], }); // Act - First update: both flags processed @@ -2240,6 +2336,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['persistentFlag'], }); // Act - Multiple updates with same flag @@ -2286,6 +2383,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['testFlag'], state: { thresholdCache: { [`${differentUserId}:oldFlag`]: 0.123, // Different user's cache @@ -2322,6 +2420,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['newFlag'], }); // Act - Process with empty cache @@ -2351,6 +2450,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['oldFlag', 'newFlag'], }); await messenger.call( @@ -2413,6 +2513,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => '', // Empty metaMetricsId + metaMetricsFlags: ['thresholdFlag'], }); // Act @@ -2443,6 +2544,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['feature:v2'], }); // Act @@ -2491,6 +2593,7 @@ describe('RemoteFeatureFlagController', () => { const { controller, messenger } = createController({ clientConfigApiService, getMetaMetricsId: () => MOCK_METRICS_ID, + metaMetricsFlags: ['flagA', 'flagB'], }); // Act - First update populates cache diff --git a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts index 4c9546beae8..55a87a6f1e5 100644 --- a/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts +++ b/packages/remote-feature-flag-controller/src/remote-feature-flag-controller.ts @@ -180,6 +180,10 @@ export class RemoteFeatureFlagController extends BaseController< readonly #getMetaMetricsId: () => string; + readonly #getCanonicalProfileId: () => string; + + readonly #metaMetricsFlags: ReadonlySet; + readonly #clientVersion: SemVerVersion; readonly #defaultFeatureFlags: FeatureFlags; @@ -196,6 +200,8 @@ export class RemoteFeatureFlagController extends BaseController< * @param options.fetchInterval - The interval in milliseconds before cached flags expire. Defaults to 1 day. * @param options.disabled - Determines if the controller should be disabled initially. Defaults to false. * @param options.getMetaMetricsId - Returns metaMetricsId. + * @param options.getCanonicalProfileId - Returns the canonical profile identifier used for threshold flags by default. Must return an empty string, rather than throwing, when the identifier is unavailable. + * @param options.metaMetricsFlags - Names of feature flags that should use MetaMetrics ID for threshold assignment. * @param options.clientVersion - The current client version for version-based feature flag filtering. Must be a valid 3-part SemVer version string. * @param options.prevClientVersion - The previous client version for feature flag cache invalidation. * @param options.defaultFeatureFlags - Client-side default feature flags used as the lowest-precedence layer under processed remote flags and local overrides. Not persisted. @@ -207,6 +213,8 @@ export class RemoteFeatureFlagController extends BaseController< fetchInterval = DEFAULT_CACHE_DURATION, disabled = false, getMetaMetricsId, + getCanonicalProfileId, + metaMetricsFlags = [], clientVersion, prevClientVersion, defaultFeatureFlags = {}, @@ -215,6 +223,8 @@ export class RemoteFeatureFlagController extends BaseController< state?: Partial; clientConfigApiService: AbstractClientConfigApiService; getMetaMetricsId: () => string; + getCanonicalProfileId: () => string; + metaMetricsFlags?: readonly string[]; fetchInterval?: number; disabled?: boolean; clientVersion: string; @@ -258,6 +268,8 @@ export class RemoteFeatureFlagController extends BaseController< this.#disabled = disabled; this.#clientConfigApiService = clientConfigApiService; this.#getMetaMetricsId = getMetaMetricsId; + this.#getCanonicalProfileId = getCanonicalProfileId; + this.#metaMetricsFlags = new Set(metaMetricsFlags); this.#clientVersion = clientVersion; this.messenger.registerMethodActionHandlers( @@ -404,6 +416,21 @@ export class RemoteFeatureFlagController extends BaseController< return getVersionData(flagValue, this.#clientVersion); } + /** + * Selects the identifier used to bucket a threshold flag. Flags named in + * `metaMetricsFlags` segment by MetaMetrics ID; all others segment by + * canonical profile ID. + * + * @param featureFlagName - The name of the feature flag being processed. + * @returns The segmentation identifier, which may be empty when unavailable. + */ + #getSegmentationId(featureFlagName: string): string { + if (this.#metaMetricsFlags.has(featureFlagName)) { + return this.#getMetaMetricsId(); + } + return this.#getCanonicalProfileId(); + } + /** * Resolves raw feature flags into the values that apply to this client and * user, selecting version and threshold entries and reconciling the @@ -446,19 +473,11 @@ export class RemoteFeatureFlagController extends BaseController< continue; } - // Skip threshold processing if metaMetricsId is not available - if (!metaMetricsId) { - // Preserve array as-is when user hasn't opted into MetaMetrics - processedFlags[remoteFeatureFlagName] = processedValue; - continue; - } - // Explicit-ID matching: check before hash-based threshold, bypasses cache const normalizedMetaMetricsId = metaMetricsId.trim().toLowerCase(); - const explicitMatch = findExplicitIdMatch( - processedValue, - normalizedMetaMetricsId, - ); + const explicitMatch = normalizedMetaMetricsId + ? findExplicitIdMatch(processedValue, normalizedMetaMetricsId) + : undefined; if (explicitMatch) { processedValue = explicitMatch.value; @@ -467,13 +486,21 @@ export class RemoteFeatureFlagController extends BaseController< explicitMatch.name; } } else { + const segmentationId = this.#getSegmentationId(remoteFeatureFlagName); + + if (!segmentationId) { + processedFlags[remoteFeatureFlagName] = processedValue; + continue; + } + // Fall back to hash-based threshold selection with cache - const cacheKey = `${metaMetricsId}:${remoteFeatureFlagName}` as const; + const cacheKey = + `${segmentationId}:${remoteFeatureFlagName}` as const; let thresholdValue = this.state.thresholdCache?.[cacheKey]; if (thresholdValue === undefined) { thresholdValue = await calculateThresholdForFlag( - metaMetricsId, + segmentationId, remoteFeatureFlagName, ); @@ -510,13 +537,17 @@ export class RemoteFeatureFlagController extends BaseController< ...thresholdCacheUpdates, }; - // Drop cached thresholds for flags this user is no longer served. + // Drop cached thresholds for flags this user is no longer served, under + // either identifier they may have been bucketed by. + const canonicalProfileId = this.#getCanonicalProfileId(); const currentFlagNames = Object.keys(remoteFeatureFlags); for (const cacheKey of Object.keys(thresholdCache)) { - const [cachedMetaMetricsId, ...cachedFlagNameParts] = cacheKey.split(':'); + const [cachedSegmentationId, ...cachedFlagNameParts] = + cacheKey.split(':'); const cachedFlagName = cachedFlagNameParts.join(':'); if ( - cachedMetaMetricsId === metaMetricsId && + (cachedSegmentationId === metaMetricsId || + cachedSegmentationId === canonicalProfileId) && !currentFlagNames.includes(cachedFlagName) ) { delete thresholdCache[cacheKey]; diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts index aa11941462f..647020e0010 100644 --- a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.test.ts @@ -124,15 +124,15 @@ describe('user-segmentation-utils', () => { expect(threshold).toBeLessThanOrEqual(1); }); - it('throws error when metaMetricsId is empty', async () => { + it('throws error when segmentation ID is empty', async () => { // Arrange - const emptyMetaMetricsId = ''; + const emptySegmentationId = ''; const flagName = 'testFlag'; // Act & Assert await expect( - calculateThresholdForFlag(emptyMetaMetricsId, flagName), - ).rejects.toThrow('MetaMetrics ID cannot be empty'); + calculateThresholdForFlag(emptySegmentationId, flagName), + ).rejects.toThrow('Segmentation ID cannot be empty'); }); it('throws error when featureFlagName is empty', async () => { diff --git a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts index e41a721e4b0..b85d9a8ab5d 100644 --- a/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts +++ b/packages/remote-feature-flag-controller/src/utils/user-segmentation-utils.ts @@ -22,27 +22,27 @@ const UUID_V4_VALUE_RANGE_BIGINT = MAX_UUID_V4_BIGINT - MIN_UUID_V4_BIGINT; /** * Calculates a deterministic threshold value between 0 and 1 for A/B testing. - * This function hashes the user's MetaMetrics ID combined with the feature flag name + * This function hashes the segmentation ID combined with the feature flag name * to ensure consistent group assignment across sessions while varying across different flags. * - * @param metaMetricsId - The user's MetaMetrics ID (must be non-empty) + * @param segmentationId - The identifier used for threshold segmentation (must be non-empty) * @param featureFlagName - The feature flag name to create unique threshold per flag * @returns A promise that resolves to a number between 0 and 1 - * @throws Error if metaMetricsId is empty + * @throws Error if segmentationId is empty */ export async function calculateThresholdForFlag( - metaMetricsId: string, + segmentationId: string, featureFlagName: string, ): Promise { - if (!metaMetricsId) { - throw new Error('MetaMetrics ID cannot be empty'); + if (!segmentationId) { + throw new Error('Segmentation ID cannot be empty'); } if (!featureFlagName) { throw new Error('Feature flag name cannot be empty'); } - const seed = metaMetricsId + featureFlagName; + const seed = segmentationId + featureFlagName; // Hash the combined seed const encoder = new TextEncoder(); diff --git a/packages/wallet/CHANGELOG.md b/packages/wallet/CHANGELOG.md index 4e1aaa40ce5..a2587781b13 100644 --- a/packages/wallet/CHANGELOG.md +++ b/packages/wallet/CHANGELOG.md @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add optional `instanceOptions.remoteFeatureFlagController.getCanonicalProfileId` constructor option to `RemoteFeatureFlagController` for threshold flag segmentation ([#9325](https://github.com/MetaMask/core/pull/9325)) +- Add optional `instanceOptions.remoteFeatureFlagController.metaMetricsFlags` constructor option to `RemoteFeatureFlagController` to segment flags by MetaMetrics ID ([#9325](https://github.com/MetaMask/core/pull/9325)) + ## [11.0.0] ### Changed diff --git a/packages/wallet/src/initialization/instances/remote-feature-flag-controller/remote-feature-flag-controller.ts b/packages/wallet/src/initialization/instances/remote-feature-flag-controller/remote-feature-flag-controller.ts index 5d20fde04ea..29b29fdcd3b 100644 --- a/packages/wallet/src/initialization/instances/remote-feature-flag-controller/remote-feature-flag-controller.ts +++ b/packages/wallet/src/initialization/instances/remote-feature-flag-controller/remote-feature-flag-controller.ts @@ -17,6 +17,9 @@ export const remoteFeatureFlagController: InitializationConfiguration< messenger, clientConfigApiService: options.clientConfigApiService, getMetaMetricsId: options.getMetaMetricsId ?? ((): string => ''), + getCanonicalProfileId: + options.getCanonicalProfileId ?? ((): string => ''), + metaMetricsFlags: options.metaMetricsFlags, clientVersion: options.clientVersion ?? '0.0.0', prevClientVersion: options.prevClientVersion, fetchInterval: options.fetchInterval, diff --git a/packages/wallet/src/initialization/instances/remote-feature-flag-controller/types.ts b/packages/wallet/src/initialization/instances/remote-feature-flag-controller/types.ts index d6210ee0ebb..60e0e7867fc 100644 --- a/packages/wallet/src/initialization/instances/remote-feature-flag-controller/types.ts +++ b/packages/wallet/src/initialization/instances/remote-feature-flag-controller/types.ts @@ -21,6 +21,16 @@ export type RemoteFeatureFlagControllerInstanceOptions = { * Defaults to `() => ''`. */ getMetaMetricsId?: RemoteFeatureFlagControllerOptions['getMetaMetricsId']; + /** + * Returns the canonical profile identifier used for threshold flags by + * default. Defaults to `() => ''`. + */ + getCanonicalProfileId?: RemoteFeatureFlagControllerOptions['getCanonicalProfileId']; + /** + * Names of feature flags that should use MetaMetrics ID for threshold + * assignment. Flags not listed here use the canonical profile ID. + */ + metaMetricsFlags?: RemoteFeatureFlagControllerOptions['metaMetricsFlags']; /** * The current client version for version-based flag filtering. Must be a * valid 3-part SemVer or the controller throws. Defaults to `'0.0.0'`.