From c7573663557b0db778a2907eaf2fd1918ced4914 Mon Sep 17 00:00:00 2001 From: Dan J Miller Date: Tue, 5 Mar 2019 12:15:01 -0330 Subject: Metametrics (#6171) * Add metametrics provider and util. * Add backend api and state for participating in metametrics. * Add frontend action for participating in metametrics. * Add metametrics opt-in screen. * Add metametrics events to first time flow. * Add metametrics events for route changes * Add metametrics events for send and confirm screens * Add metametrics events to dropdowns, transactions, log in and out, settings, sig requests and main screen * Ensures each log in is measured as a new visit by metametrics. * Ensure metametrics is called with an empty string for dimensions params if specified * Adds opt in metametrics modal after unlock for existing users * Adds settings page toggle for opting in and out of MetaMetrics * Switch metametrics dimensions to page level scope * Lint, test and translation fixes for metametrics. * Update design for metametrics opt-in screen * Complete responsive styling of metametrics-opt-in modal * Use new chart image on metrics opt in screens * Incorporate the metametrics opt-in screen into the new onboarding flow * Update e2e tests to accomodate metametrics changes * Mock out metametrics network requests in integration tests * Fix tx-list integration test to support metametrics provider. * Send number of tokens and accounts data with every metametrics event. * Update metametrics event descriptor schema and add new events. * Fix import tos bug and send gas button bug due to metametrics changes. * Various small fixes on the metametrics branch. * Add origin custom variable type to metametrics.util * Fix names of onboarding complete actions (metametrics). * Fix names of Metrics Options actions (metametrics). * Clean up code related to metametrics. * Fix bad merge conflict resolution and improve promise handling in sendMetaMetrics event and confrim tx base * Don't send a second metrics event if user has gone back during first time flow. * Collect metametrics on going back from onboarding create/import. * Add missing custom variable constants for metametrics * Fix metametrics provider * Make height of opt-in modal responsive. * Adjust text content for opt-in modal. * Update metametrics event names and clean up code in opt-in-modal * Put phishing warning step next to last in onboarding flow * Link terms of service on create and import screens of first time flow * Add subtext to options on the onboarding select action screen. * Fix styling of bullet points on end of onboarding screen. * Combine phishing warning and congratulations screens. * Fix placement of users if unlocking after an incomplete onboarding import flow. * Fix capitalization in opt-in screen * Fix last onboarding screen translations * Add link to 'Learn More' on the last screen of onboarding * Code clean up: metametrics branch * Update e2e tests for phishing warning step removal * e2e tests passing on metametrics branch * Different tracking urls for metametrics on development and prod --- .../confirm-transaction-base.component.js | 131 ++++++++++++---- .../confirm-transaction-base.container.js | 5 +- .../pages/create-account/connect-hardware/index.js | 18 +++ .../pages/create-account/import-account/json.js | 15 ++ .../create-account/import-account/private-key.js | 15 ++ .../components/pages/create-account/new-account.js | 24 ++- .../create-password/create-password.component.js | 17 +-- .../import-with-seed-phrase.component.js | 37 ++++- .../new-account/new-account.component.js | 39 ++++- .../unique-image/unique-image.component.js | 19 ++- .../end-of-flow/end-of-flow.component.js | 37 ++++- .../end-of-flow/end-of-flow.container.js | 16 +- .../pages/first-time-flow/end-of-flow/index.scss | 14 +- .../first-time-flow-switch.component.js | 9 +- .../first-time-flow-switch.container.js | 2 + .../first-time-flow/first-time-flow.component.js | 12 +- .../first-time-flow/first-time-flow.container.js | 2 + .../first-time-flow/first-time-flow.selectors.js | 26 ++++ ui/app/components/pages/first-time-flow/index.scss | 7 + .../first-time-flow/metametrics-opt-in/index.js | 1 + .../first-time-flow/metametrics-opt-in/index.scss | 136 +++++++++++++++++ .../metametrics-opt-in.component.js | 169 +++++++++++++++++++++ .../metametrics-opt-in.container.js | 27 ++++ .../confirm-seed-phrase.component.js | 13 +- .../reveal-seed-phrase.component.js | 20 ++- .../pages/first-time-flow/select-action/index.js | 2 +- .../pages/first-time-flow/select-action/index.scss | 3 +- .../select-action/select-action.component.js | 22 ++- .../select-action/select-action.container.js | 23 +++ .../first-time-flow/welcome/welcome.component.js | 12 +- .../first-time-flow/welcome/welcome.container.js | 3 +- ui/app/components/pages/keychains/restore-vault.js | 16 +- .../settings-tab/settings-tab.component.js | 67 ++++++++ .../settings-tab/settings-tab.container.js | 4 + .../pages/unlock-page/unlock-page.component.js | 31 +++- .../pages/unlock-page/unlock-page.container.js | 4 + 36 files changed, 903 insertions(+), 95 deletions(-) create mode 100644 ui/app/components/pages/first-time-flow/first-time-flow.selectors.js create mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/index.js create mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/index.scss create mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.component.js create mode 100644 ui/app/components/pages/first-time-flow/metametrics-opt-in/metametrics-opt-in.container.js (limited to 'ui/app/components/pages') diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js index 3650dc869..e76b4699b 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.component.js @@ -16,6 +16,7 @@ import AdvancedGasInputs from '../../gas-customization/advanced-gas-inputs' export default class ConfirmTransactionBase extends Component { static contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } static propTypes = { @@ -77,6 +78,8 @@ export default class ConfirmTransactionBase extends Component { onEdit: PropTypes.func, onEditGas: PropTypes.func, onSubmit: PropTypes.func, + setMetaMetricsSendCount: PropTypes.func, + metaMetricsSendCount: PropTypes.number, subtitle: PropTypes.string, subtitleComponent: PropTypes.node, summaryComponent: PropTypes.node, @@ -154,7 +157,20 @@ export default class ConfirmTransactionBase extends Component { } handleEditGas () { - const { onEditGas, showCustomizeGasModal } = this.props + const { onEditGas, showCustomizeGasModal, methodData = {}, txData: { origin } } = this.props + + this.context.metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'User clicks "Edit" on gas', + }, + customVariables: { + recipientKnown: null, + functionType: methodData.name || 'notFound', + origin, + }, + }) if (onEditGas) { onEditGas() @@ -274,7 +290,21 @@ export default class ConfirmTransactionBase extends Component { } handleEdit () { - const { txData, tokenData, tokenProps, onEdit } = this.props + const { txData, tokenData, tokenProps, onEdit, methodData = {}, txData: { origin } } = this.props + + this.context.metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'Edit Transaction', + }, + customVariables: { + recipientKnown: null, + functionType: methodData.name || 'notFound', + origin, + }, + }) + onEdit({ txData, tokenData, tokenProps }) } @@ -298,9 +328,22 @@ export default class ConfirmTransactionBase extends Component { } handleCancel () { - const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction } = this.props + const { metricsEvent } = this.context + const { onCancel, txData, cancelTransaction, history, clearConfirmTransaction, methodData = {}, txData: { origin } } = this.props if (onCancel) { + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'Cancel', + }, + customVariables: { + recipientKnown: null, + functionType: methodData.name || 'notFound', + origin, + }, + }) onCancel(txData) } else { cancelTransaction(txData) @@ -312,7 +355,8 @@ export default class ConfirmTransactionBase extends Component { } handleSubmit () { - const { sendTransaction, clearConfirmTransaction, txData, history, onSubmit } = this.props + const { metricsEvent } = this.context + const { txData: { origin }, sendTransaction, clearConfirmTransaction, txData, history, onSubmit, methodData = {}, metaMetricsSendCount = 0, setMetaMetricsSendCount } = this.props const { submitting } = this.state if (submitting) { @@ -323,30 +367,46 @@ export default class ConfirmTransactionBase extends Component { submitting: true, submitError: null, }, () => { - if (onSubmit) { - Promise.resolve(onSubmit(txData)) - .then(() => { - this.setState({ - submitting: false, - }) - }) - } else { - sendTransaction(txData) - .then(() => { - clearConfirmTransaction() - this.setState({ - submitting: false, - }, () => { - history.push(DEFAULT_ROUTE) - }) - }) - .catch(error => { - this.setState({ - submitting: false, - submitError: error.message, - }) - }) - } + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'Transaction Completed', + }, + customVariables: { + recipientKnown: null, + functionType: methodData.name || 'notFound', + origin, + }, + }) + + setMetaMetricsSendCount(metaMetricsSendCount + 1) + .then(() => { + if (onSubmit) { + Promise.resolve(onSubmit(txData)) + .then(() => { + this.setState({ + submitting: false, + }) + }) + } else { + sendTransaction(txData) + .then(() => { + clearConfirmTransaction() + this.setState({ + submitting: false, + }, () => { + history.push(DEFAULT_ROUTE) + }) + }) + .catch(error => { + this.setState({ + submitting: false, + submitError: error.message, + }) + }) + } + }) }) } @@ -413,6 +473,21 @@ export default class ConfirmTransactionBase extends Component { } } + componentDidMount () { + const { txData: { origin } = {} } = this.props + const { metricsEvent } = this.context + metricsEvent({ + eventOpts: { + category: 'Transactions', + action: 'Confirm Screen', + name: 'Confirm: Started', + }, + customVariables: { + origin, + }, + }) + } + render () { const { isTxReprice, diff --git a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js index 2a8033c8f..22f509905 100644 --- a/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js +++ b/ui/app/components/pages/confirm-transaction-base/confirm-transaction-base.container.js @@ -8,7 +8,7 @@ import { clearConfirmTransaction, updateGasAndCalculate, } from '../../../ducks/confirm-transaction.duck' -import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal } from '../../../actions' +import { clearSend, cancelTx, cancelTxs, updateAndApproveTx, showModal, setMetaMetricsSendCount } from '../../../actions' import { INSUFFICIENT_FUNDS_ERROR_KEY, GAS_LIMIT_TOO_LOW_ERROR_KEY, @@ -66,6 +66,7 @@ const mapStateToProps = (state, props) => { assetImages, network, unapprovedTxs, + metaMetricsSendCount, } = metamask const assetImage = assetImages[txParamsToAddress] @@ -139,6 +140,7 @@ const mapStateToProps = (state, props) => { insufficientBalance, hideSubtitle: (!isMainnet && !showFiatInTestnets), hideFiatConversion: (!isMainnet && !showFiatInTestnets), + metaMetricsSendCount, } } @@ -161,6 +163,7 @@ const mapDispatchToProps = dispatch => { cancelTransaction: ({ id }) => dispatch(cancelTx({ id })), cancelAllTransactions: (txList) => dispatch(cancelTxs(txList)), sendTransaction: txData => dispatch(updateAndApproveTx(txData)), + setMetaMetricsSendCount: val => dispatch(setMetaMetricsSendCount(val)), } } diff --git a/ui/app/components/pages/create-account/connect-hardware/index.js b/ui/app/components/pages/create-account/connect-hardware/index.js index bd877fd4e..712cc5cbb 100644 --- a/ui/app/components/pages/create-account/connect-hardware/index.js +++ b/ui/app/components/pages/create-account/connect-hardware/index.js @@ -154,8 +154,25 @@ class ConnectHardwareForm extends Component { this.props.unlockHardwareWalletAccount(this.state.selectedAccount, device) .then(_ => { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Connected Hardware Wallet', + name: 'Connected Account with: ' + device, + }, + }) this.props.history.push(DEFAULT_ROUTE) }).catch(e => { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Connected Hardware Wallet', + name: 'Error connecting hardware wallet', + }, + customVariables: { + error: e.toString(), + }, + }) this.setState({ error: e.toString() }) }) } @@ -268,6 +285,7 @@ const mapDispatchToProps = dispatch => { ConnectHardwareForm.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = connect(mapStateToProps, mapDispatchToProps)( diff --git a/ui/app/components/pages/create-account/import-account/json.js b/ui/app/components/pages/create-account/import-account/json.js index 8bb6e154b..9aeea5579 100644 --- a/ui/app/components/pages/create-account/import-account/json.js +++ b/ui/app/components/pages/create-account/import-account/json.js @@ -108,9 +108,23 @@ class JsonImportSubview extends Component { .then(({ selectedAddress }) => { if (selectedAddress) { history.push(DEFAULT_ROUTE) + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Import Account', + name: 'Imported Account with JSON', + }, + }) displayWarning(null) } else { displayWarning('Error importing account.') + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Import Account', + name: 'Error importing JSON', + }, + }) setSelectedAddress(firstAddress) } }) @@ -147,6 +161,7 @@ const mapDispatchToProps = dispatch => { JsonImportSubview.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = compose( diff --git a/ui/app/components/pages/create-account/import-account/private-key.js b/ui/app/components/pages/create-account/import-account/private-key.js index 45068b96e..4ba31806f 100644 --- a/ui/app/components/pages/create-account/import-account/private-key.js +++ b/ui/app/components/pages/create-account/import-account/private-key.js @@ -12,6 +12,7 @@ import Button from '../../../button' PrivateKeyImportView.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = compose( @@ -102,10 +103,24 @@ PrivateKeyImportView.prototype.createNewKeychain = function () { importNewAccount('Private Key', [ privateKey ]) .then(({ selectedAddress }) => { if (selectedAddress) { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Import Account', + name: 'Imported Account with Private Key', + }, + }) history.push(DEFAULT_ROUTE) displayWarning(null) } else { displayWarning('Error importing account.') + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Import Account', + name: 'Error importing with Private Key', + }, + }) setSelectedAddress(firstAddress) } }) diff --git a/ui/app/components/pages/create-account/new-account.js b/ui/app/components/pages/create-account/new-account.js index 94a5fa487..a7595e346 100644 --- a/ui/app/components/pages/create-account/new-account.js +++ b/ui/app/components/pages/create-account/new-account.js @@ -52,7 +52,28 @@ class NewAccountCreateForm extends Component { className: 'new-account-create-form__button', onClick: () => { createAccount(newAccountName || defaultAccountName) - .then(() => history.push(DEFAULT_ROUTE)) + .then(() => { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Add New Account', + name: 'Added New Account', + }, + }) + history.push(DEFAULT_ROUTE) + }) + .catch((e) => { + this.context.metricsEvent({ + eventOpts: { + category: 'Accounts', + action: 'Add New Account', + name: 'Error', + }, + customVariables: { + errorMessage: e.message, + }, + }) + }) }, }, [this.context.t('create')]), @@ -102,6 +123,7 @@ const mapDispatchToProps = dispatch => { NewAccountCreateForm.contextTypes = { t: PropTypes.func, + metricsEvent: PropTypes.func, } module.exports = connect(mapStateToProps, mapDispatchToProps)(NewAccountCreateForm) diff --git a/ui/app/components/pages/first-time-flow/create-password/create-password.component.js b/ui/app/components/pages/first-time-flow/create-password/create-password.component.js index 7cca82ca6..3faaa3764 100644 --- a/ui/app/components/pages/first-time-flow/create-password/create-password.component.js +++ b/ui/app/components/pages/first-time-flow/create-password/create-password.component.js @@ -3,18 +3,16 @@ import PropTypes from 'prop-types' import { Switch, Route } from 'react-router-dom' import NewAccount from './new-account' import ImportWithSeedPhrase from './import-with-seed-phrase' -import UniqueImage from './unique-image' import { INITIALIZE_CREATE_PASSWORD_ROUTE, INITIALIZE_IMPORT_WITH_SEED_PHRASE_ROUTE, - INITIALIZE_UNIQUE_IMAGE_ROUTE, + INITIALIZE_END_OF_FLOW_ROUTE, } from '../../../../routes' export default class CreatePassword extends PureComponent { static propTypes = { history: PropTypes.object, isInitialized: PropTypes.bool, - isImportedKeyring: PropTypes.bool, onCreateNewAccount: PropTypes.func, onCreateNewAccountFromSeed: PropTypes.func, } @@ -23,12 +21,12 @@ export default class CreatePassword extends PureComponent { const { isInitialized, history } = this.props if (isInitialized) { - history.push(INITIALIZE_UNIQUE_IMAGE_ROUTE) + history.push(INITIALIZE_END_OF_FLOW_ROUTE) } } render () { - const { onCreateNewAccount, onCreateNewAccountFromSeed, isImportedKeyring } = this.props + const { onCreateNewAccount, onCreateNewAccountFromSeed } = this.props return (
@@ -46,15 +44,6 @@ export default class CreatePassword extends PureComponent { />
- ( - - )} - /> { + this.context.metricsEvent({ + eventOpts: { + category: 'Onboarding', + action: 'Import Seed Phrase', + name: 'Check ToS', + }, + }) + this.setState((prevState) => ({ termsChecked: !prevState.termsChecked, })) @@ -150,6 +166,13 @@ export default class ImportWithSeedPhrase extends PureComponent { { e.preventDefault() + this.context.metricsEvent({ + eventOpts: { + category: 'Onboarding', + action: 'Import Seed Phrase', + name: 'Go Back from Onboarding Import', + }, + }) this.props.history.push(INITIALIZE_SELECT_ACTION_ROUTE) }} href="#" @@ -208,7 +231,15 @@ export default class ImportWithSeedPhrase extends PureComponent { {termsChecked ? : null} - { t('agreeTermsOfService') } + I have read and agree to the + + { 'Terms of Use' } + +