1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-10-22 11:22:43 +02: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:
Brad Decker 2020-10-21 16:10:55 -05:00 committed by GitHub
parent b369a68eb3
commit e5688c024e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 300 additions and 259 deletions

View File

@ -76,7 +76,7 @@ export default class TransactionController extends EventEmitter {
this.blockTracker = opts.blockTracker
this.signEthTx = opts.signTransaction
this.inProcessOfSigning = new Set()
this._trackSegmentEvent = opts.trackSegmentEvent
this._trackMetaMetricsEvent = opts.trackMetaMetricsEvent
this._getParticipateInMetrics = opts.getParticipateInMetrics
this.memStore = new ObservableStore({})
@ -829,13 +829,13 @@ export default class TransactionController extends EventEmitter {
_trackSwapsMetrics (txMeta, approvalTxMeta) {
if (this._getParticipateInMetrics() && txMeta.swapMetaData) {
if (txMeta.txReceipt.status === '0x0') {
this._trackSegmentEvent({
this._trackMetaMetricsEvent({
event: 'Swap Failed',
category: 'swaps',
excludeMetaMetricsId: false,
})
this._trackSegmentEvent({
this._trackMetaMetricsEvent({
event: 'Swap Failed',
properties: { ...txMeta.swapMetaData },
category: 'swaps',
@ -865,13 +865,13 @@ export default class TransactionController extends EventEmitter {
.round(2)
}%`
this._trackSegmentEvent({
this._trackMetaMetricsEvent({
event: 'Swap Completed',
category: 'swaps',
excludeMetaMetricsId: false,
})
this._trackSegmentEvent({
this._trackMetaMetricsEvent({
event: 'Swap Completed',
category: 'swaps',
properties: {

View File

@ -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_NOTIFICATION = 'notification'
const ENVIRONMENT_TYPE_FULLSCREEN = 'fullscreen'

View File

@ -50,7 +50,12 @@ function logWeb3UsageHandler (
event: `Website Accessed window.web3`,
category: 'inpage_provider',
properties: { action, web3Property: name },
referrerUrl: origin,
eventContext: {
referrer: {
url: origin,
},
},
excludeMetaMetricsId: true,
})
}

View File

@ -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)
}
}

View File

@ -24,6 +24,7 @@ import {
CurrencyRateController,
PhishingController,
} from '@metamask/controllers'
import { getTrackMetaMetricsEvent } from '../../shared/modules/metametrics'
import ComposableObservableStore from './lib/ComposableObservableStore'
import AccountTracker from './lib/account-tracker'
import createLoggerMiddleware from './lib/createLoggerMiddleware'
@ -55,9 +56,9 @@ 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 { getTrackSegmentEvent } from './lib/segment'
import backgroundMetaMetricsEvent from './lib/background-metametrics'
import { ENVIRONMENT_TYPE_BACKGROUND } from './lib/enums'
export default class MetamaskController extends EventEmitter {
@ -115,16 +116,32 @@ export default class MetamaskController extends EventEmitter {
migrateAddressBookState: this.migrateAddressBookState.bind(this),
})
// This depends on preferences controller state
this.trackSegmentEvent = getTrackSegmentEvent(
this.trackMetaMetricsEvent = getTrackMetaMetricsEvent(
this.platform.getVersion(),
() => this.preferencesController.getParticipateInMetaMetrics(),
() => {
const participateInMetaMetrics = this.preferencesController.getParticipateInMetaMetrics()
const {
currentLocale,
metaMetricsId,
} = 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),
provider: this.provider,
blockTracker: this.blockTracker,
trackSegmentEvent: this.trackSegmentEvent,
trackMetaMetricsEvent: this.trackMetaMetricsEvent,
getParticipateInMetrics: () => this.preferencesController.getParticipateInMetaMetrics(),
})
this.txController.on('newUnapprovedTx', () => opts.showUnapprovedTx())
@ -1671,7 +1688,7 @@ export default class MetamaskController extends EventEmitter {
}))
engine.push(createMethodMiddleware({
origin,
sendMetrics: this.trackSegmentEvent,
sendMetrics: this.trackMetaMetricsEvent,
}))
// filter and subscription polyfills
engine.push(filterMiddleware)

View 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()
})
})
}
}

View File

@ -17,8 +17,8 @@ import {
import { getEnvironmentType } from '../../../app/scripts/lib/util'
import {
sendMetaMetricsEvent,
sendCountIsTrackable,
} from '../helpers/utils/metametrics.util'
import { sendCountIsTrackable } from '../../../shared/modules/metametrics'
export const MetaMetricsContext = createContext(() => {
captureException(

View File

@ -3,26 +3,19 @@
* MetaMetrics is our own brand, and should remain aptly named regardless of the underlying
* 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 PropTypes from 'prop-types'
import { useLocation, matchPath, useRouteMatch } from 'react-router-dom'
import { captureException, captureMessage } from '@sentry/browser'
import { omit } from 'lodash'
import {
getCurrentNetworkId,
} from '../selectors/selectors'
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 { 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(() => {
captureException(
@ -34,7 +27,6 @@ const PATHS_TO_CHECK = Object.keys(PATH_NAME_MAP)
function useSegmentContext () {
const match = useRouteMatch({ path: PATHS_TO_CHECK, exact: true, strict: true })
const locale = useSelector(getCurrentLocale)
const txData = useSelector(txDataSelector) || {}
const confirmTransactionOrigin = txData.origin
@ -42,11 +34,6 @@ function useSegmentContext () {
url: confirmTransactionOrigin,
} : undefined
let version = global.platform.getVersion()
if (process.env.METAMASK_ENVIRONMENT !== 'production') {
version = `${version}-${process.env.METAMASK_ENVIRONMENT}`
}
const page = match ? {
path: match.path,
title: PATH_NAME_MAP[match.path],
@ -54,24 +41,39 @@ function useSegmentContext () {
} : undefined
return {
app: {
version,
name: 'MetaMask Extension',
},
locale: locale.replace('_', '-'),
page,
referrer,
userAgent: window.navigator.userAgent,
}
}
export function MetaMetricsProvider ({ children }) {
const network = useSelector(getCurrentNetworkId)
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)
/**
* 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
const previousMatch = useRef()
@ -131,72 +133,6 @@ export function MetaMetricsProvider ({ children }) {
}
}, [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 (
<MetaMetricsContext.Provider value={trackEvent}>
{children}

View File

@ -1,7 +1,6 @@
/* eslint camelcase: 0 */
import ethUtil from 'ethereumjs-util'
import Analytics from 'analytics-node'
const inDevelopment = process.env.METAMASK_DEBUG || process.env.IN_TEST
@ -74,8 +73,6 @@ const customDimensionsNameIdMap = {
[METAMETRICS_CUSTOM_VERSION]: 11,
}
export const METAMETRICS_ANONYMOUS_ID = '0x0000000000000000'
function composeUrlRefParamAddition (previousPath, confirmTransactionOrigin) {
const externalOrigin = confirmTransactionOrigin && confirmTransactionOrigin !== 'metamask'
return `&urlref=${externalOrigin ? 'EXTERNAL' : encodeURIComponent(`${METAMETRICS_TRACKING_URL}${previousPath}`)}`
@ -217,43 +214,3 @@ export function verifyUserPermission (config, props) {
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

View File

@ -14,6 +14,16 @@ export function getNetworkIdentifier (state) {
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) {
const identity = getSelectedIdentity(state)
@ -156,7 +166,7 @@ export function getAssetImages (state) {
}
export function getAddressBook (state) {
const { chainId } = state.metamask.provider
const chainId = getCurrentChainId(state)
if (!state.metamask.addressBook[chainId]) {
return []
}