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

Persisting segment events in MetaMetricsController store (#16198)

This commit is contained in:
Jyoti Puri 2022-11-08 04:33:03 +05:30 committed by GitHub
parent ec11ff66ee
commit e7deab4b9b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 138 additions and 23 deletions

View File

@ -20,7 +20,7 @@ import {
import { SECOND } from '../../../shared/constants/time'; import { SECOND } from '../../../shared/constants/time';
import { isManifestV3 } from '../../../shared/modules/mv3.utils'; import { isManifestV3 } from '../../../shared/modules/mv3.utils';
import { METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM } from '../../../shared/constants/alarms'; import { METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM } from '../../../shared/constants/alarms';
import { checkAlarmExists } from '../lib/util'; import { checkAlarmExists, generateRandomId, isValidDate } from '../lib/util';
const EXTENSION_UNINSTALL_URL = 'https://metamask.io/uninstalled'; const EXTENSION_UNINSTALL_URL = 'https://metamask.io/uninstalled';
@ -110,6 +110,7 @@ export default class MetaMetricsController {
this.environment = environment; this.environment = environment;
const abandonedFragments = omitBy(initState?.fragments, 'persist'); const abandonedFragments = omitBy(initState?.fragments, 'persist');
const segmentApiCalls = initState?.segmentApiCalls || {};
this.store = new ObservableStore({ this.store = new ObservableStore({
participateInMetaMetrics: null, participateInMetaMetrics: null,
@ -120,6 +121,9 @@ export default class MetaMetricsController {
fragments: { fragments: {
...initState?.fragments, ...initState?.fragments,
}, },
segmentApiCalls: {
...segmentApiCalls,
},
}); });
preferencesStore.subscribe(({ currentLocale }) => { preferencesStore.subscribe(({ currentLocale }) => {
@ -142,6 +146,15 @@ export default class MetaMetricsController {
this.finalizeEventFragment(fragment.id, { abandoned: true }); this.finalizeEventFragment(fragment.id, { abandoned: true });
}); });
// Code below submits any pending segmentApiCalls to Segment if/when the controller is re-instantiated
if (isManifestV3) {
Object.values(segmentApiCalls).forEach(
({ eventType, payload, callback }) => {
this._submitSegmentAPICall(eventType, payload, callback);
},
);
}
// Close out event fragments that were created but not progressed. An // Close out event fragments that were created but not progressed. An
// interval is used to routinely check if a fragment has not been updated // 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 // within the fragment's timeout window. When creating a new event fragment
@ -455,7 +468,7 @@ export default class MetaMetricsController {
const { metaMetricsId } = this.state; const { metaMetricsId } = this.state;
const idTrait = metaMetricsId ? 'userId' : 'anonymousId'; const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID; const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
this.segment.page({ this._submitSegmentAPICall('page', {
[idTrait]: idValue, [idTrait]: idValue,
name, name,
properties: { properties: {
@ -808,7 +821,7 @@ export default class MetaMetricsController {
} }
try { try {
this.segment.identify({ this._submitSegmentAPICall('identify', {
userId: metaMetricsId, userId: metaMetricsId,
traits: userTraits, traits: userTraits,
}); });
@ -937,10 +950,49 @@ export default class MetaMetricsController {
return resolve(); return resolve();
}; };
this.segment.track(payload, callback); this._submitSegmentAPICall('track', payload, callback);
if (flushImmediately) { if (flushImmediately) {
this.segment.flush(); this.segment.flush();
} }
}); });
} }
// Method below submits the request to analytics SDK.
// It will also add event to controller store
// and pass a callback to remove it from store once request is submitted to segment
// Saving segmentApiCalls in controller store in MV3 ensures that events are tracked
// even if service worker terminates before events are submiteed to segment.
_submitSegmentAPICall(eventType, payload, callback) {
const messageId = payload.messageId || generateRandomId();
let timestamp = new Date();
if (payload.timestamp) {
const payloadDate = new Date(payload.timestamp);
if (isValidDate(payloadDate)) {
timestamp = payloadDate;
}
}
const modifiedPayload = { ...payload, messageId, timestamp };
this.store.updateState({
segmentApiCalls: {
...this.store.getState().segmentApiCalls,
[messageId]: {
eventType,
payload: {
...modifiedPayload,
timestamp: modifiedPayload.timestamp.toString(),
},
callback,
},
},
});
const modifiedCallback = (result) => {
const { segmentApiCalls } = this.store.getState();
delete segmentApiCalls[messageId];
this.store.updateState({
segmentApiCalls,
});
return callback?.(result);
};
this.segment[eventType](modifiedPayload, modifiedCallback);
}
} }

View File

@ -9,6 +9,7 @@ import {
} from '../../../shared/constants/metametrics'; } from '../../../shared/constants/metametrics';
import waitUntilCalled from '../../../test/lib/wait-until-called'; import waitUntilCalled from '../../../test/lib/wait-until-called';
import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../../shared/constants/network'; import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../../shared/constants/network';
import * as Utils from '../lib/util';
import MetaMetricsController from './metametrics'; import MetaMetricsController from './metametrics';
import { NETWORK_EVENTS } from './network'; import { NETWORK_EVENTS } from './network';
@ -124,9 +125,10 @@ function getMetaMetricsController({
metaMetricsId = TEST_META_METRICS_ID, metaMetricsId = TEST_META_METRICS_ID,
preferencesStore = getMockPreferencesStore(), preferencesStore = getMockPreferencesStore(),
networkController = getMockNetworkController(), networkController = getMockNetworkController(),
segmentInstance,
} = {}) { } = {}) {
return new MetaMetricsController({ return new MetaMetricsController({
segment, segment: segmentInstance || segment,
getNetworkIdentifier: getNetworkIdentifier:
networkController.getNetworkIdentifier.bind(networkController), networkController.getNetworkIdentifier.bind(networkController),
getCurrentChainId: getCurrentChainId:
@ -145,10 +147,17 @@ function getMetaMetricsController({
testid: SAMPLE_PERSISTED_EVENT, testid: SAMPLE_PERSISTED_EVENT,
testid2: SAMPLE_NON_PERSISTED_EVENT, testid2: SAMPLE_NON_PERSISTED_EVENT,
}, },
events: {},
}, },
}); });
} }
describe('MetaMetricsController', function () { describe('MetaMetricsController', function () {
const now = new Date();
let clock;
beforeEach(function () {
clock = sinon.useFakeTimers(now.getTime());
sinon.stub(Utils, 'generateRandomId').returns('DUMMY_RANDOM_ID');
});
describe('constructor', function () { describe('constructor', function () {
it('should properly initialize', function () { it('should properly initialize', function () {
const mock = sinon.mock(segment); const mock = sinon.mock(segment);
@ -163,6 +172,8 @@ describe('MetaMetricsController', function () {
...DEFAULT_EVENT_PROPERTIES, ...DEFAULT_EVENT_PROPERTIES,
test: true, test: true,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
const metaMetricsController = getMetaMetricsController(); const metaMetricsController = getMetaMetricsController();
assert.strictEqual(metaMetricsController.version, VERSION); assert.strictEqual(metaMetricsController.version, VERSION);
@ -233,15 +244,18 @@ describe('MetaMetricsController', function () {
}); });
const mock = sinon.mock(segment); const mock = sinon.mock(segment);
mock mock.expects('identify').once().withArgs({
.expects('identify') userId: TEST_META_METRICS_ID,
.once() traits: MOCK_TRAITS,
.withArgs({ userId: TEST_META_METRICS_ID, traits: MOCK_TRAITS }); messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.identify({ metaMetricsController.identify({
...MOCK_TRAITS, ...MOCK_TRAITS,
...MOCK_INVALID_TRAITS, ...MOCK_INVALID_TRAITS,
}); });
mock.verify(); mock.verify();
}); });
@ -263,6 +277,8 @@ describe('MetaMetricsController', function () {
traits: { traits: {
test_date: mockDateISOString, test_date: mockDateISOString,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
metaMetricsController.identify({ metaMetricsController.identify({
@ -358,6 +374,8 @@ describe('MetaMetricsController', function () {
test: 1, test: 1,
...DEFAULT_EVENT_PROPERTIES, ...DEFAULT_EVENT_PROPERTIES,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
metaMetricsController.submitEvent( metaMetricsController.submitEvent(
{ {
@ -388,6 +406,8 @@ describe('MetaMetricsController', function () {
test: 1, test: 1,
...DEFAULT_EVENT_PROPERTIES, ...DEFAULT_EVENT_PROPERTIES,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
metaMetricsController.submitEvent( metaMetricsController.submitEvent(
{ {
@ -417,6 +437,8 @@ describe('MetaMetricsController', function () {
legacy_event: true, legacy_event: true,
...DEFAULT_EVENT_PROPERTIES, ...DEFAULT_EVENT_PROPERTIES,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
metaMetricsController.submitEvent( metaMetricsController.submitEvent(
{ {
@ -439,12 +461,14 @@ describe('MetaMetricsController', function () {
.once() .once()
.withArgs({ .withArgs({
event: 'Fake Event', event: 'Fake Event',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: { properties: {
test: 1, test: 1,
...DEFAULT_EVENT_PROPERTIES, ...DEFAULT_EVENT_PROPERTIES,
}, },
context: DEFAULT_TEST_CONTEXT,
userId: TEST_META_METRICS_ID,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
metaMetricsController.submitEvent({ metaMetricsController.submitEvent({
event: 'Fake Event', event: 'Fake Event',
@ -519,6 +543,8 @@ describe('MetaMetricsController', function () {
foo: 'bar', foo: 'bar',
...DEFAULT_EVENT_PROPERTIES, ...DEFAULT_EVENT_PROPERTIES,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}), }),
); );
assert.ok( assert.ok(
@ -527,6 +553,8 @@ describe('MetaMetricsController', function () {
userId: TEST_META_METRICS_ID, userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT, context: DEFAULT_TEST_CONTEXT,
properties: DEFAULT_EVENT_PROPERTIES, properties: DEFAULT_EVENT_PROPERTIES,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}), }),
); );
}); });
@ -547,6 +575,8 @@ describe('MetaMetricsController', function () {
params: null, params: null,
...DEFAULT_PAGE_PROPERTIES, ...DEFAULT_PAGE_PROPERTIES,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
metaMetricsController.trackPage({ metaMetricsController.trackPage({
name: 'home', name: 'home',
@ -590,6 +620,8 @@ describe('MetaMetricsController', function () {
params: null, params: null,
...DEFAULT_PAGE_PROPERTIES, ...DEFAULT_PAGE_PROPERTIES,
}, },
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}); });
metaMetricsController.trackPage( metaMetricsController.trackPage(
{ {
@ -788,9 +820,35 @@ describe('MetaMetricsController', function () {
}); });
}); });
describe('submitting segmentApiCalls to segment SDK', function () {
it('should add event to store when submitting to SDK', function () {
const metaMetricsController = getMetaMetricsController({});
metaMetricsController.trackPage({}, { isOptIn: true });
const { segmentApiCalls } = metaMetricsController.store.getState();
assert(Object.keys(segmentApiCalls).length > 0);
});
it('should remove event from store when callback is invoked', function () {
const segmentInstance = createSegmentMock(2, 10000);
const stubFn = (_, cb) => {
cb();
};
sinon.stub(segmentInstance, 'track').callsFake(stubFn);
sinon.stub(segmentInstance, 'page').callsFake(stubFn);
const metaMetricsController = getMetaMetricsController({
segmentInstance,
});
metaMetricsController.trackPage({}, { isOptIn: true });
const { segmentApiCalls } = metaMetricsController.store.getState();
assert(Object.keys(segmentApiCalls).length === 0);
});
});
afterEach(function () { afterEach(function () {
// flush the queues manually after each test // flush the queues manually after each test
segment.flush(); segment.flush();
clock.restore();
sinon.restore(); sinon.restore();
}); });
}); });

View File

@ -2,21 +2,10 @@ import removeSlash from 'remove-trailing-slash';
import looselyValidate from '@segment/loosely-validate-event'; import looselyValidate from '@segment/loosely-validate-event';
import { isString } from 'lodash'; import { isString } from 'lodash';
import isRetryAllowed from 'is-retry-allowed'; import isRetryAllowed from 'is-retry-allowed';
import { generateRandomId } from '../util';
const noop = () => ({}); const noop = () => ({});
// Taken from https://stackoverflow.com/a/1349426/3696652
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const generateRandomId = () => {
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 20; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
// Method below is inspired from axios-retry https://github.com/softonic/axios-retry // Method below is inspired from axios-retry https://github.com/softonic/axios-retry
function isNetworkError(error) { function isNetworkError(error) {
return ( return (

View File

@ -174,3 +174,19 @@ export {
getChainType, getChainType,
checkAlarmExists, checkAlarmExists,
}; };
// Taken from https://stackoverflow.com/a/1349426/3696652
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
export const generateRandomId = () => {
let result = '';
const charactersLength = characters.length;
for (let i = 0; i < 20; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
};
export const isValidDate = (d) => {
return d instanceof Date && !isNaN(d);
};