mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-23 09:52:26 +01:00
4cdf251ea5
This mock Segment server can be used to test our extension metrics. It will respond to all request with HTTP 200, and will print the requests to the console. It also has parsing built-in for Segment request payloads. Right now only the event name is printed, but we can enhance this in the future to print more event information. We can also enhance the mock to be a more realistic representation of the API. The extension has been modified to allow the Segment host to be overwritten with the `SEGMENT_HOST` environment variable. This will ensure that all Segment events are redirected to that host. So for example, to create a dev build that uses this server, you could set the `SEGMENT_WRITE_KEY` and `SEGMENT_LEGACY_WRITE_KEY` values to any non-empty string, and set `SEGMENT_HOST` to `http://localhost:9090`. This was created originally to test PR #9768
279 lines
11 KiB
JavaScript
279 lines
11 KiB
JavaScript
import Analytics from 'analytics-node'
|
|
import { 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 {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 = {},
|
|
revenue,
|
|
currency,
|
|
value,
|
|
metaMetricsId: metaMetricsIdOverride,
|
|
excludeMetaMetricsId: excludeId,
|
|
matomoEvent = false,
|
|
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,
|
|
},
|
|
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)
|
|
}
|
|
})
|
|
}
|
|
}
|