From 766a1b9c3460c69681203b23108d946f9e597a2c Mon Sep 17 00:00:00 2001 From: tommasini <46944231+tommasini@users.noreply.github.com> Date: Mon, 28 Oct 2024 12:48:13 +0000 Subject: [PATCH] chore: cherry-pick custom spans PR (#12031) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## **Description** Cherry pick: https://github.com/MetaMask/metamask-mobile/pull/11935 ## **Related issues** Fixes: ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** ### **Before** ### **After** ## **Pre-merge author checklist** - [ ] I’ve followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [ ] I've completed the PR template to the best of my ability - [ ] I’ve included tests if applicable - [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [ ] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. --- app/components/Nav/App/index.js | 11 +- app/components/Views/LockScreen/index.js | 18 +- .../Login/__snapshots__/index.test.tsx.snap | 422 +++++++++++++++++- app/components/Views/Login/index.js | 36 +- app/components/Views/Login/index.test.tsx | 30 +- app/components/Views/Onboarding/index.js | 38 +- .../Views/Onboarding/index.test.tsx | 20 + app/components/Views/Wallet/index.tsx | 1 + app/store/index.ts | 36 +- app/util/trace.ts | 35 +- 10 files changed, 573 insertions(+), 74 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/__snapshots__/index.test.tsx.snap b/app/components/Views/Login/__snapshots__/index.test.tsx.snap index 94ce77ff11d..df38946a89c 100644 --- a/app/components/Views/Login/__snapshots__/index.test.tsx.snap +++ b/app/components/Views/Login/__snapshots__/index.test.tsx.snap @@ -1,32 +1,406 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Login should render correctly 1`] = ` - - - + + + + + + An error occurred + + + Your information can't be shown. Don’t worry, your wallet and funds are safe. + + + + + View: Login +TypeError: (0 , _reactNativeDeviceInfo.getTotalMemorySync) is not a function + + + + + + +  + + + Try again + + + + + + + Please report this issue so we can fix it: + + + + + +  + + + Take a screenshot of this screen. + + + +  + + + + Copy + + + the error message to clipboard. + + + +  + + + Submit a ticket + + + here. + + + Please include the error message and the screenshot. + + + +  + + + Send us a bug report + + + here. + + + Please include details about what happened. + + + + If this error persists, + + + save your Secret Recovery Phrase + + + & re-install the app. Note: you can NOT restore your wallet without your Secret Recovery Phrase. + + + + + `; diff --git a/app/components/Views/Login/index.js b/app/components/Views/Login/index.js index 488804f7a36..e737df72178 100644 --- a/app/components/Views/Login/index.js +++ b/app/components/Views/Login/index.js @@ -58,6 +58,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'; const deviceHeight = Device.getDeviceHeight(); const breakPoint = deviceHeight < 700; @@ -244,6 +250,10 @@ class Login extends PureComponent { fieldRef = React.createRef(); async componentDidMount() { + trace({ + name: TraceName.LoginToPasswordEntry, + op: TraceOperation.LoginToPasswordEntry, + }); this.props.metrics.trackEvent(MetaMetricsEvents.LOGIN_SCREEN_VIEWED); BackHandler.addEventListener('hardwareBackPress', this.handleBackPress); @@ -367,7 +377,15 @@ class Login extends PureComponent { ); try { - await Authentication.userEntryAuth(password, authType); + await trace( + { + name: TraceName.AuthenticateUser, + op: TraceOperation.AuthenticateUser, + }, + async () => { + await Authentication.userEntryAuth(password, authType); + }, + ); Keyboard.dismiss(); @@ -435,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); @@ -454,6 +480,7 @@ class Login extends PureComponent { }; triggerLogIn = () => { + endTrace({ name: TraceName.LoginToPasswordEntry }); this.onLogin(); }; @@ -536,10 +563,7 @@ class Login extends PureComponent { )} - + {strings('login.title')} diff --git a/app/components/Views/Login/index.test.tsx b/app/components/Views/Login/index.test.tsx index 07938c54984..b1e9b78248f 100644 --- a/app/components/Views/Login/index.test.tsx +++ b/app/components/Views/Login/index.test.tsx @@ -1,28 +1,16 @@ import React from 'react'; -import { shallow } from 'enzyme'; import Login from './'; -import configureMockStore from 'redux-mock-store'; -import { Provider } from 'react-redux'; -import { backgroundState } from '../../../util/test/initial-root-state'; - -const mockStore = configureMockStore(); -const initialState = { - engine: { - backgroundState, - }, - user: { - passwordSet: true, - }, -}; -const store = mockStore(initialState); +import renderWithProvider from '../../../util/test/renderWithProvider'; +// eslint-disable-next-line import/no-namespace +import * as traceObj from '../../../util/trace'; describe('Login', () => { it('should render correctly', () => { - const wrapper = shallow( - - - , - ); - expect(wrapper).toMatchSnapshot(); + const spyFetch = jest + .spyOn(traceObj, 'trace') + .mockImplementation(() => undefined); + const { toJSON } = renderWithProvider(); + expect(toJSON()).toMatchSnapshot(); + expect(spyFetch).toHaveBeenCalledTimes(1); }); }); diff --git a/app/components/Views/Onboarding/index.js b/app/components/Views/Onboarding/index.js index f15aa33c3c5..52ea24f6192 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,24 +276,33 @@ 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, + }, + () => { + 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); + }, + }); + } + }, + ); }; + this.handleExistingUser(action); }; diff --git a/app/components/Views/Onboarding/index.test.tsx b/app/components/Views/Onboarding/index.test.tsx index 9bb3fe0e865..1945e0fc2ee 100644 --- a/app/components/Views/Onboarding/index.test.tsx +++ b/app/components/Views/Onboarding/index.test.tsx @@ -1,6 +1,10 @@ import { renderScreen } from '../../../util/test/renderWithProvider'; import Onboarding from './'; import { backgroundState } from '../../../util/test/initial-root-state'; +import { OnboardingSelectorIDs } from '../../../../e2e/selectors/Onboarding/Onboarding.selectors'; +import { fireEvent } from '@testing-library/react-native'; +// eslint-disable-next-line import/no-namespace +import * as traceObj from '../../../util/trace'; const mockInitialState = { engine: { @@ -21,4 +25,20 @@ describe('Onboarding', () => { ); expect(toJSON()).toMatchSnapshot(); }); + it('must call trace when press start', () => { + const spyFetch = jest + .spyOn(traceObj, 'trace') + .mockImplementation(() => undefined); + const { getByTestId } = renderScreen( + Onboarding, + { name: 'Onboarding' }, + { + state: mockInitialState, + }, + ); + + const startButton = getByTestId(OnboardingSelectorIDs.NEW_WALLET_BUTTON); + fireEvent.press(startButton); + expect(spyFetch).toHaveBeenCalledTimes(1); + }); }); diff --git a/app/components/Views/Wallet/index.tsx b/app/components/Views/Wallet/index.tsx index 045a835bcf7..83f3b25629d 100644 --- a/app/components/Views/Wallet/index.tsx +++ b/app/components/Views/Wallet/index.tsx @@ -106,6 +106,7 @@ import { ButtonVariants } from '../../../component-library/components/Buttons/Bu import { useListNotifications } from '../../../util/notifications/hooks/useNotifications'; import { PortfolioBalance } from '../../UI/Tokens/TokenList/PortfolioBalance'; import { isObject } from 'lodash'; + const createStyles = ({ colors, typography }: Theme) => StyleSheet.create({ base: { diff --git a/app/store/index.ts b/app/store/index.ts index aa7a4df512f..e246a4c6615 100644 --- a/app/store/index.ts +++ b/app/store/index.ts @@ -9,6 +9,8 @@ import { Authentication } from '../core'; import LockManagerService from '../core/LockManagerService'; import ReadOnlyNetworkStore from '../util/test/network-store'; import { isE2E } from '../util/test/utils'; +import { trace, endTrace, TraceName, TraceOperation } from '../util/trace'; + import thunk from 'redux-thunk'; import persistConfig from './persistConfig'; @@ -46,6 +48,11 @@ const createStoreAndPersistor = async () => { middlewares.push(createReduxFlipperDebugger()); } + trace({ + name: TraceName.CreateStore, + op: TraceOperation.CreateStore, + }); + store = configureStore({ reducer: pReducer, middleware: middlewares, @@ -54,10 +61,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 = () => { + endTrace({ name: TraceName.StorageRehydration }); + /** * EngineService.initalizeEngine(store) with SES/lockdown: * Requires ethjs nested patches (lib->src) @@ -73,6 +89,7 @@ const createStoreAndPersistor = async () => { * - TypeError: undefined is not an object (evaluating 'TokenListController.tokenList') * - V8: SES_UNHANDLED_REJECTION */ + store.dispatch({ type: 'TOGGLE_BASIC_FUNCTIONALITY', basicFunctionalityEnabled: @@ -83,7 +100,16 @@ const createStoreAndPersistor = async () => { store.dispatch({ type: 'FETCH_FEATURE_FLAGS', }); - EngineService.initalizeEngine(store); + trace( + { + name: TraceName.EngineInitialization, + op: TraceOperation.EngineInitialization, + }, + () => { + EngineService.initalizeEngine(store); + }, + ); + Authentication.init(store); AppStateEventProcessor.init(store); LockManagerService.init(store); @@ -93,7 +119,13 @@ const createStoreAndPersistor = async () => { }; (async () => { - await createStoreAndPersistor(); + await trace( + { + name: TraceName.UIStartup, + op: TraceOperation.UIStartup, + }, + async () => await createStoreAndPersistor(), + ); })(); export { store, persistor }; diff --git a/app/util/trace.ts b/app/util/trace.ts index 8275c521b84..339943be165 100644 --- a/app/util/trace.ts +++ b/app/util/trace.ts @@ -19,6 +19,29 @@ export enum TraceName { NotificationDisplay = 'Notification Display', PPOMValidation = 'PPOM Validation', Signature = 'Signature', + LoadScripts = 'Load Scripts', + 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', + StorageRehydration = 'Storage Rehydration', + UIStartup = 'UIStartup', +} + +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', + CreateNewWalletToChoosePassword = 'custom.create.new.wallet', + StorageRehydration = 'custom.storage.rehydration', + UIStartup = 'custom.ui.startup', } const ID_DEFAULT = 'default'; @@ -45,6 +68,7 @@ export interface TraceRequest { parentContext?: TraceContext; startTime?: number; tags?: Record; + op?: string; } export interface EndTraceRequest { @@ -154,13 +178,20 @@ 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,