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,