1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-22 01:47:00 +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 { isManifestV3 } from '../../../shared/modules/mv3.utils';
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';
@ -110,6 +110,7 @@ export default class MetaMetricsController {
this.environment = environment;
const abandonedFragments = omitBy(initState?.fragments, 'persist');
const segmentApiCalls = initState?.segmentApiCalls || {};
this.store = new ObservableStore({
participateInMetaMetrics: null,
@ -120,6 +121,9 @@ export default class MetaMetricsController {
fragments: {
...initState?.fragments,
},
segmentApiCalls: {
...segmentApiCalls,
},
});
preferencesStore.subscribe(({ currentLocale }) => {
@ -142,6 +146,15 @@ export default class MetaMetricsController {
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
// 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
@ -455,7 +468,7 @@ export default class MetaMetricsController {
const { metaMetricsId } = this.state;
const idTrait = metaMetricsId ? 'userId' : 'anonymousId';
const idValue = metaMetricsId ?? METAMETRICS_ANONYMOUS_ID;
this.segment.page({
this._submitSegmentAPICall('page', {
[idTrait]: idValue,
name,
properties: {
@ -808,7 +821,7 @@ export default class MetaMetricsController {
}
try {
this.segment.identify({
this._submitSegmentAPICall('identify', {
userId: metaMetricsId,
traits: userTraits,
});
@ -937,10 +950,49 @@ export default class MetaMetricsController {
return resolve();
};
this.segment.track(payload, callback);
this._submitSegmentAPICall('track', payload, callback);
if (flushImmediately) {
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';
import waitUntilCalled from '../../../test/lib/wait-until-called';
import { CHAIN_IDS, CURRENCY_SYMBOLS } from '../../../shared/constants/network';
import * as Utils from '../lib/util';
import MetaMetricsController from './metametrics';
import { NETWORK_EVENTS } from './network';
@ -124,9 +125,10 @@ function getMetaMetricsController({
metaMetricsId = TEST_META_METRICS_ID,
preferencesStore = getMockPreferencesStore(),
networkController = getMockNetworkController(),
segmentInstance,
} = {}) {
return new MetaMetricsController({
segment,
segment: segmentInstance || segment,
getNetworkIdentifier:
networkController.getNetworkIdentifier.bind(networkController),
getCurrentChainId:
@ -145,10 +147,17 @@ function getMetaMetricsController({
testid: SAMPLE_PERSISTED_EVENT,
testid2: SAMPLE_NON_PERSISTED_EVENT,
},
events: {},
},
});
}
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 () {
it('should properly initialize', function () {
const mock = sinon.mock(segment);
@ -163,6 +172,8 @@ describe('MetaMetricsController', function () {
...DEFAULT_EVENT_PROPERTIES,
test: true,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
const metaMetricsController = getMetaMetricsController();
assert.strictEqual(metaMetricsController.version, VERSION);
@ -233,15 +244,18 @@ describe('MetaMetricsController', function () {
});
const mock = sinon.mock(segment);
mock
.expects('identify')
.once()
.withArgs({ userId: TEST_META_METRICS_ID, traits: MOCK_TRAITS });
mock.expects('identify').once().withArgs({
userId: TEST_META_METRICS_ID,
traits: MOCK_TRAITS,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.identify({
...MOCK_TRAITS,
...MOCK_INVALID_TRAITS,
});
mock.verify();
});
@ -263,6 +277,8 @@ describe('MetaMetricsController', function () {
traits: {
test_date: mockDateISOString,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.identify({
@ -358,6 +374,8 @@ describe('MetaMetricsController', function () {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent(
{
@ -388,6 +406,8 @@ describe('MetaMetricsController', function () {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent(
{
@ -417,6 +437,8 @@ describe('MetaMetricsController', function () {
legacy_event: true,
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent(
{
@ -439,12 +461,14 @@ describe('MetaMetricsController', function () {
.once()
.withArgs({
event: 'Fake Event',
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: {
test: 1,
...DEFAULT_EVENT_PROPERTIES,
},
context: DEFAULT_TEST_CONTEXT,
userId: TEST_META_METRICS_ID,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.submitEvent({
event: 'Fake Event',
@ -519,6 +543,8 @@ describe('MetaMetricsController', function () {
foo: 'bar',
...DEFAULT_EVENT_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}),
);
assert.ok(
@ -527,6 +553,8 @@ describe('MetaMetricsController', function () {
userId: TEST_META_METRICS_ID,
context: DEFAULT_TEST_CONTEXT,
properties: DEFAULT_EVENT_PROPERTIES,
messageId: Utils.generateRandomId(),
timestamp: new Date(),
}),
);
});
@ -547,6 +575,8 @@ describe('MetaMetricsController', function () {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
metaMetricsController.trackPage({
name: 'home',
@ -590,6 +620,8 @@ describe('MetaMetricsController', function () {
params: null,
...DEFAULT_PAGE_PROPERTIES,
},
messageId: Utils.generateRandomId(),
timestamp: new Date(),
});
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 () {
// flush the queues manually after each test
segment.flush();
clock.restore();
sinon.restore();
});
});

View File

@ -2,21 +2,10 @@ import removeSlash from 'remove-trailing-slash';
import looselyValidate from '@segment/loosely-validate-event';
import { isString } from 'lodash';
import isRetryAllowed from 'is-retry-allowed';
import { generateRandomId } from '../util';
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
function isNetworkError(error) {
return (

View File

@ -174,3 +174,19 @@ export {
getChainType,
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);
};