diff --git a/__mocks__/react-native-device-info.ts b/__mocks__/react-native-device-info.ts index cccc8e84153..89c226ea0b7 100644 --- a/__mocks__/react-native-device-info.ts +++ b/__mocks__/react-native-device-info.ts @@ -4,6 +4,7 @@ mockRNDeviceInfo.getBundleId.mockImplementation(() => 'org.celo.mobile.debug') mockRNDeviceInfo.getVersion.mockImplementation(() => '0.0.1') mockRNDeviceInfo.getBuildNumber.mockImplementation(() => '1') mockRNDeviceInfo.getUniqueId.mockImplementation(() => Promise.resolve('abc-def-123')) +mockRNDeviceInfo.getUniqueIdSync.mockImplementation(() => 'abc-def-123') mockRNDeviceInfo.getDeviceId.mockImplementation(() => 'someDeviceId') mockRNDeviceInfo.getBrand.mockImplementation(() => 'someBrand') mockRNDeviceInfo.getModel.mockImplementation(() => 'someModel') diff --git a/src/analytics/ValoraAnalytics.test.ts b/src/analytics/ValoraAnalytics.test.ts index eecf19e2446..083afecbfea 100644 --- a/src/analytics/ValoraAnalytics.test.ts +++ b/src/analytics/ValoraAnalytics.test.ts @@ -29,6 +29,12 @@ jest.mock('@segment/analytics-react-native-firebase', () => ({})) jest.mock('react-native-permissions', () => ({})) jest.mock('@sentry/react-native', () => ({ init: jest.fn() })) jest.mock('src/redux/store', () => ({ store: { getState: jest.fn() } })) +jest.mock('src/config', () => ({ + // @ts-expect-error + ...jest.requireActual('src/config'), + STATSIG_API_KEY: 'statsig-key', +})) +jest.mock('statsig-react-native') const mockDeviceId = 'abc-def-123' // mocked in __mocks__/react-native-device-info.ts (but importing from that file causes weird errors) const expectedSessionId = '205ac8350460ad427e35658006b409bbb0ee86c22c57648fe69f359c2da648' @@ -91,7 +97,6 @@ const state = getMockStoreData({ pincodeType: PincodeType.CustomPin, }, }) -mockStore.getState.mockImplementation(() => state) // Disable __DEV__ so analytics is enabled // @ts-ignore @@ -142,11 +147,31 @@ describe('ValoraAnalytics', () => { beforeEach(() => { jest.clearAllMocks() jest.unmock('src/analytics/ValoraAnalytics') - Statsig.initialize = jest.fn() - Statsig.updateUser = jest.fn() jest.isolateModules(() => { ValoraAnalytics = require('src/analytics/ValoraAnalytics').default }) + mockStore.getState.mockImplementation(() => state) + }) + + it('creates statsig client on initialization with wallet address as user id', async () => { + mockStore.getState.mockImplementation(() => + getMockStoreData({ web3: { account: '0x1234ABC', mtwAddress: '0x0000' } }) + ) + await ValoraAnalytics.init() + expect(Statsig.initialize).toHaveBeenCalledWith( + 'statsig-key', + { userID: '0x1234abc' }, + { environment: { tier: 'development' }, overrideStableID: 'anonId' } + ) + }) + + it('creates statsig client on initialization with null as user id if wallet address is not set', async () => { + mockStore.getState.mockImplementation(() => getMockStoreData({ web3: { account: undefined } })) + await ValoraAnalytics.init() + expect(Statsig.initialize).toHaveBeenCalledWith('statsig-key', null, { + environment: { tier: 'development' }, + overrideStableID: 'anonId', + }) }) it('delays identify calls until async init has finished', async () => { diff --git a/src/analytics/ValoraAnalytics.ts b/src/analytics/ValoraAnalytics.ts index 518a7f7d0ae..c29bbade70d 100644 --- a/src/analytics/ValoraAnalytics.ts +++ b/src/analytics/ValoraAnalytics.ts @@ -130,16 +130,17 @@ class ValoraAnalytics { } try { - const { accountAddress } = getCurrentUserTraits(store.getState()) - const stasigUser = accountAddress - ? { - userID: accountAddress, - } - : null + const { walletAddress } = getCurrentUserTraits(store.getState()) + const statsigUser = + typeof walletAddress === 'string' + ? { + userID: walletAddress, + } + : null // getAnonymousId causes the e2e tests to fail const overrideStableID = isE2EEnv ? E2E_TEST_STATSIG_ID : await Analytics.getAnonymousId() - await Statsig.initialize(STATSIG_API_KEY, stasigUser, { + await Statsig.initialize(STATSIG_API_KEY, statsigUser, { // StableID should match Segment anonymousId overrideStableID, environment: STATSIG_ENV, @@ -301,10 +302,6 @@ class ValoraAnalytics { const prefixedSuperProps = Object.fromEntries( Object.entries({ ...traits, - deviceId: this.deviceInfo?.UniqueID, - appVersion: this.deviceInfo?.Version, - appBuildNumber: this.deviceInfo?.BuildNumber, - appBundleId: this.deviceInfo?.BundleId, currentScreenId: this.currentScreenId, prevScreenId: this.prevScreenId, }).map(([key, value]) => [`s${key.charAt(0).toUpperCase() + key.slice(1)}`, value]) diff --git a/src/analytics/saga.ts b/src/analytics/saga.ts index 1d43adef4ae..a4b3667b5db 100644 --- a/src/analytics/saga.ts +++ b/src/analytics/saga.ts @@ -8,7 +8,7 @@ export function* updateUserTraits() { const traits: ReturnType = yield select(getCurrentUserTraits) if (traits !== prevTraits) { const { walletAddress } = traits - yield call([ValoraAnalytics, 'identify'], walletAddress, traits) + yield call([ValoraAnalytics, 'identify'], walletAddress as string | null, traits) prevTraits = traits } diff --git a/src/analytics/selectors.test.ts b/src/analytics/selectors.test.ts index 46eea249743..83832386cdb 100644 --- a/src/analytics/selectors.test.ts +++ b/src/analytics/selectors.test.ts @@ -171,10 +171,14 @@ describe('getCurrentUserTraits', () => { }) expect(getCurrentUserTraits(state)).toStrictEqual({ accountAddress: '0x0000000000000000000000000000000000007E57', + appBuildNumber: '1', + appBundleId: 'org.celo.mobile.debug', + appVersion: '0.0.1', celoBalance: 0, ceurBalance: 21, countryCodeAlpha2: 'US', cusdBalance: 10, + deviceId: 'abc-def-123', deviceLanguage: 'en-US', hasCompletedBackup: false, hasVerifiedNumber: false, diff --git a/src/analytics/selectors.ts b/src/analytics/selectors.ts index 5cb9a4b93ca..0baaf513a19 100644 --- a/src/analytics/selectors.ts +++ b/src/analytics/selectors.ts @@ -1,5 +1,6 @@ import { getRegionCodeFromCountryCode } from '@celo/phone-utils' import BigNumber from 'bignumber.js' +import DeviceInfo from 'react-native-device-info' import * as RNLocalize from 'react-native-localize' import { createSelector } from 'reselect' import { defaultCountryCodeSelector, pincodeTypeSelector } from 'src/account/selectors' @@ -41,7 +42,9 @@ export const getCurrentUserTraits = createSelector( hasCompletedBackup, pincodeType, superchargeInfo - ) => { + ): // Enforce primitive types, TODO: check this using `satisfies` once we upgrade to TS >= 4.9 + // so we don't need to erase the named keys + Record => { const coreTokensAddresses = new Set(coreTokens.map((token) => token?.address)) const tokensByUsdBalance = tokens.sort(sortByUsdBalance) @@ -89,6 +92,10 @@ export const getCurrentUserTraits = createSelector( localCurrencyCode, hasVerifiedNumber, hasCompletedBackup, + deviceId: DeviceInfo.getUniqueIdSync(), + appVersion: DeviceInfo.getVersion(), + appBuildNumber: DeviceInfo.getBuildNumber(), + appBundleId: DeviceInfo.getBundleId(), pincodeType, superchargingToken: superchargeInfo.superchargingTokenConfig?.tokenSymbol, superchargingAmountInUsd: superchargeInfo.superchargeUsdBalance,