diff --git a/src/analytics/AppAnalytics.test.ts b/src/analytics/AppAnalytics.test.ts index 594fb550214..6e0fa334e85 100644 --- a/src/analytics/AppAnalytics.test.ts +++ b/src/analytics/AppAnalytics.test.ts @@ -2,6 +2,7 @@ import { createClient } from '@segment/analytics-react-native' import { PincodeType } from 'src/account/reducer' import AppAnalyticsModule from 'src/analytics/AppAnalytics' import { OnboardingEvents } from 'src/analytics/Events' +import * as config from 'src/config' import { store } from 'src/redux/store' import { getDefaultStatsigUser, getFeatureGate, getMultichainFeatures } from 'src/statsig' import { NetworkId } from 'src/transactions/types' @@ -25,10 +26,6 @@ jest.mock('@segment/analytics-react-native-plugin-clevertap') jest.mock('@segment/analytics-react-native-plugin-firebase') jest.mock('@sentry/react-native', () => ({ init: jest.fn() })) jest.mock('src/redux/store', () => ({ store: { getState: jest.fn() } })) -jest.mock('src/config', () => ({ - ...(jest.requireActual('src/config') as any), - STATSIG_API_KEY: 'statsig-key', -})) jest.mock('statsig-react-native') jest.mock('src/statsig') jest.mock('src/web3/networkConfig', () => { @@ -186,6 +183,7 @@ beforeAll(() => { describe('AppAnalytics', () => { let AppAnalytics: typeof AppAnalyticsModule + let mockConfig: jest.MockedObject const mockSegmentClient = { identify: jest.fn().mockResolvedValue(undefined), track: jest.fn().mockResolvedValue(undefined), @@ -205,7 +203,11 @@ describe('AppAnalytics', () => { jest.unmock('src/analytics/AppAnalytics') jest.isolateModules(() => { AppAnalytics = require('src/analytics/AppAnalytics').default + mockConfig = require('src/config') }) + + mockConfig.STATSIG_API_KEY = 'statsig-key' + mockConfig.STATSIG_ENABLED = true mockStore.getState.mockImplementation(() => state) jest.mocked(getFeatureGate).mockReturnValue(true) jest.mocked(getMultichainFeatures).mockReturnValue({ @@ -213,14 +215,33 @@ describe('AppAnalytics', () => { }) }) - it('creates statsig client on initialization with default statsig user', async () => { - jest.mocked(getDefaultStatsigUser).mockReturnValue({ userID: 'someUserId' }) - await AppAnalytics.init() - expect(Statsig.initialize).toHaveBeenCalledWith( - 'statsig-key', - { userID: 'someUserId' }, - { environment: { tier: 'development' }, overrideStableID: 'anonId', localMode: false } - ) + describe('init', () => { + it('initializes segment', async () => { + await AppAnalytics.init() + expect(mockCreateSegmentClient).toHaveBeenCalled() + }) + + it('does not initialize segment if SEGMENT_API_KEY is not present', async () => { + mockConfig.SEGMENT_API_KEY = undefined + await AppAnalytics.init() + expect(mockCreateSegmentClient).not.toHaveBeenCalled() + }) + + it('creates statsig client on initialization with default statsig user', async () => { + jest.mocked(getDefaultStatsigUser).mockReturnValue({ userID: 'someUserId' }) + await AppAnalytics.init() + expect(Statsig.initialize).toHaveBeenCalledWith( + 'statsig-key', + { userID: 'someUserId' }, + { environment: { tier: 'development' }, overrideStableID: 'anonId' } + ) + }) + + it('does not initialize statsig if STATSIG_ENABLED is false', async () => { + mockConfig.STATSIG_ENABLED = false + await AppAnalytics.init() + expect(Statsig.initialize).not.toHaveBeenCalled() + }) }) it('delays identify calls until async init has finished', async () => { diff --git a/src/analytics/AppAnalytics.ts b/src/analytics/AppAnalytics.ts index 3697dcc1df1..ee1bea5ca77 100644 --- a/src/analytics/AppAnalytics.ts +++ b/src/analytics/AppAnalytics.ts @@ -14,11 +14,11 @@ import { AnalyticsPropertiesList } from 'src/analytics/Properties' import { getCurrentUserTraits } from 'src/analytics/selectors' import { DEFAULT_TESTNET, - E2E_TEST_STATSIG_ID, FIREBASE_ENABLED, isE2EEnv, SEGMENT_API_KEY, STATSIG_API_KEY, + STATSIG_ENABLED, STATSIG_ENV, } from 'src/config' import { store } from 'src/redux/store' @@ -101,60 +101,60 @@ class AppAnalytics { async init() { let uniqueID - try { - if (!SEGMENT_API_KEY) { - throw Error('API Key not present, likely due to environment. Skipping enabling') - } - this.segmentClient = createClient({ - debug: __DEV__, - trackAppLifecycleEvents: true, - trackDeepLinks: true, - writeKey: SEGMENT_API_KEY, - storePersistor: AsyncStoragePersistor, - }) + if (SEGMENT_API_KEY) { + try { + this.segmentClient = createClient({ + debug: __DEV__, + trackAppLifecycleEvents: true, + trackDeepLinks: true, + writeKey: SEGMENT_API_KEY, + storePersistor: AsyncStoragePersistor, + }) - this.segmentClient.add({ plugin: new DestinationFiltersPlugin() }) - this.segmentClient.add({ plugin: new InjectTraits() }) - this.segmentClient.add({ plugin: new AdjustPlugin() }) - this.segmentClient.add({ plugin: new ClevertapPlugin() }) - if (FIREBASE_ENABLED) { - this.segmentClient.add({ plugin: new FirebasePlugin() }) - } + this.segmentClient.add({ plugin: new DestinationFiltersPlugin() }) + this.segmentClient.add({ plugin: new InjectTraits() }) + this.segmentClient.add({ plugin: new AdjustPlugin() }) + this.segmentClient.add({ plugin: new ClevertapPlugin() }) + if (FIREBASE_ENABLED) { + this.segmentClient.add({ plugin: new FirebasePlugin() }) + } - try { - const deviceInfo = await getDeviceInfo() - this.deviceInfo = deviceInfo - uniqueID = deviceInfo.UniqueID - this.sessionId = sha256(Buffer.from(uniqueID + String(Date.now()))).slice(2) - } catch (error) { - Logger.error(TAG, 'getDeviceInfo error', error) - } + try { + const deviceInfo = await getDeviceInfo() + this.deviceInfo = deviceInfo + uniqueID = deviceInfo.UniqueID + this.sessionId = sha256(Buffer.from(uniqueID + String(Date.now()))).slice(2) + } catch (error) { + Logger.error(TAG, 'getDeviceInfo error', error) + } - Logger.info(TAG, 'Segment Analytics Integration initialized!') - } catch (err) { - const error = ensureError(err) - Logger.error(TAG, `Segment setup error: ${error.message}\n`, error) + Logger.info(TAG, 'Segment Analytics Integration initialized!') + } catch (err) { + const error = ensureError(err) + Logger.error(TAG, `Segment setup error: ${error.message}\n`, error) + } + } else { + Logger.info(TAG, 'Segment API key not present, skipping setup') } - try { - const statsigUser = getDefaultStatsigUser() - // getAnonymousId causes the e2e tests to fail - let overrideStableID: string = E2E_TEST_STATSIG_ID - if (!isE2EEnv) { + if (STATSIG_ENABLED) { + try { + const statsigUser = getDefaultStatsigUser() if (!this.segmentClient) { throw new Error('segmentClient is undefined, cannot get anonymous ID') } - overrideStableID = this.segmentClient.userInfo.get().anonymousId + const overrideStableID = this.segmentClient.userInfo.get().anonymousId + Logger.debug(TAG, 'Statsig stable ID', overrideStableID) + await Statsig.initialize(STATSIG_API_KEY, statsigUser, { + // StableID should match Segment anonymousId + overrideStableID, + environment: STATSIG_ENV, + }) + } catch (error) { + Logger.warn(TAG, `Statsig setup error`, error) } - Logger.debug(TAG, 'Statsig stable ID', overrideStableID) - await Statsig.initialize(STATSIG_API_KEY, statsigUser, { - // StableID should match Segment anonymousId - overrideStableID, - environment: STATSIG_ENV, - localMode: isE2EEnv, - }) - } catch (error) { - Logger.warn(TAG, `Statsig setup error`, error) + } else { + Logger.info(TAG, 'Statsig is not enabled, skipping setup') } } diff --git a/src/config.ts b/src/config.ts index 701def330e9..92f1b7d8eeb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -79,7 +79,6 @@ export const FIATCONNECT_NETWORK = export const STATSIG_ENV = { tier: DEFAULT_TESTNET === 'mainnet' ? 'production' : 'development', } -export const E2E_TEST_STATSIG_ID = 'e2e_test_statsig_id' // Keyless backup settings export const TORUS_NETWORK = @@ -122,10 +121,8 @@ export const ALCHEMY_BASE_API_KEY = keyOrUndefined( ) export const ZENDESK_API_KEY = keyOrUndefined(secretsFile, DEFAULT_TESTNET, 'ZENDESK_API_KEY') -export const STATSIG_API_KEY = - keyOrUndefined(secretsFile, DEFAULT_TESTNET, 'STATSIG_API_KEY') ?? - // dummy key as fallback for e2e tests, which use local mode - 'client-key' +export const STATSIG_API_KEY = keyOrUndefined(secretsFile, DEFAULT_TESTNET, 'STATSIG_API_KEY') +export const STATSIG_ENABLED = !isE2EEnv && !!STATSIG_API_KEY export const SEGMENT_API_KEY = keyOrUndefined(secretsFile, DEFAULT_TESTNET, 'SEGMENT_API_KEY') export const SENTRY_CLIENT_URL = keyOrUndefined(secretsFile, DEFAULT_TESTNET, 'SENTRY_CLIENT_URL') export const BIDALI_URL = keyOrUndefined(secretsFile, DEFAULT_TESTNET, 'BIDALI_URL') diff --git a/src/navigator/SettingsMenu.test.tsx b/src/navigator/SettingsMenu.test.tsx index 34769e24034..d9670e3af13 100644 --- a/src/navigator/SettingsMenu.test.tsx +++ b/src/navigator/SettingsMenu.test.tsx @@ -23,6 +23,11 @@ jest.mock('statsig-react-native', () => ({ }, })) +jest.mock('src/config', () => ({ + ...jest.requireActual('src/config'), + STATSIG_ENABLED: true, +})) + describe('SettingsMenu', () => { beforeEach(() => { jest.clearAllMocks() diff --git a/src/navigator/SettingsMenu.tsx b/src/navigator/SettingsMenu.tsx index 63121065ba4..d097ba63390 100644 --- a/src/navigator/SettingsMenu.tsx +++ b/src/navigator/SettingsMenu.tsx @@ -31,6 +31,7 @@ import ContactCircleSelf from 'src/components/ContactCircleSelf' import GradientBlock from 'src/components/GradientBlock' import { SettingsItemTextValue } from 'src/components/SettingsItem' import Touchable from 'src/components/Touchable' +import { STATSIG_ENABLED } from 'src/config' import Envelope from 'src/icons/Envelope' import ForwardChevron from 'src/icons/ForwardChevron' import Lock from 'src/icons/Lock' @@ -162,7 +163,7 @@ export default function SettingsMenu({ route }: Props) { if (!devModeActive) { return null } else { - const statsigStableId = Statsig.getStableID() + const statsigStableId = STATSIG_ENABLED ? Statsig.getStableID() : 'statsig-not-enabled' return ( diff --git a/src/statsig/index.test.ts b/src/statsig/index.test.ts index 9d4951d7edd..fad0fd0bd1c 100644 --- a/src/statsig/index.test.ts +++ b/src/statsig/index.test.ts @@ -1,7 +1,9 @@ import { LaunchArguments } from 'react-native-launch-arguments' +import * as config from 'src/config' import { store } from 'src/redux/store' import { DynamicConfigs, ExperimentConfigs } from 'src/statsig/constants' import { + _getGateOverrides, getDynamicConfigParams, getExperimentParams, getFeatureGate, @@ -25,6 +27,8 @@ jest.mock('src/redux/store', () => ({ store: { getState: jest.fn() } })) jest.mock('statsig-react-native') jest.mock('src/utils/Logger') +const mockConfig = jest.mocked(config) + const mockStore = jest.mocked(store) const MOCK_ACCOUNT = '0x000000000000000000000000000000000000000000' const MOCK_START_ONBOARDING_TIME = 1680563877 @@ -38,6 +42,10 @@ mockStore.getState.mockImplementation(() => describe('Statsig helpers', () => { beforeEach(() => { jest.clearAllMocks() + mockConfig.STATSIG_ENABLED = true + // Reset gate overrides before each test + jest.mocked(LaunchArguments.value).mockReturnValue({ statsigGateOverrides: 'dummy=true' }) + setupOverridesFromLaunchArgs() }) describe('data validation', () => { it.each(Object.entries(ExperimentConfigs))( @@ -110,6 +118,14 @@ describe('Statsig helpers', () => { expect(getMock).toHaveBeenCalledWith('param2', 'defaultValue2') expect(output).toEqual({ param1: 'statsigValue1', param2: 'statsigValue2' }) }) + it('returns default values if statsig is not enabled', () => { + mockConfig.STATSIG_ENABLED = false + const defaultValues = { param1: 'defaultValue1', param2: 'defaultValue2' } + const experimentName = 'mock_experiment_name' as StatsigExperiments + const output = getExperimentParams({ experimentName, defaultValues }) + expect(output).toEqual(defaultValues) + expect(Logger.warn).not.toHaveBeenCalled() + }) }) describe('getFeatureGate', () => { @@ -127,6 +143,20 @@ describe('Statsig helpers', () => { expect(Logger.warn).not.toHaveBeenCalled() expect(output).toEqual(true) }) + it('returns gate overrides if set', () => { + jest.mocked(Statsig.checkGate).mockImplementation(() => true) + jest + .mocked(LaunchArguments.value) + .mockReturnValue({ statsigGateOverrides: 'app_review=false' }) + setupOverridesFromLaunchArgs() + expect(getFeatureGate(StatsigFeatureGates.APP_REVIEW)).toEqual(false) + }) + it('returns default values if statsig is not enabled', () => { + mockConfig.STATSIG_ENABLED = false + const output = getFeatureGate(StatsigFeatureGates.APP_REVIEW) + expect(output).toEqual(false) + expect(Logger.warn).not.toHaveBeenCalled() + }) }) describe('getMultichainFeatures', () => { @@ -208,6 +238,14 @@ describe('Statsig helpers', () => { StatsigMultiNetworkDynamicConfig.MULTI_CHAIN_FEATURES ) }) + it('returns default values if statsig is not enabled', () => { + mockConfig.STATSIG_ENABLED = false + const output = getMultichainFeatures() + expect(output).toEqual( + DynamicConfigs[StatsigMultiNetworkDynamicConfig.MULTI_CHAIN_FEATURES].defaultValues + ) + expect(Logger.warn).not.toHaveBeenCalled() + }) }) describe('getDynamicConfigParams', () => { @@ -267,6 +305,14 @@ describe('Statsig helpers', () => { expect(getMock).toHaveBeenCalledWith('param2', 'defaultValue2') expect(output).toEqual({ param1: 'statsigValue1', param2: 'statsigValue2' }) }) + it('returns default values if statsig is not enabled', () => { + mockConfig.STATSIG_ENABLED = false + const defaultValues = { param1: 'defaultValue1', param2: 'defaultValue2' } + const configName = 'mock_config' as StatsigDynamicConfigs + const output = getDynamicConfigParams({ configName, defaultValues }) + expect(output).toEqual(defaultValues) + expect(Logger.warn).not.toHaveBeenCalled() + }) }) describe('patchUpdateStatsigUser', () => { let mockDateNow: jest.SpyInstance @@ -349,14 +395,18 @@ describe('Statsig helpers', () => { }, }) }) + it('does not update user if statsig is not enabled', async () => { + mockConfig.STATSIG_ENABLED = false + await patchUpdateStatsigUser() + expect(Statsig.updateUser).not.toHaveBeenCalled() + }) }) describe('setupOverridesFromLaunchArgs', () => { it('cleans up overrides and skips setup if no override is set', () => { - jest.mocked(LaunchArguments.value).mockReturnValue({}) + jest.mocked(LaunchArguments.value).mockReturnValue({ statsigGateOverrides: '' }) setupOverridesFromLaunchArgs() - expect(Statsig.removeGateOverride).toHaveBeenCalledWith() - expect(Statsig.overrideGate).not.toHaveBeenCalled() + expect(_getGateOverrides()).toEqual({}) }) it('cleans up and sets up gate overrides if set', () => { @@ -364,10 +414,7 @@ describe('Statsig helpers', () => { .mocked(LaunchArguments.value) .mockReturnValue({ statsigGateOverrides: 'gate1=true,gate2=false' }) setupOverridesFromLaunchArgs() - expect(Statsig.removeGateOverride).toHaveBeenCalledWith() - expect(Statsig.overrideGate).toHaveBeenCalledTimes(2) - expect(Statsig.overrideGate).toHaveBeenCalledWith('gate1', true) - expect(Statsig.overrideGate).toHaveBeenCalledWith('gate2', false) + expect(_getGateOverrides()).toEqual({ gate1: true, gate2: false }) }) }) }) diff --git a/src/statsig/index.ts b/src/statsig/index.ts index 39aae1f9289..98ca06abbad 100644 --- a/src/statsig/index.ts +++ b/src/statsig/index.ts @@ -1,7 +1,7 @@ import * as _ from 'lodash' import { LaunchArguments } from 'react-native-launch-arguments' import { startOnboardingTimeSelector } from 'src/account/selectors' -import { ExpectedLaunchArgs, isE2EEnv } from 'src/config' +import { ExpectedLaunchArgs, STATSIG_ENABLED } from 'src/config' import { DynamicConfigs } from 'src/statsig/constants' import { StatsigDynamicConfigs, @@ -18,6 +18,13 @@ import { DynamicConfig, Statsig, StatsigUser } from 'statsig-react-native' const TAG = 'Statsig' +let gateOverrides: { [key: string]: boolean } = {} + +// Only for testing +export function _getGateOverrides() { + return gateOverrides +} + function getParams>({ config, defaultValues, @@ -45,8 +52,11 @@ export function getExperimentParams>( defaultValues: T }): T { try { + if (!STATSIG_ENABLED) { + return defaultValues + } const experiment = Statsig.getExperiment(experimentName) - if (!isE2EEnv && experiment.getEvaluationDetails().reason === EvaluationReason.Uninitialized) { + if (experiment.getEvaluationDetails().reason === EvaluationReason.Uninitialized) { Logger.warn( TAG, 'getExperimentParams: SDK is uninitialized when getting experiment', @@ -72,8 +82,11 @@ function _getDynamicConfigParams>({ defaultValues: T }): T { try { + if (!STATSIG_ENABLED) { + return defaultValues + } const config = Statsig.getConfig(configName) - if (!isE2EEnv && config.getEvaluationDetails().reason === EvaluationReason.Uninitialized) { + if (config.getEvaluationDetails().reason === EvaluationReason.Uninitialized) { Logger.warn( TAG, 'getDynamicConfigParams: SDK is uninitialized when getting experiment', @@ -110,13 +123,21 @@ export function getDynamicConfigParams() if (statsigGateOverrides) { Logger.debug(TAG, 'Setting up gate overrides', statsigGateOverrides) statsigGateOverrides.split(',').forEach((gateOverride: string) => { const [gate, value] = gateOverride.split('=') - Statsig.overrideGate(gate, value === 'true') + newGateOverrides[gate] = value === 'true' }) } + gateOverrides = newGateOverrides } catch (err) { Logger.debug(TAG, 'Overrides setup failed', err) }