Skip to content
5 changes: 5 additions & 0 deletions app/scripts/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ import getObjStructure from './lib/getObjStructure';
import setupEnsIpfsResolver from './lib/ens-ipfs/setup';
import {
getPlatform,
initInstallType,
isWebOrigin,
shouldEmitDappViewedEvent,
} from './lib/util';
Expand Down Expand Up @@ -709,6 +710,10 @@ function saveTimestamp() {
* @returns {Promise} Setup complete.
*/
async function initialize(backup) {
// Initialize install type early so it's cached for MetaMetrics user traits
// This is fire-and-forget - we don't await it to avoid blocking initialization
initInstallType();

const offscreenPromise = isManifestV3 ? createOffscreen() : null;

// Set up connectivity listener IMMEDIATELY for MV3 (before any awaits)
Expand Down
2 changes: 2 additions & 0 deletions app/scripts/controllers/metametrics-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1610,6 +1610,8 @@ describe('MetaMetricsController', function () {
[MetaMetricsUserTrait.TokenSortPreference]: 'token-sort-key',
[MetaMetricsUserTrait.PrivacyModeEnabled]: true,
[MetaMetricsUserTrait.NetworkFilterPreference]: [],
[MetaMetricsUserTrait.Platform]: 'Chrome',
[MetaMetricsUserTrait.InstallType]: 'unknown',
});
});
});
Expand Down
10 changes: 9 additions & 1 deletion app/scripts/controllers/metametrics-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,13 @@ import type {
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, generateRandomId, isValidDate } from '../lib/util';
import {
checkAlarmExists,
generateRandomId,
getInstallType,
getPlatform,
isValidDate,
} from '../lib/util';
import {
AnonymousTransactionMetaMetricsEvent,
TransactionMetaMetricsEvent,
Expand Down Expand Up @@ -1435,6 +1441,8 @@ export default class MetaMetricsController extends BaseController<
[MetaMetricsUserTrait.ProfileId]: Object.entries(
metamaskState.srpSessionData || {},
)?.[0]?.[1]?.profile?.profileId,
[MetaMetricsUserTrait.Platform]: getPlatform(),
[MetaMetricsUserTrait.InstallType]: getInstallType(),
};

if (!this.previousUserTraits && metamaskState.participateInMetaMetrics) {
Expand Down
45 changes: 44 additions & 1 deletion app/scripts/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import urlLib from 'url';
import { AccessList } from '@ethereumjs/tx';
import BN from 'bn.js';
import { memoize } from 'lodash';
import browser from 'webextension-polyfill';
import {
TransactionEnvelopeType,
TransactionMeta,
Expand All @@ -21,6 +22,9 @@ import {
PLATFORM_EDGE,
PLATFORM_FIREFOX,
PLATFORM_OPERA,
INSTALL_TYPE,
type InstallType,
type Platform,
} from '../../../shared/constants/app';
import { CHAIN_IDS, TEST_CHAINS } from '../../../shared/constants/network';
import { stripHexPrefix } from '../../../shared/modules/hexstring-utils';
Expand Down Expand Up @@ -65,7 +69,7 @@ const getEnvironmentType = (url = window.location.href) =>
*
* @returns the platform ENUM
*/
const getPlatform = () => {
const getPlatform = (): Platform => {
const { navigator } = window;
const { userAgent } = navigator;

Expand All @@ -81,6 +85,43 @@ const getPlatform = () => {
return PLATFORM_CHROME;
};

/**
* Cached install type value.
*
* @see {@link INSTALL_TYPE} for possible values and their meanings.
*/
let cachedInstallType: InstallType = INSTALL_TYPE.UNKNOWN;

/**
* Initializes the install type by fetching it from the browser API.
* This should be called early in the extension lifecycle.
* The result is cached and can be retrieved synchronously via getInstallType().
*
* @returns A promise that resolves to the install type
*/
const initInstallType = async (): Promise<InstallType> => {
try {
const extensionInfo = await browser.management.getSelf();
if (extensionInfo.installType) {
cachedInstallType = extensionInfo.installType as InstallType;
}
} catch (error) {
// Silently fail - install type will remain 'unknown'
console.error('Error getting extension installType', error);
}
return cachedInstallType;
};

/**
* Returns the cached install type.
* Call initInstallType() first to populate the cache.
*
* @returns The install type
*/
const getInstallType = (): InstallType => {
return cachedInstallType;
};

/**
* Converts a hex string to a BN object
*
Expand Down Expand Up @@ -159,8 +200,10 @@ export {
checkAlarmExists,
getChainType,
getEnvironmentType,
getInstallType,
getPlatform,
hexToBn,
initInstallType,
};

// Taken from https://stackoverflow.com/a/1349426/3696652
Expand Down
33 changes: 33 additions & 0 deletions shared/constants/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,39 @@ export const PLATFORM_EDGE = 'Edge';
export const PLATFORM_FIREFOX = 'Firefox';
export const PLATFORM_OPERA = 'Opera';

/**
* Object containing all platform constants for type-safe usage.
*/
export const PLATFORM = {
BRAVE: PLATFORM_BRAVE,
CHROME: PLATFORM_CHROME,
EDGE: PLATFORM_EDGE,
FIREFOX: PLATFORM_FIREFOX,
OPERA: PLATFORM_OPERA,
} as const;

export type Platform = (typeof PLATFORM)[keyof typeof PLATFORM];

/**
* The type of installation of the extension.
* - 'normal' means installed from official store (Chrome Web Store, Firefox Add-ons, etc.)
* - 'development' means loaded unpacked in developer mode
* - 'sideload' means installed by other software
* - 'admin' means installed by admin policy (enterprise)
* - 'other' means other installation type
* - 'unknown' means the value hasn't been fetched yet or fetch failed
*/
export const INSTALL_TYPE = {
ADMIN: 'admin',
DEVELOPMENT: 'development',
NORMAL: 'normal',
SIDELOAD: 'sideload',
OTHER: 'other',
UNKNOWN: 'unknown',
} as const;

export type InstallType = (typeof INSTALL_TYPE)[keyof typeof INSTALL_TYPE];

export const MESSAGE_TYPE = {
ADD_ETHEREUM_CHAIN: 'wallet_addEthereumChain',
ETH_ACCOUNTS: RestrictedMethods.eth_accounts,
Expand Down
20 changes: 19 additions & 1 deletion shared/constants/metametrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Json } from '@metamask/utils';
import type { EnvironmentType } from './app';
import type { EnvironmentType, InstallType, Platform } from './app';
import { LedgerTransportTypes } from './hardware-wallets';

type JsonWithUndefined =
Expand Down Expand Up @@ -565,6 +565,16 @@ export type MetaMetricsUserTraits = {
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
rewards_referral_code_used?: string;
/**
* The platform (browser) where the extension is running.
*/
platform?: Platform;
/**
* The installation type of the extension.
*/
// TODO: Fix in https://github.com/MetaMask/metamask-extension/issues/31860
// eslint-disable-next-line @typescript-eslint/naming-convention
install_type?: InstallType;
};

export enum MetaMetricsUserTrait {
Expand Down Expand Up @@ -681,6 +691,14 @@ export enum MetaMetricsUserTrait {
HasRewardsOptedIn = 'has_rewards_opted_in',
RewardsReferred = 'rewards_referred',
RewardsReferralCodeUsed = 'rewards_referral_code_used',
/**
* The platform (browser) where the extension is running.
*/
Platform = 'platform',
/**
* The installation type of the extension.
*/
InstallType = 'install_type',
}

/**
Expand Down