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