Skip to content

Commit

Permalink
fix: wallet telemetry identify (#6258)
Browse files Browse the repository at this point in the history
* sentry hash alignment, amend interface to match bx

* assign telemetry wallet context in useInitializeWallet

fix: lint

* fix: walletType could be undefined during onboarding, import

* fix: redux strore mocking in utils tests
  • Loading branch information
DanielSinclair authored Nov 22, 2024
1 parent 78ba999 commit f8808d8
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 50 deletions.
29 changes: 5 additions & 24 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,16 @@ import { designSystemPlaygroundEnabled, reactNativeDisableYellowBox, showNetwork
import monitorNetwork from '@/debugging/network';
import { Playground } from '@/design-system/playground/Playground';
import RainbowContextWrapper from '@/helpers/RainbowContext';
import * as keychain from '@/model/keychain';
import { Navigation } from '@/navigation';
import { PersistQueryClientProvider, persistOptions, queryClient } from '@/react-query';
import store, { AppDispatch, type AppState } from '@/redux/store';
import { MainThemeProvider, useTheme } from '@/theme/ThemeContext';
import { addressKey } from '@/utils/keychainConstants';
import { MainThemeProvider } from '@/theme/ThemeContext';
import { SharedValuesProvider } from '@/helpers/SharedValuesContext';
import { InitialRouteContext } from '@/navigation/initialRoute';
import { Portal } from '@/react-native-cool-modals/Portal';
import { NotificationsHandler } from '@/notifications/NotificationsHandler';
import { analyticsV2 } from '@/analytics';
import { getOrCreateDeviceId, securelyHashWalletAddress } from '@/analytics/utils';
import { getOrCreateDeviceId } from '@/analytics/utils';
import { logger, RainbowError } from '@/logger';
import * as ls from '@/storage';
import { migrate } from '@/migrations';
Expand All @@ -38,7 +36,6 @@ import { ReviewPromptAction } from '@/storage/schema';
import { initializeRemoteConfig } from '@/model/remoteConfig';
import { NavigationContainerRef } from '@react-navigation/native';
import { RootStackParamList } from '@/navigation/types';
import { Address } from 'viem';
import { IS_ANDROID, IS_DEV } from '@/env';
import { prefetchDefaultFavorites } from '@/resources/favorites';
import Routes from '@/navigation/Routes';
Expand Down Expand Up @@ -102,27 +99,11 @@ function Root() {

const isReturningUser = ls.device.get(['isReturningUser']);
const [deviceId, deviceIdWasJustCreated] = await getOrCreateDeviceId();
const currentWalletAddress = await keychain.loadString(addressKey);
const currentWalletAddressHash =
typeof currentWalletAddress === 'string' ? securelyHashWalletAddress(currentWalletAddress as Address) : undefined;

Sentry.setUser({
id: deviceId,
currentWalletAddress: currentWalletAddressHash,
});

/**
* Add helpful values to `analyticsV2` instance
*/
// Initial telemetry; amended with wallet context later in `useInitializeWallet`
Sentry.setUser({ id: deviceId });
analyticsV2.setDeviceId(deviceId);
if (currentWalletAddressHash) {
analyticsV2.setCurrentWalletAddressHash(currentWalletAddressHash);
}

/**
* `analyticsv2` has all it needs to function.
*/
analyticsV2.identify({});
analyticsV2.identify();

const isReviewInitialized = ls.review.get(['initialized']);
if (!isReviewInitialized) {
Expand Down
2 changes: 1 addition & 1 deletion src/analytics/__mocks__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const analyticsV2 = {
screen: jest.fn(),
track: jest.fn(),
setDeviceId: jest.fn(),
setCurrentWalletAddressHash: jest.fn(),
setWalletContext: jest.fn(),
enable: jest.fn(),
disable: jest.fn(),
event,
Expand Down
6 changes: 3 additions & 3 deletions src/analytics/__tests__/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ describe.skip('@/analytics', () => {
test('track', () => {
const analytics = new Analytics();

analytics.setCurrentWalletAddressHash('hash');
analytics.setWalletContext({ walletAddressHash: 'hash', walletType: 'owned' });
analytics.track(analytics.event.pressedButton);

expect(analytics.client.track).toHaveBeenCalledWith(analytics.event.pressedButton, {
Expand All @@ -29,7 +29,7 @@ describe.skip('@/analytics', () => {
test('identify', () => {
const analytics = new Analytics();

analytics.setCurrentWalletAddressHash('hash');
analytics.setWalletContext({ walletAddressHash: 'hash', walletType: 'owned' });
analytics.setDeviceId('id');
analytics.identify({ currency: 'USD' });

Expand All @@ -42,7 +42,7 @@ describe.skip('@/analytics', () => {
test('screen', () => {
const analytics = new Analytics();

analytics.setCurrentWalletAddressHash('hash');
analytics.setWalletContext({ walletAddressHash: 'hash', walletType: 'owned' });
analytics.screen(Routes.BACKUP_SHEET);

expect(analytics.client.screen).toHaveBeenCalledWith(Routes.BACKUP_SHEET, {
Expand Down
2 changes: 2 additions & 0 deletions src/analytics/__tests__/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ jest.mock('@/model/keychain', () => ({
loadString: jest.fn(),
}));

jest.mock('@/redux/store');

jest.mock('@sentry/react-native', () => ({
setUser: jest.fn(),
}));
Expand Down
43 changes: 26 additions & 17 deletions src/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import { EventProperties, event } from '@/analytics/event';
import { UserProperties } from '@/analytics/userProperties';
import { logger, RainbowError } from '@/logger';
import { device } from '@/storage';
import { WalletContext } from './utils';

const isTesting = IS_TESTING === 'true';

export class Analytics {
client: any;
currentWalletAddressHash?: string;
client: typeof rudderClient;
deviceId?: string;
walletAddressHash?: WalletContext['walletAddressHash'];
walletType?: WalletContext['walletType'];
event = event;
disabled: boolean;

Expand All @@ -30,38 +32,44 @@ export class Analytics {
* here. This uses the `deviceId` as the identifier, and attaches the hashed
* wallet address as a property, if available.
*/
identify(userProperties: UserProperties) {
identify(userProperties?: UserProperties) {
if (this.disabled) return;
const metadata = this.getDefaultMetadata();
this.client.identify(this.deviceId, {
...userProperties,
...metadata,
});
this.client.identify(
this.deviceId as string,
{
...metadata,
...userProperties,
},
{}
);
}

/**
* Sends a `screen` event.
*/
screen(routeName: string, params: Record<string, any> = {}): void {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
screen(routeName: string, params: Record<string, any> = {}, walletContext?: WalletContext): void {
if (this.disabled) return;
const metadata = this.getDefaultMetadata();
this.client.screen(routeName, { ...params, ...metadata });
this.client.screen(routeName, { ...metadata, ...walletContext, ...params });
}

/**
* Sends an event. Param `event` must exist in
* `@/analytics/event`, and if properties are associated with it, they must
* be defined as part of `EventProperties` in the same file
*/
track<T extends keyof EventProperties>(event: T, params?: EventProperties[T]) {
track<T extends keyof EventProperties>(event: T, params?: EventProperties[T], walletContext?: WalletContext) {
if (this.disabled) return;
const metadata = this.getDefaultMetadata();
this.client.track(event, { ...params, ...metadata });
this.client.track(event, { ...metadata, ...walletContext, ...params });
}

private getDefaultMetadata() {
return {
walletAddressHash: this.currentWalletAddressHash,
walletAddressHash: this.walletAddressHash,
walletType: this.walletType,
};
}

Expand All @@ -80,17 +88,18 @@ export class Analytics {
* `identify()`, you must do that on your own.
*/
setDeviceId(deviceId: string) {
logger.debug(`[Analytics]: Set deviceId on analytics instance`);
this.deviceId = deviceId;
logger.debug(`[Analytics]: Set deviceId on analytics instance`);
}

/**
* Set `currentWalletAddressHash` for use in events. This DOES NOT call
* Set `walletAddressHash` and `walletType` for use in events. This DOES NOT call
* `identify()`, you must do that on your own.
*/
setCurrentWalletAddressHash(currentWalletAddressHash: string) {
logger.debug(`[Analytics]: Set currentWalletAddressHash on analytics instance`);
this.currentWalletAddressHash = currentWalletAddressHash;
setWalletContext(walletContext: WalletContext) {
this.walletAddressHash = walletContext.walletAddressHash;
this.walletType = walletContext.walletType;
logger.debug(`[Analytics]: Set walletAddressHash on analytics instance`);
}

/**
Expand Down
36 changes: 35 additions & 1 deletion src/analytics/utils.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import { nanoid } from 'nanoid/non-secure';
import { SECURE_WALLET_HASH_KEY } from 'react-native-dotenv';
import type { Address } from 'viem';

import * as ls from '@/storage';
import * as keychain from '@/model/keychain';
import { analyticsUserIdentifier } from '@/utils/keychainConstants';
import { logger, RainbowError } from '@/logger';
import { computeHmac, SupportedAlgorithm } from '@ethersproject/sha2';
import { findWalletWithAccount } from '@/helpers/findWalletWithAccount';
import store from '@/redux/store';
import { EthereumWalletType } from '@/helpers/walletTypes';

/**
* Returns the device id in a type-safe manner. It will throw if no device ID
Expand Down Expand Up @@ -58,7 +62,7 @@ export async function getOrCreateDeviceId(): Promise<[string, boolean]> {
}
}

export function securelyHashWalletAddress(walletAddress: `0x${string}`): string | undefined {
function securelyHashWalletAddress(walletAddress: Address): string | undefined {
if (!SECURE_WALLET_HASH_KEY) {
logger.error(new RainbowError(`[securelyHashWalletAddress]: Required .env variable SECURE_WALLET_HASH_KEY does not exist`));
}
Expand All @@ -80,3 +84,33 @@ export function securelyHashWalletAddress(walletAddress: `0x${string}`): string
logger.error(new RainbowError(`[securelyHashWalletAddress]: Wallet address hashing failed`));
}
}

export type WalletContext = {
walletType?: 'owned' | 'hardware' | 'watched';
walletAddressHash?: string;
};

export async function getWalletContext(address: Address): Promise<WalletContext> {
// currentAddressStore address is initialized to ''
if (!address || address === ('' as Address)) return {};

// walletType maybe undefined after initial wallet creation
const { wallets } = store.getState();
const wallet = findWalletWithAccount(wallets.wallets || {}, address);

const walletType = (
{
[EthereumWalletType.mnemonic]: 'owned',
[EthereumWalletType.privateKey]: 'owned',
[EthereumWalletType.seed]: 'owned',
[EthereumWalletType.readOnly]: 'watched',
[EthereumWalletType.bluetooth]: 'hardware',
} as const
)[wallet?.type!];
const walletAddressHash = securelyHashWalletAddress(address);

return {
walletType,
walletAddressHash,
};
}
20 changes: 20 additions & 0 deletions src/hooks/useInitializeWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ import { WrappedAlert as Alert } from '@/helpers/alert';
import { PROFILES, useExperimentalFlag } from '@/config';
import { runKeychainIntegrityChecks } from '@/handlers/walletReadyEvents';
import { RainbowError, logger } from '@/logger';
import { getOrCreateDeviceId, getWalletContext } from '@/analytics/utils';
import * as Sentry from '@sentry/react-native';
import { analyticsV2 } from '@/analytics';
import { Address } from 'viem';

export default function useInitializeWallet() {
const dispatch = useDispatch();
Expand Down Expand Up @@ -82,6 +86,22 @@ export default function useInitializeWallet() {
walletAddress,
});

// Capture wallet context in telemetry
// walletType maybe undefied after initial wallet creation
const { walletType, walletAddressHash } = await getWalletContext(walletAddress as Address);
const [deviceId] = await getOrCreateDeviceId();

Sentry.setUser({
id: deviceId,
walletAddressHash,
walletType,
});

// Allows calling telemetry before currentAddress is available (i.e. onboarding)
if (walletType || walletAddressHash) analyticsV2.setWalletContext({ walletAddressHash, walletType });
analyticsV2.setDeviceId(deviceId);
analyticsV2.identify();

if (!switching) {
// Run keychain integrity checks right after walletInit
// Except when switching wallets!
Expand Down
8 changes: 4 additions & 4 deletions src/redux/__mocks__/store.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export default {
getState: jest.fn(),
dispatch: jest.fn(),
};
import { jest } from '@jest/globals';

export const getState = jest.fn();
export const dispatch = jest.fn();

0 comments on commit f8808d8

Please sign in to comment.