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:
parent
673371d013
commit
0653a489b0
366
app/scripts/controllers/metametrics.js
Normal file
366
app/scripts/controllers/metametrics.js
Normal 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)
|
||||
}
|
||||
}
|
@ -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
|
||||
//
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
101
app/scripts/lib/segment.js
Normal 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,
|
||||
})
|
@ -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) {
|
||||
|
44
app/scripts/migrations/049.js
Normal file
44
app/scripts/migrations/049.js
Normal 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
|
||||
}
|
@ -53,6 +53,7 @@ const migrations = [
|
||||
require('./046').default,
|
||||
require('./047').default,
|
||||
require('./048').default,
|
||||
require('./049').default,
|
||||
]
|
||||
|
||||
export default migrations
|
||||
|
140
shared/constants/metametrics.js
Normal file
140
shared/constants/metametrics.js
Normal 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
3
shared/modules/README.md
Normal 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.
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -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 () {
|
||||
|
546
test/unit/app/controllers/metametrics-test.js
Normal file
546
test/unit/app/controllers/metametrics-test.js
Normal 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()
|
||||
})
|
||||
})
|
132
test/unit/migrations/049-test.js
Normal file
132
test/unit/migrations/049-test.js
Normal 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',
|
||||
})
|
||||
})
|
||||
})
|
@ -54,6 +54,7 @@ describe('Actions', function () {
|
||||
return Promise.resolve(this.object)
|
||||
},
|
||||
},
|
||||
initLangCode: 'en_US',
|
||||
initState: cloneDeep(firstTimeState),
|
||||
infuraProjectId: 'foo',
|
||||
})
|
||||
|
@ -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 (
|
||||
|
@ -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}>
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
])
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user