Skip to content

Commit 4da8cf9

Browse files
feat: add platform and install_type as MetaMetrics user traits (#39606)
<!-- Please submit this PR as a draft initially. Do not mark it as "Ready for review" until the template has been completely filled out, and PR status checks have passed at least once. --> ## **Description** <!-- Write a short description of the changes included in this pull request, also include relevant motivation and context. Have in mind the following questions: 1. What is the reason for the change? 2. What is the improvement/solution? --> Add platform (browser type) and install_type (installation source) as user traits sent via identify() instead of as properties on every event. This is the appropriate approach for static installation properties. - Add initInstallType() to cache install type from browser.management API - Add Platform and InstallType to MetaMetricsUserTrait enum - Update _buildUserTraitsObject to include these traits - Update unit test expectations [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/39606?quickstart=1) ## **Changelog** <!-- If this PR is not End-User-Facing and should not show up in the CHANGELOG, you can choose to either: 1. Write `CHANGELOG entry: null` 2. Label with `no-changelog` If this PR is End-User-Facing, please write a short User-Facing description in the past tense like: `CHANGELOG entry: Added a new tab for users to see their NFTs` `CHANGELOG entry: Fixed a bug that was causing some NFTs to flicker` (This helps the Release Engineer do their job more quickly and accurately) --> CHANGELOG entry: null ## **Related issues** Fixes: ## **Manual testing steps** 1. Lauch Extension 2. Enable metametrics 3. Get your metametricsId 4. Go to MixPanel 5. Search user with your metametricsId 6. Confirm it has `platform` and `install_type` defined in its user profile properties ## **Screenshots/Recordings** <img width="1080" height="898" alt="Screenshot 2026-01-28 at 15 39 35" src="https://github.com/user-attachments/assets/a0b09edf-3a4c-4eda-b0f7-bd14e7250f54" /> <img width="1083" height="902" alt="Screenshot 2026-01-28 at 15 40 08" src="https://github.com/user-attachments/assets/fb1c79db-b64f-439c-afe2-8e938f8da007" /> ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Extension Coding Standards](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I’ve included tests if applicable - [x] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable - [x] I’ve applied the right labels on the PR (see [labeling guidelines](https://github.com/MetaMask/metamask-extension/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. ## **Pre-merge reviewer checklist** - [ ] I've manually tested the PR (e.g. pull and build branch, run the app, test code being changed). - [ ] I confirm that this PR addresses all acceptance criteria described in the ticket it closes and includes the necessary testing evidence such as recordings and or screenshots. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Medium risk because it touches analytics trait generation and Sentry error-tagging, plus adds an early background initialization side-effect via `browser.management.getSelf()` (albeit fire-and-forget). Functionality is additive but could impact telemetry correctness and startup behavior across browsers. > > **Overview** > Adds `platform` and `install_type` as new MetaMetrics *user traits* (via `identify()`), and wires `MetaMetricsController._buildUserTraitsObject` to populate them using `getPlatform()` and a new cached `getInstallType()`. > > Introduces a dedicated `install-type` helper (with `initInstallType()` + cached value) and initializes it early in both `background.js` startup and Sentry setup; Sentry reports now tag/include `installType` via the cache rather than an inline async fetch. Updates shared constants/types to formalize `Platform`/`InstallType` and adjusts MetaMetrics controller tests accordingly. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cbb9c64. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 2a212d3 commit 4da8cf9

File tree

8 files changed

+113
-16
lines changed

8 files changed

+113
-16
lines changed

app/scripts/background.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import getObjStructure from './lib/getObjStructure';
7979
import setupEnsIpfsResolver from './lib/ens-ipfs/setup';
8080
import {
8181
getPlatform,
82+
initInstallType,
8283
isWebOrigin,
8384
shouldEmitDappViewedEvent,
8485
} from './lib/util';
@@ -711,6 +712,10 @@ function saveTimestamp() {
711712
* @returns {Promise} Setup complete.
712713
*/
713714
async function initialize(backup) {
715+
// Initialize install type early so it's cached for MetaMetrics user traits
716+
// This is fire-and-forget - we don't await it to avoid blocking initialization
717+
initInstallType();
718+
714719
const offscreenPromise = isManifestV3 ? createOffscreen() : null;
715720

716721
// Set up connectivity listener IMMEDIATELY for MV3 (before any awaits)

app/scripts/controllers/metametrics-controller.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,6 +1610,8 @@ describe('MetaMetricsController', function () {
16101610
[MetaMetricsUserTrait.TokenSortPreference]: 'token-sort-key',
16111611
[MetaMetricsUserTrait.PrivacyModeEnabled]: true,
16121612
[MetaMetricsUserTrait.NetworkFilterPreference]: [],
1613+
[MetaMetricsUserTrait.Platform]: 'Chrome',
1614+
[MetaMetricsUserTrait.InstallType]: 'unknown',
16131615
});
16141616
});
16151617
});

app/scripts/controllers/metametrics-controller.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ import { METAMETRICS_FINALIZE_EVENT_FRAGMENT_ALARM } from '../../../shared/const
6363
import {
6464
checkAlarmExists,
6565
generateRandomId,
66+
getInstallType,
6667
getPlatform,
6768
isValidDate,
6869
} from '../lib/util';
@@ -1459,6 +1460,8 @@ export default class MetaMetricsController extends BaseController<
14591460
[MetaMetricsUserTrait.ProfileId]: Object.entries(
14601461
metamaskState.srpSessionData || {},
14611462
)?.[0]?.[1]?.profile?.profileId,
1463+
[MetaMetricsUserTrait.Platform]: getPlatform(),
1464+
[MetaMetricsUserTrait.InstallType]: getInstallType(),
14621465
};
14631466

14641467
if (!this.previousUserTraits && metamaskState.participateInMetaMetrics) {

app/scripts/lib/install-type.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import browser from 'webextension-polyfill';
2+
import { INSTALL_TYPE, type InstallType } from '../../../shared/constants/app';
3+
4+
/**
5+
* Cached install type value.
6+
*
7+
* @see {@link INSTALL_TYPE} for possible values and their meanings.
8+
*/
9+
let cachedInstallType: InstallType = INSTALL_TYPE.UNKNOWN;
10+
11+
/**
12+
* Initializes the install type by fetching it from the browser API.
13+
* This should be called early in the extension lifecycle.
14+
* The result is cached and can be retrieved synchronously via getInstallType().
15+
*
16+
* @returns A promise that resolves to the install type
17+
*/
18+
export const initInstallType = async (): Promise<InstallType> => {
19+
try {
20+
const extensionInfo = await browser.management.getSelf();
21+
if (extensionInfo.installType) {
22+
cachedInstallType = extensionInfo.installType as InstallType;
23+
}
24+
} catch (error) {
25+
// Silently fail - install type will remain 'unknown'
26+
console.error('Error getting extension installType', error);
27+
}
28+
return cachedInstallType;
29+
};
30+
31+
/**
32+
* Returns the cached install type.
33+
* Call initInstallType() first to populate the cache.
34+
*
35+
* @returns The install type
36+
*/
37+
export const getInstallType = (): InstallType => {
38+
return cachedInstallType;
39+
};

app/scripts/lib/setupSentry.js

Lines changed: 7 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@ import { getManifestFlags } from '../../../shared/lib/manifestFlags';
88
import { getSentryRelease } from '../../../shared/lib/sentry-release';
99
import extractEthjsErrorMessage from './extractEthjsErrorMessage';
1010
import { filterEvents } from './sentry-filter-events';
11-
12-
let installType = 'unknown';
11+
import { getInstallType, initInstallType } from './install-type';
1312

1413
const internalLog = createModuleLogger(log, 'internal');
1514

@@ -50,18 +49,10 @@ export default function setupSentry() {
5049

5150
log('Initializing');
5251

53-
// Normally this would be awaited, but getSelf should be available by the time the report is finalized.
54-
// If it's not, we still get the extensionId, but the installType will default to "unknown"
55-
browser.management
56-
.getSelf()
57-
.then((extensionInfo) => {
58-
if (extensionInfo.installType) {
59-
installType = extensionInfo.installType;
60-
}
61-
})
62-
.catch((error) => {
63-
log('Error getting extension installType', error);
64-
});
52+
// Initialize install type early - fire and forget.
53+
// By the time errors are reported, the cache should be populated.
54+
initInstallType();
55+
6556
integrateLogging();
6657
setSentryClient();
6758

@@ -414,6 +405,8 @@ export function rewriteReport(report) {
414405
report.tags = {};
415406
}
416407

408+
const installType = getInstallType();
409+
417410
Object.assign(report.extra, {
418411
appState,
419412
installType,

app/scripts/lib/util.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
PLATFORM_EDGE,
2323
PLATFORM_FIREFOX,
2424
PLATFORM_OPERA,
25+
type Platform,
2526
} from '../../../shared/constants/app';
2627
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
2728
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
@@ -32,6 +33,9 @@ import {
3233
getIsQuicknodeEndpointUrl,
3334
KNOWN_CUSTOM_ENDPOINT_URLS,
3435
} from '../../../shared/lib/network-utils';
36+
// Re-export install type utilities from dedicated module to avoid circular dependencies
37+
// and keep the sentry bundle lightweight
38+
export { getInstallType, initInstallType } from './install-type';
3539

3640
/**
3741
* @see {@link getEnvironmentType}
@@ -71,7 +75,7 @@ const getEnvironmentType = (url = window.location.href) =>
7175
*
7276
* @returns the platform ENUM
7377
*/
74-
const getPlatform = () => {
78+
const getPlatform = (): Platform => {
7579
const { navigator } = window;
7680
const { userAgent } = navigator;
7781

shared/constants/app.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,39 @@ export const PLATFORM_EDGE = 'Edge';
2626
export const PLATFORM_FIREFOX = 'Firefox';
2727
export const PLATFORM_OPERA = 'Opera';
2828

29+
/**
30+
* Object containing all platform constants for type-safe usage.
31+
*/
32+
export const PLATFORM = {
33+
BRAVE: PLATFORM_BRAVE,
34+
CHROME: PLATFORM_CHROME,
35+
EDGE: PLATFORM_EDGE,
36+
FIREFOX: PLATFORM_FIREFOX,
37+
OPERA: PLATFORM_OPERA,
38+
} as const;
39+
40+
export type Platform = (typeof PLATFORM)[keyof typeof PLATFORM];
41+
42+
/**
43+
* The type of installation of the extension.
44+
* - 'normal' means installed from official store (Chrome Web Store, Firefox Add-ons, etc.)
45+
* - 'development' means loaded unpacked in developer mode
46+
* - 'sideload' means installed by other software
47+
* - 'admin' means installed by admin policy (enterprise)
48+
* - 'other' means other installation type
49+
* - 'unknown' means the value hasn't been fetched yet or fetch failed
50+
*/
51+
export const INSTALL_TYPE = {
52+
ADMIN: 'admin',
53+
DEVELOPMENT: 'development',
54+
NORMAL: 'normal',
55+
SIDELOAD: 'sideload',
56+
OTHER: 'other',
57+
UNKNOWN: 'unknown',
58+
} as const;
59+
60+
export type InstallType = (typeof INSTALL_TYPE)[keyof typeof INSTALL_TYPE];
61+
2962
export const MESSAGE_TYPE = {
3063
ADD_ETHEREUM_CHAIN: 'wallet_addEthereumChain',
3164
ETH_ACCOUNTS: RestrictedMethods.eth_accounts,

shared/constants/metametrics.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Json } from '@metamask/utils';
2-
import type { EnvironmentType } from './app';
2+
import type { EnvironmentType, InstallType, Platform } from './app';
33
import { LedgerTransportTypes } from './hardware-wallets';
44

55
type JsonWithUndefined =
@@ -565,6 +565,16 @@ export type MetaMetricsUserTraits = {
565565
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
566566
// eslint-disable-next-line @typescript-eslint/naming-convention
567567
rewards_referral_code_used?: string;
568+
/**
569+
* The platform (browser) where the extension is running.
570+
*/
571+
platform?: Platform;
572+
/**
573+
* The installation type of the extension.
574+
*/
575+
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
576+
// eslint-disable-next-line @typescript-eslint/naming-convention
577+
install_type?: InstallType;
568578
};
569579

570580
export enum MetaMetricsUserTrait {
@@ -681,6 +691,14 @@ export enum MetaMetricsUserTrait {
681691
HasRewardsOptedIn = 'has_rewards_opted_in',
682692
RewardsReferred = 'rewards_referred',
683693
RewardsReferralCodeUsed = 'rewards_referral_code_used',
694+
/**
695+
* The platform (browser) where the extension is running.
696+
*/
697+
Platform = 'platform',
698+
/**
699+
* The installation type of the extension.
700+
*/
701+
InstallType = 'install_type',
684702
}
685703

686704
/**

0 commit comments

Comments
 (0)