From 3071e27b5cc91e7780f7f99987924a00f1e25665 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Wed, 2 Oct 2024 19:02:57 +0100 Subject: [PATCH 01/22] feat: Add custom traces --- app/components/Views/Login/index.js | 4 ++++ app/components/Views/Wallet/index.tsx | 9 +++++++-- app/core/EngineService/EngineService.ts | 3 +++ app/store/index.ts | 2 ++ app/store/persistConfig.ts | 2 ++ app/util/trace.ts | 5 +++++ 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 7820e848bb5..19e93adb257 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -59,6 +59,7 @@ import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selector import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; import { downloadStateLogs } from '../../../util/logs'; +import { trace, endTrace, TraceName } from '../../../util/trace'; const deviceHeight = Device.getDeviceHeight(); const breakPoint = deviceHeight < 700; @@ -245,6 +246,7 @@ class Login extends PureComponent { fieldRef = React.createRef(); async componentDidMount() { + trace({ name: TraceName.LoginPasswordEntry }); this.props.metrics.trackEvent(MetaMetricsEvents.LOGIN_SCREEN_VIEWED); BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); @@ -355,6 +357,7 @@ class Login extends PureComponent { }; onLogin = async () => { + endTrace({ name: TraceName.LoginPasswordEntry }); const { password } = this.state; const { current: field } = this.fieldRef; const locked = !passwordRequirementsMet(password); @@ -429,6 +432,7 @@ class Login extends PureComponent { } Logger.error(e, 'Failed to unlock'); } + trace({ name: TraceName.LoginUser }); }; tryBiometric = async (e) => { diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 886a36d645a..5136413e38a 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -96,6 +96,8 @@ import { } from '../../../selectors/notifications'; import { ButtonVariants } from '../../../component-library/components/Buttons/Button'; import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; +import { endTrace, TraceName } from '../../../util/trace'; + const createStyles = ({ colors, typography }: Theme) => StyleSheet.create({ base: { @@ -222,6 +224,10 @@ const Wallet = ({ const currentToast = toastRef?.current; + useEffect(() => { + endTrace({ name: TraceName.LoginUser }); + }, []); + useEffect(() => { if ( isDataCollectionForMarketingEnabled === null && @@ -406,8 +412,7 @@ const Wallet = ({ useEffect( () => { requestAnimationFrame(async () => { - const { AccountTrackerController } = - Engine.context; + const { AccountTrackerController } = Engine.context; AccountTrackerController.refresh(); }); }, diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts index 7f5334a80f0..3f91c64b5b7 100644 --- a/app/core/EngineService/EngineService.ts +++ b/app/core/EngineService/EngineService.ts @@ -3,6 +3,7 @@ import AppConstants from '../AppConstants'; import { getVaultFromBackup } from '../BackupVault'; import { store as importedStore } from '../../store'; import Logger from '../../util/Logger'; +import { trace, endTrace, TraceName } from '../../util/trace'; import { NO_VAULT_IN_BACKUP_ERROR, VAULT_CREATION_ERROR, @@ -32,7 +33,9 @@ class EngineService { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const Engine = UntypedEngine as any; + trace({ name: TraceName.EngineInitialization }); Engine.init(state); + endTrace({ name: TraceName.EngineInitialization }); this.updateControllers(store, Engine); }; diff --git a/app/store/index.ts b/app/store/index.ts index aa7a4df512f..254548856f4 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -9,6 +9,7 @@ import { Authentication } from '../core'; import LockManagerService from '../core/LockManagerService'; import ReadOnlyNetworkStore from '../util/test/network-store'; import { isE2E } from '../util/test/utils'; +import { endTrace, TraceName } from '../util/trace'; import thunk from 'redux-thunk'; import persistConfig from './persistConfig'; @@ -52,6 +53,7 @@ const createStoreAndPersistor = async () => { preloadedState: initialState, }); + endTrace({ name: TraceName.StorageRehydration }); sagaMiddleware.run(rootSaga); /** diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts index 2cd7477a432..6ed403f88c1 100644 --- a/app/store/persistConfig.ts +++ b/app/store/persistConfig.ts @@ -7,11 +7,13 @@ import { migrations, version } from './migrations'; import Logger from '../util/Logger'; import Device from '../util/device'; import { IUserReducer } from '../reducers/user'; +import { trace, TraceName } from '../util/trace'; const TIMEOUT = 40000; const MigratedStorage = { async getItem(key: string) { + trace({ name: TraceName.StorageRehydration }); try { const res = await FilesystemStorage.getItem(key); if (res) { diff --git a/app/util/trace.ts b/app/util/trace.ts index d74ca003249..4383b8c840b 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -16,6 +16,11 @@ export enum TraceName { Middleware = 'Middleware', NestedTest1 = 'Nested Test 1', NestedTest2 = 'Nested Test 2', + SetupStore = 'Setup Store', + LoginPasswordEntry = 'Login Password Entry', + LoginUser = 'Login User', + EngineInitialization = 'Engine Initialization', + StorageRehydration = 'Storage Rehydration', } const ID_DEFAULT = 'default'; From f2c4baf01119eac87fb95bc343834a0758ea6707 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Wed, 2 Oct 2024 20:03:49 +0100 Subject: [PATCH 02/22] feat: add custom operation name --- app/components/Views/Login/index.js | 6 +++--- app/core/EngineService/EngineService.ts | 4 ++-- app/store/index.ts | 7 +++++-- app/store/persistConfig.ts | 4 ++-- app/util/sentry/utils.js | 17 ++++++++--------- app/util/trace.ts | 13 +++++++++++-- 6 files changed, 31 insertions(+), 20 deletions(-) diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 19e93adb257..a727468812b 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -59,7 +59,7 @@ import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selector import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; import { downloadStateLogs } from '../../../util/logs'; -import { trace, endTrace, TraceName } from '../../../util/trace'; +import { trace, endTrace, TraceName, TraceOperation } from '../../../util/trace'; const deviceHeight = Device.getDeviceHeight(); const breakPoint = deviceHeight < 700; @@ -246,7 +246,7 @@ class Login extends PureComponent { fieldRef = React.createRef(); async componentDidMount() { - trace({ name: TraceName.LoginPasswordEntry }); + trace({ name: TraceName.LoginPasswordEntry, op: TraceOperation.LoginPasswordEntry }); this.props.metrics.trackEvent(MetaMetricsEvents.LOGIN_SCREEN_VIEWED); BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); @@ -432,7 +432,7 @@ class Login extends PureComponent { } Logger.error(e, 'Failed to unlock'); } - trace({ name: TraceName.LoginUser }); + trace({ name: TraceName.LoginUser, op: TraceOperation.LoginUser }); }; tryBiometric = async (e) => { diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts index 3f91c64b5b7..d33a44f1556 100644 --- a/app/core/EngineService/EngineService.ts +++ b/app/core/EngineService/EngineService.ts @@ -3,7 +3,7 @@ import AppConstants from '../AppConstants'; import { getVaultFromBackup } from '../BackupVault'; import { store as importedStore } from '../../store'; import Logger from '../../util/Logger'; -import { trace, endTrace, TraceName } from '../../util/trace'; +import { trace, endTrace, TraceName, TraceOperation } from '../../util/trace'; import { NO_VAULT_IN_BACKUP_ERROR, VAULT_CREATION_ERROR, @@ -33,7 +33,7 @@ class EngineService { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const Engine = UntypedEngine as any; - trace({ name: TraceName.EngineInitialization }); + trace({ name: TraceName.EngineInitialization, op: TraceOperation.EngineInitialization }); Engine.init(state); endTrace({ name: TraceName.EngineInitialization }); this.updateControllers(store, Engine); diff --git a/app/store/index.ts b/app/store/index.ts index 254548856f4..60461dcacf2 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -9,7 +9,7 @@ import { Authentication } from '../core'; import LockManagerService from '../core/LockManagerService'; import ReadOnlyNetworkStore from '../util/test/network-store'; import { isE2E } from '../util/test/utils'; -import { endTrace, TraceName } from '../util/trace'; +import { endTrace, TraceName, trace, TraceOperation } from '../util/trace'; import thunk from 'redux-thunk'; import persistConfig from './persistConfig'; @@ -47,13 +47,14 @@ const createStoreAndPersistor = async () => { middlewares.push(createReduxFlipperDebugger()); } + trace({ name: TraceName.StorageRehydration, op: TraceOperation.StorageRehydration }); + store = configureStore({ reducer: pReducer, middleware: middlewares, preloadedState: initialState, }); - endTrace({ name: TraceName.StorageRehydration }); sagaMiddleware.run(rootSaga); /** @@ -75,6 +76,8 @@ const createStoreAndPersistor = async () => { * - TypeError: undefined is not an object (evaluating 'TokenListController.tokenList') * - V8: SES_UNHANDLED_REJECTION */ + endTrace({ name: TraceName.StorageRehydration }); + store.dispatch({ type: 'TOGGLE_BASIC_FUNCTIONALITY', basicFunctionalityEnabled: diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts index 6ed403f88c1..c98e8f82c4f 100644 --- a/app/store/persistConfig.ts +++ b/app/store/persistConfig.ts @@ -7,13 +7,13 @@ import { migrations, version } from './migrations'; import Logger from '../util/Logger'; import Device from '../util/device'; import { IUserReducer } from '../reducers/user'; -import { trace, TraceName } from '../util/trace'; +import { trace, TraceName, TraceOperation } from '../util/trace'; const TIMEOUT = 40000; const MigratedStorage = { async getItem(key: string) { - trace({ name: TraceName.StorageRehydration }); + trace({ name: TraceName.StorageRehydration, op: TraceOperation.StorageRehydration }); try { const res = await FilesystemStorage.getItem(key); if (res) { diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js index 26448b31041..0d2efebb014 100644 --- a/app/util/sentry/utils.js +++ b/app/util/sentry/utils.js @@ -493,15 +493,14 @@ export function setupSentry() { dsn, debug: __DEV__, environment, - integrations: - metricsOptIn === AGREED - ? [ - ...integrations, - new Sentry.ReactNativeTracing({ - routingInstrumentation, - }), - ] - : integrations, + integrations: true + ? [ + ...integrations, + new Sentry.ReactNativeTracing({ + routingInstrumentation, + }), + ] + : integrations, // Set tracesSampleRate to 1.0, as that ensures that every transaction will be sent to Sentry for development builds. tracesSampleRate: __DEV__ ? 1.0 : 0.08, beforeSend: (report) => rewriteReport(report), diff --git a/app/util/trace.ts b/app/util/trace.ts index 4383b8c840b..26b2a55b01f 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -23,6 +23,14 @@ export enum TraceName { StorageRehydration = 'Storage Rehydration', } +export enum TraceOperation { + SetupStore = 'setup.store', + LoginPasswordEntry = 'login.password.entry', + LoginUser = 'login.user', + EngineInitialization = 'engine.initialization', + StorageRehydration = 'storage.rehydration', +} + const ID_DEFAULT = 'default'; const OP_DEFAULT = 'custom'; export const TRACES_CLEANUP_INTERVAL = 5 * 60 * 1000; // 5 minutes @@ -47,6 +55,7 @@ export interface TraceRequest { parentContext?: TraceContext; startTime?: number; tags?: Record; + op?: string; } export interface EndTraceRequest { @@ -156,13 +165,13 @@ function startSpan( request: TraceRequest, callback: (spanOptions: StartSpanOptions) => T, ) { - const { data: attributes, name, parentContext, startTime, tags } = request; + const { data: attributes, name, parentContext, startTime, tags, op } = request; const parentSpan = (parentContext ?? null) as Span | null; const spanOptions: StartSpanOptions = { attributes, name, - op: OP_DEFAULT, + op: op || OP_DEFAULT, // This needs to be parentSpan once we have the withIsolatedScope implementation in place in the Sentry SDK for React Native // Reference PR that updates @sentry/react-native: https://github.com/getsentry/sentry-react-native/pull/3895 parentSpanId: parentSpan?.spanId, From e13f650c36ffad957dd6cd15751b205cd4ce9bf5 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Thu, 3 Oct 2024 15:20:06 +0100 Subject: [PATCH 03/22] feat: revert integrations hardcode --- app/util/sentry/utils.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js index 0d2efebb014..1cc5364379c 100644 --- a/app/util/sentry/utils.js +++ b/app/util/sentry/utils.js @@ -475,9 +475,9 @@ export function setupSentry() { const dsn = process.env.MM_SENTRY_DSN; // Disable Sentry for E2E tests or when DSN is not provided - if (isE2E || !dsn) { - return; - } + // if (isE2E || !dsn) { + // return; + // } const init = async () => { const metricsOptIn = await StorageWrapper.getItem(METRICS_OPT_IN); @@ -490,10 +490,10 @@ export function setupSentry() { ); Sentry.init({ - dsn, + dsn: 'https://cb74c22fd913425a859dc4b4514efbfb@o379908.ingest.us.sentry.io/5205240', debug: __DEV__, environment, - integrations: true + integrations: metricsOptIn === AGREED ? [ ...integrations, new Sentry.ReactNativeTracing({ From 8e423eb70b0938c1dd25ecf42952790a2b7143ba Mon Sep 17 00:00:00 2001 From: tommasini Date: Fri, 4 Oct 2024 10:50:25 +0100 Subject: [PATCH 04/22] add tags file and folder --- app/util/sentry/tags/index.ts | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 app/util/sentry/tags/index.ts diff --git a/app/util/sentry/tags/index.ts b/app/util/sentry/tags/index.ts new file mode 100644 index 00000000000..34c767d5aac --- /dev/null +++ b/app/util/sentry/tags/index.ts @@ -0,0 +1,35 @@ +import { RootState } from '../../../reducers'; +import { selectAllNftsFlat } from '../../../selectors/nftController'; +import { selectInternalAccounts } from '../../../selectors/accountsController'; +import { selectAllTokensFlat } from '../../../selectors/tokensController'; +import { getNotificationsList } from '../../../selectors/notifications'; +import { selectTransactions } from '../../../selectors/transactionController'; + +export function getTraceTags(state: RootState) { + // Replace this to say how metamask was opened, deeplink, wallet connect, sdk, or just tap + //const uiType = getEnvironmentType(); + const unlocked = state.user.userLoggedIn; + const accountCount = selectInternalAccounts(state).length; + const nftCount = selectAllNftsFlat(state).length; + const notificationCount = getNotificationsList(state).length; + const tokenCount = selectAllTokensFlat(state).length as number; + const transactionCount = selectTransactions(state).length; + + /* Understand the pending approvals and implement those how mobile does it + const pendingApprovals = getPendingApprovals( + state as unknown as ApprovalsMetaMaskState, + ); */ + + //const firstApprovalType = pendingApprovals?.[0]?.type; + + return { + 'wallet.account_count': accountCount, + 'wallet.nft_count': nftCount, + 'wallet.notification_count': notificationCount, + 'wallet.pending_approval': null, //firstApprovalType, + 'wallet.token_count': tokenCount, + 'wallet.transaction_count': transactionCount, + 'wallet.unlocked': unlocked, + 'wallet.ui_type': null, //uiType, + }; +} From 401c6ff6bd0002ba4b49117eb96fd0f328783db8 Mon Sep 17 00:00:00 2001 From: tommasini Date: Fri, 4 Oct 2024 10:51:20 +0100 Subject: [PATCH 05/22] add selectAllTokens and selectAllNfts selectors --- app/selectors/nftController.ts | 13 ++++++++++++- app/selectors/tokensController.ts | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/app/selectors/nftController.ts b/app/selectors/nftController.ts index 5d6a0239ee7..a56b5e9afed 100644 --- a/app/selectors/nftController.ts +++ b/app/selectors/nftController.ts @@ -1,5 +1,5 @@ import { createSelector } from 'reselect'; -import { NftState } from '@metamask/assets-controllers'; +import { Nft, NftState } from '@metamask/assets-controllers'; import { RootState } from '../reducers'; const selectNftControllerState = (state: RootState) => @@ -14,3 +14,14 @@ export const selectAllNfts = createSelector( selectNftControllerState, (nftControllerState: NftState) => nftControllerState.allNfts, ); + +export const selectAllNftsFlat = createSelector( + selectAllNfts, + (nftsByChainByAccount) => { + const nftsByChainArray = Object.values(nftsByChainByAccount); + return nftsByChainArray.reduce((acc, nftsByChain) => { + const nftsArrays = Object.values(nftsByChain); + return acc.concat(...nftsArrays); + }, [] as Nft[]); + }, +); diff --git a/app/selectors/tokensController.ts b/app/selectors/tokensController.ts index 9ff4b872e3c..3d91c000eef 100644 --- a/app/selectors/tokensController.ts +++ b/app/selectors/tokensController.ts @@ -34,3 +34,23 @@ export const selectDetectedTokens = createSelector( selectTokensControllerState, (tokensControllerState: TokensState) => tokensControllerState?.detectedTokens, ); + +const selectAllTokens = createSelector( + selectTokensControllerState, + (tokensControllerState: TokensState) => tokensControllerState?.allTokens, +); + +export const selectAllTokensFlat = createSelector( + selectAllTokens, + (tokensByAccountByChain) => { + if (Object.values(tokensByAccountByChain).length === 0) { + return []; + } + const tokensByAccountArray = Object.values(tokensByAccountByChain); + + return tokensByAccountArray.reduce((acc, tokensByAccount) => { + const tokensArray = Object.values(tokensByAccount); + return acc.concat(...tokensArray); + }, [] as Token[]); + }, +); From cfc0143e6303388a10f78a0a141a002866f886fd Mon Sep 17 00:00:00 2001 From: tommasini Date: Mon, 7 Oct 2024 18:36:35 +0100 Subject: [PATCH 06/22] sentry tags completed without the tag for opening by data, if it was deeplink or tap --- app/util/sentry/tags/index.test.ts | 240 +++++++++++++++++++++++++++++ app/util/sentry/tags/index.ts | 14 +- 2 files changed, 246 insertions(+), 8 deletions(-) create mode 100644 app/util/sentry/tags/index.test.ts diff --git a/app/util/sentry/tags/index.test.ts b/app/util/sentry/tags/index.test.ts new file mode 100644 index 00000000000..fb4a48bb8d5 --- /dev/null +++ b/app/util/sentry/tags/index.test.ts @@ -0,0 +1,240 @@ +import { RootState } from '../../../reducers'; +import { getTraceTags } from './'; +import initialRootState, { + backgroundState, +} from '../../../util/test/initial-root-state'; +import { userInitialState } from '../../../reducers/user'; +import { KeyringTypes } from '@metamask/keyring-controller'; +import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; + +describe('Tags Utils', () => { + //const getEnvironmentTypeMock = jest.mocked(getEnvironmentType); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('getTraceTags', () => { + /* it('includes UI type', () => { + getEnvironmentTypeMock.mockReturnValue(ENVIRONMENT_TYPE_FULLSCREEN); + + const tags = getTraceTags(STATE_EMPTY_MOCK); + + expect(tags['wallet.ui_type']).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN); + }); + */ + it('includes if unlocked', () => { + const state = { + ...initialRootState, + user: { ...userInitialState, userLoggedIn: true }, + }; + + const tags = getTraceTags(state); + + expect(tags['wallet.unlocked']).toStrictEqual(true); + }); + + it('includes if not unlocked', () => { + const state = { + ...initialRootState, + user: { ...userInitialState, userLoggedIn: false }, + }; + + const tags = getTraceTags(state); + + expect(tags['wallet.unlocked']).toStrictEqual(false); + }); + + it('includes pending approval type', () => { + const state = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + ApprovalController: { + ...backgroundState.ApprovalController, + pendingApprovals: { + 1: { + type: 'eth_sendTransaction', + }, + }, + }, + }, + }, + } as unknown as RootState; + + const tags = getTraceTags(state); + + expect(tags['wallet.pending_approval']).toStrictEqual( + 'eth_sendTransaction', + ); + }); + + it('includes first pending approval type if multiple', () => { + const state = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + + ApprovalController: { + ...backgroundState.ApprovalController, + pendingApprovals: { + 1: { + type: 'eth_sendTransaction', + }, + 2: { + type: 'personal_sign', + }, + }, + }, + }, + }, + } as unknown as RootState; + + const tags = getTraceTags(state); + + expect(tags['wallet.pending_approval']).toStrictEqual( + 'eth_sendTransaction', + ); + }); + + it('includes account count', () => { + const state = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + AccountsController: createMockAccountsControllerState([ + '0x1234', + '0x4321', + ]), + }, + }, + } as unknown as RootState; + + const tags = getTraceTags(state); + + expect(tags['wallet.account_count']).toStrictEqual(2); + }); + + it('includes nft count', () => { + const state = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + NftController: { + ...backgroundState.NftController, + allNfts: { + '0x1234': { + '0x1': [ + { + tokenId: '1', + }, + { + tokenId: '2', + }, + ], + '0x2': [ + { + tokenId: '3', + }, + { + tokenId: '4', + }, + ], + }, + '0x4321': { + '0x3': [ + { + tokenId: '5', + }, + ], + }, + }, + }, + }, + }, + } as unknown as RootState; + + const tags = getTraceTags(state); + + expect(tags['wallet.nft_count']).toStrictEqual(5); + }); + + it('includes notification count', () => { + const state = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + NotificationServicesController: { + metamaskNotificationsList: [{}, {}, {}], + }, + }, + }, + } as unknown as RootState; + + const tags = getTraceTags(state); + + expect(tags['wallet.notification_count']).toStrictEqual(3); + }); + + it('includes token count', () => { + const state = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + TokensController: { + allTokens: { + '0x1': { + '0x1234': [{}, {}], + '0x4321': [{}], + }, + '0x2': { + '0x5678': [{}], + }, + }, + }, + }, + }, + } as unknown as RootState; + + const tags = getTraceTags(state); + + expect(tags['wallet.token_count']).toStrictEqual(4); + }); + + it('includes transaction count', () => { + const state = { + ...initialRootState, + engine: { + backgroundState: { + ...backgroundState, + TransactionController: { + transactions: [ + { + id: 1, + chainId: '0x1', + }, + { + id: 2, + chainId: '0x1', + }, + { + id: 3, + chainId: '0x2', + }, + ], + }, + }, + }, + } as unknown as RootState; + const tags = getTraceTags(state); + + expect(tags['wallet.transaction_count']).toStrictEqual(3); + }); + }); +}); diff --git a/app/util/sentry/tags/index.ts b/app/util/sentry/tags/index.ts index 34c767d5aac..8723ee11434 100644 --- a/app/util/sentry/tags/index.ts +++ b/app/util/sentry/tags/index.ts @@ -4,29 +4,27 @@ import { selectInternalAccounts } from '../../../selectors/accountsController'; import { selectAllTokensFlat } from '../../../selectors/tokensController'; import { getNotificationsList } from '../../../selectors/notifications'; import { selectTransactions } from '../../../selectors/transactionController'; +import { selectPendingApprovals } from '../../../selectors/approvalController'; export function getTraceTags(state: RootState) { + // if it's cold app start or warm app start // Replace this to say how metamask was opened, deeplink, wallet connect, sdk, or just tap //const uiType = getEnvironmentType(); const unlocked = state.user.userLoggedIn; const accountCount = selectInternalAccounts(state).length; const nftCount = selectAllNftsFlat(state).length; const notificationCount = getNotificationsList(state).length; - const tokenCount = selectAllTokensFlat(state).length as number; + const tokenCount = selectAllTokensFlat(state).length; const transactionCount = selectTransactions(state).length; + const pendingApprovals = Object.values(selectPendingApprovals(state)); - /* Understand the pending approvals and implement those how mobile does it - const pendingApprovals = getPendingApprovals( - state as unknown as ApprovalsMetaMaskState, - ); */ - - //const firstApprovalType = pendingApprovals?.[0]?.type; + const firstApprovalType = pendingApprovals?.[0]?.type; return { 'wallet.account_count': accountCount, 'wallet.nft_count': nftCount, 'wallet.notification_count': notificationCount, - 'wallet.pending_approval': null, //firstApprovalType, + 'wallet.pending_approval': firstApprovalType, 'wallet.token_count': tokenCount, 'wallet.transaction_count': transactionCount, 'wallet.unlocked': unlocked, From 67efec13e7bcda6a389d4d201c228fd17bef7e09 Mon Sep 17 00:00:00 2001 From: tommasini Date: Mon, 7 Oct 2024 19:34:02 +0100 Subject: [PATCH 07/22] mimic extension on adding measurements to the spans --- app/util/trace.test.ts | 49 ++++++++++----- app/util/trace.ts | 136 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 162 insertions(+), 23 deletions(-) diff --git a/app/util/trace.test.ts b/app/util/trace.test.ts index cb90722c9cb..91195494e76 100644 --- a/app/util/trace.test.ts +++ b/app/util/trace.test.ts @@ -1,12 +1,12 @@ -import { startSpan, startSpanManual, withScope } from '@sentry/react-native'; +import { + setMeasurement, + startSpan, + startSpanManual, + withScope, +} from '@sentry/react-native'; import { Span } from '@sentry/types'; -import { - endTrace, - trace, - TraceName, - TRACES_CLEANUP_INTERVAL, -} from './trace'; +import { endTrace, trace, TraceName, TRACES_CLEANUP_INTERVAL } from './trace'; jest.mock('@sentry/react-native', () => ({ withScope: jest.fn(), @@ -36,15 +36,22 @@ describe('Trace', () => { const startSpanMock = jest.mocked(startSpan); const startSpanManualMock = jest.mocked(startSpanManual); const withScopeMock = jest.mocked(withScope); - const setTagsMock = jest.fn(); + const setMeasurementMock = jest.mocked(setMeasurement); + const setTagMock = jest.fn(); beforeEach(() => { jest.resetAllMocks(); startSpanMock.mockImplementation((_, fn) => fn({} as Span)); + startSpanManualMock.mockImplementation((_, fn) => + fn({} as Span, () => { + // Intentionally empty + }), + ); + // eslint-disable-next-line @typescript-eslint/no-explicit-any - withScopeMock.mockImplementation((fn: any) => fn({ setTags: setTagsMock })); + withScopeMock.mockImplementation((fn: any) => fn({ setTags: setTagMock })); }); describe('trace', () => { @@ -87,8 +94,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); it('invokes Sentry if no callback provided', () => { @@ -113,8 +124,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); it('invokes Sentry if no callback provided with custom start time', () => { @@ -141,8 +156,12 @@ describe('Trace', () => { expect.any(Function), ); - expect(setTagsMock).toHaveBeenCalledTimes(1); - expect(setTagsMock).toHaveBeenCalledWith(TAGS_MOCK); + expect(setTagMock).toHaveBeenCalledTimes(2); + expect(setTagMock).toHaveBeenCalledWith('tag1', 'value1'); + expect(setTagMock).toHaveBeenCalledWith('tag2', true); + + expect(setMeasurementMock).toHaveBeenCalledTimes(1); + expect(setMeasurementMock).toHaveBeenCalledWith('tag3', 123, 'none'); }); }); diff --git a/app/util/trace.ts b/app/util/trace.ts index 26b2a55b01f..8cd6f5b0b85 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -2,15 +2,20 @@ import { startSpan as sentryStartSpan, startSpanManual, withScope, + setMeasurement, + Scope, } from '@sentry/react-native'; import performance from 'react-native-performance'; -import type { Primitive, Span, StartSpanOptions } from '@sentry/types'; +import type { Span, StartSpanOptions } from '@sentry/types'; import { createModuleLogger, createProjectLogger } from '@metamask/utils'; +import { MeasurementUnit } from '@sentry/types'; // Cannot create this 'sentry' logger in Sentry util file because of circular dependency const projectLogger = createProjectLogger('sentry'); const log = createModuleLogger(projectLogger, 'trace'); - +/** + * The supported trace names. + */ export enum TraceName { DeveloperTest = 'Developer Test', Middleware = 'Middleware', @@ -43,24 +48,72 @@ export interface PendingTrace { startTime: number; timeoutId: NodeJS.Timeout; } - +/** + * A context object to associate traces with each other and generate nested traces. + */ export type TraceContext = unknown; - +/** + * A callback function that can be traced. + */ export type TraceCallback = (context?: TraceContext) => T; - +/** + * A request to create a new trace. + */ export interface TraceRequest { + /** + * Custom data to associate with the trace. + */ data?: Record; + + /** + * A unique identifier when not tracing a callback. + * Defaults to 'default' if not provided. + */ id?: string; + + /** + * The name of the trace. + */ name: TraceName; + + /** + * The parent context of the trace. + * If provided, the trace will be nested under the parent trace. + */ parentContext?: TraceContext; + + /** + * Override the start time of the trace. + */ startTime?: number; + + /** + * Custom tags to associate with the trace. + */ tags?: Record; + /** + * Custom operation name to associate with the trace. + */ op?: string; } - +/** + * A request to end a pending trace. + */ export interface EndTraceRequest { + /** + * The unique identifier of the trace. + * Defaults to 'default' if not provided. + */ id?: string; + + /** + * The name of the trace. + */ name: TraceName; + + /** + * Override the end time of the trace. + */ timestamp?: number; } @@ -68,6 +121,16 @@ export function trace(request: TraceRequest, fn: TraceCallback): T; export function trace(request: TraceRequest): TraceContext; +/** + * Create a Sentry transaction to analyse the duration of a code flow. + * If a callback is provided, the transaction will be automatically ended when the callback completes. + * If the callback returns a promise, the transaction will be ended when the promise resolves or rejects. + * If no callback is provided, the transaction must be manually ended using `endTrace`. + * + * @param request - The data associated with the trace, such as the name and tags. + * @param fn - The optional callback to record the duration of. + * @returns The context of the trace, or the result of the callback if provided. + */ export function trace( request: TraceRequest, fn?: TraceCallback, @@ -79,6 +142,12 @@ export function trace( return traceCallback(request, fn); } +/** + * End a pending trace that was started without a callback. + * Does nothing if the pending trace cannot be found. + * + * @param request - The data necessary to identify and end the pending trace. + */ export function endTrace(request: EndTraceRequest) { const { name, timestamp } = request; const id = getTraceId(request); @@ -111,6 +180,10 @@ function traceCallback(request: TraceRequest, fn: TraceCallback): T { const start = Date.now(); let error: unknown; + if (span) { + initSpan(span, request); + } + return tryCatchMaybePromise( () => fn(span), (currentError) => { @@ -141,6 +214,10 @@ function startTrace(request: TraceRequest): TraceContext { span?.end(timestamp); }; + if (span) { + initSpan(span, request); + } + const timeoutId = setTimeout(() => { log('Trace cleanup due to timeout', name, id); end(); @@ -155,6 +232,7 @@ function startTrace(request: TraceRequest): TraceContext { return span; }; + console.log('ENTER REQUEST', request); return startSpan(request, (spanOptions) => startSpanManual(spanOptions, callback), @@ -165,7 +243,7 @@ function startSpan( request: TraceRequest, callback: (spanOptions: StartSpanOptions) => T, ) { - const { data: attributes, name, parentContext, startTime, tags, op } = request; + const { data: attributes, name, parentContext, startTime, op } = request; const parentSpan = (parentContext ?? null) as Span | null; const spanOptions: StartSpanOptions = { @@ -179,7 +257,7 @@ function startSpan( }; return withScope((scope) => { - scope.setTags(tags as Record); + initScope(scope, request); return callback(spanOptions); }) as T; @@ -196,6 +274,40 @@ function getTraceKey(request: TraceRequest) { return [name, id].join(':'); } +/** + * Initialise the isolated Sentry scope created for each trace. + * Includes setting all non-numeric tags. + * + * @param scope - The Sentry scope to initialise. + * @param request - The trace request. + */ +function initScope(scope: Scope, request: TraceRequest) { + const tags = request.tags ?? {}; + + for (const [key, value] of Object.entries(tags)) { + if (typeof value !== 'number') { + scope.setTag(key, value); + } + } +} + +/** + * Initialise the Sentry span created for each trace. + * Includes setting all numeric tags as measurements so they can be queried numerically in Sentry. + * + * @param _span - The Sentry span to initialise. + * @param request - The trace request. + */ +function initSpan(_span: Span, request: TraceRequest) { + const tags = request.tags ?? {}; + + for (const [key, value] of Object.entries(tags)) { + if (typeof value === 'number') { + sentrySetMeasurement(key, value, 'none'); + } + } +} + function getPerformanceTimestamp(): number { return performance.timeOrigin + performance.now(); } @@ -228,3 +340,11 @@ function tryCatchMaybePromise( return undefined; } + +function sentrySetMeasurement( + key: string, + value: number, + unit: MeasurementUnit, +) { + setMeasurement(key, value, unit); +} From 7faddb9d2a6f8a511ff839e4608f367470db2e75 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 11 Oct 2024 17:34:00 +0300 Subject: [PATCH 08/22] feat: updated Spans --- app/components/Views/Login/index.js | 27 ++++++++--- app/components/Views/Onboarding/index.js | 41 +++++++++++------ app/components/Views/Wallet/index.tsx | 5 -- app/core/EngineService/EngineService.ts | 3 -- app/store/index.ts | 58 +++++++++++++++++++++--- app/store/persistConfig.ts | 1 - app/util/sentry/utils.js | 25 +++++----- app/util/trace.ts | 21 ++++++--- 8 files changed, 126 insertions(+), 55 deletions(-) diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index a727468812b..ddeadffc10a 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -59,7 +59,12 @@ import { LoginViewSelectors } from '../../../../e2e/selectors/LoginView.selector import { withMetricsAwareness } from '../../../components/hooks/useMetrics'; import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; import { downloadStateLogs } from '../../../util/logs'; -import { trace, endTrace, TraceName, TraceOperation } from '../../../util/trace'; +import { + trace, + endTrace, + TraceName, + TraceOperation, +} from '../../../util/trace'; const deviceHeight = Device.getDeviceHeight(); const breakPoint = deviceHeight < 700; @@ -246,7 +251,10 @@ class Login extends PureComponent { fieldRef = React.createRef(); async componentDidMount() { - trace({ name: TraceName.LoginPasswordEntry, op: TraceOperation.LoginPasswordEntry }); + trace({ + name: TraceName.LoginToPasswordEntry, + op: TraceOperation.LoginToPasswordEntry, + }); this.props.metrics.trackEvent(MetaMetricsEvents.LOGIN_SCREEN_VIEWED); BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); @@ -357,7 +365,6 @@ class Login extends PureComponent { }; onLogin = async () => { - endTrace({ name: TraceName.LoginPasswordEntry }); const { password } = this.state; const { current: field } = this.fieldRef; const locked = !passwordRequirementsMet(password); @@ -370,8 +377,14 @@ class Login extends PureComponent { this.state.rememberMe, ); + try { - await Authentication.userEntryAuth(password, authType); + await trace( + { name: TraceName.AuthenticateUser, op: TraceOperation.AuthenticateUser }, + async () => { + await Authentication.userEntryAuth(password, authType); + }, + ); Keyboard.dismiss(); @@ -432,7 +445,8 @@ class Login extends PureComponent { } Logger.error(e, 'Failed to unlock'); } - trace({ name: TraceName.LoginUser, op: TraceOperation.LoginUser }); + + }; tryBiometric = async (e) => { @@ -458,7 +472,8 @@ class Login extends PureComponent { field?.blur(); }; - triggerLogIn = () => { + triggerLogIn = async () => { + await endTrace({ name: TraceName.LoginToPasswordEntry }); this.onLogin(); }; diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js index f15aa33c3c5..fc92130ae7b 100644 --- a/app/components/Views/Onboarding/index.js +++ b/app/components/Views/Onboarding/index.js @@ -49,6 +49,7 @@ import { OnboardingSelectorIDs } from '../../../../e2e/selectors/Onboarding/Onbo import Routes from '../../../constants/navigation/Routes'; import { selectAccounts } from '../../../selectors/accountTrackerController'; import trackOnboarding from '../../../util/metrics/TrackOnboarding/trackOnboarding'; +import { trace, TraceName, TraceOperation } from '../../../util/trace'; const createStyles = (colors) => StyleSheet.create({ @@ -275,25 +276,37 @@ class Onboarding extends PureComponent { }; onPressCreate = () => { - const action = async () => { - const { metrics } = this.props; - if (metrics.isEnabled()) { - this.props.navigation.navigate('ChoosePassword', { - [PREVIOUS_SCREEN]: ONBOARDING, - }); - this.track(MetaMetricsEvents.WALLET_SETUP_STARTED); - } else { - this.props.navigation.navigate('OptinMetrics', { - onContinue: () => { - this.props.navigation.replace('ChoosePassword', { + const action = () => { + trace( + { + name: TraceName.CreateNewWalletToChoosePassword, + op: TraceOperation.CreateNewWalletToChoosePassword, + }, + async () => { + const { metrics } = this.props; + if (metrics.isEnabled()) { + this.props.navigation.navigate('ChoosePassword', { [PREVIOUS_SCREEN]: ONBOARDING, }); this.track(MetaMetricsEvents.WALLET_SETUP_STARTED); - }, - }); - } + } else { + this.props.navigation.navigate('OptinMetrics', { + onContinue: () => { + this.props.navigation.replace('ChoosePassword', { + [PREVIOUS_SCREEN]: ONBOARDING, + }); + this.track(MetaMetricsEvents.WALLET_SETUP_STARTED); + }, + }); + } + }, + ); + console.log('action finished'); }; + + console.log('calling handleExistingUser'); this.handleExistingUser(action); + console.log('onPressCreate finished'); }; onPressImport = () => { diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 5136413e38a..0f9326ed7cf 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -96,7 +96,6 @@ import { } from '../../../selectors/notifications'; import { ButtonVariants } from '../../../component-library/components/Buttons/Button'; import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; -import { endTrace, TraceName } from '../../../util/trace'; const createStyles = ({ colors, typography }: Theme) => StyleSheet.create({ @@ -224,10 +223,6 @@ const Wallet = ({ const currentToast = toastRef?.current; - useEffect(() => { - endTrace({ name: TraceName.LoginUser }); - }, []); - useEffect(() => { if ( isDataCollectionForMarketingEnabled === null && diff --git a/app/core/EngineService/EngineService.ts b/app/core/EngineService/EngineService.ts index d33a44f1556..7f5334a80f0 100644 --- a/app/core/EngineService/EngineService.ts +++ b/app/core/EngineService/EngineService.ts @@ -3,7 +3,6 @@ import AppConstants from '../AppConstants'; import { getVaultFromBackup } from '../BackupVault'; import { store as importedStore } from '../../store'; import Logger from '../../util/Logger'; -import { trace, endTrace, TraceName, TraceOperation } from '../../util/trace'; import { NO_VAULT_IN_BACKUP_ERROR, VAULT_CREATION_ERROR, @@ -33,9 +32,7 @@ class EngineService { // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any const Engine = UntypedEngine as any; - trace({ name: TraceName.EngineInitialization, op: TraceOperation.EngineInitialization }); Engine.init(state); - endTrace({ name: TraceName.EngineInitialization }); this.updateControllers(store, Engine); }; diff --git a/app/store/index.ts b/app/store/index.ts index 60461dcacf2..721f39bbb0c 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -9,7 +9,9 @@ import { Authentication } from '../core'; import LockManagerService from '../core/LockManagerService'; import ReadOnlyNetworkStore from '../util/test/network-store'; import { isE2E } from '../util/test/utils'; -import { endTrace, TraceName, trace, TraceOperation } from '../util/trace'; +import { trace, endTrace, TraceName, TraceOperation } from '../util/trace'; +import StorageWrapper from './storage-wrapper'; + import thunk from 'redux-thunk'; import persistConfig from './persistConfig'; @@ -25,7 +27,7 @@ const pReducer = persistReducer(persistConfig, rootReducer); // TODO: Replace "any" with type // eslint-disable-next-line @typescript-eslint/no-explicit-any, import/no-mutable-exports let store: Store, persistor; -const createStoreAndPersistor = async () => { +const createStoreAndPersistor = async (appStartTime: number) => { // Obtain the initial state from ReadOnlyNetworkStore for E2E tests. const initialState = isE2E ? await ReadOnlyNetworkStore.getState() @@ -47,7 +49,22 @@ const createStoreAndPersistor = async () => { middlewares.push(createReduxFlipperDebugger()); } - trace({ name: TraceName.StorageRehydration, op: TraceOperation.StorageRehydration }); + const jsStartTime = performance.now(); + + trace({ + name: TraceName.LoadScripts, + startTime: appStartTime, + }); + + endTrace({ + name: TraceName.LoadScripts, + timestamp: appStartTime + jsStartTime, + }); + + trace({ + name: TraceName.CreateStore, + op: TraceOperation.CreateStore, + }); store = configureStore({ reducer: pReducer, @@ -57,10 +74,19 @@ const createStoreAndPersistor = async () => { sagaMiddleware.run(rootSaga); + endTrace({ name: TraceName.CreateStore }); + + trace({ + name: TraceName.StorageRehydration, + op: TraceOperation.StorageRehydration, + }); + /** * Initialize services after persist is completed */ - const onPersistComplete = () => { + const onPersistComplete = async () => { + endTrace({ name: TraceName.StorageRehydration }); + /** * EngineService.initalizeEngine(store) with SES/lockdown: * Requires ethjs nested patches (lib->src) @@ -76,7 +102,6 @@ const createStoreAndPersistor = async () => { * - TypeError: undefined is not an object (evaluating 'TokenListController.tokenList') * - V8: SES_UNHANDLED_REJECTION */ - endTrace({ name: TraceName.StorageRehydration }); store.dispatch({ type: 'TOGGLE_BASIC_FUNCTIONALITY', @@ -88,7 +113,17 @@ const createStoreAndPersistor = async () => { store.dispatch({ type: 'FETCH_FEATURE_FLAGS', }); - EngineService.initalizeEngine(store); + + await trace( + { + name: TraceName.EngineInitialization, + op: TraceOperation.EngineInitialization, + }, + async () => { + EngineService.initalizeEngine(store); + }, + ); + Authentication.init(store); AppStateEventProcessor.init(store); LockManagerService.init(store); @@ -98,7 +133,16 @@ const createStoreAndPersistor = async () => { }; (async () => { - await createStoreAndPersistor(); + const appStartTime = await StorageWrapper.getItem('appStartTime'); + + await trace( + { + name: TraceName.UIStartup, + op: TraceOperation.UIStartup, + startTime: appStartTime, + }, + async () => await createStoreAndPersistor(appStartTime), + ); })(); export { store, persistor }; diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts index c98e8f82c4f..570928a30bb 100644 --- a/app/store/persistConfig.ts +++ b/app/store/persistConfig.ts @@ -13,7 +13,6 @@ const TIMEOUT = 40000; const MigratedStorage = { async getItem(key: string) { - trace({ name: TraceName.StorageRehydration, op: TraceOperation.StorageRehydration }); try { const res = await FilesystemStorage.getItem(key); if (res) { diff --git a/app/util/sentry/utils.js b/app/util/sentry/utils.js index 1cc5364379c..26448b31041 100644 --- a/app/util/sentry/utils.js +++ b/app/util/sentry/utils.js @@ -475,9 +475,9 @@ export function setupSentry() { const dsn = process.env.MM_SENTRY_DSN; // Disable Sentry for E2E tests or when DSN is not provided - // if (isE2E || !dsn) { - // return; - // } + if (isE2E || !dsn) { + return; + } const init = async () => { const metricsOptIn = await StorageWrapper.getItem(METRICS_OPT_IN); @@ -490,17 +490,18 @@ export function setupSentry() { ); Sentry.init({ - dsn: 'https://cb74c22fd913425a859dc4b4514efbfb@o379908.ingest.us.sentry.io/5205240', + dsn, debug: __DEV__, environment, - integrations: metricsOptIn === AGREED - ? [ - ...integrations, - new Sentry.ReactNativeTracing({ - routingInstrumentation, - }), - ] - : integrations, + integrations: + metricsOptIn === AGREED + ? [ + ...integrations, + new Sentry.ReactNativeTracing({ + routingInstrumentation, + }), + ] + : integrations, // Set tracesSampleRate to 1.0, as that ensures that every transaction will be sent to Sentry for development builds. tracesSampleRate: __DEV__ ? 1.0 : 0.08, beforeSend: (report) => rewriteReport(report), diff --git a/app/util/trace.ts b/app/util/trace.ts index 26b2a55b01f..ae20c3468b2 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -16,19 +16,26 @@ export enum TraceName { Middleware = 'Middleware', NestedTest1 = 'Nested Test 1', NestedTest2 = 'Nested Test 2', + LoadScripts = 'Load Scripts', SetupStore = 'Setup Store', - LoginPasswordEntry = 'Login Password Entry', - LoginUser = 'Login User', + LoginToPasswordEntry = 'Login to Password Entry', + AuthenticateUser = 'Authenticate User', EngineInitialization = 'Engine Initialization', + CreateStore = 'Create Store', + CreateNewWalletToChoosePassword = 'Create New Wallet to Choose Password', StorageRehydration = 'Storage Rehydration', + UIStartup = 'Custom UIStartup', } export enum TraceOperation { - SetupStore = 'setup.store', - LoginPasswordEntry = 'login.password.entry', - LoginUser = 'login.user', - EngineInitialization = 'engine.initialization', - StorageRehydration = 'storage.rehydration', + SetupStore = 'custom.setup.store', + LoginToPasswordEntry = 'custom.login.to.password.entry', + AuthenticateUser = 'custom.authenticate.user', + EngineInitialization = 'custom.engine.initialization', + CreateStore = 'custom.create.store', + CreateNewWalletToChoosePassword = 'custom.create.new.wallet', + StorageRehydration = 'custom.storage.rehydration', + UIStartup = 'custom.ui.startup', } const ID_DEFAULT = 'default'; From d204e069b942736a939dd393ff49585810417d90 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Fri, 11 Oct 2024 20:48:56 +0300 Subject: [PATCH 09/22] feat: add op for Load Scripts trace --- app/store/index.ts | 1 + app/store/persistConfig.ts | 1 - app/util/trace.ts | 1 + 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/store/index.ts b/app/store/index.ts index 721f39bbb0c..7d33ee89045 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -53,6 +53,7 @@ const createStoreAndPersistor = async (appStartTime: number) => { trace({ name: TraceName.LoadScripts, + op: TraceOperation.LoadScripts, startTime: appStartTime, }); diff --git a/app/store/persistConfig.ts b/app/store/persistConfig.ts index 570928a30bb..2cd7477a432 100644 --- a/app/store/persistConfig.ts +++ b/app/store/persistConfig.ts @@ -7,7 +7,6 @@ import { migrations, version } from './migrations'; import Logger from '../util/Logger'; import Device from '../util/device'; import { IUserReducer } from '../reducers/user'; -import { trace, TraceName, TraceOperation } from '../util/trace'; const TIMEOUT = 40000; diff --git a/app/util/trace.ts b/app/util/trace.ts index ae20c3468b2..ceaa2d9d436 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -28,6 +28,7 @@ export enum TraceName { } export enum TraceOperation { + LoadScripts='custom.load.scripts', SetupStore = 'custom.setup.store', LoginToPasswordEntry = 'custom.login.to.password.entry', AuthenticateUser = 'custom.authenticate.user', From a405e11d2dec574d1e46addce14b373443b5fed8 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Mon, 14 Oct 2024 18:37:40 +0300 Subject: [PATCH 10/22] feat: fix main merge --- app/util/trace.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/util/trace.ts b/app/util/trace.ts index 0023eb155db..145935d58d0 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -16,6 +16,9 @@ export enum TraceName { Middleware = 'Middleware', NestedTest1 = 'Nested Test 1', NestedTest2 = 'Nested Test 2', + NotificationDisplay = 'Notification Display', + PPOMValidation = 'PPOM Validation', + Signature = 'Signature', LoadScripts = 'Load Scripts', SetupStore = 'Setup Store', LoginToPasswordEntry = 'Login to Password Entry', @@ -37,9 +40,6 @@ export enum TraceOperation { CreateNewWalletToChoosePassword = 'custom.create.new.wallet', StorageRehydration = 'custom.storage.rehydration', UIStartup = 'custom.ui.startup', - NotificationDisplay = 'Notification Display', - PPOMValidation = 'PPOM Validation', - Signature = 'Signature', } const ID_DEFAULT = 'default'; From 2607a9593c398c01a3e0e854b96fb24491475c52 Mon Sep 17 00:00:00 2001 From: tommasini Date: Mon, 14 Oct 2024 19:31:05 +0100 Subject: [PATCH 11/22] remove comment and rmeove log --- app/store/index.ts | 1 - app/util/sentry/tags/index.test.ts | 11 ----------- 2 files changed, 12 deletions(-) diff --git a/app/store/index.ts b/app/store/index.ts index 550fe70ca38..bd13e377b79 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -115,7 +115,6 @@ const createStoreAndPersistor = async (appStartTime: number) => { store.dispatch({ type: 'FETCH_FEATURE_FLAGS', }); - console.log('ENTER store.getState?.()', store.getState?.()); await trace( { diff --git a/app/util/sentry/tags/index.test.ts b/app/util/sentry/tags/index.test.ts index fb4a48bb8d5..e7814b33f93 100644 --- a/app/util/sentry/tags/index.test.ts +++ b/app/util/sentry/tags/index.test.ts @@ -4,25 +4,14 @@ import initialRootState, { backgroundState, } from '../../../util/test/initial-root-state'; import { userInitialState } from '../../../reducers/user'; -import { KeyringTypes } from '@metamask/keyring-controller'; import { createMockAccountsControllerState } from '../../../util/test/accountsControllerTestUtils'; describe('Tags Utils', () => { - //const getEnvironmentTypeMock = jest.mocked(getEnvironmentType); - beforeEach(() => { jest.resetAllMocks(); }); describe('getTraceTags', () => { - /* it('includes UI type', () => { - getEnvironmentTypeMock.mockReturnValue(ENVIRONMENT_TYPE_FULLSCREEN); - - const tags = getTraceTags(STATE_EMPTY_MOCK); - - expect(tags['wallet.ui_type']).toStrictEqual(ENVIRONMENT_TYPE_FULLSCREEN); - }); - */ it('includes if unlocked', () => { const state = { ...initialRootState, From adc9bb9718b1e11f5dee38ed69c667700118a941 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Tue, 15 Oct 2024 18:04:29 +0300 Subject: [PATCH 12/22] feat: add custom span for biometrics --- app/components/Nav/App/index.js | 11 ++++++++++- app/components/Views/LockScreen/index.js | 18 +++++++++++++---- app/components/Views/Login/index.js | 25 ++++++++++++++---------- app/util/trace.ts | 2 ++ 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/components/Nav/App/index.js b/app/components/Nav/App/index.js index a587ef6c3ae..8f424320041 100644 --- a/app/components/Nav/App/index.js +++ b/app/components/Nav/App/index.js @@ -131,6 +131,7 @@ import OptionsSheet from '../../UI/SelectOptionSheet/OptionsSheet'; import FoxLoader from '../../../components/UI/FoxLoader'; import { AppStateEventProcessor } from '../../../core/AppStateEventListener'; import MultiRpcModal from '../../../components/Views/MultiRpcModal/MultiRpcModal'; +import { trace, TraceName, TraceOperation } from '../../../util/trace'; const clearStackNavigatorOptions = { headerShown: false, @@ -354,7 +355,15 @@ const App = (props) => { setOnboarded(!!existingUser); try { if (existingUser) { - await Authentication.appTriggeredAuth(); + await trace( + { + name: TraceName.BiometricAuthentication, + op: TraceOperation.BiometricAuthentication, + }, + async () => { + await Authentication.appTriggeredAuth(); + }, + ); // we need to reset the navigator here so that the user cannot go back to the login screen navigator.reset({ routes: [{ name: Routes.ONBOARDING.HOME_NAV }] }); } else { diff --git a/app/components/Views/LockScreen/index.js b/app/components/Views/LockScreen/index.js index 92f04193389..030bc9ace1d 100644 --- a/app/components/Views/LockScreen/index.js +++ b/app/components/Views/LockScreen/index.js @@ -22,6 +22,7 @@ import { import Routes from '../../../constants/navigation/Routes'; import { CommonActions } from '@react-navigation/native'; import trackErrorAsAnalytics from '../../../util/metrics/TrackError/trackErrorAsAnalytics'; +import { trace, TraceName, TraceOperation } from '../../../util/trace'; const LOGO_SIZE = 175; const createStyles = (colors) => @@ -134,10 +135,19 @@ class LockScreen extends PureComponent { // Retrieve the credentials Logger.log('Lockscreen::unlockKeychain - getting credentials'); - await Authentication.appTriggeredAuth({ - bioStateMachineId, - disableAutoLogout: true, - }); + await trace( + { + name: TraceName.BiometricAuthentication, + op: TraceOperation.BiometricAuthentication, + }, + async () => { + await Authentication.appTriggeredAuth({ + bioStateMachineId, + disableAutoLogout: true, + }); + }, + ); + this.setState({ ready: true }); Logger.log('Lockscreen::unlockKeychain - state: ready'); } catch (error) { diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 6676f9fab1e..13e97a01468 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -376,12 +376,14 @@ class Login extends PureComponent { this.state.rememberMe, ); - try { await trace( - { name: TraceName.AuthenticateUser, op: TraceOperation.AuthenticateUser }, + { + name: TraceName.AuthenticateUser, + op: TraceOperation.AuthenticateUser, + }, async () => { - await Authentication.userEntryAuth(password, authType); + await Authentication.userEntryAuth(password, authType); }, ); @@ -444,8 +446,6 @@ class Login extends PureComponent { } Logger.error(e, 'Failed to unlock'); } - - }; tryBiometric = async (e) => { @@ -453,7 +453,15 @@ class Login extends PureComponent { const { current: field } = this.fieldRef; field?.blur(); try { - await Authentication.appTriggeredAuth(); + await trace( + { + name: TraceName.BiometricAuthentication, + op: TraceOperation.BiometricAuthentication, + }, + async () => { + await Authentication.appTriggeredAuth(); + }, + ); const onboardingWizard = await StorageWrapper.getItem(ONBOARDING_WIZARD); if (!onboardingWizard) this.props.setOnboardingWizardStep(1); this.props.navigation.replace(Routes.ONBOARDING.HOME_NAV); @@ -555,10 +563,7 @@ class Login extends PureComponent { )} - + {strings('login.title')} diff --git a/app/util/trace.ts b/app/util/trace.ts index 145935d58d0..f08b9f9ee51 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -23,6 +23,7 @@ export enum TraceName { SetupStore = 'Setup Store', LoginToPasswordEntry = 'Login to Password Entry', AuthenticateUser = 'Authenticate User', + BiometricAuthentication = 'Biometrics Authentication', EngineInitialization = 'Engine Initialization', CreateStore = 'Create Store', CreateNewWalletToChoosePassword = 'Create New Wallet to Choose Password', @@ -34,6 +35,7 @@ export enum TraceOperation { LoadScripts='custom.load.scripts', SetupStore = 'custom.setup.store', LoginToPasswordEntry = 'custom.login.to.password.entry', + BiometricAuthentication = 'biometrics.authentication', AuthenticateUser = 'custom.authenticate.user', EngineInitialization = 'custom.engine.initialization', CreateStore = 'custom.create.store', From 6ecba9eae4becd6c0a979f688ab3ebf732d75159 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Tue, 15 Oct 2024 18:05:12 +0300 Subject: [PATCH 13/22] feat: trace file lint --- app/util/trace.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/util/trace.ts b/app/util/trace.ts index f08b9f9ee51..fd6b4c9dfb3 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -32,7 +32,7 @@ export enum TraceName { } export enum TraceOperation { - LoadScripts='custom.load.scripts', + LoadScripts = 'custom.load.scripts', SetupStore = 'custom.setup.store', LoginToPasswordEntry = 'custom.login.to.password.entry', BiometricAuthentication = 'biometrics.authentication', @@ -178,7 +178,14 @@ function startSpan( request: TraceRequest, callback: (spanOptions: StartSpanOptions) => T, ) { - const { data: attributes, name, parentContext, startTime, tags, op } = request; + const { + data: attributes, + name, + parentContext, + startTime, + tags, + op, + } = request; const parentSpan = (parentContext ?? null) as Span | null; const spanOptions: StartSpanOptions = { From 62dd6e754b07efdc51d84707e419299c57b7de82 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Tue, 15 Oct 2024 19:30:53 +0300 Subject: [PATCH 14/22] feat: remove console.log --- app/components/Views/Onboarding/index.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js index fc92130ae7b..ea22ce9d4dd 100644 --- a/app/components/Views/Onboarding/index.js +++ b/app/components/Views/Onboarding/index.js @@ -301,12 +301,9 @@ class Onboarding extends PureComponent { } }, ); - console.log('action finished'); }; - console.log('calling handleExistingUser'); this.handleExistingUser(action); - console.log('onPressCreate finished'); }; onPressImport = () => { From 01563c23922985d587b98b02b421c52493771f4f Mon Sep 17 00:00:00 2001 From: tommasini Date: Tue, 15 Oct 2024 20:31:02 +0100 Subject: [PATCH 15/22] fix unit test --- app/util/trace.test.ts | 7 +++++-- app/util/trace.ts | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/app/util/trace.test.ts b/app/util/trace.test.ts index 91195494e76..b9e541ebdc5 100644 --- a/app/util/trace.test.ts +++ b/app/util/trace.test.ts @@ -1,4 +1,5 @@ import { + Scope, setMeasurement, startSpan, startSpanManual, @@ -12,6 +13,7 @@ jest.mock('@sentry/react-native', () => ({ withScope: jest.fn(), startSpan: jest.fn(), startSpanManual: jest.fn(), + setMeasurement: jest.fn(), })); const NAME_MOCK = TraceName.Middleware; @@ -50,8 +52,9 @@ describe('Trace', () => { }), ); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - withScopeMock.mockImplementation((fn: any) => fn({ setTags: setTagMock })); + withScopeMock.mockImplementation((fn: (arg: Scope) => unknown) => + fn({ setTag: setTagMock } as unknown as Scope), + ); }); describe('trace', () => { diff --git a/app/util/trace.ts b/app/util/trace.ts index d08905f69a9..50d6663fb7a 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -245,7 +245,6 @@ function startTrace(request: TraceRequest): TraceContext { return span; }; - console.log('ENTER REQUEST', request); return startSpan(request, (spanOptions) => startSpanManual(spanOptions, callback), From 3dbdbdd10a1ee3a9af10a7afcdf686aee987a778 Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Wed, 16 Oct 2024 00:23:51 +0300 Subject: [PATCH 16/22] feat: onboarding tweak --- app/components/Views/Onboarding/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js index ea22ce9d4dd..52ea24f6192 100644 --- a/app/components/Views/Onboarding/index.js +++ b/app/components/Views/Onboarding/index.js @@ -282,7 +282,7 @@ class Onboarding extends PureComponent { name: TraceName.CreateNewWalletToChoosePassword, op: TraceOperation.CreateNewWalletToChoosePassword, }, - async () => { + () => { const { metrics } = this.props; if (metrics.isEnabled()) { this.props.navigation.navigate('ChoosePassword', { From 78131620b0ff5d0e474ea49f58fd274633cf773e Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Wed, 16 Oct 2024 00:29:36 +0300 Subject: [PATCH 17/22] feat: engine tweak --- app/store/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/store/index.ts b/app/store/index.ts index 7d33ee89045..84a500f8784 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -120,7 +120,7 @@ const createStoreAndPersistor = async (appStartTime: number) => { name: TraceName.EngineInitialization, op: TraceOperation.EngineInitialization, }, - async () => { + () => { EngineService.initalizeEngine(store); }, ); From 28bb5b961f7244dfc19f918abce7595372a4e3db Mon Sep 17 00:00:00 2001 From: Aslau Mario-Daniel Date: Wed, 16 Oct 2024 00:39:02 +0300 Subject: [PATCH 18/22] feat: triggerLogIn tweak --- app/components/Views/Login/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 13e97a01468..e737df72178 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -479,8 +479,8 @@ class Login extends PureComponent { field?.blur(); }; - triggerLogIn = async () => { - await endTrace({ name: TraceName.LoginToPasswordEntry }); + triggerLogIn = () => { + endTrace({ name: TraceName.LoginToPasswordEntry }); this.onLogin(); }; From a385d448aba5b302484236409663fa6ac201fca3 Mon Sep 17 00:00:00 2001 From: tommasini Date: Tue, 15 Oct 2024 22:51:48 +0100 Subject: [PATCH 19/22] fix duplicated import --- app/util/trace.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/util/trace.ts b/app/util/trace.ts index 50d6663fb7a..05ad23d9dd8 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -6,9 +6,8 @@ import { Scope, } from '@sentry/react-native'; import performance from 'react-native-performance'; -import type { Span, StartSpanOptions } from '@sentry/types'; +import type { Span, StartSpanOptions, MeasurementUnit } from '@sentry/types'; import { createModuleLogger, createProjectLogger } from '@metamask/utils'; -import { MeasurementUnit } from '@sentry/types'; // Cannot create this 'sentry' logger in Sentry util file because of circular dependency const projectLogger = createProjectLogger('sentry'); From b862276b66181f906cb5ad338d5a7f44e936a788 Mon Sep 17 00:00:00 2001 From: tommasini Date: Wed, 16 Oct 2024 10:07:51 +0100 Subject: [PATCH 20/22] fix tags at fresh install --- app/util/sentry/tags/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/app/util/sentry/tags/index.ts b/app/util/sentry/tags/index.ts index ab4e2bff4a4..796bb9212fe 100644 --- a/app/util/sentry/tags/index.ts +++ b/app/util/sentry/tags/index.ts @@ -7,6 +7,7 @@ import { selectTransactions } from '../../../selectors/transactionController'; import { selectPendingApprovals } from '../../../selectors/approvalController'; export function getTraceTags(state: RootState) { + if (!Object.keys(state?.engine?.backgroundState).length) return; const unlocked = state.user.userLoggedIn; const accountCount = selectInternalAccounts(state).length; const nftCount = selectAllNftsFlat(state).length; From ccceec1c15af2da6c95094fcb5b546e1e04da9e0 Mon Sep 17 00:00:00 2001 From: tommasini Date: Wed, 16 Oct 2024 12:40:33 +0100 Subject: [PATCH 21/22] fix lint issue --- app/util/sentry/tags/index.test.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/util/sentry/tags/index.test.ts b/app/util/sentry/tags/index.test.ts index e7814b33f93..5511ab7cf92 100644 --- a/app/util/sentry/tags/index.test.ts +++ b/app/util/sentry/tags/index.test.ts @@ -20,7 +20,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.unlocked']).toStrictEqual(true); + expect(tags?.['wallet.unlocked']).toStrictEqual(true); }); it('includes if not unlocked', () => { @@ -31,7 +31,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.unlocked']).toStrictEqual(false); + expect(tags?.['wallet.unlocked']).toStrictEqual(false); }); it('includes pending approval type', () => { @@ -54,7 +54,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.pending_approval']).toStrictEqual( + expect(tags?.['wallet.pending_approval']).toStrictEqual( 'eth_sendTransaction', ); }); @@ -83,7 +83,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.pending_approval']).toStrictEqual( + expect(tags?.['wallet.pending_approval']).toStrictEqual( 'eth_sendTransaction', ); }); @@ -104,7 +104,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.account_count']).toStrictEqual(2); + expect(tags?.['wallet.account_count']).toStrictEqual(2); }); it('includes nft count', () => { @@ -149,7 +149,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.nft_count']).toStrictEqual(5); + expect(tags?.['wallet.nft_count']).toStrictEqual(5); }); it('includes notification count', () => { @@ -167,7 +167,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.notification_count']).toStrictEqual(3); + expect(tags?.['wallet.notification_count']).toStrictEqual(3); }); it('includes token count', () => { @@ -193,7 +193,7 @@ describe('Tags Utils', () => { const tags = getTraceTags(state); - expect(tags['wallet.token_count']).toStrictEqual(4); + expect(tags?.['wallet.token_count']).toStrictEqual(4); }); it('includes transaction count', () => { @@ -223,7 +223,7 @@ describe('Tags Utils', () => { } as unknown as RootState; const tags = getTraceTags(state); - expect(tags['wallet.transaction_count']).toStrictEqual(3); + expect(tags?.['wallet.transaction_count']).toStrictEqual(3); }); }); }); From 6c8ac834a3e37f627d0ac175e016a3a5b70b5118 Mon Sep 17 00:00:00 2001 From: tommasini Date: Wed, 16 Oct 2024 14:49:51 +0100 Subject: [PATCH 22/22] NotificationServicesController default state added to fixture builder --- e2e/fixtures/fixture-builder.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/e2e/fixtures/fixture-builder.js b/e2e/fixtures/fixture-builder.js index c4e987b3f01..b850fab5970 100644 --- a/e2e/fixtures/fixture-builder.js +++ b/e2e/fixtures/fixture-builder.js @@ -421,6 +421,18 @@ class FixtureBuilder { pendingApprovalCount: 0, approvalFlows: [], }, + NotificationServicesController: { + subscriptionAccountsSeen: [], + isMetamaskNotificationsFeatureSeen: false, + isNotificationServicesEnabled: false, + isFeatureAnnouncementsEnabled: false, + metamaskNotificationsList: [], + metamaskNotificationsReadList: [], + isUpdatingMetamaskNotifications: false, + isFetchingMetamaskNotifications: false, + isUpdatingMetamaskNotificationsAccount: [], + isCheckingAccountsPresence: false, + }, }, }, privacy: {