mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 18:00:18 +01:00
consolidate segment setup (#9617)
Consolidates the background and UI segment implementations into a shared solution. This results in the introduction of our first shared module. Co-authored-by: Erik Marks <25517051+rekmarks@users.noreply.github.com>
This commit is contained in:
parent
b369a68eb3
commit
e5688c024e
@ -76,7 +76,7 @@ export default class TransactionController extends EventEmitter {
|
|||||||
this.blockTracker = opts.blockTracker
|
this.blockTracker = opts.blockTracker
|
||||||
this.signEthTx = opts.signTransaction
|
this.signEthTx = opts.signTransaction
|
||||||
this.inProcessOfSigning = new Set()
|
this.inProcessOfSigning = new Set()
|
||||||
this._trackSegmentEvent = opts.trackSegmentEvent
|
this._trackMetaMetricsEvent = opts.trackMetaMetricsEvent
|
||||||
this._getParticipateInMetrics = opts.getParticipateInMetrics
|
this._getParticipateInMetrics = opts.getParticipateInMetrics
|
||||||
|
|
||||||
this.memStore = new ObservableStore({})
|
this.memStore = new ObservableStore({})
|
||||||
@ -829,13 +829,13 @@ export default class TransactionController extends EventEmitter {
|
|||||||
_trackSwapsMetrics (txMeta, approvalTxMeta) {
|
_trackSwapsMetrics (txMeta, approvalTxMeta) {
|
||||||
if (this._getParticipateInMetrics() && txMeta.swapMetaData) {
|
if (this._getParticipateInMetrics() && txMeta.swapMetaData) {
|
||||||
if (txMeta.txReceipt.status === '0x0') {
|
if (txMeta.txReceipt.status === '0x0') {
|
||||||
this._trackSegmentEvent({
|
this._trackMetaMetricsEvent({
|
||||||
event: 'Swap Failed',
|
event: 'Swap Failed',
|
||||||
category: 'swaps',
|
category: 'swaps',
|
||||||
excludeMetaMetricsId: false,
|
excludeMetaMetricsId: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
this._trackSegmentEvent({
|
this._trackMetaMetricsEvent({
|
||||||
event: 'Swap Failed',
|
event: 'Swap Failed',
|
||||||
properties: { ...txMeta.swapMetaData },
|
properties: { ...txMeta.swapMetaData },
|
||||||
category: 'swaps',
|
category: 'swaps',
|
||||||
@ -865,13 +865,13 @@ export default class TransactionController extends EventEmitter {
|
|||||||
.round(2)
|
.round(2)
|
||||||
}%`
|
}%`
|
||||||
|
|
||||||
this._trackSegmentEvent({
|
this._trackMetaMetricsEvent({
|
||||||
event: 'Swap Completed',
|
event: 'Swap Completed',
|
||||||
category: 'swaps',
|
category: 'swaps',
|
||||||
excludeMetaMetricsId: false,
|
excludeMetaMetricsId: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
this._trackSegmentEvent({
|
this._trackMetaMetricsEvent({
|
||||||
event: 'Swap Completed',
|
event: 'Swap Completed',
|
||||||
category: 'swaps',
|
category: 'swaps',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -1,3 +1,12 @@
|
|||||||
|
/**
|
||||||
|
* A string representing the type of environment the application is currently running in
|
||||||
|
* popup - When the user click's the icon in their browser's extension bar; the default view
|
||||||
|
* notification - When the extension opens due to interaction with a Web3 enabled website
|
||||||
|
* fullscreen - When the user clicks 'expand view' to open the extension in a new tab
|
||||||
|
* background - The background process that powers the extension
|
||||||
|
* @typedef {'popup' | 'notification' | 'fullscreen' | 'background'} EnvironmentType
|
||||||
|
*/
|
||||||
|
|
||||||
const ENVIRONMENT_TYPE_POPUP = 'popup'
|
const ENVIRONMENT_TYPE_POPUP = 'popup'
|
||||||
const ENVIRONMENT_TYPE_NOTIFICATION = 'notification'
|
const ENVIRONMENT_TYPE_NOTIFICATION = 'notification'
|
||||||
const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen'
|
const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen'
|
||||||
|
@ -50,7 +50,12 @@ function logWeb3UsageHandler (
|
|||||||
event: `Website Accessed window.web3`,
|
event: `Website Accessed window.web3`,
|
||||||
category: 'inpage_provider',
|
category: 'inpage_provider',
|
||||||
properties: { action, web3Property: name },
|
properties: { action, web3Property: name },
|
||||||
referrerUrl: origin,
|
eventContext: {
|
||||||
|
referrer: {
|
||||||
|
url: origin,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
excludeMetaMetricsId: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,112 +0,0 @@
|
|||||||
import Analytics from 'analytics-node'
|
|
||||||
|
|
||||||
const inDevelopment = process.env.METAMASK_DEBUG || process.env.IN_TEST
|
|
||||||
|
|
||||||
const flushAt = inDevelopment ? 1 : undefined
|
|
||||||
|
|
||||||
const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'
|
|
||||||
|
|
||||||
const segmentNoop = {
|
|
||||||
track () {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
identify () {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// which process.env.IN_TEST is true
|
|
||||||
const segment = process.env.SEGMENT_WRITE_KEY
|
|
||||||
? new Analytics(process.env.SEGMENT_WRITE_KEY, { flushAt })
|
|
||||||
: segmentNoop
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a function for tracking Segment events.
|
|
||||||
*
|
|
||||||
* @param {string} metamaskVersion - The current version of the MetaMask
|
|
||||||
* extension.
|
|
||||||
* @param {Function} getParticipateInMetrics - A function that returns
|
|
||||||
* whether the user participates in MetaMetrics.
|
|
||||||
* @param {Function} getMetricsState - A function for getting state relevant
|
|
||||||
* to MetaMetrics/Segment.
|
|
||||||
*/
|
|
||||||
export function getTrackSegmentEvent (
|
|
||||||
metamaskVersion,
|
|
||||||
getParticipateInMetrics,
|
|
||||||
getMetricsState,
|
|
||||||
) {
|
|
||||||
const version = process.env.METAMASK_ENVIRONMENT === 'production'
|
|
||||||
? metamaskVersion
|
|
||||||
: `${metamaskVersion}-${process.env.METAMASK_ENVIRONMENT}`
|
|
||||||
|
|
||||||
const segmentContext = {
|
|
||||||
app: {
|
|
||||||
name: 'MetaMask Extension',
|
|
||||||
version,
|
|
||||||
},
|
|
||||||
page: {
|
|
||||||
path: '/background-process',
|
|
||||||
title: 'Background Process',
|
|
||||||
url: '/background-process',
|
|
||||||
},
|
|
||||||
userAgent: window.navigator.userAgent,
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tracks a Segment event per the given arguments.
|
|
||||||
*
|
|
||||||
* @param {string} event - The event name.
|
|
||||||
* @param {string} category - The event category.
|
|
||||||
* @param {Object} [properties] - The event properties.
|
|
||||||
* @param {string} [referrerUrl] - The event's referrer URL, if relevant.
|
|
||||||
* @param {boolean} [excludeMetaMetricsId] - `true` if the user's MetaMetrics id should
|
|
||||||
* not be included, and `false` otherwise. Default: `true`
|
|
||||||
*/
|
|
||||||
return function trackSegmentEvent ({
|
|
||||||
event,
|
|
||||||
category,
|
|
||||||
properties = {},
|
|
||||||
excludeMetaMetricsId = true,
|
|
||||||
referrerUrl,
|
|
||||||
}) {
|
|
||||||
if (!event || !category) {
|
|
||||||
throw new Error('Must specify event and category.')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!getParticipateInMetrics()) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const { currentLocale, metaMetricsId } = getMetricsState()
|
|
||||||
|
|
||||||
const trackOptions = {
|
|
||||||
event,
|
|
||||||
category,
|
|
||||||
context: {
|
|
||||||
...segmentContext,
|
|
||||||
locale: currentLocale.replace('_', '-'),
|
|
||||||
},
|
|
||||||
properties,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (excludeMetaMetricsId) {
|
|
||||||
trackOptions.anonymousId = METAMETRICS_ANONYMOUS_ID
|
|
||||||
} else {
|
|
||||||
trackOptions.userId = metaMetricsId
|
|
||||||
}
|
|
||||||
|
|
||||||
if (referrerUrl) {
|
|
||||||
trackOptions.context.referrer = {
|
|
||||||
url: referrerUrl,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
segment.track(trackOptions)
|
|
||||||
}
|
|
||||||
}
|
|
@ -24,6 +24,7 @@ import {
|
|||||||
CurrencyRateController,
|
CurrencyRateController,
|
||||||
PhishingController,
|
PhishingController,
|
||||||
} from '@metamask/controllers'
|
} from '@metamask/controllers'
|
||||||
|
import { getTrackMetaMetricsEvent } from '../../shared/modules/metametrics'
|
||||||
import ComposableObservableStore from './lib/ComposableObservableStore'
|
import ComposableObservableStore from './lib/ComposableObservableStore'
|
||||||
import AccountTracker from './lib/account-tracker'
|
import AccountTracker from './lib/account-tracker'
|
||||||
import createLoggerMiddleware from './lib/createLoggerMiddleware'
|
import createLoggerMiddleware from './lib/createLoggerMiddleware'
|
||||||
@ -55,9 +56,9 @@ import getRestrictedMethods from './controllers/permissions/restrictedMethods'
|
|||||||
import nodeify from './lib/nodeify'
|
import nodeify from './lib/nodeify'
|
||||||
import accountImporter from './account-import-strategies'
|
import accountImporter from './account-import-strategies'
|
||||||
import seedPhraseVerifier from './lib/seed-phrase-verifier'
|
import seedPhraseVerifier from './lib/seed-phrase-verifier'
|
||||||
import { getTrackSegmentEvent } from './lib/segment'
|
|
||||||
|
|
||||||
import backgroundMetaMetricsEvent from './lib/background-metametrics'
|
import backgroundMetaMetricsEvent from './lib/background-metametrics'
|
||||||
|
import { ENVIRONMENT_TYPE_BACKGROUND } from './lib/enums'
|
||||||
|
|
||||||
export default class MetamaskController extends EventEmitter {
|
export default class MetamaskController extends EventEmitter {
|
||||||
|
|
||||||
@ -115,16 +116,32 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
migrateAddressBookState: this.migrateAddressBookState.bind(this),
|
migrateAddressBookState: this.migrateAddressBookState.bind(this),
|
||||||
})
|
})
|
||||||
|
|
||||||
// This depends on preferences controller state
|
this.trackMetaMetricsEvent = getTrackMetaMetricsEvent(
|
||||||
this.trackSegmentEvent = getTrackSegmentEvent(
|
|
||||||
this.platform.getVersion(),
|
this.platform.getVersion(),
|
||||||
() => this.preferencesController.getParticipateInMetaMetrics(),
|
|
||||||
() => {
|
() => {
|
||||||
|
const participateInMetaMetrics = this.preferencesController.getParticipateInMetaMetrics()
|
||||||
const {
|
const {
|
||||||
currentLocale,
|
currentLocale,
|
||||||
metaMetricsId,
|
metaMetricsId,
|
||||||
} = this.preferencesController.store.getState()
|
} = this.preferencesController.store.getState()
|
||||||
return { currentLocale, metaMetricsId }
|
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('_', '-'),
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -252,7 +269,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
|
signTransaction: this.keyringController.signTransaction.bind(this.keyringController),
|
||||||
provider: this.provider,
|
provider: this.provider,
|
||||||
blockTracker: this.blockTracker,
|
blockTracker: this.blockTracker,
|
||||||
trackSegmentEvent: this.trackSegmentEvent,
|
trackMetaMetricsEvent: this.trackMetaMetricsEvent,
|
||||||
getParticipateInMetrics: () => this.preferencesController.getParticipateInMetaMetrics(),
|
getParticipateInMetrics: () => this.preferencesController.getParticipateInMetaMetrics(),
|
||||||
})
|
})
|
||||||
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
|
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
|
||||||
@ -1671,7 +1688,7 @@ export default class MetamaskController extends EventEmitter {
|
|||||||
}))
|
}))
|
||||||
engine.push(createMethodMiddleware({
|
engine.push(createMethodMiddleware({
|
||||||
origin,
|
origin,
|
||||||
sendMetrics: this.trackSegmentEvent,
|
sendMetrics: this.trackMetaMetricsEvent,
|
||||||
}))
|
}))
|
||||||
// filter and subscription polyfills
|
// filter and subscription polyfills
|
||||||
engine.push(filterMiddleware)
|
engine.push(filterMiddleware)
|
||||||
|
219
shared/modules/metametrics.js
Normal file
219
shared/modules/metametrics.js
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
import Analytics from 'analytics-node'
|
||||||
|
import { omit, pick } from 'lodash'
|
||||||
|
|
||||||
|
// flushAt controls how many events are collected in the queue before they
|
||||||
|
// are sent to segment. I recommend a queue size of one due to an issue with
|
||||||
|
// detecting and flushing events in an extension beforeunload doesn't work in
|
||||||
|
// a notification context. Because notification windows are opened and closed
|
||||||
|
// in reaction to the very events we want to track, it is problematic to cache
|
||||||
|
// at all.
|
||||||
|
const flushAt = 1
|
||||||
|
|
||||||
|
export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'
|
||||||
|
|
||||||
|
const segmentNoop = {
|
||||||
|
track () {
|
||||||
|
// noop
|
||||||
|
},
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
? new Analytics(process.env.SEGMENT_WRITE_KEY, { flushAt })
|
||||||
|
: segmentNoop
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {string} locale - the locale string for 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 {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 {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 = {},
|
||||||
|
revenue,
|
||||||
|
currency,
|
||||||
|
value,
|
||||||
|
excludeMetaMetricsId: excludeId,
|
||||||
|
eventContext = {},
|
||||||
|
}) {
|
||||||
|
if (!event || !category) {
|
||||||
|
throw new Error('Must specify event and category.')
|
||||||
|
}
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
locale,
|
||||||
|
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.
|
||||||
|
...omit(properties, ['revenue', 'currency', 'value']),
|
||||||
|
revenue,
|
||||||
|
value,
|
||||||
|
currency,
|
||||||
|
category,
|
||||||
|
network,
|
||||||
|
chain_id: chainId,
|
||||||
|
environment_type: environmentType,
|
||||||
|
},
|
||||||
|
context,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (excludeMetaMetricsId) {
|
||||||
|
trackOptions.anonymousId = METAMETRICS_ANONYMOUS_ID
|
||||||
|
} else {
|
||||||
|
trackOptions.userId = metaMetricsId
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
// This is only safe to do because we are no longer batching events through segment.
|
||||||
|
// If flushAt is greater than one the callback won't be triggered until after a number
|
||||||
|
// of events have been queued equal to the flushAt value OR flushInterval passes. The
|
||||||
|
// default flushInterval is ten seconds
|
||||||
|
segment.track(trackOptions, (err) => {
|
||||||
|
if (err) {
|
||||||
|
return reject(err)
|
||||||
|
}
|
||||||
|
return resolve()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -17,8 +17,8 @@ import {
|
|||||||
import { getEnvironmentType } from '../../../app/scripts/lib/util'
|
import { getEnvironmentType } from '../../../app/scripts/lib/util'
|
||||||
import {
|
import {
|
||||||
sendMetaMetricsEvent,
|
sendMetaMetricsEvent,
|
||||||
sendCountIsTrackable,
|
|
||||||
} from '../helpers/utils/metametrics.util'
|
} from '../helpers/utils/metametrics.util'
|
||||||
|
import { sendCountIsTrackable } from '../../../shared/modules/metametrics'
|
||||||
|
|
||||||
export const MetaMetricsContext = createContext(() => {
|
export const MetaMetricsContext = createContext(() => {
|
||||||
captureException(
|
captureException(
|
||||||
|
@ -3,26 +3,19 @@
|
|||||||
* MetaMetrics is our own brand, and should remain aptly named regardless of the underlying
|
* MetaMetrics is our own brand, and should remain aptly named regardless of the underlying
|
||||||
* metrics system. This file implements Segment analytics tracking.
|
* metrics system. This file implements Segment analytics tracking.
|
||||||
*/
|
*/
|
||||||
import React, { useRef, Component, createContext, useEffect, useCallback } from 'react'
|
import React, { useRef, Component, createContext, useEffect, useMemo } from 'react'
|
||||||
import { useSelector } from 'react-redux'
|
import { useSelector } from 'react-redux'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import { useLocation, matchPath, useRouteMatch } from 'react-router-dom'
|
import { useLocation, matchPath, useRouteMatch } from 'react-router-dom'
|
||||||
import { captureException, captureMessage } from '@sentry/browser'
|
import { captureException, captureMessage } from '@sentry/browser'
|
||||||
|
|
||||||
import { omit } from 'lodash'
|
import { omit } from 'lodash'
|
||||||
import {
|
|
||||||
getCurrentNetworkId,
|
|
||||||
} from '../selectors/selectors'
|
|
||||||
|
|
||||||
import { getEnvironmentType } from '../../../app/scripts/lib/util'
|
import { getEnvironmentType } from '../../../app/scripts/lib/util'
|
||||||
import {
|
|
||||||
sendCountIsTrackable,
|
|
||||||
segment,
|
|
||||||
METAMETRICS_ANONYMOUS_ID,
|
|
||||||
} from '../helpers/utils/metametrics.util'
|
|
||||||
import { PATH_NAME_MAP } from '../helpers/constants/routes'
|
import { PATH_NAME_MAP } from '../helpers/constants/routes'
|
||||||
import { getCurrentLocale } from '../ducks/metamask/metamask'
|
import { getCurrentLocale } from '../ducks/metamask/metamask'
|
||||||
import { txDataSelector } from '../selectors'
|
import { getCurrentChainId, getMetricsNetworkIdentifier, txDataSelector } from '../selectors'
|
||||||
|
import { getTrackMetaMetricsEvent, METAMETRICS_ANONYMOUS_ID, segment } from '../../../shared/modules/metametrics'
|
||||||
|
|
||||||
export const MetaMetricsContext = createContext(() => {
|
export const MetaMetricsContext = createContext(() => {
|
||||||
captureException(
|
captureException(
|
||||||
@ -34,7 +27,6 @@ const PATHS_TO_CHECK = Object.keys(PATH_NAME_MAP)
|
|||||||
|
|
||||||
function useSegmentContext () {
|
function useSegmentContext () {
|
||||||
const match = useRouteMatch({ path: PATHS_TO_CHECK, exact: true, strict: true })
|
const match = useRouteMatch({ path: PATHS_TO_CHECK, exact: true, strict: true })
|
||||||
const locale = useSelector(getCurrentLocale)
|
|
||||||
const txData = useSelector(txDataSelector) || {}
|
const txData = useSelector(txDataSelector) || {}
|
||||||
const confirmTransactionOrigin = txData.origin
|
const confirmTransactionOrigin = txData.origin
|
||||||
|
|
||||||
@ -42,11 +34,6 @@ function useSegmentContext () {
|
|||||||
url: confirmTransactionOrigin,
|
url: confirmTransactionOrigin,
|
||||||
} : undefined
|
} : undefined
|
||||||
|
|
||||||
let version = global.platform.getVersion()
|
|
||||||
if (process.env.METAMASK_ENVIRONMENT !== 'production') {
|
|
||||||
version = `${version}-${process.env.METAMASK_ENVIRONMENT}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const page = match ? {
|
const page = match ? {
|
||||||
path: match.path,
|
path: match.path,
|
||||||
title: PATH_NAME_MAP[match.path],
|
title: PATH_NAME_MAP[match.path],
|
||||||
@ -54,24 +41,39 @@ function useSegmentContext () {
|
|||||||
} : undefined
|
} : undefined
|
||||||
|
|
||||||
return {
|
return {
|
||||||
app: {
|
|
||||||
version,
|
|
||||||
name: 'MetaMask Extension',
|
|
||||||
},
|
|
||||||
locale: locale.replace('_', '-'),
|
|
||||||
page,
|
page,
|
||||||
referrer,
|
referrer,
|
||||||
userAgent: window.navigator.userAgent,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MetaMetricsProvider ({ children }) {
|
export function MetaMetricsProvider ({ children }) {
|
||||||
const network = useSelector(getCurrentNetworkId)
|
|
||||||
const metaMetricsId = useSelector((state) => state.metamask.metaMetricsId)
|
const metaMetricsId = useSelector((state) => state.metamask.metaMetricsId)
|
||||||
const participateInMetaMetrics = useSelector((state) => state.metamask.participateInMetaMetrics)
|
const participateInMetaMetrics = useSelector((state) => state.metamask.participateInMetaMetrics)
|
||||||
const metaMetricsSendCount = useSelector((state) => state.metamask.metaMetricsSendCount)
|
const metaMetricsSendCount = useSelector((state) => state.metamask.metaMetricsSendCount)
|
||||||
|
const locale = useSelector(getCurrentLocale)
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const context = useSegmentContext()
|
const context = useSegmentContext()
|
||||||
|
const network = useSelector(getMetricsNetworkIdentifier)
|
||||||
|
const chainId = useSelector(getCurrentChainId)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* track a metametrics event
|
||||||
|
*
|
||||||
|
* @param {import('../../../shared/modules/metametrics').MetaMetricsEventPayload} - payload for event
|
||||||
|
* @returns undefined
|
||||||
|
*/
|
||||||
|
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])
|
||||||
|
|
||||||
// Used to prevent double tracking page calls
|
// Used to prevent double tracking page calls
|
||||||
const previousMatch = useRef()
|
const previousMatch = useRef()
|
||||||
@ -131,72 +133,6 @@ export function MetaMetricsProvider ({ children }) {
|
|||||||
}
|
}
|
||||||
}, [location, context, network, metaMetricsId, participateInMetaMetrics])
|
}, [location, context, network, metaMetricsId, participateInMetaMetrics])
|
||||||
|
|
||||||
/**
|
|
||||||
* 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}
|
|
||||||
*/
|
|
||||||
const trackEvent = useCallback(
|
|
||||||
(config = {}) => {
|
|
||||||
const { event, category, isOptIn = false, properties = {}, revenue, value, currency } = config
|
|
||||||
if (!event) {
|
|
||||||
// Event name is required for tracking an event
|
|
||||||
throw new Error('MetaMetrics trackEvent function must be provided a payload with an "event" key')
|
|
||||||
}
|
|
||||||
if (!category) {
|
|
||||||
// Category must be supplied for every tracking event
|
|
||||||
throw new Error('MetaMetrics events must be provided a category')
|
|
||||||
}
|
|
||||||
const environmentType = getEnvironmentType()
|
|
||||||
|
|
||||||
let excludeMetaMetricsId = config.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(event.match(/^send|^confirm/u))
|
|
||||||
if (isSendFlow && !sendCountIsTrackable(metaMetricsSendCount + 1)) {
|
|
||||||
excludeMetaMetricsId = true
|
|
||||||
}
|
|
||||||
const idTrait = excludeMetaMetricsId ? 'anonymousId' : 'userId'
|
|
||||||
const idValue = excludeMetaMetricsId ? METAMETRICS_ANONYMOUS_ID : metaMetricsId
|
|
||||||
|
|
||||||
if (participateInMetaMetrics || isOptIn) {
|
|
||||||
segment.track({
|
|
||||||
[idTrait]: idValue,
|
|
||||||
event,
|
|
||||||
properties: {
|
|
||||||
...omit(properties, ['revenue', 'currency', 'value']),
|
|
||||||
revenue,
|
|
||||||
value,
|
|
||||||
currency,
|
|
||||||
category,
|
|
||||||
network,
|
|
||||||
environment_type: environmentType,
|
|
||||||
},
|
|
||||||
context,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined
|
|
||||||
}, [
|
|
||||||
context,
|
|
||||||
network,
|
|
||||||
metaMetricsId,
|
|
||||||
metaMetricsSendCount,
|
|
||||||
participateInMetaMetrics,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MetaMetricsContext.Provider value={trackEvent}>
|
<MetaMetricsContext.Provider value={trackEvent}>
|
||||||
{children}
|
{children}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
/* eslint camelcase: 0 */
|
/* eslint camelcase: 0 */
|
||||||
|
|
||||||
import ethUtil from 'ethereumjs-util'
|
import ethUtil from 'ethereumjs-util'
|
||||||
import Analytics from 'analytics-node'
|
|
||||||
|
|
||||||
const inDevelopment = process.env.METAMASK_DEBUG || process.env.IN_TEST
|
const inDevelopment = process.env.METAMASK_DEBUG || process.env.IN_TEST
|
||||||
|
|
||||||
@ -74,8 +73,6 @@ const customDimensionsNameIdMap = {
|
|||||||
[METAMETRICS_CUSTOM_VERSION]: 11,
|
[METAMETRICS_CUSTOM_VERSION]: 11,
|
||||||
}
|
}
|
||||||
|
|
||||||
export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'
|
|
||||||
|
|
||||||
function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
|
function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
|
||||||
const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'metamask'
|
const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'metamask'
|
||||||
return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(`${METAMETRICS_TRACKING_URL}${previousPath}`)}`
|
return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(`${METAMETRICS_TRACKING_URL}${previousPath}`)}`
|
||||||
@ -217,43 +214,3 @@ export function verifyUserPermission (config, props) {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
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 flushAt = inDevelopment ? 1 : undefined
|
|
||||||
|
|
||||||
const segmentNoop = {
|
|
||||||
track () {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
page () {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
identify () {
|
|
||||||
// noop
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
// which process.env.IN_TEST is true
|
|
||||||
export const segment = process.env.SEGMENT_WRITE_KEY
|
|
||||||
? new Analytics(process.env.SEGMENT_WRITE_KEY, { flushAt })
|
|
||||||
: segmentNoop
|
|
||||||
|
@ -14,6 +14,16 @@ export function getNetworkIdentifier (state) {
|
|||||||
return nickname || rpcUrl || type
|
return nickname || rpcUrl || type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMetricsNetworkIdentifier (state) {
|
||||||
|
const { provider } = state.metamask
|
||||||
|
return provider.type === 'rpc' ? provider.rpcUrl : provider.type
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentChainId (state) {
|
||||||
|
const { chainId } = state.metamask.provider
|
||||||
|
return chainId
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentKeyring (state) {
|
export function getCurrentKeyring (state) {
|
||||||
const identity = getSelectedIdentity(state)
|
const identity = getSelectedIdentity(state)
|
||||||
|
|
||||||
@ -156,7 +166,7 @@ export function getAssetImages (state) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getAddressBook (state) {
|
export function getAddressBook (state) {
|
||||||
const { chainId } = state.metamask.provider
|
const chainId = getCurrentChainId(state)
|
||||||
if (!state.metamask.addressBook[chainId]) {
|
if (!state.metamask.addressBook[chainId]) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user