mirror of
https://github.com/kremalicious/metamask-extension.git
synced 2024-11-22 01:47:00 +01:00
Build user traits object when metamask state changes (#14192)
This commit is contained in:
parent
a3ab204d11
commit
b525c620f4
@ -1,4 +1,4 @@
|
||||
import { merge, omit, omitBy } from 'lodash';
|
||||
import { isEqual, merge, omit, omitBy, pickBy } from 'lodash';
|
||||
import { ObservableStore } from '@metamask/obs-store';
|
||||
import { bufferToHex, keccak } from 'ethereumjs-util';
|
||||
import { generateUUID } from 'pubnub';
|
||||
@ -6,6 +6,7 @@ import { ENVIRONMENT_TYPE_BACKGROUND } from '../../../shared/constants/app';
|
||||
import {
|
||||
METAMETRICS_ANONYMOUS_ID,
|
||||
METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
TRAITS,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
import { SECOND } from '../../../shared/constants/time';
|
||||
|
||||
@ -303,150 +304,6 @@ export default class MetaMetricsController {
|
||||
return this.store.getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the context object to attach to page and track events.
|
||||
*
|
||||
* @private
|
||||
* @param {Pick<MetaMetricsContext, 'referrer'>} [referrer] - dapp origin that initialized
|
||||
* the notification window.
|
||||
* @param {Pick<MetaMetricsContext, 'page'>} [page] - page object describing the current
|
||||
* view of the extension. Defaults to the background-process object.
|
||||
* @returns {MetaMetricsContext}
|
||||
*/
|
||||
_buildContext(referrer, page = METAMETRICS_BACKGROUND_PAGE_OBJECT) {
|
||||
return {
|
||||
app: {
|
||||
name: 'MetaMask Extension',
|
||||
version: this.version,
|
||||
},
|
||||
userAgent: window.navigator.userAgent,
|
||||
page,
|
||||
referrer,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build's the event payload, processing all fields into a format that can be
|
||||
* fed to Segment's track method
|
||||
*
|
||||
* @private
|
||||
* @param {
|
||||
* Omit<MetaMetricsEventPayload, 'sensitiveProperties'>
|
||||
* } rawPayload - raw payload provided to trackEvent
|
||||
* @returns {SegmentEventPayload} formatted event payload for segment
|
||||
*/
|
||||
_buildEventPayload(rawPayload) {
|
||||
const {
|
||||
event,
|
||||
properties,
|
||||
revenue,
|
||||
value,
|
||||
currency,
|
||||
category,
|
||||
page,
|
||||
referrer,
|
||||
environmentType = ENVIRONMENT_TYPE_BACKGROUND,
|
||||
} = rawPayload;
|
||||
return {
|
||||
event,
|
||||
properties: {
|
||||
// These values are omitted from properties because they have special meaning
|
||||
// in segment. https://segment.com/docs/connections/spec/track/#properties.
|
||||
// to avoid accidentally using these inappropriately, you must add them as top
|
||||
// level properties on the event payload. We also exclude locale to prevent consumers
|
||||
// from overwriting this context level property. We track it as a property
|
||||
// because not all destinations map locale from context.
|
||||
...omit(properties, ['revenue', 'locale', 'currency', 'value']),
|
||||
revenue,
|
||||
value,
|
||||
currency,
|
||||
category,
|
||||
network: properties?.network ?? this.network,
|
||||
locale: this.locale,
|
||||
chain_id: properties?.chain_id ?? this.chainId,
|
||||
environment_type: environmentType,
|
||||
},
|
||||
context: this._buildContext(referrer, page),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform validation on the payload and update the id type to use before
|
||||
* sending to Segment. Also examines the options to route and handle the
|
||||
* event appropriately.
|
||||
*
|
||||
* @private
|
||||
* @param {SegmentEventPayload} payload - properties to attach to event
|
||||
* @param {MetaMetricsEventOptions} [options] - options for routing and
|
||||
* handling the event
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_track(payload, options) {
|
||||
const {
|
||||
isOptIn,
|
||||
metaMetricsId: metaMetricsIdOverride,
|
||||
matomoEvent,
|
||||
flushImmediately,
|
||||
} = options || {};
|
||||
let idType = 'userId';
|
||||
let idValue = this.state.metaMetricsId;
|
||||
let excludeMetaMetricsId = options?.excludeMetaMetricsId ?? false;
|
||||
// This is carried over from the old implementation, and will likely need
|
||||
// to be updated to work with the new tracking plan. I think we should use
|
||||
// a config setting for this instead of trying to match the event name
|
||||
const isSendFlow = Boolean(payload.event.match(/^send|^confirm/iu));
|
||||
if (isSendFlow) {
|
||||
excludeMetaMetricsId = true;
|
||||
}
|
||||
// If we are tracking sensitive data we will always use the anonymousId
|
||||
// property as well as our METAMETRICS_ANONYMOUS_ID. This prevents us from
|
||||
// associating potentially identifiable information with a specific id.
|
||||
// During the opt in flow we will track all events, but do so with the
|
||||
// anonymous id. The one exception to that rule is after the user opts in
|
||||
// to MetaMetrics. When that happens we receive back the user's new
|
||||
// MetaMetrics id before it is fully persisted to state. To avoid a race
|
||||
// condition we explicitly pass the new id to the track method. In that
|
||||
// case we will track the opt in event to the user's id. In all other cases
|
||||
// we use the metaMetricsId from state.
|
||||
if (excludeMetaMetricsId || (isOptIn && !metaMetricsIdOverride)) {
|
||||
idType = 'anonymousId';
|
||||
idValue = METAMETRICS_ANONYMOUS_ID;
|
||||
} else if (isOptIn && metaMetricsIdOverride) {
|
||||
idValue = metaMetricsIdOverride;
|
||||
}
|
||||
payload[idType] = idValue;
|
||||
|
||||
// If this is an event on the old matomo schema, add a key to the payload
|
||||
// to designate it as such
|
||||
if (matomoEvent === true) {
|
||||
payload.properties.legacy_event = true;
|
||||
}
|
||||
|
||||
// Promises will only resolve when the event is sent to segment. For any
|
||||
// event that relies on this promise being fulfilled before performing UI
|
||||
// updates, or otherwise delaying user interaction, supply the
|
||||
// 'flushImmediately' flag to the trackEvent method.
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (err) => {
|
||||
if (err) {
|
||||
// The error that segment gives us has some manipulation done to it
|
||||
// that seemingly breaks with lockdown enabled. Creating a new error
|
||||
// here prevents the system from freezing when the network request to
|
||||
// segment fails for any reason.
|
||||
const safeError = new Error(err.message);
|
||||
safeError.stack = err.stack;
|
||||
return reject(safeError);
|
||||
}
|
||||
return resolve();
|
||||
};
|
||||
|
||||
this.segment.track(payload, callback);
|
||||
if (flushImmediately) {
|
||||
this.segment.flush();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* track a page view with Segment
|
||||
*
|
||||
@ -573,4 +430,185 @@ export default class MetaMetricsController {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
handleMetaMaskStateUpdate(newState) {
|
||||
const userTraits = this._buildUserTraitsObject(newState);
|
||||
if (userTraits) {
|
||||
// this.identify(userTraits);
|
||||
}
|
||||
}
|
||||
|
||||
/** PRIVATE METHODS */
|
||||
|
||||
/**
|
||||
* Build the context object to attach to page and track events.
|
||||
*
|
||||
* @private
|
||||
* @param {Pick<MetaMetricsContext, 'referrer'>} [referrer] - dapp origin that initialized
|
||||
* the notification window.
|
||||
* @param {Pick<MetaMetricsContext, 'page'>} [page] - page object describing the current
|
||||
* view of the extension. Defaults to the background-process object.
|
||||
* @returns {MetaMetricsContext}
|
||||
*/
|
||||
_buildContext(referrer, page = METAMETRICS_BACKGROUND_PAGE_OBJECT) {
|
||||
return {
|
||||
app: {
|
||||
name: 'MetaMask Extension',
|
||||
version: this.version,
|
||||
},
|
||||
userAgent: window.navigator.userAgent,
|
||||
page,
|
||||
referrer,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Build's the event payload, processing all fields into a format that can be
|
||||
* fed to Segment's track method
|
||||
*
|
||||
* @private
|
||||
* @param {
|
||||
* Omit<MetaMetricsEventPayload, 'sensitiveProperties'>
|
||||
* } rawPayload - raw payload provided to trackEvent
|
||||
* @returns {SegmentEventPayload} formatted event payload for segment
|
||||
*/
|
||||
_buildEventPayload(rawPayload) {
|
||||
const {
|
||||
event,
|
||||
properties,
|
||||
revenue,
|
||||
value,
|
||||
currency,
|
||||
category,
|
||||
page,
|
||||
referrer,
|
||||
environmentType = ENVIRONMENT_TYPE_BACKGROUND,
|
||||
} = rawPayload;
|
||||
return {
|
||||
event,
|
||||
properties: {
|
||||
// These values are omitted from properties because they have special meaning
|
||||
// in segment. https://segment.com/docs/connections/spec/track/#properties.
|
||||
// to avoid accidentally using these inappropriately, you must add them as top
|
||||
// level properties on the event payload. We also exclude locale to prevent consumers
|
||||
// from overwriting this context level property. We track it as a property
|
||||
// because not all destinations map locale from context.
|
||||
...omit(properties, ['revenue', 'locale', 'currency', 'value']),
|
||||
revenue,
|
||||
value,
|
||||
currency,
|
||||
category,
|
||||
network: properties?.network ?? this.network,
|
||||
locale: this.locale,
|
||||
chain_id: properties?.chain_id ?? this.chainId,
|
||||
environment_type: environmentType,
|
||||
},
|
||||
context: this._buildContext(referrer, page),
|
||||
};
|
||||
}
|
||||
|
||||
_buildUserTraitsObject(metamaskState) {
|
||||
const currentTraits = {
|
||||
[TRAITS.LEDGER_CONNECTION_TYPE]: metamaskState.ledgerTransportType,
|
||||
[TRAITS.NUMBER_OF_ACCOUNTS]: Object.values(metamaskState.identities)
|
||||
.length,
|
||||
[TRAITS.NETWORKS_ADDED]: metamaskState.frequentRpcListDetail.map(
|
||||
(rpc) => rpc.chainId,
|
||||
),
|
||||
[TRAITS.THREE_BOX_ENABLED]: metamaskState.threeBoxSyncingAllowed,
|
||||
};
|
||||
|
||||
if (!this.previousTraits) {
|
||||
this.previousTraits = currentTraits;
|
||||
return currentTraits;
|
||||
}
|
||||
|
||||
if (this.previousTraits && !isEqual(this.previousTraits, currentTraits)) {
|
||||
const updates = pickBy(
|
||||
currentTraits,
|
||||
(v, k) => !isEqual(this.previousTraits[k], v),
|
||||
);
|
||||
this.previousTraits = currentTraits;
|
||||
return updates;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform validation on the payload and update the id type to use before
|
||||
* sending to Segment. Also examines the options to route and handle the
|
||||
* event appropriately.
|
||||
*
|
||||
* @private
|
||||
* @param {SegmentEventPayload} payload - properties to attach to event
|
||||
* @param {MetaMetricsEventOptions} [options] - options for routing and
|
||||
* handling the event
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
_track(payload, options) {
|
||||
const {
|
||||
isOptIn,
|
||||
metaMetricsId: metaMetricsIdOverride,
|
||||
matomoEvent,
|
||||
flushImmediately,
|
||||
} = options || {};
|
||||
let idType = 'userId';
|
||||
let idValue = this.state.metaMetricsId;
|
||||
let excludeMetaMetricsId = options?.excludeMetaMetricsId ?? false;
|
||||
// This is carried over from the old implementation, and will likely need
|
||||
// to be updated to work with the new tracking plan. I think we should use
|
||||
// a config setting for this instead of trying to match the event name
|
||||
const isSendFlow = Boolean(payload.event.match(/^send|^confirm/iu));
|
||||
if (isSendFlow) {
|
||||
excludeMetaMetricsId = true;
|
||||
}
|
||||
// If we are tracking sensitive data we will always use the anonymousId
|
||||
// property as well as our METAMETRICS_ANONYMOUS_ID. This prevents us from
|
||||
// associating potentially identifiable information with a specific id.
|
||||
// During the opt in flow we will track all events, but do so with the
|
||||
// anonymous id. The one exception to that rule is after the user opts in
|
||||
// to MetaMetrics. When that happens we receive back the user's new
|
||||
// MetaMetrics id before it is fully persisted to state. To avoid a race
|
||||
// condition we explicitly pass the new id to the track method. In that
|
||||
// case we will track the opt in event to the user's id. In all other cases
|
||||
// we use the metaMetricsId from state.
|
||||
if (excludeMetaMetricsId || (isOptIn && !metaMetricsIdOverride)) {
|
||||
idType = 'anonymousId';
|
||||
idValue = METAMETRICS_ANONYMOUS_ID;
|
||||
} else if (isOptIn && metaMetricsIdOverride) {
|
||||
idValue = metaMetricsIdOverride;
|
||||
}
|
||||
payload[idType] = idValue;
|
||||
|
||||
// If this is an event on the old matomo schema, add a key to the payload
|
||||
// to designate it as such
|
||||
if (matomoEvent === true) {
|
||||
payload.properties.legacy_event = true;
|
||||
}
|
||||
|
||||
// Promises will only resolve when the event is sent to segment. For any
|
||||
// event that relies on this promise being fulfilled before performing UI
|
||||
// updates, or otherwise delaying user interaction, supply the
|
||||
// 'flushImmediately' flag to the trackEvent method.
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (err) => {
|
||||
if (err) {
|
||||
// The error that segment gives us has some manipulation done to it
|
||||
// that seemingly breaks with lockdown enabled. Creating a new error
|
||||
// here prevents the system from freezing when the network request to
|
||||
// segment fails for any reason.
|
||||
const safeError = new Error(err.message);
|
||||
safeError.stack = err.stack;
|
||||
return reject(safeError);
|
||||
}
|
||||
return resolve();
|
||||
};
|
||||
|
||||
this.segment.track(payload, callback);
|
||||
if (flushImmediately) {
|
||||
this.segment.flush();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -5,8 +5,13 @@ import { createSegmentMock } from '../lib/segment';
|
||||
import {
|
||||
METAMETRICS_ANONYMOUS_ID,
|
||||
METAMETRICS_BACKGROUND_PAGE_OBJECT,
|
||||
TRAITS,
|
||||
} from '../../../shared/constants/metametrics';
|
||||
import waitUntilCalled from '../../../test/lib/wait-until-called';
|
||||
import {
|
||||
MAINNET_CHAIN_ID,
|
||||
ROPSTEN_CHAIN_ID,
|
||||
} from '../../../shared/constants/network';
|
||||
import MetaMetricsController from './metametrics';
|
||||
import { NETWORK_EVENTS } from './network';
|
||||
|
||||
@ -518,6 +523,80 @@ describe('MetaMetricsController', function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe('_buildUserTraitsObject', function () {
|
||||
it('should return full user traits object on first call', function () {
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
const traits = metaMetricsController._buildUserTraitsObject({
|
||||
frequentRpcListDetail: [
|
||||
{ chainId: MAINNET_CHAIN_ID },
|
||||
{ chainId: ROPSTEN_CHAIN_ID },
|
||||
],
|
||||
ledgerTransportType: 'web-hid',
|
||||
identities: [{}, {}],
|
||||
threeBoxSyncingAllowed: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(traits, {
|
||||
[TRAITS.THREE_BOX_ENABLED]: false,
|
||||
[TRAITS.LEDGER_CONNECTION_TYPE]: 'web-hid',
|
||||
[TRAITS.NUMBER_OF_ACCOUNTS]: 2,
|
||||
[TRAITS.NETWORKS_ADDED]: [MAINNET_CHAIN_ID, ROPSTEN_CHAIN_ID],
|
||||
});
|
||||
});
|
||||
|
||||
it('should return only changed traits object on subsequent calls', function () {
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
metaMetricsController._buildUserTraitsObject({
|
||||
frequentRpcListDetail: [
|
||||
{ chainId: MAINNET_CHAIN_ID },
|
||||
{ chainId: ROPSTEN_CHAIN_ID },
|
||||
],
|
||||
ledgerTransportType: 'web-hid',
|
||||
identities: [{}, {}],
|
||||
threeBoxSyncingAllowed: false,
|
||||
});
|
||||
|
||||
const updatedTraits = metaMetricsController._buildUserTraitsObject({
|
||||
frequentRpcListDetail: [
|
||||
{ chainId: MAINNET_CHAIN_ID },
|
||||
{ chainId: ROPSTEN_CHAIN_ID },
|
||||
],
|
||||
ledgerTransportType: 'web-hid',
|
||||
identities: [{}, {}, {}],
|
||||
threeBoxSyncingAllowed: false,
|
||||
});
|
||||
|
||||
assert.deepEqual(updatedTraits, {
|
||||
[TRAITS.NUMBER_OF_ACCOUNTS]: 3,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return null if no traits changed', function () {
|
||||
const metaMetricsController = getMetaMetricsController();
|
||||
metaMetricsController._buildUserTraitsObject({
|
||||
frequentRpcListDetail: [
|
||||
{ chainId: MAINNET_CHAIN_ID },
|
||||
{ chainId: ROPSTEN_CHAIN_ID },
|
||||
],
|
||||
ledgerTransportType: 'web-hid',
|
||||
identities: [{}, {}],
|
||||
threeBoxSyncingAllowed: false,
|
||||
});
|
||||
|
||||
const updatedTraits = metaMetricsController._buildUserTraitsObject({
|
||||
frequentRpcListDetail: [
|
||||
{ chainId: MAINNET_CHAIN_ID },
|
||||
{ chainId: ROPSTEN_CHAIN_ID },
|
||||
],
|
||||
ledgerTransportType: 'web-hid',
|
||||
identities: [{}, {}],
|
||||
threeBoxSyncingAllowed: false,
|
||||
});
|
||||
|
||||
assert.equal(updatedTraits, null);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
// flush the queues manually after each test
|
||||
segment.flush();
|
||||
|
@ -322,6 +322,10 @@ export default class MetamaskController extends EventEmitter {
|
||||
captureException,
|
||||
});
|
||||
|
||||
this.on('update', (update) => {
|
||||
this.metaMetricsController.handleMetaMaskStateUpdate(update);
|
||||
});
|
||||
|
||||
const gasFeeMessenger = this.controllerMessenger.getRestricted({
|
||||
name: 'GasFeeController',
|
||||
});
|
||||
|
Loading…
Reference in New Issue
Block a user