Make event tracking idempotent using unique messageId (#16267)

feature/default_network_editable
Jyoti Puri 2 years ago committed by GitHub
parent 6f27c9065c
commit 6c0930899d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 55
      app/scripts/controllers/metametrics.js
  2. 45
      app/scripts/controllers/metametrics.test.js
  3. 3
      app/scripts/metamask-controller.js
  4. 16
      ui/store/actions.js

@ -32,6 +32,22 @@ const defaultCaptureException = (err) => {
});
};
// The function is used to build a unique messageId for segment messages
// It uses actionId and uniqueIdentifier from event if present
const buildUniqueMessageId = (args) => {
let messageId = '';
if (args.uniqueIdentifier) {
messageId += `${args.uniqueIdentifier}-`;
}
if (args.actionId) {
messageId += args.actionId;
}
if (messageId.length) {
return messageId;
}
return generateRandomId();
};
const exceptionsToFilter = {
[`You must pass either an "anonymousId" or a "userId".`]: true,
};
@ -231,14 +247,6 @@ export default class MetaMetricsController {
);
}
const existingFragment = this.getExistingEventFragment(
options.actionId,
options.uniqueIdentifier,
);
if (existingFragment) {
return existingFragment;
}
const { fragments } = this.store.getState();
const id = options.uniqueIdentifier ?? uuidv4();
@ -266,6 +274,8 @@ export default class MetaMetricsController {
value: fragment.value,
currency: fragment.currency,
environmentType: fragment.environmentType,
actionId: options.actionId,
uniqueIdentifier: options.uniqueIdentifier,
});
}
@ -287,26 +297,6 @@ export default class MetaMetricsController {
return fragment;
}
/**
* Returns the fragment stored in memory with provided id or undefined if it
* does not exist.
*
* @param {string} actionId - actionId passed from UI
* @param {string} uniqueIdentifier - uniqueIdentifier of the event
* @returns {[MetaMetricsEventFragment]}
*/
getExistingEventFragment(actionId, uniqueIdentifier) {
const { fragments } = this.store.getState();
const existingFragment = Object.values(fragments).find(
(fragment) =>
fragment.actionId === actionId &&
fragment.uniqueIdentifier === uniqueIdentifier,
);
return existingFragment;
}
/**
* Updates an event fragment in state
*
@ -367,6 +357,8 @@ export default class MetaMetricsController {
value: fragment.value,
currency: fragment.currency,
environmentType: fragment.environmentType,
actionId: fragment.actionId,
uniqueIdentifier: fragment.uniqueIdentifier,
});
const { fragments } = this.store.getState();
delete fragments[id];
@ -453,7 +445,10 @@ export default class MetaMetricsController {
* @param {MetaMetricsPageOptions} [options] - options for handling the page
* view
*/
trackPage({ name, params, environmentType, page, referrer }, options) {
trackPage(
{ name, params, environmentType, page, referrer, actionId },
options,
) {
try {
if (this.state.participateInMetaMetrics === false) {
return;
@ -469,6 +464,7 @@ export default class MetaMetricsController {
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
this._submitSegmentAPICall('page', {
messageId: buildUniqueMessageId({ actionId }),
[idTrait]: idValue,
name,
properties: {
@ -659,6 +655,7 @@ export default class MetaMetricsController {
} = rawPayload;
return {
event,
messageId: buildUniqueMessageId(rawPayload),
properties: {
// These values are omitted from properties because they have special meaning
// in segment. https://segment.com/docs/connections/spec/track/#properties.

@ -20,6 +20,7 @@ const NETWORK = 'Mainnet';
const FAKE_CHAIN_ID = '0x1338';
const LOCALE = 'en_US';
const TEST_META_METRICS_ID = '0xabc';
const DUMMY_ACTION_ID = 'DUMMY_ACTION_ID';
const MOCK_TRAITS = {
test_boolean: true,
@ -634,6 +635,50 @@ describe('MetaMetricsController', function () {
);
mock.verify();
});
it('multiple trackPage call with same actionId should result in same messageId being sent to segment', function () {
const mock = sinon.mock(segment);
const metaMetricsController = getMetaMetricsController({
preferencesStore: getMockPreferencesStore({
participateInMetaMetrics: null,
}),
});
mock
.expects('page')
.twice()
.withArgs({
name: 'home',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
messageId: DUMMY_ACTION_ID,
timestamp: new Date(),
});
metaMetricsController.trackPage(
{
name: 'home',
params: null,
actionId: DUMMY_ACTION_ID,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
},
{ isOptInPath: true },
);
metaMetricsController.trackPage(
{
name: 'home',
params: null,
actionId: DUMMY_ACTION_ID,
environmentType: ENVIRONMENT_TYPE_BACKGROUND,
page: METAMETRICS_BACKGROUND_PAGE_OBJECT,
},
{ isOptInPath: true },
);
mock.verify();
});
});
describe('_buildUserTraitsObject', function () {

@ -2041,7 +2041,7 @@ export default class MetamaskController extends EventEmitter {
}
}
async addCustomNetwork(customRpc) {
async addCustomNetwork(customRpc, actionId) {
const { chainId, chainName, rpcUrl, ticker, blockExplorerUrl } = customRpc;
await this.preferencesController.addToFrequentRpcList(
@ -2077,6 +2077,7 @@ export default class MetamaskController extends EventEmitter {
sensitiveProperties: {
rpc_url: rpcUrlOrigin,
},
actionId,
});
}

@ -3516,7 +3516,10 @@ export async function closeNotificationPopup() {
* @returns {Promise<void>}
*/
export function trackMetaMetricsEvent(payload, options) {
return submitRequestToBackground('trackMetaMetricsEvent', [payload, options]);
return submitRequestToBackground('trackMetaMetricsEvent', [
{ ...payload, actionId: generateActionId() },
options,
]);
}
export function createEventFragment(options) {
@ -3548,7 +3551,10 @@ export function finalizeEventFragment(id, options) {
* @param {MetaMetricsPageOptions} options - options for handling the page view
*/
export function trackMetaMetricsPage(payload, options) {
return submitRequestToBackground('trackMetaMetricsPage', [payload, options]);
return submitRequestToBackground('trackMetaMetricsPage', [
{ ...payload, actionId: generateActionId() },
options,
]);
}
export function updateViewedNotifications(notificationIdViewedStatusMap) {
@ -3578,6 +3584,7 @@ export async function setSmartTransactionsOptInStatus(
prevOptInState,
) {
trackMetaMetricsEvent({
actionId: generateActionId(),
event: 'STX OptIn',
category: EVENT.CATEGORIES.SWAPS,
sensitiveProperties: {
@ -3841,7 +3848,10 @@ export function addCustomNetwork(customRpc) {
return async (dispatch) => {
try {
dispatch(setNewCustomNetworkAdded(customRpc));
await submitRequestToBackground('addCustomNetwork', [customRpc]);
await submitRequestToBackground('addCustomNetwork', [
customRpc,
generateActionId(),
]);
} catch (error) {
log.error(error);
dispatch(displayWarning('Had a problem changing networks!'));

Loading…
Cancel
Save