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

add new MetaMetricsController (#9857)

This commit is contained in:
Brad Decker 2020-12-02 15:41:30 -06:00 committed by GitHub
parent 673371d013
commit 0653a489b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1595 additions and 644 deletions

View File

@ -0,0 +1,366 @@
import { merge, omit } from 'lodash'
import ObservableStore from 'obs-store'
import { bufferToHex, sha3 } from 'ethereumjs-util'
import { ENVIRONMENT_TYPE_BACKGROUND } from '../lib/enums'
import {
METAMETRICS_ANONYMOUS_ID,
METAMETRICS_BACKGROUND_PAGE_OBJECT,
} from '../../../shared/constants/metametrics'
/**
* Used to determine whether or not to attach a user's metametrics id
* to events that include on-chain data. This helps to prevent identifying
* a user by being able to trace their activity on etherscan/block exploring
*/
const trackableSendCounts = {
1: true,
10: true,
30: true,
50: true,
100: true,
250: true,
500: true,
1000: true,
2500: true,
5000: true,
10000: true,
25000: true,
}
export function sendCountIsTrackable(sendCount) {
return Boolean(trackableSendCounts[sendCount])
}
/**
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsContext} MetaMetricsContext
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventOptions} MetaMetricsEventOptions
* @typedef {import('../../../shared/constants/metametrics').SegmentEventPayload} SegmentEventPayload
* @typedef {import('../../../shared/constants/metametrics').SegmentInterface} SegmentInterface
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPagePayload} MetaMetricsPagePayload
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPageOptions} MetaMetricsPageOptions
*/
/**
* @typedef {Object} MetaMetricsControllerState
* @property {?string} metaMetricsId - The user's metaMetricsId that will be
* attached to all non-anonymized event payloads
* @property {?boolean} participateInMetaMetrics - The user's preference for
* participating in the MetaMetrics analytics program. This setting controls
* whether or not events are tracked
* @property {number} metaMetricsSendCount - How many send transactions have
* been tracked through this controller. Used to prevent attaching sensitive
* data that can be traced through on chain data.
*/
export default class MetaMetricsController {
/**
* @param {Object} segment - an instance of analytics-node for tracking
* events that conform to the new MetaMetrics tracking plan.
* @param {Object} segmentLegacy - an instance of analytics-node for
* tracking legacy schema events. Will eventually be phased out
* @param {Object} preferencesStore - The preferences controller store, used
* to access and subscribe to preferences that will be attached to events
* @param {function} onNetworkDidChange - Used to attach a listener to the
* networkDidChange event emitted by the networkController
* @param {function} getCurrentChainId - Gets the current chain id from the
* network controller
* @param {function} getNetworkIdentifier - Gets the current network
* identifier from the network controller
* @param {string} version - The version of the extension
* @param {string} environment - The environment the extension is running in
* @param {MetaMetricsControllerState} initState - State to initialized with
*/
constructor({
segment,
segmentLegacy,
preferencesStore,
onNetworkDidChange,
getCurrentChainId,
getNetworkIdentifier,
version,
environment,
initState,
}) {
const prefState = preferencesStore.getState()
this.chainId = getCurrentChainId()
this.network = getNetworkIdentifier()
this.locale = prefState.currentLocale.replace('_', '-')
this.version =
environment === 'production' ? version : `${version}-${environment}`
this.store = new ObservableStore({
participateInMetaMetrics: null,
metaMetricsId: null,
metaMetricsSendCount: 0,
...initState,
})
preferencesStore.subscribe(({ currentLocale }) => {
this.locale = currentLocale.replace('_', '-')
})
onNetworkDidChange(() => {
this.chainId = getCurrentChainId()
this.network = getNetworkIdentifier()
})
this.segment = segment
this.segmentLegacy = segmentLegacy
}
generateMetaMetricsId() {
return bufferToHex(
sha3(
String(Date.now()) +
String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)),
),
)
}
/**
* Setter for the `participateInMetaMetrics` property
*
* @param {boolean} participateInMetaMetrics - Whether or not the user wants
* to participate in MetaMetrics
* @returns {string|null} the string of the new metametrics id, or null
* if not set
*/
setParticipateInMetaMetrics(participateInMetaMetrics) {
let { metaMetricsId } = this.state
if (participateInMetaMetrics && !metaMetricsId) {
metaMetricsId = this.generateMetaMetricsId()
} else if (participateInMetaMetrics === false) {
metaMetricsId = null
}
this.store.updateState({ participateInMetaMetrics, metaMetricsId })
return metaMetricsId
}
get state() {
return this.store.getState()
}
setMetaMetricsSendCount(val) {
this.store.updateState({ metaMetricsSendCount: val })
}
/**
* Build the context object to attach to page and track events.
* @private
* @param {Pick<MetaMetricsContext, 'referrer'>} [referrer] - dapp origin that initialized
* the notification window.
* @param {Pick<MetaMetricsContext, 'page'>} [page] - page object describing the current
* view of the extension. Defaults to the background-process object.
* @returns {MetaMetricsContext}
*/
_buildContext(referrer, page = METAMETRICS_BACKGROUND_PAGE_OBJECT) {
return {
app: {
name: 'MetaMask Extension',
version: this.version,
},
userAgent: window.navigator.userAgent,
page,
referrer,
}
}
/**
* Build's the event payload, processing all fields into a format that can be
* fed to Segment's track method
* @private
* @param {
* Omit<MetaMetricsEventPayload, 'sensitiveProperties'>
* } rawPayload - raw payload provided to trackEvent
* @returns {SegmentEventPayload} - formatted event payload for segment
*/
_buildEventPayload(rawPayload) {
const {
event,
properties,
revenue,
value,
currency,
category,
page,
referrer,
environmentType = ENVIRONMENT_TYPE_BACKGROUND,
} = rawPayload
return {
event,
properties: {
// These values are omitted from properties because they have special meaning
// in segment. https://segment.com/docs/connections/spec/track/#properties.
// to avoid accidentally using these inappropriately, you must add them as top
// level properties on the event payload. We also exclude locale to prevent consumers
// from overwriting this context level property. We track it as a property
// because not all destinations map locale from context.
...omit(properties, ['revenue', 'locale', 'currency', 'value']),
revenue,
value,
currency,
category,
network: this.network,
locale: this.locale,
chain_id: this.chainId,
environment_type: environmentType,
},
context: this._buildContext(referrer, page),
}
}
/**
* Perform validation on the payload and update the id type to use before
* sending to Segment. Also examines the options to route and handle the
* event appropriately.
* @private
* @param {SegmentEventPayload} payload - properties to attach to event
* @param {MetaMetricsEventOptions} options - options for routing and
* handling the event
* @returns {Promise<void>}
*/
_track(payload, options) {
const {
isOptIn,
metaMetricsId: metaMetricsIdOverride,
matomoEvent,
flushImmediately,
} = options
let idType = 'userId'
let idValue = this.state.metaMetricsId
let excludeMetaMetricsId = options.excludeMetaMetricsId ?? false
// This is carried over from the old implementation, and will likely need
// to be updated to work with the new tracking plan. I think we should use
// a config setting for this instead of trying to match the event name
const isSendFlow = Boolean(payload.event.match(/^send|^confirm/iu))
if (
isSendFlow &&
this.state.metaMetricsSendCount &&
!sendCountIsTrackable(this.state.metaMetricsSendCount + 1)
) {
excludeMetaMetricsId = true
}
// If we are tracking sensitive data we will always use the anonymousId
// property as well as our METAMETRICS_ANONYMOUS_ID. This prevents us from
// associating potentially identifiable information with a specific id.
// During the opt in flow we will track all events, but do so with the
// anonymous id. The one exception to that rule is after the user opts in
// to MetaMetrics. When that happens we receive back the user's new
// MetaMetrics id before it is fully persisted to state. To avoid a race
// condition we explicitly pass the new id to the track method. In that
// case we will track the opt in event to the user's id. In all other cases
// we use the metaMetricsId from state.
if (excludeMetaMetricsId || (isOptIn && !metaMetricsIdOverride)) {
idType = 'anonymousId'
idValue = METAMETRICS_ANONYMOUS_ID
} else if (isOptIn && metaMetricsIdOverride) {
idValue = metaMetricsIdOverride
}
payload[idType] = idValue
// Promises will only resolve when the event is sent to segment. For any
// event that relies on this promise being fulfilled before performing UI
// updates, or otherwise delaying user interaction, supply the
// 'flushImmediately' flag to the trackEvent method.
return new Promise((resolve, reject) => {
const callback = (err) => {
if (err) {
return reject(err)
}
return resolve()
}
const target = matomoEvent === true ? this.segmentLegacy : this.segment
target.track(payload, callback)
if (flushImmediately) {
target.flush()
}
})
}
/**
* track a page view with Segment
* @param {MetaMetricsPagePayload} payload - details of the page viewed
* @param {MetaMetricsPageOptions} options - options for handling the page
* view
*/
trackPage({ name, params, environmentType, page, referrer }, options = {}) {
if (this.state.participateInMetaMetrics === false) {
return
}
if (this.state.participateInMetaMetrics === null && !options.isOptInPath) {
return
}
const { metaMetricsId } = this.state
const idTrait = metaMetricsId ? 'userId' : 'anonymousId'
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID
this.segment.page({
[idTrait]: idValue,
name,
properties: {
params,
locale: this.locale,
network: this.network,
chain_id: this.chainId,
environment_type: environmentType,
},
context: this._buildContext(referrer, page),
})
}
/**
* track a metametrics event, performing necessary payload manipulation and
* routing the event to the appropriate segment source. Will split events
* with sensitiveProperties into two events, tracking the sensitiveProperties
* with the anonymousId only.
* @param {MetaMetricsEventPayload} payload - details of the event
* @param {MetaMetricsEventOptions} options - options for handling/routing the event
* @returns {Promise<void>}
*/
async trackEvent(payload, options = {}) {
// event and category are required fields for all payloads
if (!payload.event || !payload.category) {
throw new Error('Must specify event and category.')
}
if (!this.state.participateInMetaMetrics && !options.isOptIn) {
return
}
// We might track multiple events if sensitiveProperties is included, this array will hold
// the promises returned from this._track.
const events = []
if (payload.sensitiveProperties) {
// sensitiveProperties will only be tracked using the anonymousId property and generic id
// If the event options already specify to exclude the metaMetricsId we throw an error as
// a signal to the developer that the event was implemented incorrectly
if (options.excludeMetaMetricsId === true) {
throw new Error(
'sensitiveProperties was specified in an event payload that also set the excludeMetaMetricsId flag',
)
}
const combinedProperties = merge(
payload.sensitiveProperties,
payload.properties,
)
events.push(
this._track(
this._buildEventPayload({
...payload,
properties: combinedProperties,
}),
{ ...options, excludeMetaMetricsId: true },
),
)
}
events.push(this._track(this._buildEventPayload(payload), options))
await Promise.all(events)
}
}

View File

@ -195,6 +195,11 @@ export default class NetworkController extends EventEmitter {
return this.providerStore.getState()
}
getNetworkIdentifier() {
const provider = this.providerStore.getState()
return provider.type === 'rpc' ? provider.rpcUrl : provider.type
}
//
// Private
//

View File

@ -2,7 +2,7 @@ import { strict as assert } from 'assert'
import ObservableStore from 'obs-store'
import { ethErrors } from 'eth-json-rpc-errors'
import { normalize as normalizeAddress } from 'eth-sig-util'
import { isValidAddress, sha3, bufferToHex } from 'ethereumjs-util'
import { isValidAddress } from 'ethereumjs-util'
import ethers from 'ethers'
import log from 'loglevel'
import { isPrefixedFormattedHexString } from '../lib/util'
@ -50,7 +50,6 @@ export default class PreferencesController {
transactionTime: false,
},
knownMethodData: {},
participateInMetaMetrics: null,
firstTimeFlowType: null,
currentLocale: opts.initLangCode,
identities: {},
@ -62,9 +61,6 @@ export default class PreferencesController {
useNativeCurrencyAsPrimaryCurrency: true,
},
completedOnboarding: false,
metaMetricsId: null,
metaMetricsSendCount: 0,
// ENS decentralized website resolution
ipfsGateway: 'dweb.link',
...opts.initState,
@ -121,38 +117,6 @@ export default class PreferencesController {
this.store.updateState({ usePhishDetect: val })
}
/**
* Setter for the `participateInMetaMetrics` property
*
* @param {boolean} bool - Whether or not the user wants to participate in MetaMetrics
* @returns {string|null} the string of the new metametrics id, or null if not set
*
*/
setParticipateInMetaMetrics(bool) {
this.store.updateState({ participateInMetaMetrics: bool })
let metaMetricsId = null
if (bool && !this.store.getState().metaMetricsId) {
metaMetricsId = bufferToHex(
sha3(
String(Date.now()) +
String(Math.round(Math.random() * Number.MAX_SAFE_INTEGER)),
),
)
this.store.updateState({ metaMetricsId })
} else if (bool === false) {
this.store.updateState({ metaMetricsId })
}
return metaMetricsId
}
getParticipateInMetaMetrics() {
return this.store.getState().participateInMetaMetrics
}
setMetaMetricsSendCount(val) {
this.store.updateState({ metaMetricsSendCount: val })
}
/**
* Setter for the `firstTimeFlowType` property
*

View File

@ -929,15 +929,8 @@ export default class TransactionController extends EventEmitter {
if (txMeta.txReceipt.status === '0x0') {
this._trackMetaMetricsEvent({
event: 'Swap Failed',
sensitiveProperties: { ...txMeta.swapMetaData },
category: 'swaps',
excludeMetaMetricsId: false,
})
this._trackMetaMetricsEvent({
event: 'Swap Failed',
properties: { ...txMeta.swapMetaData },
category: 'swaps',
excludeMetaMetricsId: true,
})
} else {
const tokensReceived = getSwapsTokensReceivedFromTxMeta(
@ -965,19 +958,12 @@ export default class TransactionController extends EventEmitter {
this._trackMetaMetricsEvent({
event: 'Swap Completed',
category: 'swaps',
excludeMetaMetricsId: false,
})
this._trackMetaMetricsEvent({
event: 'Swap Completed',
category: 'swaps',
properties: {
sensitiveProperties: {
...txMeta.swapMetaData,
token_to_amount_received: tokensReceived,
quote_vs_executionRatio: quoteVsExecutionRatio,
estimated_vs_used_gasRatio: estimatedVsUsedGasRatio,
},
excludeMetaMetricsId: true,
})
}
}

View File

@ -43,17 +43,19 @@ function logWeb3UsageHandler(req, res, _next, end, { origin, sendMetrics }) {
if (!recordedWeb3Usage[origin][path]) {
recordedWeb3Usage[origin][path] = true
sendMetrics({
event: `Website Used window.web3`,
category: 'inpage_provider',
properties: { action, web3Path: path },
eventContext: {
sendMetrics(
{
event: `Website Used window.web3`,
category: 'inpage_provider',
properties: { action, web3Path: path },
referrer: {
url: origin,
},
},
excludeMetaMetricsId: true,
})
{
excludeMetaMetricsId: true,
},
)
}
res.result = true

101
app/scripts/lib/segment.js Normal file
View File

@ -0,0 +1,101 @@
import Analytics from 'analytics-node'
const isDevOrTestEnvironment = Boolean(
process.env.METAMASK_DEBUG || process.env.IN_TEST,
)
const SEGMENT_WRITE_KEY = process.env.SEGMENT_WRITE_KEY ?? null
const SEGMENT_LEGACY_WRITE_KEY = process.env.SEGMENT_LEGACY_WRITE_KEY ?? null
const SEGMENT_HOST = process.env.SEGMENT_HOST ?? null
// flushAt controls how many events are sent to segment at once. Segment will
// hold onto a queue of events until it hits this number, then it sends them as
// a batch. This setting defaults to 20, but in development we likely want to
// see events in real time for debugging, so this is set to 1 to disable the
// queueing mechanism.
const SEGMENT_FLUSH_AT =
process.env.METAMASK_ENVIRONMENT === 'production' ? undefined : 1
// flushInterval controls how frequently the queue is flushed to segment.
// This happens regardless of the size of the queue. The default setting is
// 10,000ms (10 seconds). This default is rather high, though thankfully
// using the background process as our event handler means we don't have to
// deal with short lived sessions that happen faster than the interval
// e.g confirmations. This is set to 5,000ms (5 seconds) arbitrarily with the
// intent of having a value less than 10 seconds.
const SEGMENT_FLUSH_INTERVAL = 5000
/**
* Creates a mock segment module for usage in test environments. This is used
* when building the application in test mode to catch event calls and prevent
* them from being sent to segment. It is also used in unit tests to mock and
* spy on the methods to ensure proper behavior
* @param {number} flushAt - number of events to queue before sending to segment
* @param {number} flushInterval - ms interval to flush queue and send to segment
* @returns {SegmentInterface}
*/
export const createSegmentMock = (
flushAt = SEGMENT_FLUSH_AT,
flushInterval = SEGMENT_FLUSH_INTERVAL,
) => {
const segmentMock = {
// Internal queue to keep track of events and properly mimic segment's
// queueing behavior.
queue: [],
/**
* Used to immediately send all queued events and reset the queue to zero.
* For our purposes this simply triggers the callback method registered with
* the event.
*/
flush() {
segmentMock.queue.forEach(([_, callback]) => {
callback()
})
segmentMock.queue = []
},
/**
* Track an event and add it to the queue. If the queue size reaches the
* flushAt threshold, flush the queue.
*/
track(payload, callback = () => undefined) {
segmentMock.queue.push([payload, callback])
if (segmentMock.queue.length >= flushAt) {
segmentMock.flush()
}
},
/**
* A true NOOP, these methods are either not used or do not await callback
* and therefore require no functionality.
*/
page() {
// noop
},
identify() {
// noop
},
}
// Mimic the flushInterval behavior with an interval
setInterval(segmentMock.flush, flushInterval)
return segmentMock
}
export const segment =
!SEGMENT_WRITE_KEY || (isDevOrTestEnvironment && !SEGMENT_HOST)
? createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL)
: new Analytics(SEGMENT_WRITE_KEY, {
host: SEGMENT_HOST,
flushAt: SEGMENT_FLUSH_AT,
flushInterval: SEGMENT_FLUSH_INTERVAL,
})
export const segmentLegacy =
!SEGMENT_LEGACY_WRITE_KEY || (isDevOrTestEnvironment && !SEGMENT_HOST)
? createSegmentMock(SEGMENT_FLUSH_AT, SEGMENT_FLUSH_INTERVAL)
: new Analytics(SEGMENT_LEGACY_WRITE_KEY, {
host: SEGMENT_HOST,
flushAt: SEGMENT_FLUSH_AT,
flushInterval: SEGMENT_FLUSH_INTERVAL,
})

View File

@ -24,7 +24,6 @@ import {
CurrencyRateController,
PhishingController,
} from '@metamask/controllers'
import { getTrackMetaMetricsEvent } from '../../shared/modules/metametrics'
import { getBackgroundMetaMetricState } from '../../ui/app/selectors'
import { TRANSACTION_STATUSES } from '../../shared/constants/transaction'
import ComposableObservableStore from './lib/ComposableObservableStore'
@ -58,7 +57,8 @@ import getRestrictedMethods from './controllers/permissions/restrictedMethods'
import nodeify from './lib/nodeify'
import accountImporter from './account-import-strategies'
import seedPhraseVerifier from './lib/seed-phrase-verifier'
import { ENVIRONMENT_TYPE_BACKGROUND } from './lib/enums'
import MetaMetricsController from './controllers/metametrics'
import { segment, segmentLegacy } from './lib/segment'
export default class MetamaskController extends EventEmitter {
/**
@ -115,35 +115,24 @@ export default class MetamaskController extends EventEmitter {
migrateAddressBookState: this.migrateAddressBookState.bind(this),
})
this.trackMetaMetricsEvent = getTrackMetaMetricsEvent(
this.platform.getVersion(),
() => {
const participateInMetaMetrics = this.preferencesController.getParticipateInMetaMetrics()
const {
currentLocale,
metaMetricsId,
} = this.preferencesController.store.getState()
const chainId = this.networkController.getCurrentChainId()
const provider = this.networkController.getProviderConfig()
const network =
provider.type === 'rpc' ? provider.rpcUrl : provider.type
return {
participateInMetaMetrics,
metaMetricsId,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
chainId,
network,
context: {
page: {
path: '/background-process',
title: 'Background Process',
url: '/background-process',
},
locale: currentLocale.replace('_', '-'),
},
}
},
)
this.metaMetricsController = new MetaMetricsController({
segment,
segmentLegacy,
preferencesStore: this.preferencesController.store,
onNetworkDidChange: this.networkController.on.bind(
this.networkController,
'networkDidChange',
),
getNetworkIdentifier: this.networkController.getNetworkIdentifier.bind(
this.networkController,
),
getCurrentChainId: this.networkController.getCurrentChainId.bind(
this.networkController,
),
version: this.platform.getVersion(),
environment: process.env.METAMASK_ENVIRONMENT,
initState: initState.MetaMetricsController,
})
this.appStateController = new AppStateController({
addUnlockListener: this.on.bind(this, 'unlock'),
@ -298,9 +287,9 @@ export default class MetamaskController extends EventEmitter {
),
provider: this.provider,
blockTracker: this.blockTracker,
trackMetaMetricsEvent: this.trackMetaMetricsEvent,
trackMetaMetricsEvent: this.metaMetricsController.trackEvent,
getParticipateInMetrics: () =>
this.preferencesController.getParticipateInMetaMetrics(),
this.metaMetricsController.state.participateInMetaMetrics,
})
this.txController.on('newUnapprovedTx', () => opts.showUserConfirmation())
@ -362,6 +351,7 @@ export default class MetamaskController extends EventEmitter {
TransactionController: this.txController.store,
KeyringController: this.keyringController.store,
PreferencesController: this.preferencesController.store,
MetaMetricsController: this.metaMetricsController.store,
AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController,
NetworkController: this.networkController.store,
@ -388,6 +378,7 @@ export default class MetamaskController extends EventEmitter {
TypesMessageManager: this.typedMessageManager.memStore,
KeyringController: this.keyringController.memStore,
PreferencesController: this.preferencesController.store,
MetaMetricsController: this.metaMetricsController.store,
AddressBookController: this.addressBookController,
CurrencyController: this.currencyRateController,
AlertController: this.alertController.store,
@ -528,6 +519,7 @@ export default class MetamaskController extends EventEmitter {
threeBoxController,
txController,
swapsController,
metaMetricsController,
} = this
return {
@ -825,6 +817,16 @@ export default class MetamaskController extends EventEmitter {
swapsController.setSwapsLiveness,
swapsController,
),
// MetaMetrics
trackMetaMetricsEvent: nodeify(
metaMetricsController.trackEvent,
metaMetricsController,
),
trackMetaMetricsPage: nodeify(
metaMetricsController.trackPage,
metaMetricsController,
),
}
}
@ -1967,7 +1969,7 @@ export default class MetamaskController extends EventEmitter {
engine.push(
createMethodMiddleware({
origin,
sendMetrics: this.trackMetaMetricsEvent,
sendMetrics: this.metaMetricsController.trackEvent,
handleWatchAssetRequest: this.preferencesController.requestWatchAsset.bind(
this.preferencesController,
),
@ -2177,16 +2179,20 @@ export default class MetamaskController extends EventEmitter {
metamask: metamaskState,
})
this.trackMetaMetricsEvent({
event: name,
category: 'Background',
matomoEvent: true,
properties: {
action,
...additionalProperties,
...customVariables,
this.metaMetricsController.trackEvent(
{
event: name,
category: 'Background',
properties: {
action,
...additionalProperties,
...customVariables,
},
},
})
{
matomoEvent: true,
},
)
}
/**
@ -2421,7 +2427,7 @@ export default class MetamaskController extends EventEmitter {
*/
setParticipateInMetaMetrics(bool, cb) {
try {
const metaMetricsId = this.preferencesController.setParticipateInMetaMetrics(
const metaMetricsId = this.metaMetricsController.setParticipateInMetaMetrics(
bool,
)
cb(null, metaMetricsId)
@ -2435,7 +2441,7 @@ export default class MetamaskController extends EventEmitter {
setMetaMetricsSendCount(val, cb) {
try {
this.preferencesController.setMetaMetricsSendCount(val)
this.metaMetricsController.setMetaMetricsSendCount(val)
cb(null)
return
} catch (err) {

View File

@ -0,0 +1,44 @@
import { cloneDeep } from 'lodash'
const version = 49
/**
* Migrate metaMetrics state to the new MetaMetrics controller
*/
export default {
version,
async migrate(originalVersionedData) {
const versionedData = cloneDeep(originalVersionedData)
versionedData.meta.version = version
const state = versionedData.data
versionedData.data = transformState(state)
return versionedData
},
}
function transformState(state = {}) {
if (state.PreferencesController) {
const {
metaMetricsId,
participateInMetaMetrics,
metaMetricsSendCount,
} = state.PreferencesController
state.MetaMetricsController = state.MetaMetricsController ?? {}
if (metaMetricsId !== undefined) {
state.MetaMetricsController.metaMetricsId = metaMetricsId
delete state.PreferencesController.metaMetricsId
}
if (participateInMetaMetrics !== undefined) {
state.MetaMetricsController.participateInMetaMetrics = participateInMetaMetrics
delete state.PreferencesController.participateInMetaMetrics
}
if (metaMetricsSendCount !== undefined) {
state.MetaMetricsController.metaMetricsSendCount = metaMetricsSendCount
delete state.PreferencesController.metaMetricsSendCount
}
}
return state
}

View File

@ -53,6 +53,7 @@ const migrations = [
require('./046').default,
require('./047').default,
require('./048').default,
require('./049').default,
]
export default migrations

View File

@ -0,0 +1,140 @@
// Type Imports
/**
* @typedef {import('../../app/scripts/lib/enums').EnvironmentType} EnvironmentType
*/
// Type Declarations
/**
* Used to attach context of where the user was at in the application when the
* event was triggered. Also included as full details of the current page in
* page events.
* @typedef {Object} MetaMetricsPageObject
* @property {string} [path] - the path of the current page (e.g /home)
* @property {string} [title] - the title of the current page (e.g 'home')
* @property {string} [url] - the fully qualified url of the current page
*/
/**
* For metamask, this is the dapp that triggered an interaction
* @typedef {Object} MetaMetricsReferrerObject
* @property {string} [url] - the origin of the dapp issuing the
* notification
*/
/**
* We attach context to every meta metrics event that help to qualify our
* analytics. This type has all optional values because it represents a
* returned object from a method call. Ideally app and userAgent are
* defined on every event. This is confirmed in the getTrackMetaMetricsEvent
* function, but still provides the consumer a way to override these values if
* necessary.
* @typedef {Object} MetaMetricsContext
* @property {Object} app
* @property {string} app.name - the name of the application tracking the event
* @property {string} app.version - the version of the application
* @property {string} userAgent - the useragent string of the user
* @property {MetaMetricsPageObject} [page] - an object representing details of
* the current page
* @property {MetaMetricsReferrerObject} [referrer] - for metamask, this is the
* dapp that triggered an interaction
*/
/**
* @typedef {Object} MetaMetricsEventPayload
* @property {string} event - event name to track
* @property {string} category - category to associate event to
* @property {string} [environmentType] - The type of environment this event
* occurred in. Defaults to the background process type
* @property {object} [properties] - object of custom values to track, keys
* in this object must be in snake_case
* @property {object} [sensitiveProperties] - Object of sensitive values to
* track. Keys in this object must be in snake_case. These properties will be
* sent in an additional event that excludes the user's metaMetricsId
* @property {number} [revenue] - amount of currency that event creates in
* revenue for MetaMask
* @property {string} [currency] - ISO 4127 format currency for events with
* revenue, defaults to US dollars
* @property {number} [value] - Abstract business "value" attributable to
* customers who trigger this event
* @property {MetaMetricsPageObject} [page] - the page/route that the event
* occurred on
* @property {MetaMetricsReferrerObject} [referrer] - the origin of the dapp
* that triggered the event
*/
/**
* @typedef {Object} MetaMetricsEventOptions
* @property {boolean} [isOptIn] - happened during opt in/out workflow
* @property {boolean} [flushImmediately] - When true will automatically flush
* the segment queue after tracking the event. Recommended if the result of
* tracking the event must be known before UI transition or update
* @property {boolean} [excludeMetaMetricsId] - whether to exclude the user's
* metametrics id for anonymity
* @property {string} [metaMetricsId] - an override for the metaMetricsId in
* the event one is created as part of an asynchronous workflow, such as
* awaiting the result of the metametrics opt-in function that generates the
* user's metametrics id
* @property {boolean} [matomoEvent] - is this event a holdover from matomo
* that needs further migration? when true, sends the data to a special
* segment source that marks the event data as not conforming to our schema
*/
/**
* Represents the shape of data sent to the segment.track method.
* @typedef {Object} SegmentEventPayload
* @property {string} [userId] - The metametrics id for the user
* @property {string} [anonymousId] - An anonymousId that is used to track
* sensitive data while preserving anonymity.
* @property {string} event - name of the event to track
* @property {Object} properties - properties to attach to the event
* @property {MetaMetricsContext} context - the context the event occurred in
*/
/**
* @typedef {Object} MetaMetricsPagePayload
* @property {string} name - The name of the page that was viewed
* @property {Object} [params] - The variadic parts of the page url
* example (route: `/asset/:asset`, path: `/asset/ETH`)
* params: { asset: 'ETH' }
* @property {EnvironmentType} environmentType - the environment type that the
* page was viewed in
* @property {MetaMetricsPageObject} [page] - the details of the page
* @property {MetaMetricsReferrerObject} [referrer] - dapp that triggered the page
* view
*/
/**
* @typedef {Object} MetaMetricsPageOptions
* @property {boolean} [isOptInPath] - is the current path one of the pages in
* the onboarding workflow? If true and participateInMetaMetrics is null track
* the page view
*/
export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'
/**
* This object is used to identify events that are triggered by the background
* process.
* @type {MetaMetricsPageObject}
*/
export const METAMETRICS_BACKGROUND_PAGE_OBJECT = {
path: '/background-process',
title: 'Background Process',
url: '/background-process',
}
/**
* @typedef {Object} SegmentInterface
* @property {SegmentEventPayload[]} queue - A queue of events to be sent when
* the flushAt limit has been reached, or flushInterval occurs
* @property {() => void} flush - Immediately flush the queue, resetting it to
* an empty array and sending the pending events to Segment
* @property {(
* payload: SegmentEventPayload,
* callback: (err?: Error) => void
* ) => void} track - Track an event with Segment, using the internal batching
* mechanism to optimize network requests
* @property {(payload: Object) => void} page - Track a page view with Segment
* @property {() => void} identify - Identify an anonymous user. We do not
* currently use this method.
*/

3
shared/modules/README.md Normal file
View File

@ -0,0 +1,3 @@
### Shared Modules
This folder is reserved for modules that can be used globally within both the background and ui applications.

View File

@ -1,302 +0,0 @@
import Analytics from 'analytics-node'
import { merge, omit, pick } from 'lodash'
// flushAt controls how many events are sent to segment at once. Segment
// will hold onto a queue of events until it hits this number, then it sends
// them as a batch. This setting defaults to 20, but that is too high for
// notification workflows. We also cannot send each event as singular payloads
// because it seems to bombard segment and potentially cause event loss.
// I chose 5 here because it is sufficiently high enough to optimize our network
// requests, while also being low enough to be reasonable.
const flushAt = process.env.METAMASK_ENVIRONMENT === 'production' ? 5 : 1
// flushInterval controls how frequently the queue is flushed to segment.
// This happens regardless of the size of the queue. The default setting is
// 10,000ms (10 seconds). This default is absurdly high for our typical user
// flow through confirmations. I have chosen 10 ms here because it works really
// well with our wrapped track function. The track function returns a promise
// that is only fulfilled when it has been sent to segment. A 10 ms delay is
// negligible to the user, but allows us to properly batch events that happen
// in rapid succession.
const flushInterval = 10
export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'
const segmentNoop = {
track(_, callback = () => undefined) {
// Need to call the callback so that environments without a segment id still
// resolve the promise from trackMetaMetricsEvent
return callback()
},
page() {
// noop
},
identify() {
// noop
},
}
/**
* Used to determine whether or not to attach a user's metametrics id
* to events that include on-chain data. This helps to prevent identifying
* a user by being able to trace their activity on etherscan/block exploring
*/
const trackableSendCounts = {
1: true,
10: true,
30: true,
50: true,
100: true,
250: true,
500: true,
1000: true,
2500: true,
5000: true,
10000: true,
25000: true,
}
export function sendCountIsTrackable(sendCount) {
return Boolean(trackableSendCounts[sendCount])
}
const isDevOrTestEnvironment = Boolean(
process.env.METAMASK_DEBUG || process.env.IN_TEST,
)
// This allows us to overwrite the metric destination for testing purposes
const host = process.env.SEGMENT_HOST ?? undefined
// We do not want to track events on development builds unless specifically
// provided a SEGMENT_WRITE_KEY. This also holds true for test environments and
// E2E, which is handled in the build process by never providing the SEGMENT_WRITE_KEY
// when process.env.IN_TEST is truthy
export const segment =
!process.env.SEGMENT_WRITE_KEY || (isDevOrTestEnvironment && !host)
? segmentNoop
: new Analytics(process.env.SEGMENT_WRITE_KEY, {
host,
flushAt,
flushInterval,
})
export const segmentLegacy =
!process.env.SEGMENT_LEGACY_WRITE_KEY || (isDevOrTestEnvironment && !host)
? segmentNoop
: new Analytics(process.env.SEGMENT_LEGACY_WRITE_KEY, {
host,
flushAt,
flushInterval,
})
/**
* We attach context to every meta metrics event that help to qualify our analytics.
* This type has all optional values because it represents a returned object from a
* method call. Ideally app and userAgent are defined on every event. This is confirmed
* in the getTrackMetaMetricsEvent function, but still provides the consumer a way to
* override these values if necessary.
* @typedef {Object} MetaMetricsContext
* @property {Object} app
* @property {string} app.name - the name of the application tracking the event
* @property {string} app.version - the version of the application
* @property {string} userAgent - the useragent string of the user
* @property {Object} [page] - an object representing details of the current page
* @property {string} [page.path] - the path of the current page (e.g /home)
* @property {string} [page.title] - the title of the current page (e.g 'home')
* @property {string} [page.url] - the fully qualified url of the current page
* @property {Object} [referrer] - for metamask, this is the dapp that triggered an interaction
* @property {string} [referrer.url] - the origin of the dapp issuing the notification
*/
/**
* page and referrer from the MetaMetricsContext are very dynamic in nature and may be
* provided as part of the initial context payload when creating the trackMetaMetricsEvent function,
* or at the event level when calling the trackMetaMetricsEvent function.
* @typedef {Pick<MetaMetricsContext, 'page' | 'referrer'>} MetaMetricsDynamicContext
*/
/**
* @typedef {import('../../app/scripts/lib/enums').EnvironmentType} EnvironmentType
*/
/**
* @typedef {Object} MetaMetricsRequiredState
* @property {bool} participateInMetaMetrics - has the user opted into metametrics
* @property {string} [metaMetricsId] - the user's metaMetricsId, if they have opted in
* @property {MetaMetricsDynamicContext} context - context about the event
* @property {string} chainId - the chain id of the current network
* @property {string} locale - the locale string of the current user
* @property {string} network - the name of the current network
* @property {EnvironmentType} environmentType - environment that the event happened in
* @property {string} [metaMetricsSendCount] - number of transactions sent, used to add metametricsId
* intermittently to events with onchain data attached to them used to protect identity of users.
*/
/**
* @typedef {Object} MetaMetricsEventPayload
* @property {string} event - event name to track
* @property {string} category - category to associate event to
* @property {boolean} [isOptIn] - happened during opt in/out workflow
* @property {object} [properties] - object of custom values to track, snake_case
* @property {object} [sensitiveProperties] - Object of sensitive values to track, snake_case.
* These properties will be sent in an additional event that excludes the user's metaMetricsId.
* @property {number} [revenue] - amount of currency that event creates in revenue for MetaMask
* @property {string} [currency] - ISO 4127 format currency for events with revenue, defaults to US dollars
* @property {number} [value] - Abstract "value" that this event has for MetaMask.
* @property {boolean} [excludeMetaMetricsId] - whether to exclude the user's metametrics id for anonymity
* @property {string} [metaMetricsId] - an override for the metaMetricsId in the event one is created as part
* of an asynchronous workflow, such as awaiting the result of the metametrics opt-in function that generates the
* user's metametrics id.
* @property {boolean} [matomoEvent] - is this event a holdover from matomo that needs further migration?
* when true, sends the data to a special segment source that marks the event data as not conforming to our
* ideal schema
* @property {MetaMetricsDynamicContext} [eventContext] - additional context to attach to event
*/
/**
* Returns a function for tracking Segment events.
*
* @param {string} metamaskVersion - The current version of the MetaMask extension.
* @param {() => MetaMetricsRequiredState} getDynamicState - A function returning required fields
* @returns {(payload: MetaMetricsEventPayload) => Promise<void>} function to track an event
*/
export function getTrackMetaMetricsEvent(metamaskVersion, getDynamicState) {
const version =
process.env.METAMASK_ENVIRONMENT === 'production'
? metamaskVersion
: `${metamaskVersion}-${process.env.METAMASK_ENVIRONMENT}`
return function trackMetaMetricsEvent({
event,
category,
isOptIn,
properties = {},
sensitiveProperties,
revenue,
currency,
value,
metaMetricsId: metaMetricsIdOverride,
excludeMetaMetricsId: excludeId,
matomoEvent = false,
eventContext = {},
}) {
if (!event || !category) {
throw new Error('Must specify event and category.')
}
// Uses recursion to track a duplicate event with sensitive properties included,
// but metaMetricsId excluded
if (sensitiveProperties) {
if (excludeId === true) {
throw new Error(
'sensitiveProperties was specified in an event payload that also set the excludeMetaMetricsId flag',
)
}
trackMetaMetricsEvent({
event,
category,
isOptIn,
properties: merge(sensitiveProperties, properties),
revenue,
currency,
value,
excludeMetaMetricsId: true,
matomoEvent,
eventContext,
})
}
const {
participateInMetaMetrics,
context: providedContext,
metaMetricsId,
environmentType,
chainId,
locale,
network,
metaMetricsSendCount,
} = getDynamicState()
let excludeMetaMetricsId = excludeId ?? false
// This is carried over from the old implementation, and will likely need
// to be updated to work with the new tracking plan. I think we should use
// a config setting for this instead of trying to match the event name
const isSendFlow = Boolean(event.match(/^send|^confirm/u))
if (
isSendFlow &&
metaMetricsSendCount &&
!sendCountIsTrackable(metaMetricsSendCount + 1)
) {
excludeMetaMetricsId = true
}
if (!participateInMetaMetrics && !isOptIn) {
return Promise.resolve()
}
/** @type {MetaMetricsContext} */
const context = {
app: {
name: 'MetaMask Extension',
version,
},
userAgent: window.navigator.userAgent,
...pick(providedContext, ['page', 'referrer']),
...pick(eventContext, ['page', 'referrer']),
}
const trackOptions = {
event,
properties: {
// These values are omitted from properties because they have special meaning
// in segment. https://segment.com/docs/connections/spec/track/#properties.
// to avoid accidentally using these inappropriately, you must add them as top
// level properties on the event payload. We also exclude locale to prevent consumers
// from overwriting this context level property. We track it as a property
// because not all destinations map locale from context.
...omit(properties, ['revenue', 'locale', 'currency', 'value']),
revenue,
value,
currency,
category,
network,
locale,
chain_id: chainId,
environment_type: environmentType,
},
context,
}
// If we are tracking sensitive data we will always use the anonymousId property
// as well as our METAMETRICS_ANONYMOUS_ID. This prevents us from associating potentially
// identifiable information with a specific id. During the opt in flow we will track all
// events, but do so with the anonymous id. The one exception to that rule is after the
// user opts in to MetaMetrics. When that happens we receive back the user's new MetaMetrics
// id before it is fully persisted to state. To avoid a race condition we explicitly pass the
// new id to the track method. In that case we will track the opt in event to the user's id.
// In all other cases we use the metaMetricsId from state.
if (excludeMetaMetricsId) {
trackOptions.anonymousId = METAMETRICS_ANONYMOUS_ID
} else if (isOptIn && metaMetricsIdOverride) {
trackOptions.userId = metaMetricsIdOverride
} else if (isOptIn) {
trackOptions.anonymousId = METAMETRICS_ANONYMOUS_ID
} else {
trackOptions.userId = metaMetricsId
}
return new Promise((resolve, reject) => {
// This is only safe to do because we have set an extremely low (10ms) flushInterval.
const callback = (err) => {
if (err) {
return reject(err)
}
return resolve()
}
if (matomoEvent === true) {
segmentLegacy.track(trackOptions, callback)
} else {
segment.track(trackOptions, callback)
}
})
}
}

View File

@ -108,6 +108,7 @@ describe('MetaMaskController', function () {
},
},
initState: cloneDeep(firstTimeState),
initLangCode: 'en_US',
platform: {
showTransactionNotification: () => undefined,
getVersion: () => 'foo',
@ -799,7 +800,7 @@ describe('MetaMaskController', function () {
it('checks the default currentLocale', function () {
const preferenceCurrentLocale = metamaskController.preferencesController.store.getState()
.currentLocale
assert.equal(preferenceCurrentLocale, undefined)
assert.equal(preferenceCurrentLocale, 'en_US')
})
it('sets current locale in preferences controller', function () {

View File

@ -0,0 +1,546 @@
import { strict as assert } from 'assert'
import sinon from 'sinon'
import MetaMetricsController from '../../../../app/scripts/controllers/metametrics'
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../../app/scripts/lib/enums'
import { createSegmentMock } from '../../../../app/scripts/lib/segment'
import {
METAMETRICS_ANONYMOUS_ID,
METAMETRICS_BACKGROUND_PAGE_OBJECT,
} from '../../../../shared/constants/metametrics'
import waitUntilCalled from '../../../lib/wait-until-called'
const segment = createSegmentMock(2, 10000)
const segmentLegacy = createSegmentMock(2, 10000)
const VERSION = '0.0.1-test'
const NETWORK = 'Mainnet'
const FAKE_CHAIN_ID = '0x1338'
const LOCALE = 'en_US'
const TEST_META_METRICS_ID = '0xabc'
const DEFAULT_TEST_CONTEXT = {
app: { name: 'MetaMask Extension', version: VERSION },
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
referrer: undefined,
userAgent: window.navigator.userAgent,
}
const DEFAULT_SHARED_PROPERTIES = {
chain_id: FAKE_CHAIN_ID,
locale: LOCALE.replace('_', '-'),
network: NETWORK,
environment_type: 'background',
}
const DEFAULT_EVENT_PROPERTIES = {
category: 'Unit Test',
revenue: undefined,
value: undefined,
currency: undefined,
...DEFAULT_SHARED_PROPERTIES,
}
const DEFAULT_PAGE_PROPERTIES = {
...DEFAULT_SHARED_PROPERTIES,
}
function getMockNetworkController(
chainId = FAKE_CHAIN_ID,
provider = { type: NETWORK },
) {
let networkStore = { chainId, provider }
const on = sinon.stub().withArgs('networkDidChange')
const updateState = (newState) => {
networkStore = { ...networkStore, ...newState }
on.getCall(0).args[1]()
}
return {
store: {
getState: () => networkStore,
updateState,
},
getCurrentChainId: () => networkStore.chainId,
getNetworkIdentifier: () => networkStore.provider.type,
on,
}
}
function getMockPreferencesStore({ currentLocale = LOCALE } = {}) {
let preferencesStore = {
currentLocale,
}
const subscribe = sinon.stub()
const updateState = (newState) => {
preferencesStore = { ...preferencesStore, ...newState }
subscribe.getCall(0).args[0](preferencesStore)
}
return {
getState: sinon.stub().returns(preferencesStore),
updateState,
subscribe,
}
}
function getMetaMetricsController({
participateInMetaMetrics = true,
metaMetricsId = TEST_META_METRICS_ID,
metaMetricsSendCount = 0,
preferencesStore = getMockPreferencesStore(),
networkController = getMockNetworkController(),
} = {}) {
return new MetaMetricsController({
segment,
segmentLegacy,
getNetworkIdentifier: networkController.getNetworkIdentifier.bind(
networkController,
),
getCurrentChainId: networkController.getCurrentChainId.bind(
networkController,
),
onNetworkDidChange: networkController.on.bind(
networkController,
'networkDidChange',
),
preferencesStore,
version: '0.0.1',
environment: 'test',
initState: {
participateInMetaMetrics,
metaMetricsId,
metaMetricsSendCount,
},
})
}
describe('MetaMetricsController', function () {
describe('constructor', function () {
it('should properly initialize', function () {
const metaMetricsController = getMetaMetricsController()
assert.strictEqual(metaMetricsController.version, VERSION)
assert.strictEqual(metaMetricsController.network, NETWORK)
assert.strictEqual(metaMetricsController.chainId, FAKE_CHAIN_ID)
assert.strictEqual(
metaMetricsController.state.participateInMetaMetrics,
true,
)
assert.strictEqual(
metaMetricsController.state.metaMetricsId,
TEST_META_METRICS_ID,
)
assert.strictEqual(metaMetricsController.locale, LOCALE.replace('_', '-'))
})
it('should update when network changes', function () {
const networkController = getMockNetworkController()
const metaMetricsController = getMetaMetricsController({
networkController,
})
assert.strictEqual(metaMetricsController.network, NETWORK)
networkController.store.updateState({
provider: {
type: 'NEW_NETWORK',
},
chainId: '0xaab',
})
assert.strictEqual(metaMetricsController.network, 'NEW_NETWORK')
assert.strictEqual(metaMetricsController.chainId, '0xaab')
})
it('should update when preferences changes', function () {
const preferencesStore = getMockPreferencesStore()
const metaMetricsController = getMetaMetricsController({
preferencesStore,
})
assert.strictEqual(metaMetricsController.network, NETWORK)
preferencesStore.updateState({
currentLocale: 'en_UK',
})
assert.strictEqual(metaMetricsController.locale, 'en-UK')
})
})
describe('generateMetaMetricsId', function () {
it('should generate an 0x prefixed hex string', function () {
const metaMetricsController = getMetaMetricsController()
assert.equal(
metaMetricsController.generateMetaMetricsId().startsWith('0x'),
true,
)
})
})
describe('setParticipateInMetaMetrics', function () {
it('should update the value of participateInMetaMetrics', function () {
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: null,
metaMetricsId: null,
})
assert.equal(metaMetricsController.state.participateInMetaMetrics, null)
metaMetricsController.setParticipateInMetaMetrics(true)
assert.equal(metaMetricsController.state.participateInMetaMetrics, true)
metaMetricsController.setParticipateInMetaMetrics(false)
assert.equal(metaMetricsController.state.participateInMetaMetrics, false)
})
it('should generate and update the metaMetricsId when set to true', function () {
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: null,
metaMetricsId: null,
})
assert.equal(metaMetricsController.state.metaMetricsId, null)
metaMetricsController.setParticipateInMetaMetrics(true)
assert.equal(typeof metaMetricsController.state.metaMetricsId, 'string')
})
it('should nullify the metaMetricsId when set to false', function () {
const metaMetricsController = getMetaMetricsController()
metaMetricsController.setParticipateInMetaMetrics(false)
assert.equal(metaMetricsController.state.metaMetricsId, null)
})
})
describe('setMetaMetricsSendCount', function () {
it('should update the send count in state', function () {
const metaMetricsController = getMetaMetricsController()
metaMetricsController.setMetaMetricsSendCount(1)
assert.equal(metaMetricsController.state.metaMetricsSendCount, 1)
})
})
describe('trackEvent', function () {
it('should not track an event if user is not participating in metametrics', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: false,
})
mock.expects('track').never()
metaMetricsController.trackEvent({
event: 'Fake Event',
category: 'Unit Test',
properties: {
test: 1,
},
})
mock.verify()
})
it('should track an event if user has not opted in, but isOptIn is true', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: false,
})
mock
.expects('track')
.once()
.withArgs({
event: 'Fake Event',
anonymousId: METAMETRICS_ANONYMOUS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
})
metaMetricsController.trackEvent(
{
event: 'Fake Event',
category: 'Unit Test',
properties: {
test: 1,
},
},
{ isOptIn: true },
)
mock.verify()
})
it('should track an event during optin and allow for metaMetricsId override', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: false,
})
mock
.expects('track')
.once()
.withArgs({
event: 'Fake Event',
userId: 'TESTID',
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
})
metaMetricsController.trackEvent(
{
event: 'Fake Event',
category: 'Unit Test',
properties: {
test: 1,
},
},
{ isOptIn: true, metaMetricsId: 'TESTID' },
)
mock.verify()
})
it('should track a legacy event', function () {
const mock = sinon.mock(segmentLegacy)
const metaMetricsController = getMetaMetricsController()
mock
.expects('track')
.once()
.withArgs({
event: 'Fake Event',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
})
metaMetricsController.trackEvent(
{
event: 'Fake Event',
category: 'Unit Test',
properties: {
test: 1,
},
},
{ matomoEvent: true },
)
mock.verify()
})
it('should track a non legacy event', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController()
mock
.expects('track')
.once()
.withArgs({
event: 'Fake Event',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
})
metaMetricsController.trackEvent({
event: 'Fake Event',
category: 'Unit Test',
properties: {
test: 1,
},
})
mock.verify()
})
it('should use anonymousId when metametrics send count is not trackable in send flow', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController({
metaMetricsSendCount: 1,
})
mock
.expects('track')
.once()
.withArgs({
event: 'Send Fake Event',
anonymousId: METAMETRICS_ANONYMOUS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
})
metaMetricsController.trackEvent({
event: 'Send Fake Event',
category: 'Unit Test',
properties: {
test: 1,
},
})
mock.verify()
})
it('should use user metametrics id when metametrics send count is trackable in send flow', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController()
mock
.expects('track')
.once()
.withArgs({
event: 'Send Fake Event',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
})
metaMetricsController.trackEvent(
{
event: 'Send Fake Event',
category: 'Unit Test',
properties: {
test: 1,
},
},
{ metaMetricsSendCount: 0 },
)
mock.verify()
})
it('should immediately flush queue if flushImmediately set to true', async function () {
const metaMetricsController = getMetaMetricsController()
const flushStub = sinon.stub(segment, 'flush')
const flushCalled = waitUntilCalled(flushStub, segment)
metaMetricsController.trackEvent(
{
event: 'Fake Event',
category: 'Unit Test',
},
{ flushImmediately: true },
)
assert.doesNotReject(flushCalled)
})
it('should throw if event or category not provided', function () {
const metaMetricsController = getMetaMetricsController()
assert.rejects(
() => metaMetricsController.trackEvent({ event: 'test' }),
/Must specify event and category\./u,
'must specify category',
)
assert.rejects(
() => metaMetricsController.trackEvent({ category: 'test' }),
/Must specify event and category\./u,
'must specify event',
)
})
it('should throw if provided sensitiveProperties, when excludeMetaMetricsId is true', function () {
const metaMetricsController = getMetaMetricsController()
assert.rejects(
() =>
metaMetricsController.trackEvent(
{
event: 'Fake Event',
category: 'Unit Test',
sensitiveProperties: { foo: 'bar' },
},
{ excludeMetaMetricsId: true },
),
/sensitiveProperties was specified in an event payload that also set the excludeMetaMetricsId flag/u,
)
})
it('should track sensitiveProperties in a separate, anonymous event', function () {
const metaMetricsController = getMetaMetricsController()
const spy = sinon.spy(segment, 'track')
metaMetricsController.trackEvent({
event: 'Fake Event',
category: 'Unit Test',
sensitiveProperties: { foo: 'bar' },
})
assert.ok(spy.calledTwice)
assert.ok(
spy.calledWith({
event: 'Fake Event',
anonymousId: METAMETRICS_ANONYMOUS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
foo: 'bar',
...DEFAULT_EVENT_PROPERTIES,
},
}),
)
assert.ok(
spy.calledWith({
event: 'Fake Event',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: DEFAULT_EVENT_PROPERTIES,
}),
)
})
})
describe('trackPage', function () {
it('should track a page view', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController()
mock
.expects('page')
.once()
.withArgs({
name: 'home',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
})
metaMetricsController.trackPage({
name: 'home',
params: null,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
})
mock.verify()
})
it('should not track a page view if user is not participating in metametrics', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController({
participateInMetaMetrics: false,
})
mock.expects('page').never()
metaMetricsController.trackPage({
name: 'home',
params: null,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
})
mock.verify()
})
it('should track a page view if isOptInPath is true and user not yet opted in', function () {
const mock = sinon.mock(segment)
const metaMetricsController = getMetaMetricsController({
preferencesStore: getMockPreferencesStore({
participateInMetaMetrics: null,
}),
})
mock
.expects('page')
.once()
.withArgs({
name: 'home',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
})
metaMetricsController.trackPage(
{
name: 'home',
params: null,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
},
{ isOptInPath: true },
)
mock.verify()
})
})
afterEach(function () {
// flush the queues manually after each test
segment.flush()
segmentLegacy.flush()
sinon.restore()
})
})

View File

@ -0,0 +1,132 @@
import assert from 'assert'
import migration49 from '../../../app/scripts/migrations/049'
describe('migration #49', function () {
it('should update the version metadata', async function () {
const oldStorage = {
meta: {
version: 48,
},
data: {},
}
const newStorage = await migration49.migrate(oldStorage)
assert.deepStrictEqual(newStorage.meta, {
version: 49,
})
})
it('should move metaMetricsId to MetaMetricsController', async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
metaMetricsId: '0xaab',
bar: 'baz',
},
foo: 'bar',
},
}
const newStorage = await migration49.migrate(oldStorage)
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
bar: 'baz',
},
MetaMetricsController: {
metaMetricsId: '0xaab',
},
foo: 'bar',
})
})
it('should move participateInMetaMetrics to MetaMetricsController', async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
participateInMetaMetrics: false,
bar: 'baz',
},
foo: 'bar',
},
}
const newStorage = await migration49.migrate(oldStorage)
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
bar: 'baz',
},
MetaMetricsController: {
participateInMetaMetrics: false,
},
foo: 'bar',
})
})
it('should move metaMetricsSendCount to MetaMetricsController', async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
metaMetricsSendCount: 1,
bar: 'baz',
},
foo: 'bar',
},
}
const newStorage = await migration49.migrate(oldStorage)
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
bar: 'baz',
},
MetaMetricsController: {
metaMetricsSendCount: 1,
},
foo: 'bar',
})
})
it('should move all metaMetrics fields to MetaMetricsController', async function () {
const oldStorage = {
meta: {},
data: {
PreferencesController: {
metaMetricsSendCount: 1,
metaMetricsId: '0xaab',
participateInMetaMetrics: true,
bar: 'baz',
},
foo: 'bar',
},
}
const newStorage = await migration49.migrate(oldStorage)
assert.deepStrictEqual(newStorage.data, {
PreferencesController: {
bar: 'baz',
},
MetaMetricsController: {
metaMetricsSendCount: 1,
metaMetricsId: '0xaab',
participateInMetaMetrics: true,
},
foo: 'bar',
})
})
it('should do nothing if no PreferencesController key', async function () {
const oldStorage = {
meta: {},
data: {
foo: 'bar',
},
}
const newStorage = await migration49.migrate(oldStorage)
assert.deepStrictEqual(newStorage.data, {
foo: 'bar',
})
})
})

View File

@ -54,6 +54,7 @@ describe('Actions', function () {
return Promise.resolve(this.object)
},
},
initLangCode: 'en_US',
initState: cloneDeep(firstTimeState),
infuraProjectId: 'foo',
})

View File

@ -4,7 +4,6 @@ import React, {
useEffect,
useCallback,
useState,
useMemo,
} from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
@ -12,17 +11,14 @@ import { useHistory } from 'react-router-dom'
import { captureException } from '@sentry/browser'
import {
getCurrentNetworkId,
getAccountType,
getNumberOfAccounts,
getNumberOfTokens,
getCurrentChainId,
} from '../selectors/selectors'
import { getSendToken } from '../selectors/send'
import { txDataSelector } from '../selectors/confirm-transaction'
import { getEnvironmentType } from '../../../app/scripts/lib/util'
import { getTrackMetaMetricsEvent } from '../../../shared/modules/metametrics'
import { getCurrentLocale } from '../ducks/metamask/metamask'
import { trackMetaMetricsEvent } from '../store/actions'
export const MetaMetricsContext = createContext(() => {
captureException(
@ -34,20 +30,10 @@ export const MetaMetricsContext = createContext(() => {
export function MetaMetricsProvider({ children }) {
const txData = useSelector(txDataSelector) || {}
const network = useSelector(getCurrentNetworkId)
const environmentType = getEnvironmentType()
const chainId = useSelector(getCurrentChainId)
const locale = useSelector(getCurrentLocale)
const activeCurrency = useSelector(getSendToken)?.symbol
const accountType = useSelector(getAccountType)
const confirmTransactionOrigin = txData.origin
const metaMetricsId = useSelector((state) => state.metamask.metaMetricsId)
const participateInMetaMetrics = useSelector(
(state) => state.metamask.participateInMetaMetrics,
)
const metaMetricsSendCount = useSelector(
(state) => state.metamask.metaMetricsSendCount,
)
const numberOfTokens = useSelector(getNumberOfTokens)
const numberOfAccounts = useSelector(getNumberOfAccounts)
const history = useHistory()
@ -69,75 +55,58 @@ export function MetaMetricsProvider({ children }) {
return unlisten
}, [history])
/**
* track a metametrics event
*
* @param {import('../../../shared/modules/metametrics').MetaMetricsEventPayload} payload - payload for event
* @returns undefined
*/
const trackEvent = useMemo(() => {
const referrer = confirmTransactionOrigin
? { url: confirmTransactionOrigin }
: undefined
const page = {
path: currentPath,
}
return getTrackMetaMetricsEvent(global.platform.getVersion(), () => ({
context: {
referrer,
page,
},
environmentType,
locale: locale.replace('_', '-'),
network,
chainId,
participateInMetaMetrics,
metaMetricsId,
metaMetricsSendCount,
}))
}, [
network,
chainId,
locale,
environmentType,
participateInMetaMetrics,
currentPath,
confirmTransactionOrigin,
metaMetricsId,
metaMetricsSendCount,
])
const metricsEvent = useCallback(
(config = {}, overrides = {}) => {
const { eventOpts = {} } = config
return trackEvent({
event: eventOpts.name,
category: eventOpts.category,
isOptIn: config.isOptIn,
excludeMetaMetricsId:
eventOpts.excludeMetaMetricsId ??
overrides.excludeMetaMetricsId ??
false,
metaMetricsId: config.metaMetricsId,
matomoEvent: true,
properties: {
action: eventOpts.action,
number_of_tokens: numberOfTokens,
number_of_accounts: numberOfAccounts,
active_currency: activeCurrency,
account_type: accountType,
is_new_visit: config.is_new_visit,
// the properties coming from this key will not match our standards for
// snake_case on properties, and they may be redundant and/or not in the
// proper location (origin not as a referrer, for example). This is a temporary
// solution to not lose data, and the entire event system will be reworked in
// forthcoming PRs to deprecate the old Matomo events in favor of the new schema.
...config.customVariables,
const referrer = confirmTransactionOrigin
? { url: confirmTransactionOrigin }
: undefined
const page = {
path: currentPath,
}
return trackMetaMetricsEvent(
{
event: eventOpts.name,
category: eventOpts.category,
properties: {
action: eventOpts.action,
number_of_tokens: numberOfTokens,
number_of_accounts: numberOfAccounts,
active_currency: activeCurrency,
account_type: accountType,
is_new_visit: config.is_new_visit,
// the properties coming from this key will not match our standards for
// snake_case on properties, and they may be redundant and/or not in the
// proper location (origin not as a referrer, for example). This is a temporary
// solution to not lose data, and the entire event system will be reworked in
// forthcoming PRs to deprecate the old Matomo events in favor of the new schema.
...config.customVariables,
},
page,
referrer,
environmentType,
},
})
{
isOptIn: config.isOptIn,
excludeMetaMetricsId:
eventOpts.excludeMetaMetricsId ??
overrides.excludeMetaMetricsId ??
false,
metaMetricsId: config.metaMetricsId,
matomoEvent: true,
flushImmediately: config.flushImmediately,
},
)
},
[accountType, activeCurrency, numberOfTokens, numberOfAccounts, trackEvent],
[
accountType,
currentPath,
confirmTransactionOrigin,
activeCurrency,
numberOfTokens,
numberOfAccounts,
environmentType,
],
)
return (

View File

@ -4,33 +4,46 @@
* metrics system. This file implements Segment analytics tracking.
*/
import React, {
useRef,
Component,
createContext,
useEffect,
useMemo,
useRef,
useCallback,
} from 'react'
import { useSelector } from 'react-redux'
import PropTypes from 'prop-types'
import { useLocation, matchPath, useRouteMatch } from 'react-router-dom'
import { matchPath, useLocation, useRouteMatch } from 'react-router-dom'
import { captureException, captureMessage } from '@sentry/browser'
import { omit } from 'lodash'
import { getEnvironmentType } from '../../../app/scripts/lib/util'
import { PATH_NAME_MAP } from '../helpers/constants/routes'
import { getCurrentLocale } from '../ducks/metamask/metamask'
import {
getCurrentChainId,
getMetricsNetworkIdentifier,
txDataSelector,
} from '../selectors'
import {
getTrackMetaMetricsEvent,
METAMETRICS_ANONYMOUS_ID,
segment,
} from '../../../shared/modules/metametrics'
import { txDataSelector } from '../selectors'
import { trackMetaMetricsEvent, trackMetaMetricsPage } from '../store/actions'
// type imports
/**
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventOptions} MetaMetricsEventOptions
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPageObject} MetaMetricsPageObject
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsReferrerObject} MetaMetricsReferrerObject
*/
// types
/**
* @typedef {Omit<MetaMetricsEventPayload, 'environmentType' | 'page' | 'referrer'>} UIMetricsEventPayload
*/
/**
* @typedef {(
* payload: UIMetricsEventPayload,
* options: MetaMetricsEventOptions
* ) => Promise<void>} UITrackEventMethod
*/
/**
* @type {React.Context<UITrackEventMethod>}
*/
export const MetaMetricsContext = createContext(() => {
captureException(
Error(
@ -41,6 +54,14 @@ export const MetaMetricsContext = createContext(() => {
const PATHS_TO_CHECK = Object.keys(PATH_NAME_MAP)
/**
* Returns the current page if it matches out route map, as well as the origin
* if there is a confirmation that was triggered by a dapp
* @returns {{
* page?: MetaMetricsPageObject
* referrer?: MetaMetricsReferrerObject
* }}
*/
function useSegmentContext() {
const match = useRouteMatch({
path: PATHS_TO_CHECK,
@ -71,51 +92,25 @@ function useSegmentContext() {
}
export function MetaMetricsProvider({ children }) {
const metaMetricsId = useSelector((state) => state.metamask.metaMetricsId)
const participateInMetaMetrics = useSelector(
(state) => state.metamask.participateInMetaMetrics,
)
const metaMetricsSendCount = useSelector(
(state) => state.metamask.metaMetricsSendCount,
)
const locale = useSelector(getCurrentLocale)
const location = useLocation()
const context = useSegmentContext()
const network = useSelector(getMetricsNetworkIdentifier)
const chainId = useSelector(getCurrentChainId)
// Temporary until the background controller refactor merges:
const baseVersion = global.platform.getVersion()
const version =
process.env.METAMASK_ENVIRONMENT === 'production'
? baseVersion
: `${baseVersion}-${process.env.METAMASK_ENVIRONMENT}`
/**
* track a metametrics event
*
* @param {import('../../../shared/modules/metametrics').MetaMetricsEventPayload} payload - payload for event
* @returns undefined
* @type {UITrackEventMethod}
*/
const trackEvent = useMemo(() => {
return getTrackMetaMetricsEvent(global.platform.getVersion(), () => ({
context,
locale: locale.replace('_', '-'),
environmentType: getEnvironmentType(),
chainId,
network,
participateInMetaMetrics,
metaMetricsId,
metaMetricsSendCount,
}))
}, [
network,
participateInMetaMetrics,
locale,
metaMetricsId,
metaMetricsSendCount,
chainId,
context,
])
const trackEvent = useCallback(
(payload, options) => {
trackMetaMetricsEvent(
{
...payload,
environmentType: getEnvironmentType(),
...context,
},
options,
)
},
[context],
)
// Used to prevent double tracking page calls
const previousMatch = useRef()
@ -128,72 +123,52 @@ export function MetaMetricsProvider({ children }) {
*/
useEffect(() => {
const environmentType = getEnvironmentType()
if (
(participateInMetaMetrics === null &&
location.pathname.startsWith('/initialize')) ||
participateInMetaMetrics
) {
// Events that happen during initialization before the user opts into
// MetaMetrics will be anonymous
const idTrait = metaMetricsId ? 'userId' : 'anonymousId'
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID
const match = matchPath(location.pathname, {
path: PATHS_TO_CHECK,
exact: true,
strict: true,
const match = matchPath(location.pathname, {
path: PATHS_TO_CHECK,
exact: true,
strict: true,
})
// Start by checking for a missing match route. If this falls through to
// the else if, then we know we have a matched route for tracking.
if (!match) {
captureMessage(`Segment page tracking found unmatched route`, {
extra: {
previousMatch,
currentPath: location.pathname,
},
})
// Start by checking for a missing match route. If this falls through to
// the else if, then we know we have a matched route for tracking.
if (!match) {
captureMessage(`Segment page tracking found unmatched route`, {
extra: {
previousMatch,
currentPath: location.pathname,
},
})
} else if (
previousMatch.current !== match.path &&
!(
environmentType === 'notification' &&
match.path === '/' &&
previousMatch.current === undefined
)
) {
// When a notification window is open by a Dapp we do not want to track
// the initial home route load that can sometimes happen. To handle
// this we keep track of the previousMatch, and we skip the event track
// in the event that we are dealing with the initial load of the
// homepage
const { path, params } = match
const name = PATH_NAME_MAP[path]
segment.page({
[idTrait]: idValue,
} else if (
previousMatch.current !== match.path &&
!(
environmentType === 'notification' &&
match.path === '/' &&
previousMatch.current === undefined
)
) {
// When a notification window is open by a Dapp we do not want to track
// the initial home route load that can sometimes happen. To handle
// this we keep track of the previousMatch, and we skip the event track
// in the event that we are dealing with the initial load of the
// homepage
const { path, params } = match
const name = PATH_NAME_MAP[path]
trackMetaMetricsPage(
{
name,
properties: {
// We do not want to send addresses or accounts in any events
// Some routes include these as params.
params: omit(params, ['account', 'address']),
locale: locale.replace('_', '-'),
network,
environment_type: environmentType,
},
context: {
...context,
version,
},
})
}
previousMatch.current = match?.path
// We do not want to send addresses or accounts in any events
// Some routes include these as params.
params: omit(params, ['account', 'address']),
environmentType,
page: context.page,
referrer: context.referrer,
},
{
isOptInPath: location.pathname.startsWith('/initialize'),
},
)
}
}, [
location,
version,
locale,
context,
network,
metaMetricsId,
participateInMetaMetrics,
])
previousMatch.current = match?.path
}, [location, context])
return (
<MetaMetricsContext.Provider value={trackEvent}>

View File

@ -467,12 +467,7 @@ export const fetchQuotesAndSetQuoteState = (
metaMetricsEvent({
event: 'Quotes Requested',
category: 'swaps',
})
metaMetricsEvent({
event: 'Quotes Requested',
category: 'swaps',
excludeMetaMetricsId: true,
properties: {
sensitiveProperties: {
token_from: fromTokenSymbol,
token_from_amount: String(inputValue),
token_to: toTokenSymbol,
@ -518,12 +513,7 @@ export const fetchQuotesAndSetQuoteState = (
metaMetricsEvent({
event: 'No Quotes Available',
category: 'swaps',
})
metaMetricsEvent({
event: 'No Quotes Available',
category: 'swaps',
excludeMetaMetricsId: true,
properties: {
sensitiveProperties: {
token_from: fromTokenSymbol,
token_from_amount: String(inputValue),
token_to: toTokenSymbol,
@ -539,12 +529,7 @@ export const fetchQuotesAndSetQuoteState = (
metaMetricsEvent({
event: 'Quotes Received',
category: 'swaps',
})
metaMetricsEvent({
event: 'Quotes Received',
category: 'swaps',
excludeMetaMetricsId: true,
properties: {
sensitiveProperties: {
token_from: fromTokenSymbol,
token_from_amount: String(inputValue),
token_to: toTokenSymbol,
@ -671,16 +656,10 @@ export const signAndSendTransactions = (history, metaMetricsEvent) => {
median_metamask_fee: usedQuote.savings?.medianMetaMaskFee,
}
const metaMetricsConfig = {
metaMetricsEvent({
event: 'Swap Started',
category: 'swaps',
}
metaMetricsEvent({ ...metaMetricsConfig })
metaMetricsEvent({
...metaMetricsConfig,
excludeMetaMetricsId: true,
properties: swapMetaData,
sensitiveProperties: swapMetaData,
})
let finalApproveTxMeta

View File

@ -3,6 +3,12 @@ import { MetaMetricsContext } from '../contexts/metametrics'
import { MetaMetricsContext as NewMetaMetricsContext } from '../contexts/metametrics.new'
import { useEqualityCheck } from './useEqualityCheck'
// Type imports
/**
* @typedef {import('../contexts/metametrics.new').UIMetricsEventPayload} UIMetricsEventPayload
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventOptions} MetaMetricsEventOptions
*/
export function useMetricEvent(config = {}, overrides = {}) {
const metricsEvent = useContext(MetaMetricsContext)
const trackEvent = useCallback(() => metricsEvent(config, overrides), [
@ -17,21 +23,18 @@ export function useMetricEvent(config = {}, overrides = {}) {
* track a metametrics event using segment
* e.g metricsEvent({ event: 'Unlocked MetaMask', category: 'Navigation' })
*
* @param {Object} config - configuration object for the event to track
* @param {string} config.event - event name to track
* @param {string} config.category - category to associate event to
* @param {boolean} [config.isOptIn] - happened during opt in/out workflow
* @param {Object} [config.properties] - object of custom values to track, snake_case
* @param {number} [config.revenue] - amount of currency that event creates in revenue for MetaMask
* @param {string} [config.currency] - ISO 4127 format currency for events with revenue, defaults to US dollars
* @param {number} [config.value] - Abstract "value" that this event has for MetaMask.
* @return {() => undefined} function to execute the tracking event
* @param {UIMetricsEventPayload} payload - payload of the event to track
* @param {MetaMetricsEventOptions} options - options for handling/routing event
* @return {() => Promise<void>} function to execute the tracking event
*/
export function useNewMetricEvent(config) {
const memoizedConfig = useEqualityCheck(config)
export function useNewMetricEvent(payload, options) {
const memoizedPayload = useEqualityCheck(payload)
const memoizedOptions = useEqualityCheck(options)
const metricsEvent = useContext(NewMetaMetricsContext)
return useCallback(() => metricsEvent(memoizedConfig), [
return useCallback(() => metricsEvent(memoizedPayload, memoizedOptions), [
metricsEvent,
memoizedConfig,
memoizedPayload,
memoizedOptions,
])
}

View File

@ -113,6 +113,7 @@ export default class MetaMetricsOptIn extends Component {
name: 'Metrics Opt Out',
},
isOptIn: true,
flushImmediately: true,
})
}
} finally {
@ -136,6 +137,7 @@ export default class MetaMetricsOptIn extends Component {
name: 'Metrics Opt In',
},
isOptIn: true,
flushImmediately: true,
}),
)
}
@ -148,6 +150,7 @@ export default class MetaMetricsOptIn extends Component {
},
isOptIn: true,
metaMetricsId,
flushImmediately: true,
}),
)
await Promise.all(metrics)

View File

@ -2746,3 +2746,29 @@ export function getCurrentWindowTab() {
dispatch(setCurrentWindowTab(currentWindowTab))
}
}
// MetaMetrics
/**
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventPayload} MetaMetricsEventPayload
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventOptions} MetaMetricsEventOptions
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPagePayload} MetaMetricsPagePayload
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPageOptions} MetaMetricsPageOptions
*/
/**
* @param {MetaMetricsEventPayload} payload - details of the event to track
* @param {MetaMetricsEventOptions} options - options for routing/handling of event
* @returns {Promise<void>}
*/
export function trackMetaMetricsEvent(payload, options) {
return promisifiedBackground.trackMetaMetricsEvent(payload, options)
}
/**
* @param {MetaMetricsPagePayload} payload - details of the page viewed
* @param {MetaMetricsPageOptions} options - options for handling the page view
* @returns {void}
*/
export function trackMetaMetricsPage(payload, options) {
return promisifiedBackground.trackMetaMetricsPage(payload, options)
}