Skip to content

feat: Accounts Metrics#29015

Merged
owencraston merged 12 commits intomainfrom
fix/account-metric-user-properties
Apr 30, 2026
Merged

feat: Accounts Metrics#29015
owencraston merged 12 commits intomainfrom
fix/account-metric-user-properties

Conversation

@owencraston
Copy link
Copy Markdown
Contributor

@owencraston owencraston commented Apr 18, 2026

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:

  │            Property            │  Type   │                       Description                       │
  ├────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────┤
  │ number_of_hd_entropies         │ integer │ Count of unique SRP sources, deduped by entropy.id      │
  │                                │         │ (fixes existing inaccurate value)                       │
  ├────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────┤
  │ number_of_account_groups       │ integer │ BIP-44 account groups deduped by entropy.id:groupIndex  │
  │                                │         │ — what users see as "accounts" in the UI                │
  ├────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────┤
  │ number_of_imported_accounts    │ integer │ Accounts added via raw private key (Simple Key Pair     │
  │                                │         │ keyring)                                                │
  ├────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────┤
  │ number_of_ledger_accounts      │ integer │ Account groups from Ledger hardware wallet              │
  ├────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────┤
  │ number_of_qr_hardware_accounts │ integer │ Account groups from QR-based hardware wallets           │
  │                                │         │ (Keystone, OneKey, etc.)                                │
  ├────────────────────────────────┼─────────┼─────────────────────────────────────────────────────────┤
  │ number_of_hardware_wallets     │ integer │ Count of distinct hardware wallet device types in use   │
  │                                │         │ (Ledger + QR)                                           │
  └────────────────────────────────┴─────────┴─────────────────────────────────────────────────────────┘

▎ 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

  • getAccountCompositionTraits() iterates internalAccounts.accounts, classifies each account by
    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.
  • Traits are included in every identify call via generateUserProfileAnalyticsMetaData().
  • A subscription to AccountsController:stateChange in analytics-controller-init.ts re-fires the identify
    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.
  • The old useEffect in the Wallet view that tracked number_of_hd_entropies via hdKeyrings.length is
    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

  1. Build the extension locally
  2. Monitor the sentry events with the following steps: Mobile docs
    • set export IS_TEST="false" in your .js.env
    • open the react native debugger console by pressing j inside the metro server
    • Open the console and filter by IDENTIFY event
    • Pro tip: set your unlock time to never in settings. If the device is locked you'll need to re open the debugger all over again.
  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. Import a new SRP with X accounts. You the identify event should be called with updated number_of_hd_entropies and updated number_of_account_groups
  8. 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
  9. repeat the above steps with qr. The respective properties should all be updated.

Screenshots/Recordings

Before

n/A

After

  1. create a fresh wallet with one account
{
    "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
    }
}
  1. adding a new HD account
{
    "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
}
  1. import a private key account
{
    "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
}
  1. import a Ledger account with 3 accounts
{
    "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
}
  1. adding a qr hardware wallet with 3 accounts
{
    "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
}
  1. import an SRP with 42 accounts accounts
{
    "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)

  • I've tested on Android
    • Ideally on a mid-range device; emulator is acceptable
  • I've tested with a power user scenario
    • Use these power-user SRPs to import wallets with many accounts and tokens
  • I've instrumented key operations with Sentry traces for production performance metrics

For performance guidelines and tooling, see the Performance Guide.

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.

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 making number_of_hd_entropies mandatory and entropy-aware (BIP-44 multichain dedupe via entropy.id:groupIndex).

Moves trait emission out of the Wallet view by removing the addTraitsToUser effect and instead wiring analytics-controller-init to subscribe to AccountsController: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.

@owencraston owencraston requested a review from a team as a code owner April 18, 2026 22:48
@github-actions
Copy link
Copy Markdown
Contributor

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.

@metamaskbotv2 metamaskbotv2 Bot added the team-accounts-framework Accounts team label Apr 18, 2026
@github-actions github-actions Bot added size-M risk-low Low testing needed · Low bug introduction risk and removed risk-low Low testing needed · Low bug introduction risk labels Apr 18, 2026
@github-actions github-actions Bot added size-L risk-medium Moderate testing recommended · Possible bug introduction risk and removed size-M risk-low Low testing needed · Low bug introduction risk labels Apr 19, 2026
@owencraston owencraston force-pushed the fix/account-metric-user-properties branch from 08c536a to 9f9d675 Compare April 20, 2026 16:43
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 20, 2026
@github-actions github-actions Bot added risk-medium Moderate testing recommended · Possible bug introduction risk and removed risk-medium Moderate testing recommended · Possible bug introduction risk labels Apr 20, 2026
Comment thread app/core/Engine/controllers/analytics-controller/analytics-controller-init.ts Outdated
@owencraston owencraston force-pushed the fix/account-metric-user-properties branch from 641c014 to 588f5db Compare April 28, 2026 03:13
@owencraston owencraston force-pushed the fix/account-metric-user-properties branch from 5aa8715 to 791f532 Compare April 28, 2026 16:44
gantunesr
gantunesr previously approved these changes Apr 29, 2026
pull Bot pushed a commit to Eric-Johnson-1/metamask-extension that referenced this pull request Apr 29, 2026
<!--
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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the try fails, don't we need to re-assign this to it's previous value?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I adjusted the logic for this case.

@owencraston owencraston force-pushed the fix/account-metric-user-properties branch from 933a18f to 7614ac8 Compare April 29, 2026 23:24
Comment thread app/core/Engine/messengers/analytics-controller-messenger.ts
Comment thread app/core/Engine/controllers/analytics-controller/analytics-controller-init.ts Outdated
Copy link
Copy Markdown
Contributor

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ 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.

gantunesr
gantunesr previously approved these changes Apr 30, 2026
@github-actions
Copy link
Copy Markdown
Contributor

🔍 Smart E2E Test Selection

  • Selected E2E tags: SmokeWalletPlatform, SmokeAccounts, SmokeIdentity
  • Selected Performance tags: None (no tests recommended)
  • Risk Level: medium
  • AI Confidence: 82%
click to see 🤖 AI reasoning details

E2E Test Selection:

The changes refactor analytics trait tracking from the React component layer (Wallet view) to the Engine controller layer (AnalyticsController init). Key changes:

  1. analytics-controller-init.ts: Adds a subscription to AccountsController:stateChange that calls analytics.identify() with account composition traits (HD entropies, account groups, imported/ledger/QR hardware accounts). Uses fingerprint-based deduplication to avoid redundant calls.

  2. analytics-controller-messenger.ts: New AnalyticsControllerInitMessenger scoped to AccountsController:stateChange events.

  3. messengers/index.ts: Wires up the new init messenger instead of noop.

  4. Wallet/index.tsx: Removes the useEffect that called addTraitsToUser({NUMBER_OF_HD_ENTROPIES: hdKeyrings.length}) - this logic is now in the controller layer.

  5. generateUserProfileAnalyticsMetaData.ts: New getAccountCompositionTraits() function computing 6 new wallet composition traits from internalAccounts.

  6. UserProfileAnalyticsMetaData.types.ts: 5 new UserProfileProperty enum values added.

Test selection rationale:

  • SmokeWalletPlatform: Tests wallet lifecycle analytics tracking for new wallet creation and SRP import events. The changes directly affect how analytics identify calls are made during wallet lifecycle events. Also covers multi-SRP architecture which exercises the new account composition tracking.
  • SmokeAccounts: Tests multi-account workflows including creating HD wallet accounts, importing accounts via private key, QR hardware wallet accounts - all of which would trigger the new AccountsController:stateChange subscription and the new composition traits.
  • SmokeIdentity: Tests multi-SRP account synchronization and account discovery - directly exercises the account composition tracking logic (HD entropy IDs, account groups).

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:
The changes are purely analytics-related (moving identify() calls from React component to controller layer). No UI rendering, list performance, data loading, or app startup changes are involved. The fingerprint deduplication in the new subscription minimizes any overhead. No performance tests are warranted.

View GitHub Actions results

@github-actions
Copy link
Copy Markdown
Contributor

E2E Fixture Validation — Schema is up to date
12 value mismatches detected (expected — fixture represents an existing user).
View details

@sonarqubecloud
Copy link
Copy Markdown

NidhiKJha pushed a commit to MetaMask/metamask-extension that referenced this pull request Apr 30, 2026
<!--
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 -->
@owencraston owencraston added this pull request to the merge queue Apr 30, 2026
Merged via the queue into main with commit 576cfe3 Apr 30, 2026
97 checks passed
@owencraston owencraston deleted the fix/account-metric-user-properties branch April 30, 2026 14:51
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 30, 2026
@metamaskbotv2 metamaskbotv2 Bot added the release-7.76.0 Issue or pull request that will be included in release 7.76.0 label Apr 30, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

release-7.76.0 Issue or pull request that will be included in release 7.76.0 risk-medium Moderate testing recommended · Possible bug introduction risk size-L team-accounts-framework Accounts team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants