/** * This file is intended to be renamed to metametrics.js once the conversion is complete. * MetaMetrics is our own brand, and should remain aptly named regardless of the underlying * metrics system. This file implements Segment analytics tracking. */ import React, { Component, createContext, useEffect, useRef, useCallback, } from 'react'; import { useSelector } from 'react-redux'; import PropTypes from 'prop-types'; 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 { 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} UIMetricsEventPayload */ /** * @typedef {( * payload: UIMetricsEventPayload, * options: MetaMetricsEventOptions * ) => Promise} UITrackEventMethod */ /** * @type {React.Context} */ export const MetaMetricsContext = createContext(() => { captureException( Error( `MetaMetrics context function was called from a react node that is not a descendant of a MetaMetrics context provider`, ), ); }); 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, exact: true, strict: true, }); const txData = useSelector(txDataSelector) || {}; const confirmTransactionOrigin = txData.origin; const referrer = confirmTransactionOrigin ? { url: confirmTransactionOrigin, } : undefined; const page = match ? { path: match.path, title: PATH_NAME_MAP[match.path], url: match.path, } : undefined; return { page, referrer, }; } export function MetaMetricsProvider({ children }) { const location = useLocation(); const context = useSegmentContext(); /** * @type {UITrackEventMethod} */ const trackEvent = useCallback( (payload, options) => { trackMetaMetricsEvent( { ...payload, environmentType: getEnvironmentType(), ...context, }, options, ); }, [context], ); // Used to prevent double tracking page calls const previousMatch = useRef(); /** * Anytime the location changes, track a page change with segment. * Previously we would manually track changes to history and keep a * reference to the previous url, but with page tracking we can see * which page the user is on and their navigation path. */ useEffect(() => { const environmentType = getEnvironmentType(); 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, }, }); } 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, // 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'), }, ); } previousMatch.current = match?.path; }, [location, context]); return ( {children} ); } MetaMetricsProvider.propTypes = { children: PropTypes.node }; export class LegacyMetaMetricsProvider extends Component { static propTypes = { children: PropTypes.node, }; static defaultProps = { children: undefined, }; static contextType = MetaMetricsContext; static childContextTypes = { // This has to be different than the type name for the old metametrics file // using the same name would result in whichever was lower in the tree to be // used. trackEvent: PropTypes.func, }; getChildContext() { return { trackEvent: this.context, }; } render() { return this.props.children; } }