1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 18:00:18 +01:00

Metrics adjustments (#15313)

* Don't send errors to sentry if users have not opted-in to participate in metametrics

* Don't capture opt-out metrics

* Move the metrics-opt in screen to immediately after the welcome screen

* Ensure that global.getSentryState is set in the background

* Fix e2e tests after rearranging onboardin flow

* Fix unit tests

* More e2e test fixes

* Remove unnecessary wrappers around capture exception
This commit is contained in:
Dan J Miller 2022-07-22 18:09:48 -02:30 committed by Dan Miller
parent d35d3ca745
commit 99f753d73f
14 changed files with 103 additions and 80 deletions

View File

@ -24,11 +24,14 @@ import {
REJECT_NOTFICIATION_CLOSE_SIG, REJECT_NOTFICIATION_CLOSE_SIG,
} from '../../shared/constants/metametrics'; } from '../../shared/constants/metametrics';
import { isManifestV3 } from '../../shared/modules/mv3.utils'; import { isManifestV3 } from '../../shared/modules/mv3.utils';
import { maskObject } from '../../shared/modules/object.utils';
import migrations from './migrations'; import migrations from './migrations';
import Migrator from './lib/migrator'; import Migrator from './lib/migrator';
import ExtensionPlatform from './platforms/extension'; import ExtensionPlatform from './platforms/extension';
import LocalStore from './lib/local-store'; import LocalStore from './lib/local-store';
import ReadOnlyNetworkStore from './lib/network-store'; import ReadOnlyNetworkStore from './lib/network-store';
import { SENTRY_STATE } from './lib/setupSentry';
import createStreamSink from './lib/createStreamSink'; import createStreamSink from './lib/createStreamSink';
import NotificationManager, { import NotificationManager, {
NOTIFICATION_MANAGER_EVENTS, NOTIFICATION_MANAGER_EVENTS,
@ -353,6 +356,8 @@ function setupController(initState, initLangCode, remoteSourcePort) {
}, },
); );
setupSentryGetStateGlobal(controller.store);
/** /**
* Assigns the given state to the versioned object (with metadata), and returns that. * Assigns the given state to the versioned object (with metadata), and returns that.
* *
@ -755,3 +760,15 @@ browser.runtime.onInstalled.addListener(({ reason }) => {
platform.openExtensionInBrowser(); platform.openExtensionInBrowser();
} }
}); });
function setupSentryGetStateGlobal(store) {
global.getSentryState = function () {
const fullState = store.getState();
const debugState = maskObject(fullState, SENTRY_STATE);
return {
browser: window.navigator.userAgent,
store: debugState,
version: global.platform.getVersion(),
};
};
}

View File

@ -103,7 +103,17 @@ export default function setupSentry({ release, getState }) {
environment, environment,
integrations: [new Dedupe(), new ExtraErrorData()], integrations: [new Dedupe(), new ExtraErrorData()],
release, release,
beforeSend: (report) => rewriteReport(report), beforeSend: (report) => {
if (getState) {
const appState = getState();
if (!appState?.store?.metamask?.participateInMetaMetrics) {
return null;
}
} else {
return null;
}
return rewriteReport(report);
},
}); });
function rewriteReport(report) { function rewriteReport(report) {

View File

@ -0,0 +1,22 @@
/**
* Return a "masked" copy of the given object.
*
* The returned object includes only the properties present in the mask. The
* mask is an object that mirrors the structure of the given object, except
* the only values are `true` or a sub-mask. `true` implies the property
* should be included, and a sub-mask implies the property should be further
* masked according to that sub-mask.
*
* @param {Object} object - The object to mask
* @param {Object<Object|boolean>} mask - The mask to apply to the object
*/
export function maskObject(object, mask) {
return Object.keys(object).reduce((state, key) => {
if (mask[key] === true) {
state[key] = object[key];
} else if (mask[key]) {
state[key] = maskObject(object[key], mask[key]);
}
return state;
}, {});
}

View File

@ -237,12 +237,12 @@ const completeImportSRPOnboardingFlow = async (
tag: 'button', tag: 'button',
}); });
// clicks the "Import Wallet" option
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
// clicks the "No thanks" option on the metametrics opt-in screen // clicks the "No thanks" option on the metametrics opt-in screen
await driver.clickElement('.btn-secondary'); await driver.clickElement('.btn-secondary');
// clicks the "Import Wallet" option
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
// Import Secret Recovery Phrase // Import Secret Recovery Phrase
await driver.pasteIntoField( await driver.pasteIntoField(
'[data-testid="import-srp__srp-word-0"]', '[data-testid="import-srp__srp-word-0"]',
@ -279,12 +279,12 @@ const completeImportSRPOnboardingFlowWordByWord = async (
tag: 'button', tag: 'button',
}); });
// clicks the "Import Wallet" option
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
// clicks the "No thanks" option on the metametrics opt-in screen // clicks the "No thanks" option on the metametrics opt-in screen
await driver.clickElement('.btn-secondary'); await driver.clickElement('.btn-secondary');
// clicks the "Import Wallet" option
await driver.clickElement({ text: 'Import wallet', tag: 'button' });
const words = seedPhrase.split(' '); const words = seedPhrase.split(' ');
for (const word of words) { for (const word of words) {
await driver.pasteIntoField( await driver.pasteIntoField(

View File

@ -99,13 +99,13 @@ describe('MetaMask', function () {
await driver.delay(largeDelayMs); await driver.delay(largeDelayMs);
}); });
it('clicks the "Create New Wallet" option', async function () { it('clicks the "No thanks" option on the metametrics opt-in screen', async function () {
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' }); await driver.clickElement('.btn-secondary');
await driver.delay(largeDelayMs); await driver.delay(largeDelayMs);
}); });
it('clicks the "No thanks" option on the metametrics opt-in screen', async function () { it('clicks the "Create New Wallet" option', async function () {
await driver.clickElement('.btn-secondary'); await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
await driver.delay(largeDelayMs); await driver.delay(largeDelayMs);
}); });

View File

@ -38,12 +38,12 @@ describe('Incremental Security', function () {
tag: 'button', tag: 'button',
}); });
// clicks the "Create New Wallet" option
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
// clicks the "No thanks" option on the metametrics opt-in screen // clicks the "No thanks" option on the metametrics opt-in screen
await driver.clickElement('.btn-secondary'); await driver.clickElement('.btn-secondary');
// clicks the "Create New Wallet" option
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
// accepts a secure password // accepts a secure password
await driver.fill( await driver.fill(
'.first-time-flow__form #create-password', '.first-time-flow__form #create-password',

View File

@ -87,12 +87,12 @@ describe('MetaMask Responsive UI', function () {
}); });
await driver.delay(tinyDelayMs); await driver.delay(tinyDelayMs);
// clicks the "Create New Wallet" option
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
// clicks the "I Agree" option on the metametrics opt-in screen // clicks the "I Agree" option on the metametrics opt-in screen
await driver.clickElement('.btn-primary'); await driver.clickElement('.btn-primary');
// clicks the "Create New Wallet" option
await driver.clickElement({ text: 'Create a Wallet', tag: 'button' });
// accepts a secure password // accepts a secure password
await driver.fill( await driver.fill(
'.first-time-flow__form #create-password', '.first-time-flow__form #create-password',

View File

@ -7,6 +7,7 @@ import browser from 'webextension-polyfill';
import { getEnvironmentType } from '../app/scripts/lib/util'; import { getEnvironmentType } from '../app/scripts/lib/util';
import { ALERT_TYPES } from '../shared/constants/alerts'; import { ALERT_TYPES } from '../shared/constants/alerts';
import { maskObject } from '../shared/modules/object.utils';
import { SENTRY_STATE } from '../app/scripts/lib/setupSentry'; import { SENTRY_STATE } from '../app/scripts/lib/setupSentry';
import { ENVIRONMENT_TYPE_POPUP } from '../shared/constants/app'; import { ENVIRONMENT_TYPE_POPUP } from '../shared/constants/app';
import * as actions from './store/actions'; import * as actions from './store/actions';
@ -171,29 +172,6 @@ async function startApp(metamaskState, backgroundConnection, opts) {
return store; return store;
} }
/**
* Return a "masked" copy of the given object.
*
* The returned object includes only the properties present in the mask. The
* mask is an object that mirrors the structure of the given object, except
* the only values are `true` or a sub-mask. `true` implies the property
* should be included, and a sub-mask implies the property should be further
* masked according to that sub-mask.
*
* @param {Object} object - The object to mask
* @param {Object<Object|boolean>} mask - The mask to apply to the object
*/
function maskObject(object, mask) {
return Object.keys(object).reduce((state, key) => {
if (mask[key] === true) {
state[key] = object[key];
} else if (mask[key]) {
state[key] = maskObject(object[key], mask[key]);
}
return state;
}, {});
}
function setupDebuggingHelpers(store) { function setupDebuggingHelpers(store) {
window.getCleanAppState = async function () { window.getCleanAppState = async function () {
const state = clone(store.getState()); const state = clone(store.getState());

View File

@ -3,12 +3,12 @@ import PropTypes from 'prop-types';
import MetaFoxLogo from '../../../components/ui/metafox-logo'; import MetaFoxLogo from '../../../components/ui/metafox-logo';
import PageContainerFooter from '../../../components/ui/page-container/page-container-footer'; import PageContainerFooter from '../../../components/ui/page-container/page-container-footer';
import { EVENT } from '../../../../shared/constants/metametrics'; import { EVENT } from '../../../../shared/constants/metametrics';
import { INITIALIZE_SELECT_ACTION_ROUTE } from '../../../helpers/constants/routes';
export default class MetaMetricsOptIn extends Component { export default class MetaMetricsOptIn extends Component {
static propTypes = { static propTypes = {
history: PropTypes.object, history: PropTypes.object,
setParticipateInMetaMetrics: PropTypes.func, setParticipateInMetaMetrics: PropTypes.func,
nextRoute: PropTypes.string,
firstTimeSelectionMetaMetricsName: PropTypes.string, firstTimeSelectionMetaMetricsName: PropTypes.string,
participateInMetaMetrics: PropTypes.bool, participateInMetaMetrics: PropTypes.bool,
}; };
@ -21,7 +21,6 @@ export default class MetaMetricsOptIn extends Component {
render() { render() {
const { trackEvent, t } = this.context; const { trackEvent, t } = this.context;
const { const {
nextRoute,
history, history,
setParticipateInMetaMetrics, setParticipateInMetaMetrics,
firstTimeSelectionMetaMetricsName, firstTimeSelectionMetaMetricsName,
@ -105,29 +104,7 @@ export default class MetaMetricsOptIn extends Component {
onCancel={async () => { onCancel={async () => {
await setParticipateInMetaMetrics(false); await setParticipateInMetaMetrics(false);
try { history.push(INITIALIZE_SELECT_ACTION_ROUTE);
if (
participateInMetaMetrics === null ||
participateInMetaMetrics === true
) {
await trackEvent(
{
category: EVENT.CATEGORIES.ONBOARDING,
event: 'Metrics Opt Out',
properties: {
action: 'Metrics Option',
legacy_event: true,
},
},
{
isOptIn: true,
flushImmediately: true,
},
);
}
} finally {
history.push(nextRoute);
}
}} }}
cancelText={t('noThanks')} cancelText={t('noThanks')}
hideCancel={false} hideCancel={false}
@ -177,7 +154,7 @@ export default class MetaMetricsOptIn extends Component {
); );
await Promise.all(metrics); await Promise.all(metrics);
} finally { } finally {
history.push(nextRoute); history.push(INITIALIZE_SELECT_ACTION_ROUTE);
} }
}} }}
submitText={t('affirmAgree')} submitText={t('affirmAgree')}

View File

@ -1,6 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { setParticipateInMetaMetrics } from '../../../store/actions'; import { setParticipateInMetaMetrics } from '../../../store/actions';
import { getFirstTimeFlowTypeRoute } from '../../../selectors';
import MetaMetricsOptIn from './metametrics-opt-in.component'; import MetaMetricsOptIn from './metametrics-opt-in.component';
const firstTimeFlowTypeNameMap = { const firstTimeFlowTypeNameMap = {
@ -12,7 +11,6 @@ const mapStateToProps = (state) => {
const { firstTimeFlowType, participateInMetaMetrics } = state.metamask; const { firstTimeFlowType, participateInMetaMetrics } = state.metamask;
return { return {
nextRoute: getFirstTimeFlowTypeRoute(state),
firstTimeSelectionMetaMetricsName: firstTimeSelectionMetaMetricsName:
firstTimeFlowTypeNameMap[firstTimeFlowType], firstTimeFlowTypeNameMap[firstTimeFlowType],
participateInMetaMetrics, participateInMetaMetrics,

View File

@ -2,7 +2,10 @@ import React, { PureComponent } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import Button from '../../../components/ui/button'; import Button from '../../../components/ui/button';
import MetaFoxLogo from '../../../components/ui/metafox-logo'; import MetaFoxLogo from '../../../components/ui/metafox-logo';
import { INITIALIZE_METAMETRICS_OPT_IN_ROUTE } from '../../../helpers/constants/routes'; import {
INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE,
} from '../../../helpers/constants/routes';
export default class SelectAction extends PureComponent { export default class SelectAction extends PureComponent {
static propTypes = { static propTypes = {
@ -26,12 +29,12 @@ export default class SelectAction extends PureComponent {
handleCreate = () => { handleCreate = () => {
this.props.setFirstTimeFlowType('create'); this.props.setFirstTimeFlowType('create');
this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE); this.props.history.push(INITIALIZE_CREATE_PASSWORD_ROUTE);
}; };
handleImport = () => { handleImport = () => {
this.props.setFirstTimeFlowType('import'); this.props.setFirstTimeFlowType('import');
this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE); this.props.history.push(INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE);
}; };
render() { render() {

View File

@ -6,6 +6,7 @@ import Button from '../../../components/ui/button';
import { import {
INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_CREATE_PASSWORD_ROUTE,
INITIALIZE_SELECT_ACTION_ROUTE, INITIALIZE_SELECT_ACTION_ROUTE,
INITIALIZE_METAMETRICS_OPT_IN_ROUTE,
} from '../../../helpers/constants/routes'; } from '../../../helpers/constants/routes';
import { isBeta } from '../../../helpers/utils/build-types'; import { isBeta } from '../../../helpers/utils/build-types';
import WelcomeFooter from './welcome-footer.component'; import WelcomeFooter from './welcome-footer.component';
@ -16,6 +17,7 @@ export default class Welcome extends PureComponent {
history: PropTypes.object, history: PropTypes.object,
participateInMetaMetrics: PropTypes.bool, participateInMetaMetrics: PropTypes.bool,
welcomeScreenSeen: PropTypes.bool, welcomeScreenSeen: PropTypes.bool,
isInitialized: PropTypes.bool,
}; };
static contextTypes = { static contextTypes = {
@ -29,17 +31,28 @@ export default class Welcome extends PureComponent {
} }
componentDidMount() { componentDidMount() {
const { history, participateInMetaMetrics, welcomeScreenSeen } = this.props; const {
history,
participateInMetaMetrics,
welcomeScreenSeen,
isInitialized,
} = this.props;
if (welcomeScreenSeen && participateInMetaMetrics !== null) { if (
welcomeScreenSeen &&
isInitialized &&
participateInMetaMetrics !== null
) {
history.push(INITIALIZE_CREATE_PASSWORD_ROUTE); history.push(INITIALIZE_CREATE_PASSWORD_ROUTE);
} else if (welcomeScreenSeen) { } else if (welcomeScreenSeen && participateInMetaMetrics !== null) {
history.push(INITIALIZE_SELECT_ACTION_ROUTE); history.push(INITIALIZE_SELECT_ACTION_ROUTE);
} else if (welcomeScreenSeen) {
history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE);
} }
} }
handleContinue = () => { handleContinue = () => {
this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE); this.props.history.push(INITIALIZE_METAMETRICS_OPT_IN_ROUTE);
}; };
render() { render() {

View File

@ -5,11 +5,16 @@ import { closeWelcomeScreen } from '../../../store/actions';
import Welcome from './welcome.component'; import Welcome from './welcome.component';
const mapStateToProps = ({ metamask }) => { const mapStateToProps = ({ metamask }) => {
const { welcomeScreenSeen, participateInMetaMetrics } = metamask; const {
welcomeScreenSeen,
participateInMetaMetrics,
isInitialized,
} = metamask;
return { return {
welcomeScreenSeen, welcomeScreenSeen,
participateInMetaMetrics, participateInMetaMetrics,
isInitialized,
}; };
}; };

View File

@ -15,7 +15,7 @@ describe('Welcome', () => {
sinon.restore(); sinon.restore();
}); });
it('routes to select action when participateInMetaMetrics is not initialized', () => { it('routes to the metametrics screen when participateInMetaMetrics is not initialized', () => {
const props = { const props = {
history: { history: {
push: sinon.spy(), push: sinon.spy(),
@ -32,11 +32,11 @@ describe('Welcome', () => {
); );
getStartedButton.simulate('click'); getStartedButton.simulate('click');
expect(props.history.push.getCall(0).args[0]).toStrictEqual( expect(props.history.push.getCall(0).args[0]).toStrictEqual(
'/initialize/select-action', '/initialize/metametrics-opt-in',
); );
}); });
it('routes to correct password when participateInMetaMetrics is initialized', () => { it('routes to select action when participateInMetaMetrics is initialized', () => {
const props = { const props = {
welcomeScreenSeen: true, welcomeScreenSeen: true,
participateInMetaMetrics: false, participateInMetaMetrics: false,
@ -55,7 +55,7 @@ describe('Welcome', () => {
); );
getStartedButton.simulate('click'); getStartedButton.simulate('click');
expect(props.history.push.getCall(0).args[0]).toStrictEqual( expect(props.history.push.getCall(0).args[0]).toStrictEqual(
'/initialize/create-password', '/initialize/select-action',
); );
}); });
}); });