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:
parent
ec11ff66ee
commit
e7deab4b9b
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
@ -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 (
|
||||
|
@ -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);
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user