Skip to content

Commit

Permalink
wallet telemetry alignment (#1762)
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielSinclair authored Nov 27, 2024
1 parent ac7de08 commit de6d416
Show file tree
Hide file tree
Showing 12 changed files with 257 additions and 66 deletions.
1 change: 1 addition & 0 deletions .env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ declare global {
RUDDERSTACK_DATA_PLANE: string;
RUDDERSTACK_WRITE_KEY: string;
SENTRY_DSN: string;
SECURE_WALLET_HASH_KEY: string;
// Development
IS_DEV: 'true' | 'false';
IS_TESTING: 'true' | 'false';
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
"@ethersproject/units": "5.7.0",
"@ethersproject/wallet": "5.7.0",
"@ledgerhq/hw-app-eth": "6.38.1",
"@ethersproject/sha2": "5.7.0",
"@ledgerhq/hw-transport-webhid": "6.29.3",
"@metamask/browser-passworder": "4.1.0",
"@metamask/eth-sig-util": "7.0.1",
Expand Down
36 changes: 30 additions & 6 deletions src/analytics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Analytics as RudderAnalytics } from '@rudderstack/analytics-js-service-

import { EventProperties, event } from '~/analytics/event';
import { UserProperties } from '~/analytics/userProperties';
import { type WalletContext } from '~/analytics/util';
import { analyticsDisabledStore } from '~/core/state/currentSettings/analyticsDisabled';
import { logger } from '~/logger';

Expand Down Expand Up @@ -33,6 +34,8 @@ const context = {
export class Analytics {
client?: RudderAnalytics;
deviceId?: string;
walletAddressHash?: WalletContext['walletAddressHash'];
walletType?: WalletContext['walletType'];
event = event;
disabled = true; // to do: check user setting here

Expand Down Expand Up @@ -73,7 +76,7 @@ export class Analytics {
identify(userProperties?: UserProperties) {
if (this.disabled || IS_DEV || IS_TESTING || !this.deviceId) return;
const metadata = this.getDefaultMetadata();
const traits = { ...userProperties, ...metadata };
const traits = { ...metadata, ...userProperties };
this.client?.identify({ userId: this.deviceId, traits, context });
logger.info('analytics.identify()', {
userId: this.deviceId,
Expand All @@ -84,10 +87,14 @@ export class Analytics {
/**
* Sends a `screen` event to RudderStack.
*/
screen(name: string, params: Record<string, string> = {}): void {
screen(
name: string,
params?: Record<string, string>,
walletContext?: WalletContext,
) {
if (this.disabled || IS_DEV || IS_TESTING || !this.deviceId) return;
const metadata = this.getDefaultMetadata();
const properties = { ...params, ...metadata };
const properties = { ...metadata, ...walletContext, ...params };
this.client?.screen({ userId: this.deviceId, name, properties, context });
logger.info('analytics.screen()', {
userId: this.deviceId,
Expand All @@ -104,10 +111,11 @@ export class Analytics {
track<T extends keyof EventProperties>(
event: T,
params?: EventProperties[T],
walletContext?: WalletContext,
) {
if (this.disabled || IS_DEV || IS_TESTING || !this.deviceId) return;
const metadata = this.getDefaultMetadata();
const properties = Object.assign(metadata, params);
const properties = { ...metadata, ...walletContext, ...params };
this.client?.track({ userId: this.deviceId, event, properties, context });
logger.info('analytics.track()', {
userId: this.deviceId,
Expand All @@ -118,10 +126,13 @@ export class Analytics {

/**
* Scaffolding for Default Metadata params
* This is used in the App for `walletAddressHash`
* This is used in the App for `walletAddressHash` and `walletType`
*/
private getDefaultMetadata() {
return {};
return {
walletAddressHash: this.walletAddressHash,
walletType: this.walletType,
};
}

/**
Expand All @@ -133,6 +144,19 @@ export class Analytics {
logger.debug(`Set deviceId on analytics instance`, { deviceId });
}

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

/**
* Enable RudderStack tracking. Defaults to enabled.
*/
Expand Down
76 changes: 76 additions & 0 deletions src/analytics/util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { SupportedAlgorithm, computeHmac } from '@ethersproject/sha2';
import { Address } from 'viem';

import { getWallet } from '~/entries/popup/handlers/wallet';
import { KeychainType } from '~/core/types/keychainTypes';
import { RainbowError, logger } from '~/logger';

const SECURE_WALLET_HASH_KEY = process.env.SECURE_WALLET_HASH_KEY;

function securelyHashWalletAddress(
walletAddress: Address | undefined,
): string | undefined {
if (!SECURE_WALLET_HASH_KEY) {
logger.error(
new RainbowError(
`[securelyHashWalletAddress]: Required .env variable SECURE_WALLET_HASH_KEY does not exist`,
),
);
return;
}

if (!walletAddress) return;

try {
const hmac = computeHmac(
SupportedAlgorithm.sha256,
// must be hex `0x<key>` string
SECURE_WALLET_HASH_KEY,
// must be hex `0x<key>` string
walletAddress,
);

logger.debug(`[securelyHashWalletAddress]: Wallet address securely hashed`);

return hmac;
} catch (e) {
// could be an invalid hashing key, or trying to hash an ENS
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 {};

const walletAddressHash = securelyHashWalletAddress(address);

// walletType is unavailable when keychain is locked
let walletType;
try {
// expect getWallet error when keychain is locked
const wallet = await getWallet(address);
walletType = ({
[KeychainType.HdKeychain]: 'owned',
[KeychainType.KeyPairKeychain]: 'owned',
[KeychainType.ReadOnlyKeychain]: 'watched',
[KeychainType.HardwareWalletKeychain]: 'hardware',
} as const)[wallet?.type];
} catch (e) {}

return {
walletType,
walletAddressHash,
};
}
12 changes: 11 additions & 1 deletion src/core/sentry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,18 @@ export function initializeSentry(context: 'popup' | 'background') {
}
}

export function setSentryUser(deviceId: string) {
export function setSentryUser({
deviceId,
walletAddressHash,
walletType,
}: {
deviceId: string;
walletAddressHash?: string;
walletType?: 'owned' | 'hardware' | 'watched';
}) {
Sentry.setUser({
id: deviceId,
walletAddressHash,
walletType,
});
}
4 changes: 4 additions & 0 deletions src/core/state/currentSettings/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ export {
currentLanguageStore,
useCurrentLanguageStore,
} from './currentLanguage';
export {
currentThemeStore,
useCurrentThemeStore,
} from './currentTheme';
export {
flashbotsEnabledStore,
useFlashbotsEnabledStore,
Expand Down
2 changes: 2 additions & 0 deletions src/core/state/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ export {
currentChainIdStore,
currentCurrencyStore,
currentLanguageStore,
currentThemeStore,
flashbotsEnabledStore,
isDefaultWalletStore,
useCurrentAddressStore,
useCurrentChainIdStore,
useCurrentCurrencyStore,
useCurrentLanguageStore,
useCurrentThemeStore,
useFlashbotsEnabledStore,
useIsDefaultWalletStore,
} from './currentSettings';
Expand Down
37 changes: 37 additions & 0 deletions src/core/telemetry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useEffect } from 'react';

import { analytics } from '~/analytics';
import { getWalletContext } from '~/analytics/util';
import { useAuth } from '~/entries/popup/hooks/useAuth';

import { setSentryUser } from '../sentry';
import { useCurrentAddressStore, useDeviceIdStore } from '../state';

export const TelemetryIdentifier = () => {
const { status: authStatus } = useAuth();
const { deviceId } = useDeviceIdStore();
const { currentAddress } = useCurrentAddressStore();

// update telemetry wallet each time selected wallet changes
useEffect(() => {
// update wallet context and trigger identify
const identify = async () => {
const { walletType, walletAddressHash } =
await getWalletContext(currentAddress);
setSentryUser({ deviceId, walletAddressHash, walletType });
// allows calling telemetry before currentAddress is available (i.e. onboarding)
if (walletType || walletAddressHash)
analytics.setWalletContext({ walletAddressHash, walletType });
analytics.setDeviceId(deviceId);
analytics.identify();
};
// Disable analytics & sentry for e2e and dev mode
if (process.env.IS_TESTING !== 'true' && process.env.IS_DEV !== 'true') {
if (authStatus === '') return; // wait for auth state to settle
else if (authStatus === 'READY') identify(); // assign full wallet context
else identify(); // assign partial wallet context immediately if available
}
}, [deviceId, currentAddress, authStatus]);

return null;
};
11 changes: 4 additions & 7 deletions src/entries/popup/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ import { flushQueuedEvents } from '~/analytics/flushQueuedEvents';
import config from '~/core/firebase/remoteConfig';
import { initializeMessenger } from '~/core/messengers';
import { persistOptions, queryClient } from '~/core/react-query';
import { initializeSentry, setSentryUser } from '~/core/sentry';
import { useCurrentLanguageStore, useDeviceIdStore } from '~/core/state';
import { useCurrentThemeStore } from '~/core/state/currentSettings/currentTheme';
import { initializeSentry } from '~/core/sentry';
import { useCurrentLanguageStore, useCurrentThemeStore } from '~/core/state';
import { TelemetryIdentifier } from '~/core/telemetry';
import { POPUP_DIMENSIONS } from '~/core/utils/dimensions';
import { WagmiConfigUpdater, wagmiConfig } from '~/core/wagmi';
import { Box, ThemeProvider } from '~/design-system';
Expand All @@ -34,7 +34,6 @@ const backgroundMessenger = initializeMessenger({ connect: 'background' });

export function App() {
const { currentLanguage, setCurrentLanguage } = useCurrentLanguageStore();
const { deviceId } = useDeviceIdStore();
const { rainbowChains } = useRainbowChains();
const prevChains = usePrevious(rainbowChains);

Expand All @@ -60,9 +59,6 @@ export function App() {
// Disable analytics & sentry for e2e and dev mode
if (process.env.IS_TESTING !== 'true' && process.env.IS_DEV !== 'true') {
initializeSentry('popup');
setSentryUser(deviceId);
analytics.setDeviceId(deviceId);
analytics.identify();
analytics.track(event.popupOpened);
setTimeout(() => flushQueuedEvents(), 1000);
}
Expand Down Expand Up @@ -122,6 +118,7 @@ export function App() {
<IdleTimer />
<OnboardingKeepAlive />
<WagmiConfigUpdater />
<TelemetryIdentifier />
</AuthProvider>
</ThemeProvider>
</QueryClientProvider>
Expand Down
38 changes: 24 additions & 14 deletions src/entries/popup/pages/messages/RequestAccounts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Address } from 'viem';

import { analytics } from '~/analytics';
import { event } from '~/analytics/event';
import { getWalletContext } from '~/analytics/util';
import { initializeMessenger } from '~/core/messengers';
import { useDappMetadata } from '~/core/resources/metadata/dapp';
import { useAppSessionsStore, useCurrentAddressStore } from '~/core/state';
Expand Down Expand Up @@ -46,7 +47,7 @@ export const RequestAccounts = ({
);
const [selectedWallet, setSelectedWallet] = useState<Address>(currentAddress);

const onAcceptRequest = useCallback(() => {
const onAcceptRequest = useCallback(async () => {
try {
setLoading(true);
approveRequest({
Expand All @@ -63,12 +64,16 @@ export const RequestAccounts = ({
address: selectedWallet,
chainId: selectedChainId,
});
analytics.track(event.dappPromptConnectApproved, {
chainId: selectedChainId,
dappURL: dappMetadata?.url || '',
dappDomain: dappMetadata?.appHost || '',
dappName: dappMetadata?.appName,
});
analytics.track(
event.dappPromptConnectApproved,
{
chainId: selectedChainId,
dappURL: dappMetadata?.url || '',
dappDomain: dappMetadata?.appHost || '',
dappName: dappMetadata?.appName,
},
await getWalletContext(selectedWallet),
);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} catch (e: any) {
logger.info('error connecting to dapp');
Expand All @@ -88,20 +93,25 @@ export const RequestAccounts = ({
dappUrl,
]);

const onRejectRequest = useCallback(() => {
const onRejectRequest = useCallback(async () => {
rejectRequest();
analytics.track(event.dappPromptConnectRejected, {
chainId: selectedChainId,
dappURL: dappMetadata?.url || '',
dappDomain: dappMetadata?.appHost || '',
dappName: dappMetadata?.appName,
});
analytics.track(
event.dappPromptConnectRejected,
{
chainId: selectedChainId,
dappURL: dappMetadata?.url || '',
dappDomain: dappMetadata?.appHost || '',
dappName: dappMetadata?.appName,
},
await getWalletContext(selectedWallet),
);
}, [
dappMetadata?.url,
dappMetadata?.appHost,
dappMetadata?.appName,
rejectRequest,
selectedChainId,
selectedWallet,
]);

return (
Expand Down
Loading

0 comments on commit de6d416

Please sign in to comment.