1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-12-23 01:39:44 +01:00

implement event fragments for tx controller (#13331)

This commit is contained in:
Brad Decker 2022-01-20 10:26:39 -06:00 committed by GitHub
parent b2a9b72c04
commit c58cc631c7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 727 additions and 105 deletions

View File

@ -148,9 +148,9 @@ export default class MetaMetricsController {
/**
* Create an event fragment in state and returns the event fragment object.
*
* @param {MetaMetricsFunnel} options - Fragment settings and properties
* @param {MetaMetricsEventFragment} options - Fragment settings and properties
* to initiate the fragment with.
* @returns {MetaMetricsFunnel}
* @returns {MetaMetricsEventFragment}
*/
createEventFragment(options) {
if (!options.successEvent || !options.category) {
@ -168,7 +168,7 @@ export default class MetaMetricsController {
}
const { fragments } = this.store.getState();
const id = generateUUID();
const id = options.uniqueIdentifier ?? generateUUID();
const fragment = {
id,
...options,
@ -180,6 +180,37 @@ export default class MetaMetricsController {
[id]: fragment,
},
});
if (options.initialEvent) {
this.trackEvent({
event: fragment.initialEvent,
category: fragment.category,
properties: fragment.properties,
sensitiveProperties: fragment.sensitiveProperties,
page: fragment.page,
referrer: fragment.referrer,
revenue: fragment.revenue,
value: fragment.value,
currency: fragment.currency,
environmentType: fragment.environmentType,
});
}
return fragment;
}
/**
* Returns the fragment stored in memory with provided id or undefined if it
* does not exist.
*
* @param {string} id - id of fragment to retrieve
* @returns {[MetaMetricsEventFragment]}
*/
getEventFragmentById(id) {
const { fragments } = this.store.getState();
const fragment = fragments[id];
return fragment;
}
@ -224,7 +255,7 @@ export default class MetaMetricsController {
* originated the fragment. This is for fallback only, the fragment referrer
* property will take precedence.
*/
finalizeEventFragment(id, { abandoned = false, page, referrer }) {
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.`);

View File

@ -25,6 +25,7 @@ import {
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
TRANSACTION_ENVELOPE_TYPES,
TRANSACTION_EVENTS,
} from '../../../../shared/constants/transaction';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
@ -54,13 +55,10 @@ const hstInterface = new ethers.utils.Interface(abi);
const MAX_MEMSTORE_TX_LIST_SIZE = 100; // Number of transactions (by unique nonces) to keep in memory
export const TRANSACTION_EVENTS = {
ADDED: 'Transaction Added',
APPROVED: 'Transaction Approved',
FINALIZED: 'Transaction Finalized',
REJECTED: 'Transaction Rejected',
SUBMITTED: 'Transaction Submitted',
};
/**
* @typedef {import('../../../../shared/constants/transaction').TransactionMeta} TransactionMeta
* @typedef {import('../../../../shared/constants/transaction').TransactionMetaMetricsEventString} TransactionMetaMetricsEventString
*/
/**
* @typedef {Object} CustomGasSettings
@ -118,6 +116,10 @@ export default class TransactionController extends EventEmitter {
this._trackMetaMetricsEvent = opts.trackMetaMetricsEvent;
this._getParticipateInMetrics = opts.getParticipateInMetrics;
this._getEIP1559GasFeeEstimates = opts.getEIP1559GasFeeEstimates;
this.createEventFragment = opts.createEventFragment;
this.updateEventFragment = opts.updateEventFragment;
this.finalizeEventFragment = opts.finalizeEventFragment;
this.getEventFragmentById = opts.getEventFragmentById;
this.memStore = new ObservableStore({});
this.query = new EthQuery(this.provider);
@ -662,9 +664,8 @@ export default class TransactionController extends EventEmitter {
* which is defined by specifying a numerator. 11 is a 10% bump, 12 would be
* a 20% bump, and so on.
*
* @param {import(
* '../../../../shared/constants/transaction'
* ).TransactionMeta} originalTxMeta - Original transaction to use as base
* @param {TransactionMeta} originalTxMeta - Original transaction to use as
* base
* @param {CustomGasSettings} [customGasSettings] - overrides for the gas
* fields to use instead of the multiplier
* @param {number} [incrementNumerator] - Numerator from which to generate a
@ -1120,6 +1121,28 @@ export default class TransactionController extends EventEmitter {
this.txStateManager.updateTransaction(txMeta, 'transactions#setTxHash');
}
/**
* Convenience method for the UI to easily create event fragments when the
* fragment does not exist in state.
*
* @param {number} transactionId - The transaction id to create the event
* fragment for
* @param {valueOf<TRANSACTION_EVENTS>} event - event type to create
*/
createTransactionEventFragment(transactionId, event) {
const txMeta = this.txStateManager.getTransaction(transactionId);
const {
properties,
sensitiveProperties,
} = this._buildEventFragmentProperties(txMeta);
this._createTransactionEventFragment(
txMeta,
event,
properties,
sensitiveProperties,
);
}
//
// PRIVATE METHODS
//
@ -1450,20 +1473,7 @@ export default class TransactionController extends EventEmitter {
}
}
/**
* Extracts relevant properties from a transaction meta
* object and uses them to create and send metrics for various transaction
* events.
*
* @param {Object} txMeta - the txMeta object
* @param {string} event - the name of the transaction event
* @param {Object} extraParams - optional props and values to include in sensitiveProperties
*/
_trackTransactionMetricsEvent(txMeta, event, extraParams = {}) {
if (!txMeta) {
return;
}
_buildEventFragmentProperties(txMeta, extraParams) {
const {
type,
time,
@ -1501,27 +1511,202 @@ export default class TransactionController extends EventEmitter {
const gasParamsInGwei = this._getGasValuesInGWEI(gasParams);
this._trackMetaMetricsEvent({
const properties = {
chain_id: chainId,
referrer,
source,
network,
type,
};
const sensitiveProperties = {
status,
transaction_envelope_type: isEIP1559Transaction(txMeta)
? TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET
: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
first_seen: time,
gas_limit: gasLimit,
...gasParamsInGwei,
...extraParams,
};
return { properties, sensitiveProperties };
}
/**
* Helper method that checks for the presence of an existing fragment by id
* appropriate for the type of event that triggered fragment creation. If the
* appropriate fragment exists, then nothing is done. If it does not exist a
* new event fragment is created with the appropriate payload.
*
* @param {TransactionMeta} txMeta - Transaction meta object
* @param {TransactionMetaMetricsEventString} event - The event type that
* triggered fragment creation
* @param {Object} properties - properties to include in the fragment
* @param {Object} [sensitiveProperties] - sensitive properties to include in
* the fragment
*/
_createTransactionEventFragment(
txMeta,
event,
properties,
sensitiveProperties,
) {
const isSubmitted = [
TRANSACTION_EVENTS.FINALIZED,
TRANSACTION_EVENTS.SUBMITTED,
].includes(event);
const uniqueIdentifier = `transaction-${
isSubmitted ? 'submitted' : 'added'
}-${txMeta.id}`;
const fragment = this.getEventFragmentById(uniqueIdentifier);
if (typeof fragment !== 'undefined') {
return;
}
switch (event) {
// When a transaction is added to the controller, we know that the user
// will be presented with a confirmation screen. The user will then
// either confirm or reject that transaction. Each has an associated
// event we want to track. While we don't necessarily need an event
// fragment to model this, having one allows us to record additional
// properties onto the event from the UI. For example, when the user
// edits the transactions gas params we can record that property and
// then get analytics on the number of transactions in which gas edits
// occur.
case TRANSACTION_EVENTS.ADDED:
this.createEventFragment({
category: 'Transactions',
initialEvent: TRANSACTION_EVENTS.ADDED,
successEvent: TRANSACTION_EVENTS.APPROVED,
failureEvent: TRANSACTION_EVENTS.REJECTED,
properties,
sensitiveProperties,
persist: true,
uniqueIdentifier,
});
break;
// If for some reason an approval or rejection occurs without the added
// fragment existing in memory, we create the added fragment but without
// the initialEvent firing. This is to prevent possible duplication of
// events. A good example why this might occur is if the user had
// unapproved transactions in memory when updating to the version that
// includes this change. A migration would have also helped here but this
// implementation hardens against other possible bugs where a fragment
// does not exist.
case TRANSACTION_EVENTS.APPROVED:
case TRANSACTION_EVENTS.REJECTED:
this.createEventFragment({
category: 'Transactions',
successEvent: TRANSACTION_EVENTS.APPROVED,
failureEvent: TRANSACTION_EVENTS.REJECTED,
properties,
sensitiveProperties,
persist: true,
uniqueIdentifier,
});
break;
// When a transaction is submitted it will always result in updating
// to a finalized state (dropped, failed, confirmed) -- eventually.
// However having a fragment started at this stage allows augmenting
// analytics data with user interactions such as speeding up and
// canceling the transactions. From this controllers perspective a new
// transaction with a new id is generated for speed up and cancel
// transactions, but from the UI we could augment the previous ID with
// supplemental data to show user intent. Such as when they open the
// cancel UI but don't submit. We can record that this happened and add
// properties to the transaction event.
case TRANSACTION_EVENTS.SUBMITTED:
this.createEventFragment({
category: 'Transactions',
initialEvent: TRANSACTION_EVENTS.SUBMITTED,
successEvent: TRANSACTION_EVENTS.FINALIZED,
properties,
sensitiveProperties,
persist: true,
uniqueIdentifier,
});
break;
// If for some reason a transaction is finalized without the submitted
// fragment existing in memory, we create the submitted fragment but
// without the initialEvent firing. This is to prevent possible
// duplication of events. A good example why this might occur is if th
// user had pending transactions in memory when updating to the version
// that includes this change. A migration would have also helped here but
// this implementation hardens against other possible bugs where a
// fragment does not exist.
case TRANSACTION_EVENTS.FINALIZED:
this.createEventFragment({
category: 'Transactions',
successEvent: TRANSACTION_EVENTS.FINALIZED,
properties,
sensitiveProperties,
persist: true,
uniqueIdentifier,
});
break;
default:
break;
}
}
/**
* Extracts relevant properties from a transaction meta
* object and uses them to create and send metrics for various transaction
* events.
*
* @param {Object} txMeta - the txMeta object
* @param {TransactionMetaMetricsEventString} event - the name of the transaction event
* @param {Object} extraParams - optional props and values to include in sensitiveProperties
*/
_trackTransactionMetricsEvent(txMeta, event, extraParams = {}) {
if (!txMeta) {
return;
}
const {
properties,
sensitiveProperties,
} = this._buildEventFragmentProperties(txMeta, extraParams);
// Create event fragments for event types that spawn fragments, and ensure
// existence of fragments for event types that act upon them.
this._createTransactionEventFragment(
txMeta,
event,
category: 'Transactions',
properties: {
chain_id: chainId,
referrer,
source,
network,
type,
},
sensitiveProperties: {
status,
transaction_envelope_type: isEIP1559Transaction(txMeta)
? TRANSACTION_ENVELOPE_TYPE_NAMES.FEE_MARKET
: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
first_seen: time,
gas_limit: gasLimit,
...gasParamsInGwei,
...extraParams,
},
});
properties,
sensitiveProperties,
);
let id;
switch (event) {
// If the user approves a transaction, finalize the transaction added
// event fragment.
case TRANSACTION_EVENTS.APPROVED:
id = `transaction-added-${txMeta.id}`;
this.updateEventFragment(id, { properties, sensitiveProperties });
this.finalizeEventFragment(id);
break;
// If the user rejects a transaction, finalize the transaction added
// event fragment. with the abandoned flag set.
case TRANSACTION_EVENTS.REJECTED:
id = `transaction-added-${txMeta.id}`;
this.updateEventFragment(id, { properties, sensitiveProperties });
this.finalizeEventFragment(id, {
abandoned: true,
});
break;
// When a transaction is finalized, also finalize the transaction
// submitted event fragment.
case TRANSACTION_EVENTS.FINALIZED:
id = `transaction-submitted-${txMeta.id}`;
this.updateEventFragment(id, { properties, sensitiveProperties });
this.finalizeEventFragment(`transaction-submitted-${txMeta.id}`);
break;
default:
break;
}
}
_getTransactionCompletionTime(submittedTime) {

View File

@ -13,6 +13,7 @@ import {
TRANSACTION_STATUSES,
TRANSACTION_TYPES,
TRANSACTION_ENVELOPE_TYPES,
TRANSACTION_EVENTS,
} from '../../../../shared/constants/transaction';
import { SECOND } from '../../../../shared/constants/time';
@ -22,7 +23,7 @@ import {
} from '../../../../shared/constants/gas';
import { TRANSACTION_ENVELOPE_TYPE_NAMES } from '../../../../ui/helpers/constants/transactions';
import { METAMASK_CONTROLLER_EVENTS } from '../../metamask-controller';
import TransactionController, { TRANSACTION_EVENTS } from '.';
import TransactionController from '.';
const noop = () => true;
const currentNetworkId = '42';
@ -35,9 +36,10 @@ const VALID_ADDRESS = '0x0000000000000000000000000000000000000000';
const VALID_ADDRESS_TWO = '0x0000000000000000000000000000000000000001';
describe('Transaction Controller', function () {
let txController, provider, providerResultStub, fromAccount;
let txController, provider, providerResultStub, fromAccount, fragmentExists;
beforeEach(function () {
fragmentExists = false;
providerResultStub = {
// 1 gwei
eth_gasPrice: '0x0de0b6b3a7640000',
@ -70,6 +72,11 @@ describe('Transaction Controller', function () {
getCurrentChainId: () => currentChainId,
getParticipateInMetrics: () => false,
trackMetaMetricsEvent: () => undefined,
createEventFragment: () => undefined,
updateEventFragment: () => undefined,
finalizeEventFragment: () => undefined,
getEventFragmentById: () =>
fragmentExists === false ? undefined : { id: 0 },
getEIP1559GasFeeEstimates: () => undefined,
});
txController.nonceTracker.getNonceLock = () =>
@ -1536,66 +1543,325 @@ describe('Transaction Controller', function () {
describe('#_trackTransactionMetricsEvent', function () {
let trackMetaMetricsEventSpy;
let createEventFragmentSpy;
let finalizeEventFragmentSpy;
beforeEach(function () {
trackMetaMetricsEventSpy = sinon.spy(
txController,
'_trackMetaMetricsEvent',
);
createEventFragmentSpy = sinon.spy(txController, 'createEventFragment');
finalizeEventFragmentSpy = sinon.spy(
txController,
'finalizeEventFragment',
);
});
afterEach(function () {
trackMetaMetricsEventSpy.restore();
createEventFragmentSpy.restore();
finalizeEventFragmentSpy.restore();
});
it('should call _trackMetaMetricsEvent with the correct payload (user source)', function () {
const txMeta = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TRANSACTION_TYPES.SIMPLE_SEND,
origin: 'metamask',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
};
const expectedPayload = {
event: 'Transaction Added',
category: 'Transactions',
properties: {
chain_id: '0x2a',
network: '42',
referrer: 'metamask',
source: 'user',
describe('On transaction created by the user', function () {
let txMeta;
before(function () {
txMeta = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TRANSACTION_TYPES.SIMPLE_SEND,
},
sensitiveProperties: {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
origin: 'metamask',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
};
});
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.ADDED,
);
assert.equal(trackMetaMetricsEventSpy.callCount, 1);
assert.deepEqual(
trackMetaMetricsEventSpy.getCall(0).args[0],
expectedPayload,
);
it('should create an event fragment when transaction added', function () {
const expectedPayload = {
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
category: 'Transactions',
persist: true,
properties: {
chain_id: '0x2a',
network: '42',
referrer: 'metamask',
source: 'user',
type: TRANSACTION_TYPES.SIMPLE_SEND,
},
sensitiveProperties: {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.ADDED,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction added fragment as abandoned if user rejects transaction', function () {
fragmentExists = true;
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.REJECTED,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(finalizeEventFragmentSpy.getCall(0).args[1], {
abandoned: true,
});
});
it('Should finalize the transaction added fragment if user approves transaction', function () {
fragmentExists = true;
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.APPROVED,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
it('should create an event fragment when transaction is submitted', function () {
const expectedPayload = {
initialEvent: 'Transaction Submitted',
successEvent: 'Transaction Finalized',
uniqueIdentifier: 'transaction-submitted-1',
category: 'Transactions',
persist: true,
properties: {
chain_id: '0x2a',
network: '42',
referrer: 'metamask',
source: 'user',
type: TRANSACTION_TYPES.SIMPLE_SEND,
},
sensitiveProperties: {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.SUBMITTED,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction submitted fragment when transaction finalizes', function () {
fragmentExists = true;
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.FINALIZED,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-submitted-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
});
it('should call _trackMetaMetricsEvent with the correct payload (dapp source)', function () {
describe('On transaction suggested by dapp', function () {
let txMeta;
before(function () {
txMeta = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
txParams: {
from: fromAccount.address,
to: '0x1678a085c290ebd122dc42cba69373b5953b831d',
gasPrice: '0x77359400',
gas: '0x7b0d',
nonce: '0x4b',
},
type: TRANSACTION_TYPES.SIMPLE_SEND,
origin: 'other',
chainId: currentChainId,
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
};
});
it('should create an event fragment when transaction added', function () {
const expectedPayload = {
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
category: 'Transactions',
persist: true,
properties: {
chain_id: '0x2a',
network: '42',
referrer: 'other',
source: 'dapp',
type: TRANSACTION_TYPES.SIMPLE_SEND,
},
sensitiveProperties: {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.ADDED,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction added fragment as abandoned if user rejects transaction', function () {
fragmentExists = true;
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.REJECTED,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(finalizeEventFragmentSpy.getCall(0).args[1], {
abandoned: true,
});
});
it('Should finalize the transaction added fragment if user approves transaction', function () {
fragmentExists = true;
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.APPROVED,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
it('should create an event fragment when transaction is submitted', function () {
const expectedPayload = {
initialEvent: 'Transaction Submitted',
successEvent: 'Transaction Finalized',
uniqueIdentifier: 'transaction-submitted-1',
category: 'Transactions',
persist: true,
properties: {
chain_id: '0x2a',
network: '42',
referrer: 'other',
source: 'dapp',
type: TRANSACTION_TYPES.SIMPLE_SEND,
},
sensitiveProperties: {
gas_price: '2',
gas_limit: '0x7b0d',
first_seen: 1624408066355,
transaction_envelope_type: TRANSACTION_ENVELOPE_TYPE_NAMES.LEGACY,
status: 'unapproved',
},
};
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.SUBMITTED,
);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
it('Should finalize the transaction submitted fragment when transaction finalizes', function () {
fragmentExists = true;
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.FINALIZED,
);
assert.equal(createEventFragmentSpy.callCount, 0);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-submitted-1',
);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[1],
undefined,
);
});
});
it('should create missing fragments when events happen out of order or are missing', function () {
const txMeta = {
id: 1,
status: TRANSACTION_STATUSES.UNAPPROVED,
@ -1612,9 +1878,13 @@ describe('Transaction Controller', function () {
time: 1624408066355,
metamaskNetworkId: currentNetworkId,
};
const expectedPayload = {
event: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
category: 'Transactions',
persist: true,
properties: {
chain_id: '0x2a',
network: '42',
@ -1630,16 +1900,21 @@ describe('Transaction Controller', function () {
status: 'unapproved',
},
};
txController._trackTransactionMetricsEvent(
txMeta,
TRANSACTION_EVENTS.ADDED,
TRANSACTION_EVENTS.APPROVED,
);
assert.equal(trackMetaMetricsEventSpy.callCount, 1);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.deepEqual(
trackMetaMetricsEventSpy.getCall(0).args[0],
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
assert.equal(finalizeEventFragmentSpy.callCount, 1);
assert.deepEqual(
finalizeEventFragmentSpy.getCall(0).args[0],
'transaction-added-1',
);
assert.deepEqual(finalizeEventFragmentSpy.getCall(0).args[1], undefined);
});
it('should call _trackMetaMetricsEvent with the correct payload (extra params)', function () {
@ -1660,7 +1935,11 @@ describe('Transaction Controller', function () {
metamaskNetworkId: currentNetworkId,
};
const expectedPayload = {
event: 'Transaction Added',
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: 'Transactions',
properties: {
network: '42',
@ -1688,9 +1967,10 @@ describe('Transaction Controller', function () {
foo: 'bar',
},
);
assert.equal(trackMetaMetricsEventSpy.callCount, 1);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
trackMetaMetricsEventSpy.getCall(0).args[0],
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});
@ -1716,7 +1996,11 @@ describe('Transaction Controller', function () {
metamaskNetworkId: currentNetworkId,
};
const expectedPayload = {
event: 'Transaction Added',
initialEvent: 'Transaction Added',
successEvent: 'Transaction Approved',
failureEvent: 'Transaction Rejected',
uniqueIdentifier: 'transaction-added-1',
persist: true,
category: 'Transactions',
properties: {
chain_id: '0x2a',
@ -1747,9 +2031,10 @@ describe('Transaction Controller', function () {
foo: 'bar',
},
);
assert.equal(trackMetaMetricsEventSpy.callCount, 1);
assert.equal(createEventFragmentSpy.callCount, 1);
assert.equal(finalizeEventFragmentSpy.callCount, 0);
assert.deepEqual(
trackMetaMetricsEventSpy.getCall(0).args[0],
createEventFragmentSpy.getCall(0).args[0],
expectedPayload,
);
});

View File

@ -577,6 +577,18 @@ export default class MetamaskController extends EventEmitter {
),
provider: this.provider,
blockTracker: this.blockTracker,
createEventFragment: this.metaMetricsController.createEventFragment.bind(
this.metaMetricsController,
),
updateEventFragment: this.metaMetricsController.updateEventFragment.bind(
this.metaMetricsController,
),
finalizeEventFragment: this.metaMetricsController.finalizeEventFragment.bind(
this.metaMetricsController,
),
getEventFragmentById: this.metaMetricsController.getEventFragmentById.bind(
this.metaMetricsController,
),
trackMetaMetricsEvent: this.metaMetricsController.trackEvent.bind(
this.metaMetricsController,
),
@ -1282,6 +1294,9 @@ export default class MetamaskController extends EventEmitter {
addUnapprovedTransaction: txController.addUnapprovedTransaction.bind(
txController,
),
createTransactionEventFragment: txController.createTransactionEventFragment.bind(
txController,
),
// messageManager
signMessage: this.signMessage.bind(this),

View File

@ -88,6 +88,9 @@
* is closed in an affirmative action.
* @property {string} [failureEvent] - The event name to fire when the fragment
* is closed with a rejection.
* @property {string} [initialEvent] - An event name to fire immediately upon
* fragment creation. This is useful for building funnels in mixpanel and for
* reduction of code duplication.
* @property {string} category - the event category to use for both the success
* and failure events
* @property {boolean} [persist] - Should this fragment be persisted in
@ -113,6 +116,10 @@
* occurred on
* @property {MetaMetricsReferrerObject} [referrer] - the origin of the dapp
* that initiated the event fragment.
* @property {string} [uniqueIdentifier] - optional argument to override the
* automatic generation of UUID for the event fragment. This is useful when
* tracking events for subsystems that already generate UUIDs so to avoid
* unnecessary lookups and reduce accidental duplication.
*/
/**

View File

@ -236,3 +236,49 @@ export const TRANSACTION_GROUP_CATEGORIES = {
* the network, in Unix epoch time (ms).
* @property {TxError} [err] - The error encountered during the transaction
*/
/**
* Defines the possible types
*
* @typedef {Object} TransactionMetaMetricsEvents
* @property {'Transaction Added'} ADDED - All transactions, except incoming
* ones, are added to the controller state in an unapproved status. When this
* happens we fire the Transaction Added event to show that the transaction
* has been added to the user's MetaMask.
* @property {'Transaction Approved'} APPROVED - When an unapproved transaction
* is in the controller state, MetaMask will render a confirmation screen for
* that transaction. If the user approves the transaction we fire this event
* to indicate that the user has approved the transaction for submission to
* the network.
* @property {'Transaction Rejected'} REJECTED - When an unapproved transaction
* is in the controller state, MetaMask will render a confirmation screen for
* that transaction. If the user rejects the transaction we fire this event
* to indicate that the user has rejected the transaction. It will be removed
* from state as a result.
* @property {'Transaction Submitted'} SUBMITTED - After a transaction is
* approved by the user, it is then submitted to the network for inclusion in
* a block. When this happens we fire the Transaction Submitted event to
* indicate that MetaMask is submitting a transaction at the user's request.
* @property {'Transaction Finalized'} FINALIZED - All transactions that are
* submitted will finalized (eventually) by either being dropped, failing
* or being confirmed. When this happens we track this event, along with the
* status.
*/
/**
* This type will work anywhere you expect a string that can be one of the
* above transaction event types.
*
* @typedef {TransactionMetaMetricsEvents[keyof TransactionMetaMetricsEvents]} TransactionMetaMetricsEventString
*/
/**
* @type {TransactionMetaMetricsEvents}
*/
export const TRANSACTION_EVENTS = {
ADDED: 'Transaction Added',
APPROVED: 'Transaction Approved',
FINALIZED: 'Transaction Finalized',
REJECTED: 'Transaction Rejected',
SUBMITTED: 'Transaction Submitted',
};

View File

@ -485,6 +485,46 @@
"usePhishDetect": true,
"useTokenDetection": true
},
"MetaMetricsController": {
"fragments": {
"transaction-added-7911313280012623": {
"category": "Transactions",
"initialEvent": "Transaction Added",
"successEvent": "Transaction Approved",
"failureEvent": "Transaction Rejected",
"properties": {},
"persist": true,
"uniqueIdentifier": "transaction-added-7911313280012623"
},
"transaction-added-7911313280012624": {
"category": "Transactions",
"initialEvent": "Transaction Added",
"successEvent": "Transaction Approved",
"failureEvent": "Transaction Rejected",
"properties": {},
"persist": true,
"uniqueIdentifier": "transaction-added-7911313280012624"
},
"transaction-added-7911313280012625": {
"category": "Transactions",
"initialEvent": "Transaction Added",
"successEvent": "Transaction Approved",
"failureEvent": "Transaction Rejected",
"properties": {},
"persist": true,
"uniqueIdentifier": "transaction-added-7911313280012625"
},
"transaction-added-7911313280012626": {
"category": "Transactions",
"initialEvent": "Transaction Added",
"successEvent": "Transaction Approved",
"failureEvent": "Transaction Rejected",
"properties": {},
"persist": true,
"uniqueIdentifier": "transaction-added-7911313280012626"
}
}
},
"TransactionController": {
"transactions": {
"7911313280012623": {

View File

@ -129,6 +129,19 @@
"useNonceField": false,
"usePhishDetect": true
},
"MetaMetricsController": {
"fragments": {
"transaction-added-4046084157914634": {
"category": "Transactions",
"initialEvent": "Transaction Added",
"successEvent": "Transaction Approved",
"failureEvent": "Transaction Rejected",
"properties": {},
"persist": true,
"uniqueIdentifier": "transaction-added-4046084157914634"
}
}
},
"TransactionController": {
"transactions": {
"4046084157914634": {