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:
parent
b2a9b72c04
commit
c58cc631c7
@ -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.`);
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
);
|
||||
});
|
||||
|
@ -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),
|
||||
|
@ -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.
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -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',
|
||||
};
|
||||
|
@ -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": {
|
||||
|
@ -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": {
|
||||
|
Loading…
Reference in New Issue
Block a user