import { strict as assert } from 'assert' import sinon from 'sinon' import MetaMetricsController from '../../../../app/scripts/controllers/metametrics' import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../../app/scripts/lib/enums' import { createSegmentMock } from '../../../../app/scripts/lib/segment' import { METAMETRICS_ANONYMOUS_ID, METAMETRICS_BACKGROUND_PAGE_OBJECT, } from '../../../../shared/constants/metametrics' import waitUntilCalled from '../../../lib/wait-until-called' const segment = createSegmentMock(2, 10000) const segmentLegacy = createSegmentMock(2, 10000) const VERSION = '0.0.1-test' const NETWORK = 'Mainnet' const FAKE_CHAIN_ID = '0x1338' const LOCALE = 'en_US' const TEST_META_METRICS_ID = '0xabc' const DEFAULT_TEST_CONTEXT = { app: { name: 'MetaMask Extension', version: VERSION }, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, referrer: undefined, userAgent: window.navigator.userAgent, } const DEFAULT_SHARED_PROPERTIES = { chain_id: FAKE_CHAIN_ID, locale: LOCALE.replace('_', '-'), network: NETWORK, environment_type: 'background', } const DEFAULT_EVENT_PROPERTIES = { category: 'Unit Test', revenue: undefined, value: undefined, currency: undefined, ...DEFAULT_SHARED_PROPERTIES, } const DEFAULT_PAGE_PROPERTIES = { ...DEFAULT_SHARED_PROPERTIES, } function getMockNetworkController( chainId = FAKE_CHAIN_ID, provider = { type: NETWORK }, ) { let networkStore = { chainId, provider } const on = sinon.stub().withArgs('networkDidChange') const updateState = (newState) => { networkStore = { ...networkStore, ...newState } on.getCall(0).args[1]() } return { store: { getState: () => networkStore, updateState, }, getCurrentChainId: () => networkStore.chainId, getNetworkIdentifier: () => networkStore.provider.type, on, } } function getMockPreferencesStore({ currentLocale = LOCALE } = {}) { let preferencesStore = { currentLocale, } const subscribe = sinon.stub() const updateState = (newState) => { preferencesStore = { ...preferencesStore, ...newState } subscribe.getCall(0).args[0](preferencesStore) } return { getState: sinon.stub().returns(preferencesStore), updateState, subscribe, } } function getMetaMetricsController({ participateInMetaMetrics = true, metaMetricsId = TEST_META_METRICS_ID, metaMetricsSendCount = 0, preferencesStore = getMockPreferencesStore(), networkController = getMockNetworkController(), } = {}) { return new MetaMetricsController({ segment, segmentLegacy, getNetworkIdentifier: networkController.getNetworkIdentifier.bind( networkController, ), getCurrentChainId: networkController.getCurrentChainId.bind( networkController, ), onNetworkDidChange: networkController.on.bind( networkController, 'networkDidChange', ), preferencesStore, version: '0.0.1', environment: 'test', initState: { participateInMetaMetrics, metaMetricsId, metaMetricsSendCount, }, }) } describe('MetaMetricsController', function () { describe('constructor', function () { it('should properly initialize', function () { const metaMetricsController = getMetaMetricsController() assert.strictEqual(metaMetricsController.version, VERSION) assert.strictEqual(metaMetricsController.network, NETWORK) assert.strictEqual(metaMetricsController.chainId, FAKE_CHAIN_ID) assert.strictEqual( metaMetricsController.state.participateInMetaMetrics, true, ) assert.strictEqual( metaMetricsController.state.metaMetricsId, TEST_META_METRICS_ID, ) assert.strictEqual(metaMetricsController.locale, LOCALE.replace('_', '-')) }) it('should update when network changes', function () { const networkController = getMockNetworkController() const metaMetricsController = getMetaMetricsController({ networkController, }) assert.strictEqual(metaMetricsController.network, NETWORK) networkController.store.updateState({ provider: { type: 'NEW_NETWORK', }, chainId: '0xaab', }) assert.strictEqual(metaMetricsController.network, 'NEW_NETWORK') assert.strictEqual(metaMetricsController.chainId, '0xaab') }) it('should update when preferences changes', function () { const preferencesStore = getMockPreferencesStore() const metaMetricsController = getMetaMetricsController({ preferencesStore, }) assert.strictEqual(metaMetricsController.network, NETWORK) preferencesStore.updateState({ currentLocale: 'en_UK', }) assert.strictEqual(metaMetricsController.locale, 'en-UK') }) }) describe('generateMetaMetricsId', function () { it('should generate an 0x prefixed hex string', function () { const metaMetricsController = getMetaMetricsController() assert.equal( metaMetricsController.generateMetaMetricsId().startsWith('0x'), true, ) }) }) describe('setParticipateInMetaMetrics', function () { it('should update the value of participateInMetaMetrics', function () { const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: null, metaMetricsId: null, }) assert.equal(metaMetricsController.state.participateInMetaMetrics, null) metaMetricsController.setParticipateInMetaMetrics(true) assert.equal(metaMetricsController.state.participateInMetaMetrics, true) metaMetricsController.setParticipateInMetaMetrics(false) assert.equal(metaMetricsController.state.participateInMetaMetrics, false) }) it('should generate and update the metaMetricsId when set to true', function () { const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: null, metaMetricsId: null, }) assert.equal(metaMetricsController.state.metaMetricsId, null) metaMetricsController.setParticipateInMetaMetrics(true) assert.equal(typeof metaMetricsController.state.metaMetricsId, 'string') }) it('should nullify the metaMetricsId when set to false', function () { const metaMetricsController = getMetaMetricsController() metaMetricsController.setParticipateInMetaMetrics(false) assert.equal(metaMetricsController.state.metaMetricsId, null) }) }) describe('setMetaMetricsSendCount', function () { it('should update the send count in state', function () { const metaMetricsController = getMetaMetricsController() metaMetricsController.setMetaMetricsSendCount(1) assert.equal(metaMetricsController.state.metaMetricsSendCount, 1) }) }) describe('trackEvent', function () { it('should not track an event if user is not participating in metametrics', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: false, }) mock.expects('track').never() metaMetricsController.trackEvent({ event: 'Fake Event', category: 'Unit Test', properties: { test: 1, }, }) mock.verify() }) it('should track an event if user has not opted in, but isOptIn is true', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: false, }) mock .expects('track') .once() .withArgs({ event: 'Fake Event', anonymousId: METAMETRICS_ANONYMOUS_ID, context: DEFAULT_TEST_CONTEXT, properties: { test: 1, ...DEFAULT_EVENT_PROPERTIES, }, }) metaMetricsController.trackEvent( { event: 'Fake Event', category: 'Unit Test', properties: { test: 1, }, }, { isOptIn: true }, ) mock.verify() }) it('should track an event during optin and allow for metaMetricsId override', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: false, }) mock .expects('track') .once() .withArgs({ event: 'Fake Event', userId: 'TESTID', context: DEFAULT_TEST_CONTEXT, properties: { test: 1, ...DEFAULT_EVENT_PROPERTIES, }, }) metaMetricsController.trackEvent( { event: 'Fake Event', category: 'Unit Test', properties: { test: 1, }, }, { isOptIn: true, metaMetricsId: 'TESTID' }, ) mock.verify() }) it('should track a legacy event', function () { const mock = sinon.mock(segmentLegacy) const metaMetricsController = getMetaMetricsController() mock .expects('track') .once() .withArgs({ event: 'Fake Event', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { test: 1, ...DEFAULT_EVENT_PROPERTIES, }, }) metaMetricsController.trackEvent( { event: 'Fake Event', category: 'Unit Test', properties: { test: 1, }, }, { matomoEvent: true }, ) mock.verify() }) it('should track a non legacy event', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController() mock .expects('track') .once() .withArgs({ event: 'Fake Event', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { test: 1, ...DEFAULT_EVENT_PROPERTIES, }, }) metaMetricsController.trackEvent({ event: 'Fake Event', category: 'Unit Test', properties: { test: 1, }, }) mock.verify() }) it('should use anonymousId when metametrics send count is not trackable in send flow', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController({ metaMetricsSendCount: 1, }) mock .expects('track') .once() .withArgs({ event: 'Send Fake Event', anonymousId: METAMETRICS_ANONYMOUS_ID, context: DEFAULT_TEST_CONTEXT, properties: { test: 1, ...DEFAULT_EVENT_PROPERTIES, }, }) metaMetricsController.trackEvent({ event: 'Send Fake Event', category: 'Unit Test', properties: { test: 1, }, }) mock.verify() }) it('should use user metametrics id when metametrics send count is trackable in send flow', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController() mock .expects('track') .once() .withArgs({ event: 'Send Fake Event', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { test: 1, ...DEFAULT_EVENT_PROPERTIES, }, }) metaMetricsController.trackEvent( { event: 'Send Fake Event', category: 'Unit Test', properties: { test: 1, }, }, { metaMetricsSendCount: 0 }, ) mock.verify() }) it('should immediately flush queue if flushImmediately set to true', async function () { const metaMetricsController = getMetaMetricsController() const flushStub = sinon.stub(segment, 'flush') const flushCalled = waitUntilCalled(flushStub, segment) metaMetricsController.trackEvent( { event: 'Fake Event', category: 'Unit Test', }, { flushImmediately: true }, ) assert.doesNotReject(flushCalled()) }) it('should throw if event or category not provided', function () { const metaMetricsController = getMetaMetricsController() assert.rejects( () => metaMetricsController.trackEvent({ event: 'test' }), /Must specify event and category\./u, 'must specify category', ) assert.rejects( () => metaMetricsController.trackEvent({ category: 'test' }), /Must specify event and category\./u, 'must specify event', ) }) it('should throw if provided sensitiveProperties, when excludeMetaMetricsId is true', function () { const metaMetricsController = getMetaMetricsController() assert.rejects( () => metaMetricsController.trackEvent( { event: 'Fake Event', category: 'Unit Test', sensitiveProperties: { foo: 'bar' }, }, { excludeMetaMetricsId: true }, ), /sensitiveProperties was specified in an event payload that also set the excludeMetaMetricsId flag/u, ) }) it('should track sensitiveProperties in a separate, anonymous event', function () { const metaMetricsController = getMetaMetricsController() const spy = sinon.spy(segment, 'track') metaMetricsController.trackEvent({ event: 'Fake Event', category: 'Unit Test', sensitiveProperties: { foo: 'bar' }, }) assert.ok(spy.calledTwice) assert.ok( spy.calledWith({ event: 'Fake Event', anonymousId: METAMETRICS_ANONYMOUS_ID, context: DEFAULT_TEST_CONTEXT, properties: { foo: 'bar', ...DEFAULT_EVENT_PROPERTIES, }, }), ) assert.ok( spy.calledWith({ event: 'Fake Event', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: DEFAULT_EVENT_PROPERTIES, }), ) }) }) describe('trackPage', function () { it('should track a page view', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController() mock .expects('page') .once() .withArgs({ name: 'home', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { params: null, ...DEFAULT_PAGE_PROPERTIES, }, }) metaMetricsController.trackPage({ name: 'home', params: null, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, }) mock.verify() }) it('should not track a page view if user is not participating in metametrics', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController({ participateInMetaMetrics: false, }) mock.expects('page').never() metaMetricsController.trackPage({ name: 'home', params: null, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, }) mock.verify() }) it('should track a page view if isOptInPath is true and user not yet opted in', function () { const mock = sinon.mock(segment) const metaMetricsController = getMetaMetricsController({ preferencesStore: getMockPreferencesStore({ participateInMetaMetrics: null, }), }) mock .expects('page') .once() .withArgs({ name: 'home', userId: TEST_META_METRICS_ID, context: DEFAULT_TEST_CONTEXT, properties: { params: null, ...DEFAULT_PAGE_PROPERTIES, }, }) metaMetricsController.trackPage( { name: 'home', params: null, environmentType: ENVIRONMENT_TYPE_BACKGROUND, page: METAMETRICS_BACKGROUND_PAGE_OBJECT, }, { isOptInPath: true }, ) mock.verify() }) }) afterEach(function () { // flush the queues manually after each test segment.flush() segmentLegacy.flush() sinon.restore() }) })