feat: Accounts Metrics#29015
Conversation
|
CLA Signature Action: All authors have signed the CLA. You may need to manually re-run the blocking PR check if it doesn't pass in a few minutes. |
08c536a to
9f9d675
Compare
641c014 to
588f5db
Compare
5aa8715 to
791f532
Compare
<!--
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**
This PR adds detailed wallet composition metrics derived from the
accounts controller. The goal here is to be able to answer questions
about...
1. how many SRPs users have
2. How many hw wallets user have
3. How many total account groups users have
4. how many hardware wallet accounts users have.
Fixes account wallet composition user properties that were unreliable
when the wallet was locked.
Previously, several traits were derived from keyrings, which is
unavailable in locked state. This PR
migrates all wallet composition traits to use internalAccounts, which is
always available.
Root cause: #getNumberOfHDEntropies read from metamaskState.keyrings,
returning stale/empty data when
locked.
Fix: Replaced with #getAccountCompositionTraits, a single O(n) pass over
internalAccounts.accounts that
computes all wallet composition traits atomically.
New/updated user properties schema
```┌────────────────────────────────┬───────────────────────────────────────────────────────────────────┐
│ Property │ Description │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_hd_entropies │ Count of unique SRP (Secret Recovery Phrase)
sources — deduped by │
│ │ entropy.id │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_account_groups │ Count of distinct {entropy.id, groupIndex}
pairs; multichain │
│ │ EVM+BTC+SOL accounts from the same SRP slot count as one group │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_imported_accounts │ Private key imports (Simple Key Pair
keyring) │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_ledger_accounts │ Ledger hardware wallet accounts │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_trezor_accounts │ Trezor hardware wallet accounts │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_lattice_accounts │ Lattice (GridPlus) hardware wallet
accounts │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_qr_hardware_accounts │ QR-based hardware wallet accounts
(Keystone, OneKey, etc.) │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_hardware_wallets │ Sum of all hardware wallet accounts —
enables total wallet count │
└────────────────────────────────┴───────────────────────────────────────────────────────────────────┘
```
Total wallet count can be derived in Segment as:
number_of_hd_entropies + number_of_hardware_wallets + number_of_imported_accounts
Test plan
- Added a #getAccountCompositionTraits test suite covering: empty accounts, single SRP with multiple
groups, multiple SRPs, hardware-only wallets, imported-only accounts, unknown/missing keyring metadata,
and the total wallet derivation formula.
Mobile equivalent: MetaMask/metamask-mobile#29015
## **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`
5. 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: Update metrics to track wallet composition (number of accounts, number of hardware wallets etc)
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/MUL-1396
## **Manual testing steps**
1. Build the extension locally
2. in another terminal window, run `node development/mock-segment.js`
3. import/create a wallet
4. you should notice the initial `Identify event received` logged with the correct `number_of_account_groups` logged as well as `number_of_hd_entropies`
5. import a private key
6. notice the `Identify event received` logged with the updated `number_of_account_groups` and `number_of_imported_accounts`
7. Connect a Ledger wallet and notice the `Identify event received` logged with updated `number_of_account_groups`, updated `number_of_ledger_accounts` and `number_of_hardware_wallets`
8. repeat the above steps with qr and trezor. The respective properties should all be updated.
## **Screenshots/Recordings**
<!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. -->
### **Before**
N/A
### **After**
1. first load with one wallet and one account
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"address_book_entries": 0,
"install_date_ext": "2026-04-18",
"storage_kind": "split",
"ledger_connection_type": "webhid",
"networks_added": [
"0x1",
"0xaa36a7",
"0xe705",
"0xe708",
"0x2105",
"0xa4b1",
"0x38",
"0xa",
"0x89",
"0x279f",
"0x18c7"
],
"networks_without_ticker": [],
"chain_id_list": [
"eip155:1",
"eip155:11155111",
"eip155:59141",
"eip155:59144",
"eip155:8453",
"eip155:42161",
"eip155:56",
"eip155:10",
"eip155:137",
"eip155:10143",
"eip155:6343",
"bip122:000000000019d6689c085ae165831e93",
"bip122:000000000933ea01ad0ee984209779ba",
"bip122:00000000da84f2bafbbc53dee25a72ae",
"bip122:00000008819873e925422c1ff0f99f7c",
"bip122:regtest",
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z",
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"tron:728126428",
"tron:3448148188",
"tron:2494104990"
],
"nft_autodetection_enabled": true,
"number_of_accounts": 1,
"number_of_nft_collections": 0,
"number_of_nfts": 0,
"number_of_tokens": 0,
"number_of_hd_entropies": 1,
"number_of_account_groups": 1,
"number_of_imported_accounts": 0,
"number_of_snap_accounts": 0,
"number_of_ledger_accounts": 0,
"number_of_trezor_accounts": 0,
"number_of_lattice_accounts": 0,
"number_of_qr_hardware_accounts": 0,
"number_of_hardware_wallets": 0,
"opensea_api_enabled": true,
"three_box_enabled": false,
"theme": "os",
"token_detection_enabled": true,
"show_native_token_as_main_balance": false,
"current_currency": "usd",
"security_providers": [
"blockaid"
],
"petname_addresses_count": 0,
"is_metrics_opted_in": true,
"has_marketing_consent": true,
"token_sort_preference": "tokenFiatAmount",
"privacy_mode_toggle": false,
"selected_network_filter": [],
"platform": "Brave",
"install_type": "development",
"device_type": "desktop",
"os": "macOS"
}
```
2. Add a new account
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"number_of_accounts": 8,
"number_of_account_groups": 2,
"number_of_snap_accounts": 6
}
```
3. import a private key
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"number_of_accounts": 9,
"number_of_account_groups": 3,
"number_of_imported_accounts": 1
}
```
4. import a ledger wallet with 3 accounts
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"address_book_entries": 0,
"install_date_ext": "2026-04-18",
"storage_kind": "split",
"ledger_connection_type": "webhid",
"networks_added": [
"0x1",
"0x18c7",
"0x2105",
"0x279f",
"0x38",
"0x89",
"0xa",
"0xa4b1",
"0xaa36a7",
"0xe705",
"0xe708"
],
"networks_without_ticker": [],
"chain_id_list": [
"eip155:1",
"eip155:6343",
"eip155:8453",
"eip155:10143",
"eip155:56",
"eip155:137",
"eip155:10",
"eip155:42161",
"eip155:11155111",
"eip155:59141",
"eip155:59144",
"bip122:000000000019d6689c085ae165831e93",
"bip122:000000000933ea01ad0ee984209779ba",
"bip122:00000000da84f2bafbbc53dee25a72ae",
"bip122:00000008819873e925422c1ff0f99f7c",
"bip122:regtest",
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z",
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"tron:728126428",
"tron:3448148188",
"tron:2494104990"
],
"nft_autodetection_enabled": true,
"number_of_accounts": 12,
"number_of_nft_collections": 0,
"number_of_nfts": 0,
"number_of_tokens": 12,
"number_of_hd_entropies": 1,
"number_of_account_groups": 6,
"number_of_imported_accounts": 1,
"number_of_snap_accounts": 6,
"number_of_ledger_accounts": 3,
"number_of_trezor_accounts": 0,
"number_of_lattice_accounts": 0,
"number_of_qr_hardware_accounts": 0,
"number_of_hardware_wallets": 1,
"opensea_api_enabled": true,
"three_box_enabled": false,
"theme": "os",
"token_detection_enabled": true,
"show_native_token_as_main_balance": false,
"current_currency": "usd",
"security_providers": [
"blockaid"
],
"petname_addresses_count": 0,
"is_metrics_opted_in": true,
"has_marketing_consent": true,
"token_sort_preference": "tokenFiatAmount",
"privacy_mode_toggle": false,
"selected_network_filter": [
"0x1"
],
"profile_id": "80c1483d-c92f-4a53-a7c5-cdc03451765a",
"platform": "Brave",
"install_type": "development",
"device_type": "desktop",
"os": "macOS"
}
```
4. import a QR hardware wallet (keystone) with 4 accounts
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"number_of_accounts": 16,
"number_of_account_groups": 10,
"number_of_qr_hardware_accounts": 4,
"number_of_hardware_wallets": 2
}
```
## **Pre-merge author checklist**
- [ ] 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).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable
- [ ] 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**
> Changes MetaMetrics identify trait calculation and schema for wallet/account composition, which could affect analytics accuracy and downstream dashboards even though it doesn't impact core wallet operations.
>
> **Overview**
> Improves MetaMetrics *wallet composition* reporting by deriving counts from `internalAccounts` instead of `keyrings` (fixing incorrect/stale values when the wallet is locked).
>
> Adds new user traits for account groups and hardware/imported account breakdown (`number_of_account_groups`, per-hardware-type counts, and `number_of_hardware_wallets`), including BIP44 multichain deduping by `{entropyId, groupIndex}`.
>
> Updates and expands tests to cover the new trait calculations (multichain grouping, SRP entropy counting, imported vs hardware types, unknown/missing metadata), and adds test helpers for building mock internal accounts.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4a7f5aa. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
| (accounts: InternalAccounts) => { | ||
| const fingerprint = getCompositionFingerprint(accounts); | ||
| if (fingerprint === lastCompositionFingerprint) return; | ||
| lastCompositionFingerprint = fingerprint; |
There was a problem hiding this comment.
If the try fails, don't we need to re-assign this to it's previous value?
There was a problem hiding this comment.
Good point, I adjusted the logic for this case.
933a18f to
7614ac8
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6c3e9e2. Configure here.
🔍 Smart E2E Test Selection
click to see 🤖 AI reasoning detailsE2E Test Selection: The changes refactor analytics trait tracking from the React component layer (Wallet view) to the Engine controller layer (AnalyticsController init). Key changes:
Test selection rationale:
No performance tests needed: The changes are analytics-only (identify calls), not UI rendering or data loading performance. The fingerprint deduplication ensures minimal overhead. No other tags needed: The changes don't affect transaction flows (SmokeConfirmations), swap/bridge (SmokeTrade), network management (SmokeNetworkAbstractions/Expansion), browser (SmokeBrowser), snaps (SmokeSnaps), ramps (SmokeRamps), or other feature areas. Performance Test Selection: |
|
✅ E2E Fixture Validation — Schema is up to date |
|
<!--
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**
This PR adds detailed wallet composition metrics derived from the
accounts controller. The goal here is to be able to answer questions
about...
1. how many SRPs users have
2. How many hw wallets user have
3. How many total account groups users have
4. how many hardware wallet accounts users have.
Fixes account wallet composition user properties that were unreliable
when the wallet was locked.
Previously, several traits were derived from keyrings, which is
unavailable in locked state. This PR
migrates all wallet composition traits to use internalAccounts, which is
always available.
Root cause: #getNumberOfHDEntropies read from metamaskState.keyrings,
returning stale/empty data when
locked.
Fix: Replaced with #getAccountCompositionTraits, a single O(n) pass over
internalAccounts.accounts that
computes all wallet composition traits atomically.
New/updated user properties schema
```┌────────────────────────────────┬───────────────────────────────────────────────────────────────────┐
│ Property │ Description │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_hd_entropies │ Count of unique SRP (Secret Recovery Phrase)
sources — deduped by │
│ │ entropy.id │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_account_groups │ Count of distinct {entropy.id, groupIndex}
pairs; multichain │
│ │ EVM+BTC+SOL accounts from the same SRP slot count as one group │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_imported_accounts │ Private key imports (Simple Key Pair
keyring) │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_ledger_accounts │ Ledger hardware wallet accounts │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_trezor_accounts │ Trezor hardware wallet accounts │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_lattice_accounts │ Lattice (GridPlus) hardware wallet
accounts │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_qr_hardware_accounts │ QR-based hardware wallet accounts
(Keystone, OneKey, etc.) │
├────────────────────────────────┼───────────────────────────────────────────────────────────────────┤
│ number_of_hardware_wallets │ Sum of all hardware wallet accounts —
enables total wallet count │
└────────────────────────────────┴───────────────────────────────────────────────────────────────────┘
```
Total wallet count can be derived in Segment as:
number_of_hd_entropies + number_of_hardware_wallets + number_of_imported_accounts
Test plan
- Added a #getAccountCompositionTraits test suite covering: empty accounts, single SRP with multiple
groups, multiple SRPs, hardware-only wallets, imported-only accounts, unknown/missing keyring metadata,
and the total wallet derivation formula.
Mobile equivalent: MetaMask/metamask-mobile#29015
## **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`
5. 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: Update metrics to track wallet composition (number of accounts, number of hardware wallets etc)
## **Related issues**
Fixes: https://consensyssoftware.atlassian.net/browse/MUL-1396
## **Manual testing steps**
1. Build the extension locally
2. in another terminal window, run `node development/mock-segment.js`
3. import/create a wallet
4. you should notice the initial `Identify event received` logged with the correct `number_of_account_groups` logged as well as `number_of_hd_entropies`
5. import a private key
6. notice the `Identify event received` logged with the updated `number_of_account_groups` and `number_of_imported_accounts`
7. Connect a Ledger wallet and notice the `Identify event received` logged with updated `number_of_account_groups`, updated `number_of_ledger_accounts` and `number_of_hardware_wallets`
8. repeat the above steps with qr and trezor. The respective properties should all be updated.
## **Screenshots/Recordings**
<!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. -->
### **Before**
N/A
### **After**
1. first load with one wallet and one account
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"address_book_entries": 0,
"install_date_ext": "2026-04-18",
"storage_kind": "split",
"ledger_connection_type": "webhid",
"networks_added": [
"0x1",
"0xaa36a7",
"0xe705",
"0xe708",
"0x2105",
"0xa4b1",
"0x38",
"0xa",
"0x89",
"0x279f",
"0x18c7"
],
"networks_without_ticker": [],
"chain_id_list": [
"eip155:1",
"eip155:11155111",
"eip155:59141",
"eip155:59144",
"eip155:8453",
"eip155:42161",
"eip155:56",
"eip155:10",
"eip155:137",
"eip155:10143",
"eip155:6343",
"bip122:000000000019d6689c085ae165831e93",
"bip122:000000000933ea01ad0ee984209779ba",
"bip122:00000000da84f2bafbbc53dee25a72ae",
"bip122:00000008819873e925422c1ff0f99f7c",
"bip122:regtest",
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z",
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"tron:728126428",
"tron:3448148188",
"tron:2494104990"
],
"nft_autodetection_enabled": true,
"number_of_accounts": 1,
"number_of_nft_collections": 0,
"number_of_nfts": 0,
"number_of_tokens": 0,
"number_of_hd_entropies": 1,
"number_of_account_groups": 1,
"number_of_imported_accounts": 0,
"number_of_snap_accounts": 0,
"number_of_ledger_accounts": 0,
"number_of_trezor_accounts": 0,
"number_of_lattice_accounts": 0,
"number_of_qr_hardware_accounts": 0,
"number_of_hardware_wallets": 0,
"opensea_api_enabled": true,
"three_box_enabled": false,
"theme": "os",
"token_detection_enabled": true,
"show_native_token_as_main_balance": false,
"current_currency": "usd",
"security_providers": [
"blockaid"
],
"petname_addresses_count": 0,
"is_metrics_opted_in": true,
"has_marketing_consent": true,
"token_sort_preference": "tokenFiatAmount",
"privacy_mode_toggle": false,
"selected_network_filter": [],
"platform": "Brave",
"install_type": "development",
"device_type": "desktop",
"os": "macOS"
}
```
2. Add a new account
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"number_of_accounts": 8,
"number_of_account_groups": 2,
"number_of_snap_accounts": 6
}
```
3. import a private key
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"number_of_accounts": 9,
"number_of_account_groups": 3,
"number_of_imported_accounts": 1
}
```
4. import a ledger wallet with 3 accounts
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"address_book_entries": 0,
"install_date_ext": "2026-04-18",
"storage_kind": "split",
"ledger_connection_type": "webhid",
"networks_added": [
"0x1",
"0x18c7",
"0x2105",
"0x279f",
"0x38",
"0x89",
"0xa",
"0xa4b1",
"0xaa36a7",
"0xe705",
"0xe708"
],
"networks_without_ticker": [],
"chain_id_list": [
"eip155:1",
"eip155:6343",
"eip155:8453",
"eip155:10143",
"eip155:56",
"eip155:137",
"eip155:10",
"eip155:42161",
"eip155:11155111",
"eip155:59141",
"eip155:59144",
"bip122:000000000019d6689c085ae165831e93",
"bip122:000000000933ea01ad0ee984209779ba",
"bip122:00000000da84f2bafbbc53dee25a72ae",
"bip122:00000008819873e925422c1ff0f99f7c",
"bip122:regtest",
"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
"solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z",
"solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
"tron:728126428",
"tron:3448148188",
"tron:2494104990"
],
"nft_autodetection_enabled": true,
"number_of_accounts": 12,
"number_of_nft_collections": 0,
"number_of_nfts": 0,
"number_of_tokens": 12,
"number_of_hd_entropies": 1,
"number_of_account_groups": 6,
"number_of_imported_accounts": 1,
"number_of_snap_accounts": 6,
"number_of_ledger_accounts": 3,
"number_of_trezor_accounts": 0,
"number_of_lattice_accounts": 0,
"number_of_qr_hardware_accounts": 0,
"number_of_hardware_wallets": 1,
"opensea_api_enabled": true,
"three_box_enabled": false,
"theme": "os",
"token_detection_enabled": true,
"show_native_token_as_main_balance": false,
"current_currency": "usd",
"security_providers": [
"blockaid"
],
"petname_addresses_count": 0,
"is_metrics_opted_in": true,
"has_marketing_consent": true,
"token_sort_preference": "tokenFiatAmount",
"privacy_mode_toggle": false,
"selected_network_filter": [
"0x1"
],
"profile_id": "80c1483d-c92f-4a53-a7c5-cdc03451765a",
"platform": "Brave",
"install_type": "development",
"device_type": "desktop",
"os": "macOS"
}
```
4. import a QR hardware wallet (keystone) with 4 accounts
```
[mock-segment]: Identify event received:
0x3f1cda5ef8550a209fe5e6ba2956f14a0db4d28dcf2413bb1915b351c3555856
{
"number_of_accounts": 16,
"number_of_account_groups": 10,
"number_of_qr_hardware_accounts": 4,
"number_of_hardware_wallets": 2
}
```
## **Pre-merge author checklist**
- [ ] 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).
- [ ] I've completed the PR template to the best of my ability
- [ ] I’ve included tests if applicable
- [ ] I’ve documented my code using [JSDoc](https://jsdoc.app/) format if applicable
- [ ] 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**
> Changes MetaMetrics identify trait calculation and schema for wallet/account composition, which could affect analytics accuracy and downstream dashboards even though it doesn't impact core wallet operations.
>
> **Overview**
> Improves MetaMetrics *wallet composition* reporting by deriving counts from `internalAccounts` instead of `keyrings` (fixing incorrect/stale values when the wallet is locked).
>
> Adds new user traits for account groups and hardware/imported account breakdown (`number_of_account_groups`, per-hardware-type counts, and `number_of_hardware_wallets`), including BIP44 multichain deduping by `{entropyId, groupIndex}`.
>
> Updates and expands tests to cover the new trait calculations (multichain grouping, SRP entropy counting, imported vs hardware types, unknown/missing metadata), and adds test helpers for building mock internal accounts.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 4a7f5aa. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->




Description
MetaMask currently tracks number_of_hd_entropies in Segment, but the value has become inaccurate: it was
derived from KeyringController keyrings rather than AccountsController internal accounts, and it had no
awareness of BIP-44 multichain account groups (where a single user "account" produces one address per
supported network). This made it impossible to accurately answer questions like "how many accounts does
the average MetaMask user have?" or segment users by hardware wallet adoption.
This PR replaces the old keyring-based tracking with a reliable, controller-driven implementation that
derives all wallet composition traits from AccountsController.internalAccounts — the same source of
truth used on Extension — so traits remain accurate regardless of wallet lock state.
What's changing
New and updated user traits emitted on the Segment identify call:
▎ Total wallet count can be derived in Segment as: number_of_hd_entropies + number_of_hardware_wallets +
▎ number_of_imported_accounts
Trezor and Lattice are intentionally excluded — those hardware wallets are not supported on mobile.
Implementation
metadata.keyring.type, and deduplicates BIP-44 multichain addresses using the
options.entropy.id:groupIndex composite key so multiple chain addresses for the same account group count
as one.
call whenever account composition changes, with two guards to avoid unnecessary calls:
a. A selector narrows the subscription to internalAccounts.accounts, skipping pure selectedAccount
changes.
b. A composition fingerprint (hashing only keyring type and entropy fields) skips analytics.identify()
when lastSelected or account names change but composition hasn't.
removed.
Equivalent extension changes: MetaMask/metamask-extension#41918
Changelog
CHANGELOG entry: Update metrics to track wallet composition (number of accounts, number of hardware wallets etc).
Related issues
Fixes: https://consensyssoftware.atlassian.net/browse/MUL-1757
Manual testing steps
Identify event receivedlogged with the correctnumber_of_account_groupslogged as well asnumber_of_hd_entropiesIdentify event receivedlogged with the updatednumber_of_account_groupsandnumber_of_imported_accountsnumber_of_hd_entropiesand updatednumber_of_account_groupsIdentify event receivedlogged with updatednumber_of_account_groups, updatednumber_of_ledger_accountsandnumber_of_hardware_walletsScreenshots/Recordings
Before
n/A
After
{ "type": "identify", "userId": "d372edfb-19ee-4664-86f6-6418bbd4a1bb", "traits": { "number_of_hd_entropies": 1, "number_of_account_groups": 1, "number_of_imported_accounts": 0, "number_of_ledger_accounts": 0, "number_of_qr_hardware_accounts": 0, "number_of_hardware_wallets": 0 } }{ "number_of_hd_entropies": 1, "number_of_account_groups": 2, "number_of_imported_accounts": 0, "number_of_ledger_accounts": 0, "number_of_qr_hardware_accounts": 0, "number_of_hardware_wallets": 0 }{ "number_of_hd_entropies": 1, "number_of_account_groups": 3, "number_of_imported_accounts": 1, "number_of_ledger_accounts": 0, "number_of_qr_hardware_accounts": 0, "number_of_hardware_wallets": 0 }{ "number_of_hd_entropies": 1, "number_of_account_groups": 6, "number_of_imported_accounts": 1, "number_of_ledger_accounts": 3, "number_of_qr_hardware_accounts": 0, "number_of_hardware_wallets": 1 }{ "number_of_hd_entropies": 1, "number_of_account_groups": 9, "number_of_imported_accounts": 1, "number_of_ledger_accounts": 3, "number_of_qr_hardware_accounts": 3, "number_of_hardware_wallets": 2 }{ "number_of_hd_entropies": 2, "number_of_account_groups": 51, "number_of_imported_accounts": 1, "number_of_ledger_accounts": 3, "number_of_qr_hardware_accounts": 3, "number_of_hardware_wallets": 2 }Pre-merge author checklist
Performance checks (if applicable)
trace()for usage andaddTokenfor an exampleFor performance guidelines and tooling, see the Performance Guide.
Pre-merge reviewer checklist
Note
Medium Risk
Changes analytics identify behavior and the trait schema (new required fields and new identify triggers on account state changes), which could impact event volume/quality if fingerprinting or account classification is wrong.
Overview
Updates Segment user-profile metrics to be AccountsController-driven wallet composition traits, adding new counts for
number_of_account_groups, imported accounts, Ledger/QR hardware accounts, and distinct hardware wallet device types, and makingnumber_of_hd_entropiesmandatory and entropy-aware (BIP-44 multichain dedupe viaentropy.id:groupIndex).Moves trait emission out of the Wallet view by removing the
addTraitsToUsereffect and instead wiringanalytics-controller-initto subscribe toAccountsController:stateChange, recomputing traits only when a stable composition fingerprint changes (with error logging), plus adds a dedicated init messenger and comprehensive unit tests for the new fingerprinting and trait counting logic.Reviewed by Cursor Bugbot for commit 3b13d21. Bugbot is set up for automated code reviews on this repo. Configure here.