mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-12-22 09:23:21 +01:00
Implement event fragments (#12251)
This commit is contained in:
parent
ff27f24ef9
commit
b820ef131b
@ -1,11 +1,13 @@
|
||||
import { merge, omit } from 'lodash';
|
||||
import { merge, omit, omitBy } from 'lodash';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import { bufferToHex, keccak } from 'ethereumjs-util';
|
||||
import { generateUUID } from 'pubnub';
|
||||
import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
|
||||
import {
|
||||
METAMETRICS_ANONYMOUS_ID,
|
||||
METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
|
||||
const defaultCaptureException = (err) => {
|
||||
// throw error on clean stack so its captured by platform integrations (eg sentry)
|
||||
@ -27,15 +29,18 @@ const exceptionsToFilter = {
|
||||
* @typedef {import('../../../shared/constants/metametrics').SegmentInterface} SegmentInterface
|
||||
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPagePayload} MetaMetricsPagePayload
|
||||
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsPageOptions} MetaMetricsPageOptions
|
||||
* @typedef {import('../../../shared/constants/metametrics').MetaMetricsEventFragment} MetaMetricsEventFragment
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MetaMetricsControllerState
|
||||
* @property {?string} metaMetricsId - The user's metaMetricsId that will be
|
||||
* @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
|
||||
* @property {boolean} [participateInMetaMetrics] - The user's preference for
|
||||
* participating in the MetaMetrics analytics program. This setting controls
|
||||
* whether or not events are tracked
|
||||
* @property {{[string]: MetaMetricsEventFragment}} [fragments] - Object keyed
|
||||
* by UUID with stored fragments as values.
|
||||
*/
|
||||
|
||||
export default class MetaMetricsController {
|
||||
@ -81,10 +86,15 @@ export default class MetaMetricsController {
|
||||
this.version =
|
||||
environment === 'production' ? version : `${version}-${environment}`;
|
||||
|
||||
const abandonedFragments = omitBy(initState?.fragments, 'persist');
|
||||
|
||||
this.store = new ObservableStore({
|
||||
participateInMetaMetrics: null,
|
||||
metaMetricsId: null,
|
||||
...initState,
|
||||
fragments: {
|
||||
...initState?.fragments,
|
||||
},
|
||||
});
|
||||
|
||||
preferencesStore.subscribe(({ currentLocale }) => {
|
||||
@ -96,6 +106,32 @@ export default class MetaMetricsController {
|
||||
this.network = getNetworkIdentifier();
|
||||
});
|
||||
this.segment = segment;
|
||||
|
||||
// Track abandoned fragments that weren't properly cleaned up.
|
||||
// Abandoned fragments are those that were stored in persistent memory
|
||||
// and are available at controller instance creation, but do not have the
|
||||
// 'persist' flag set. This means anytime the extension is unlocked, any
|
||||
// fragments that are not marked as persistent will be purged and the
|
||||
// failure event will be emitted.
|
||||
Object.values(abandonedFragments).forEach((fragment) => {
|
||||
this.finalizeEventFragment(fragment.id, { abandoned: true });
|
||||
});
|
||||
|
||||
// Close out event fragments that were created but not progressed. An
|
||||
// interval is used to routinely check if a fragment has not been updated
|
||||
// within the fragment's timeout window. When creating a new event fragment
|
||||
// a timeout can be specified that will cause an abandoned event to be
|
||||
// tracked if the event isn't progressed within that amount of time.
|
||||
setInterval(() => {
|
||||
this.store.getState().fragments.forEach((fragment) => {
|
||||
if (
|
||||
fragment.timeout &&
|
||||
Date.now() - fragment.lastUpdated / 1000 > fragment.timeout
|
||||
) {
|
||||
this.finalizeEventFragment(fragment.id, { abandoned: true });
|
||||
}
|
||||
});
|
||||
}, SECOND * 30);
|
||||
}
|
||||
|
||||
generateMetaMetricsId() {
|
||||
@ -109,6 +145,110 @@ export default class MetaMetricsController {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an event fragment in state and returns the event fragment object.
|
||||
*
|
||||
* @param {MetaMetricsFunnel} options - Fragment settings and properties
|
||||
* to initiate the fragment with.
|
||||
* @returns {MetaMetricsFunnel}
|
||||
*/
|
||||
createEventFragment(options) {
|
||||
if (!options.successEvent || !options.category) {
|
||||
throw new Error(
|
||||
`Must specify success event and category. Success event was: ${
|
||||
options.event
|
||||
}. Category was: ${options.category}. Payload keys were: ${Object.keys(
|
||||
options,
|
||||
)}. ${
|
||||
typeof options.properties === 'object'
|
||||
? `Payload property keys were: ${Object.keys(options.properties)}`
|
||||
: ''
|
||||
}`,
|
||||
);
|
||||
}
|
||||
const { fragments } = this.store.getState();
|
||||
|
||||
const id = generateUUID();
|
||||
const fragment = {
|
||||
id,
|
||||
...options,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
this.store.updateState({
|
||||
fragments: {
|
||||
...fragments,
|
||||
[id]: fragment,
|
||||
},
|
||||
});
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an event fragment in state
|
||||
*
|
||||
* @param {string} id - The fragment id to update
|
||||
* @param {MetaMetricsEventFragment} payload - Fragment settings and
|
||||
* properties to initiate the fragment with.
|
||||
*/
|
||||
updateEventFragment(id, payload) {
|
||||
const { fragments } = this.store.getState();
|
||||
|
||||
const fragment = fragments[id];
|
||||
|
||||
if (!fragment) {
|
||||
throw new Error(`Event fragment with id ${id} does not exist.`);
|
||||
}
|
||||
|
||||
this.store.updateState({
|
||||
fragments: {
|
||||
...fragments,
|
||||
[id]: merge(fragments[id], {
|
||||
...payload,
|
||||
lastUpdated: Date.now(),
|
||||
}),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Finalizes a fragment, tracking either a success event or failure Event
|
||||
* and then removes the fragment from state.
|
||||
*
|
||||
* @param {string} id - UUID of the event fragment to be closed
|
||||
* @param {object} options
|
||||
* @param {boolean} [options.abandoned] - if true track the failure
|
||||
* event instead of the success event
|
||||
* @param {MetaMetricsContext.page} [options.page] - page the final event
|
||||
* occurred on. This will override whatever is set on the fragment
|
||||
* @param {MetaMetricsContext.referrer} [options.referrer] - Dapp that
|
||||
* originated the fragment. This is for fallback only, the fragment referrer
|
||||
* property will take precedence.
|
||||
*/
|
||||
finalizeEventFragment(id, { abandoned = false, page, referrer }) {
|
||||
const fragment = this.store.getState().fragments[id];
|
||||
if (!fragment) {
|
||||
throw new Error(`Funnel with id ${id} does not exist.`);
|
||||
}
|
||||
|
||||
const eventName = abandoned ? fragment.failureEvent : fragment.successEvent;
|
||||
|
||||
this.trackEvent({
|
||||
event: eventName,
|
||||
category: fragment.category,
|
||||
properties: fragment.properties,
|
||||
sensitiveProperties: fragment.sensitiveProperties,
|
||||
page: page ?? fragment.page,
|
||||
referrer: fragment.referrer ?? referrer,
|
||||
revenue: fragment.revenue,
|
||||
value: fragment.value,
|
||||
currency: fragment.currency,
|
||||
environmentType: fragment.environmentType,
|
||||
});
|
||||
const { fragments } = this.store.getState();
|
||||
delete fragments[id];
|
||||
this.store.updateState({ fragments });
|
||||
}
|
||||
|
||||
/**
|
||||
* Setter for the `participateInMetaMetrics` property
|
||||
*
|
||||
|
@ -81,6 +81,28 @@ function getMockPreferencesStore({ currentLocale = LOCALE } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
const SAMPLE_PERSISTED_EVENT = {
|
||||
id: 'testid',
|
||||
persist: true,
|
||||
category: 'Unit Test',
|
||||
successEvent: 'sample persisted event success',
|
||||
failureEvent: 'sample persisted event failure',
|
||||
properties: {
|
||||
test: true,
|
||||
},
|
||||
};
|
||||
|
||||
const SAMPLE_NON_PERSISTED_EVENT = {
|
||||
id: 'testid2',
|
||||
persist: false,
|
||||
category: 'Unit Test',
|
||||
successEvent: 'sample non-persisted event success',
|
||||
failureEvent: 'sample non-persisted event failure',
|
||||
properties: {
|
||||
test: true,
|
||||
},
|
||||
};
|
||||
|
||||
function getMetaMetricsController({
|
||||
participateInMetaMetrics = true,
|
||||
metaMetricsId = TEST_META_METRICS_ID,
|
||||
@ -105,12 +127,29 @@ function getMetaMetricsController({
|
||||
initState: {
|
||||
participateInMetaMetrics,
|
||||
metaMetricsId,
|
||||
fragments: {
|
||||
testid: SAMPLE_PERSISTED_EVENT,
|
||||
testid2: SAMPLE_NON_PERSISTED_EVENT,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
describe('MetaMetricsController', function () {
|
||||
describe('constructor', function () {
|
||||
it('should properly initialize', function () {
|
||||
const mock = sinon.mock(segment);
|
||||
mock
|
||||
.expects('track')
|
||||
.once()
|
||||
.withArgs({
|
||||
event: 'sample non-persisted event failure',
|
||||
userId: TEST_META_METRICS_ID,
|
||||
context: DEFAULT_TEST_CONTEXT,
|
||||
properties: {
|
||||
...DEFAULT_EVENT_PROPERTIES,
|
||||
test: true,
|
||||
},
|
||||
});
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
assert.strictEqual(metaMetricsController.version, VERSION);
|
||||
assert.strictEqual(metaMetricsController.network, NETWORK);
|
||||
@ -127,6 +166,10 @@ describe('MetaMetricsController', function () {
|
||||
metaMetricsController.locale,
|
||||
LOCALE.replace('_', '-'),
|
||||
);
|
||||
assert.deepStrictEqual(metaMetricsController.state.fragments, {
|
||||
testid: SAMPLE_PERSISTED_EVENT,
|
||||
});
|
||||
mock.verify();
|
||||
});
|
||||
|
||||
it('should update when network changes', function () {
|
||||
|
@ -1408,6 +1408,15 @@ export default class MetamaskController extends EventEmitter {
|
||||
trackMetaMetricsPage: metaMetricsController.trackPage.bind(
|
||||
metaMetricsController,
|
||||
),
|
||||
createEventFragment: metaMetricsController.createEventFragment.bind(
|
||||
metaMetricsController,
|
||||
),
|
||||
updateEventFragment: metaMetricsController.updateEventFragment.bind(
|
||||
metaMetricsController,
|
||||
),
|
||||
finalizeEventFragment: metaMetricsController.finalizeEventFragment.bind(
|
||||
metaMetricsController,
|
||||
),
|
||||
|
||||
// approval controller
|
||||
resolvePendingApproval: approvalController.accept.bind(
|
||||
|
@ -82,6 +82,39 @@
|
||||
* segment source that marks the event data as not conforming to our schema
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {Object} MetaMetricsEventFragment
|
||||
* @property {string} successEvent - The event name to fire when the fragment
|
||||
* is closed in an affirmative action.
|
||||
* @property {string} [failureEvent] - The event name to fire when the fragment
|
||||
* is closed with a rejection.
|
||||
* @property {string} category - the event category to use for both the success
|
||||
* and failure events
|
||||
* @property {boolean} [persist] - Should this fragment be persisted in
|
||||
* state and progressed after the extension is locked and unlocked.
|
||||
* @property {number} [timeout] - Time in seconds the event should be persisted
|
||||
* for. After the timeout the fragment will be closed as abandoned. if not
|
||||
* supplied the fragment is stored indefinitely.
|
||||
* @property {number} [lastUpdated] - Date.now() when the fragment was last
|
||||
* updated. Used to determine if the timeout has expired and the fragment
|
||||
* should be closed.
|
||||
* @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 if fragment is successful.
|
||||
* @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 successfully complete this fragment
|
||||
* @property {MetaMetricsPageObject} [page] - the page/route that the event
|
||||
* occurred on
|
||||
* @property {MetaMetricsReferrerObject} [referrer] - the origin of the dapp
|
||||
* that initiated the event fragment.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents the shape of data sent to the segment.track method.
|
||||
*
|
||||
|
@ -3,7 +3,6 @@ import React, {
|
||||
createContext,
|
||||
useEffect,
|
||||
useCallback,
|
||||
useContext,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
@ -125,10 +124,6 @@ export function MetaMetricsProvider({ children }) {
|
||||
|
||||
MetaMetricsProvider.propTypes = { children: PropTypes.node };
|
||||
|
||||
export function useMetaMetricsContext() {
|
||||
return useContext(MetaMetricsContext);
|
||||
}
|
||||
|
||||
export class LegacyMetaMetricsProvider extends Component {
|
||||
static propTypes = {
|
||||
children: PropTypes.node,
|
||||
|
@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import { TRANSACTION_TYPES } from '../../shared/constants/transaction';
|
||||
import { getMethodName } from '../helpers/utils/metrics';
|
||||
import { useGasFeeContext } from './gasFee';
|
||||
import { useMetaMetricsContext } from './metametrics';
|
||||
import { MetaMetricsContext } from './metametrics';
|
||||
|
||||
export const TransactionModalContext = createContext({});
|
||||
|
||||
@ -15,7 +15,7 @@ export const TransactionModalContextProvider = ({
|
||||
captureEventEnabled = true,
|
||||
}) => {
|
||||
const [openModals, setOpenModals] = useState([]);
|
||||
const metricsEvent = useMetaMetricsContext();
|
||||
const metricsEvent = useContext(MetaMetricsContext);
|
||||
const { transaction: { origin } = {} } = useGasFeeContext();
|
||||
|
||||
const captureEvent = () => {
|
||||
|
100
ui/hooks/useEventFragment.js
Normal file
100
ui/hooks/useEventFragment.js
Normal file
@ -0,0 +1,100 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { getEnvironmentType } from '../../app/scripts/lib/util';
|
||||
import { selectMatchingFragment } from '../selectors';
|
||||
import {
|
||||
finalizeEventFragment,
|
||||
createEventFragment,
|
||||
updateEventFragment,
|
||||
} from '../store/actions';
|
||||
import { useMetaMetricsContext } from './useMetricEvent';
|
||||
|
||||
/**
|
||||
* Retrieves a fragment from memory or initializes new fragment if one does not
|
||||
* exist. Returns three methods that are tied to the fragment, as well as the
|
||||
* fragment id.
|
||||
*
|
||||
* @param {string} existingId
|
||||
* @param {Object} fragmentOptions
|
||||
* @returns
|
||||
*/
|
||||
export function useEventFragment(existingId, fragmentOptions) {
|
||||
// To prevent overcalling the createEventFragment background method a ref
|
||||
// is used to store a boolean value of whether we have already called the
|
||||
// method.
|
||||
const createEventFragmentCalled = useRef(false);
|
||||
|
||||
// In order to immediately return a created fragment, instead of waiting for
|
||||
// background state to update and find the newly created fragment, we have a
|
||||
// state element that is updated with the fragmentId returned from the
|
||||
// call into the background process.
|
||||
const [createdFragmentId, setCreatedFragmentId] = useState(undefined);
|
||||
|
||||
// Select a matching fragment from state if one exists that matches the
|
||||
// criteria. If an existingId is passed in it is preferred, if not and the
|
||||
// fragmentOptions has the persist key set to true, a fragment with matching
|
||||
// successEvent will be pulled from memory if it exists.
|
||||
const fragment = useSelector((state) =>
|
||||
selectMatchingFragment(state, {
|
||||
fragmentOptions,
|
||||
existingId: existingId ?? createdFragmentId,
|
||||
}),
|
||||
);
|
||||
|
||||
// If no valid existing fragment can be found, a new one must be created that
|
||||
// will then be found by the selector above. To do this, invoke the
|
||||
// createEventFragment method with the fragmentOptions and current sessionId.
|
||||
// As soon as we call the background method we also update the
|
||||
// createEventFragmentCalled ref's current value to true so that future calls
|
||||
// are suppressed.
|
||||
useEffect(() => {
|
||||
if (fragment === undefined && createEventFragmentCalled.current === false) {
|
||||
createEventFragmentCalled.current = true;
|
||||
createEventFragment({
|
||||
...fragmentOptions,
|
||||
environmentType: getEnvironmentType(),
|
||||
}).then((createdFragment) => {
|
||||
setCreatedFragmentId(createdFragment.id);
|
||||
});
|
||||
}
|
||||
}, [fragment, fragmentOptions]);
|
||||
|
||||
const context = useMetaMetricsContext();
|
||||
|
||||
/**
|
||||
* trackSuccess is used to close a fragment with the affirmative action. This
|
||||
* method is just a thin wrapper around the background method that sets the
|
||||
* necessary values.
|
||||
*/
|
||||
const trackSuccess = useCallback(() => {
|
||||
finalizeEventFragment(fragment.id, { context });
|
||||
}, [fragment, context]);
|
||||
|
||||
/**
|
||||
* trackFailure is used to close a fragment as abandoned. This method is just a
|
||||
* thin wrapper around the background method that sets the necessary values.
|
||||
*/
|
||||
const trackFailure = useCallback(() => {
|
||||
finalizeEventFragment(fragment.id, { abandoned: true, context });
|
||||
}, [fragment, context]);
|
||||
|
||||
/**
|
||||
* updateEventFragmentProperties is a thin wrapper around updateEventFragment
|
||||
* that supplies the fragment id as the first parameter. This function will
|
||||
* be passed back from the hook as 'updateEventFragment', but is named
|
||||
* updateEventFragmentProperties to avoid naming conflicts.
|
||||
*/
|
||||
const updateEventFragmentProperties = useCallback(
|
||||
(payload) => {
|
||||
updateEventFragment(fragment.id, payload);
|
||||
},
|
||||
[fragment],
|
||||
);
|
||||
|
||||
return {
|
||||
trackSuccess,
|
||||
trackFailure,
|
||||
updateEventFragment: updateEventFragmentProperties,
|
||||
fragment,
|
||||
};
|
||||
}
|
211
ui/hooks/useEventFragment.test.js
Normal file
211
ui/hooks/useEventFragment.test.js
Normal file
@ -0,0 +1,211 @@
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
finalizeEventFragment,
|
||||
createEventFragment,
|
||||
updateEventFragment,
|
||||
} from '../store/actions';
|
||||
import { useEventFragment } from './useEventFragment';
|
||||
|
||||
jest.mock('../store/actions', () => ({
|
||||
finalizeEventFragment: jest.fn(),
|
||||
updateEventFragment: jest.fn(),
|
||||
createEventFragment: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('./useMetricEvent', () => ({
|
||||
useMetaMetricsContext: jest.fn(() => ({ page: '/' })),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useEventFragment', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('return shape', () => {
|
||||
let value;
|
||||
beforeAll(async () => {
|
||||
useSelector.mockImplementation((selector) =>
|
||||
selector({ metamask: { fragments: { testid: { id: 'testid' } } } }),
|
||||
);
|
||||
createEventFragment.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: 'testid',
|
||||
}),
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useEventFragment(undefined, {
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
persist: true,
|
||||
}),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
value = result.current;
|
||||
});
|
||||
|
||||
it('should have trackSuccess method', () => {
|
||||
expect(value).toHaveProperty('trackSuccess');
|
||||
expect(typeof value.trackSuccess).toBe('function');
|
||||
});
|
||||
|
||||
it('should have trackFailure method', () => {
|
||||
expect(value).toHaveProperty('trackFailure');
|
||||
expect(typeof value.trackFailure).toBe('function');
|
||||
});
|
||||
|
||||
it('should have updateEventFragment method', () => {
|
||||
expect(value).toHaveProperty('updateEventFragment');
|
||||
expect(typeof value.updateEventFragment).toBe('function');
|
||||
});
|
||||
|
||||
it('should have fragment property', () => {
|
||||
expect(value).toHaveProperty('fragment');
|
||||
expect(value.fragment).toMatchObject({
|
||||
id: 'testid',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('identifying appropriate fragment', () => {
|
||||
it('should create a new fragment when a matching fragment does not exist', async () => {
|
||||
useSelector.mockImplementation((selector) =>
|
||||
selector({
|
||||
metamask: {
|
||||
fragments: {
|
||||
testid: {
|
||||
id: 'testid',
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
createEventFragment.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: 'testid',
|
||||
}),
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useEventFragment(undefined, {
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
}),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
expect(createEventFragment).toHaveBeenCalledTimes(1);
|
||||
const returnValue = result.current;
|
||||
expect(returnValue.fragment).toMatchObject({
|
||||
id: 'testid',
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return the matching fragment by id when existingId is provided', async () => {
|
||||
useSelector.mockImplementation((selector) =>
|
||||
selector({
|
||||
metamask: {
|
||||
fragments: {
|
||||
testid: {
|
||||
id: 'testid',
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(() =>
|
||||
useEventFragment('testid', {
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
}),
|
||||
);
|
||||
const returnValue = result.current;
|
||||
expect(returnValue.fragment).toMatchObject({
|
||||
id: 'testid',
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return matching fragment by successEvent when no id is provided, but persist is true', async () => {
|
||||
useSelector.mockImplementation((selector) =>
|
||||
selector({
|
||||
metamask: {
|
||||
fragments: {
|
||||
testid: {
|
||||
persist: true,
|
||||
id: 'testid',
|
||||
successEvent: 'track new event',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
const { result } = renderHook(() =>
|
||||
useEventFragment(undefined, {
|
||||
successEvent: 'track new event',
|
||||
persist: true,
|
||||
}),
|
||||
);
|
||||
const returnValue = result.current;
|
||||
expect(returnValue.fragment).toMatchObject({
|
||||
id: 'testid',
|
||||
persist: true,
|
||||
successEvent: 'track new event',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('methods', () => {
|
||||
let value;
|
||||
beforeAll(async () => {
|
||||
useSelector.mockImplementation((selector) =>
|
||||
selector({ metamask: { fragments: { testid: { id: 'testid' } } } }),
|
||||
);
|
||||
createEventFragment.mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
id: 'testid',
|
||||
}),
|
||||
);
|
||||
const { result, waitForNextUpdate } = renderHook(() =>
|
||||
useEventFragment(undefined, {
|
||||
successEvent: 'success',
|
||||
failureEvent: 'failure',
|
||||
persist: true,
|
||||
}),
|
||||
);
|
||||
await waitForNextUpdate();
|
||||
value = result.current;
|
||||
});
|
||||
|
||||
it('trackSuccess method should invoke the background finalizeEventFragment method', () => {
|
||||
value.trackSuccess();
|
||||
expect(finalizeEventFragment).toHaveBeenCalledWith('testid', {
|
||||
context: { page: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
it('trackFailure method should invoke the background finalizeEventFragment method', () => {
|
||||
value.trackFailure();
|
||||
expect(finalizeEventFragment).toHaveBeenCalledWith('testid', {
|
||||
abandoned: true,
|
||||
context: { page: '/' },
|
||||
});
|
||||
});
|
||||
|
||||
it('updateEventFragment method should invoke the background updateEventFragment method', () => {
|
||||
value.updateEventFragment({ properties: { count: 1 } });
|
||||
expect(updateEventFragment).toHaveBeenCalledWith('testid', {
|
||||
properties: { count: 1 },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,6 +1,10 @@
|
||||
import { useContext, useCallback } from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { MetaMetricsContext } from '../contexts/metametrics';
|
||||
import { MetaMetricsContext as NewMetaMetricsContext } from '../contexts/metametrics.new';
|
||||
import { PATH_NAME_MAP } from '../helpers/constants/routes';
|
||||
import { txDataSelector } from '../selectors';
|
||||
import { useEqualityCheck } from './useEqualityCheck';
|
||||
|
||||
// Type imports
|
||||
@ -38,3 +42,45 @@ export function useNewMetricEvent(payload, options) {
|
||||
memoizedOptions,
|
||||
]);
|
||||
}
|
||||
|
||||
const PATHS_TO_CHECK = Object.keys(PATH_NAME_MAP);
|
||||
|
||||
/**
|
||||
* Returns the current page if it matches our route map, as well as the origin
|
||||
* if there is a confirmation that was triggered by a dapp. These values are
|
||||
* not required but add valuable context to events, and should be included in
|
||||
* the context object on the event payload.
|
||||
*
|
||||
* @returns {{
|
||||
* page?: MetaMetricsPageObject
|
||||
* referrer?: MetaMetricsReferrerObject
|
||||
* }}
|
||||
*/
|
||||
export function useMetaMetricsContext() {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
export * from './confirm-transaction';
|
||||
export * from './custom-gas';
|
||||
export * from './first-time-flow';
|
||||
export * from './metametrics';
|
||||
export * from './permissions';
|
||||
export * from './selectors';
|
||||
export * from './transactions';
|
||||
|
36
ui/selectors/metametrics.js
Normal file
36
ui/selectors/metametrics.js
Normal file
@ -0,0 +1,36 @@
|
||||
import { createSelector } from 'reselect';
|
||||
|
||||
export const selectFragments = (state) => state.metamask.fragments;
|
||||
|
||||
export const selectFragmentBySuccessEvent = createSelector(
|
||||
selectFragments,
|
||||
(_, fragmentOptions) => fragmentOptions,
|
||||
(fragments, fragmentOptions) => {
|
||||
if (fragmentOptions.persist) {
|
||||
return Object.values(fragments).find(
|
||||
(fragment) => fragment.successEvent === fragmentOptions.successEvent,
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
export const selectFragmentById = createSelector(
|
||||
selectFragments,
|
||||
(_, fragmentId) => fragmentId,
|
||||
(fragments, fragmentId) => {
|
||||
// A valid existing fragment must exist in state.
|
||||
// If these conditions are not meant we will create a new fragment.
|
||||
if (fragmentId && fragments?.[fragmentId]) {
|
||||
return fragments[fragmentId];
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
);
|
||||
|
||||
export const selectMatchingFragment = createSelector(
|
||||
(state, params) =>
|
||||
selectFragmentBySuccessEvent(state, params.fragmentOptions),
|
||||
(state, params) => selectFragmentById(state, params.existingId),
|
||||
(matchedBySuccessEvent, matchedById) => matchedById ?? matchedBySuccessEvent,
|
||||
);
|
71
ui/selectors/metametrics.test.js
Normal file
71
ui/selectors/metametrics.test.js
Normal file
@ -0,0 +1,71 @@
|
||||
const {
|
||||
selectFragmentBySuccessEvent,
|
||||
selectFragmentById,
|
||||
selectMatchingFragment,
|
||||
} = require('.');
|
||||
|
||||
describe('selectFragmentBySuccessEvent', () => {
|
||||
it('should find matching fragment in state by successEvent', () => {
|
||||
const state = {
|
||||
metamask: {
|
||||
fragments: {
|
||||
randomid: {
|
||||
successEvent: 'example event',
|
||||
persist: true,
|
||||
id: 'randomid',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const selected = selectFragmentBySuccessEvent(state, {
|
||||
successEvent: 'example event',
|
||||
persist: true,
|
||||
});
|
||||
expect(selected).toHaveProperty('id', 'randomid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectFragmentById', () => {
|
||||
it('should find matching fragment in state by id', () => {
|
||||
const state = {
|
||||
metamask: {
|
||||
fragments: {
|
||||
randomid: {
|
||||
successEvent: 'example event',
|
||||
persist: true,
|
||||
id: 'randomid',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const selected = selectFragmentById(state, 'randomid');
|
||||
expect(selected).toHaveProperty('id', 'randomid');
|
||||
});
|
||||
});
|
||||
|
||||
describe('selectMatchingFragment', () => {
|
||||
it('should find matching fragment in state by id', () => {
|
||||
const state = {
|
||||
metamask: {
|
||||
fragments: {
|
||||
notthecorrectid: {
|
||||
successEvent: 'event name',
|
||||
id: 'notthecorrectid',
|
||||
},
|
||||
randomid: {
|
||||
successEvent: 'example event',
|
||||
persist: true,
|
||||
id: 'randomid',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const selected = selectMatchingFragment(state, {
|
||||
fragmentOptions: {
|
||||
successEvent: 'event name',
|
||||
},
|
||||
existingId: 'randomid',
|
||||
});
|
||||
expect(selected).toHaveProperty('id', 'randomid');
|
||||
});
|
||||
});
|
@ -3072,6 +3072,18 @@ export function trackMetaMetricsEvent(payload, options) {
|
||||
return promisifiedBackground.trackMetaMetricsEvent(payload, options);
|
||||
}
|
||||
|
||||
export function createEventFragment(options) {
|
||||
return promisifiedBackground.createEventFragment(options);
|
||||
}
|
||||
|
||||
export function updateEventFragment(id, payload) {
|
||||
return promisifiedBackground.updateEventFragment(id, payload);
|
||||
}
|
||||
|
||||
export function finalizeEventFragment(id, options) {
|
||||
return promisifiedBackground.finalizeEventFragment(id, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {MetaMetricsPagePayload} payload - details of the page viewed
|
||||
* @param {MetaMetricsPageOptions} options - options for handling the page view
|
||||
|
Loading…
Reference in New Issue
Block a user