Skip to content

Commit 9f36182

Browse files
authored
feat: Track when RPC update from network banner is completed (#37751)
## **Description** ### Track RPC Update Completion from Network Banner ### Summary Adds `NetworkConnectionBannerRpcUpdated` event to track when users **complete** the RPC update flow initiated from the network connection banner. ### Changes - **New MetaMetrics event**: `NetworkConnectionBannerRpcUpdated` - **Tracked on**: Network form submission when `trackRpcUpdateFromBanner` flag is set - **Properties**: - `chain_id_caip`: CAIP-2 format (e.g., `eip155:1`) - `from_rpc_domain`: Original RPC domain being switched from (hostname for public RPCs, `'custom'` for private) - `to_rpc_domain`: New RPC domain being switched to (hostname for public RPCs, `'custom'` for private) ### Why Currently we track when users **click** "Update RPC" (`NetworkConnectionBannerUpdateRpcClicked`), but not when they **complete** the update. ```mermaid sequenceDiagram participant User participant Banner as Network Banner participant Form as Network Form participant Analytics Note over Banner: Network issue detected Banner->>User: Show "degraded" or "unavailable" banner Note over Analytics: 📊 NetworkConnectionBannerShown User->>Banner: Click "Update RPC" Banner->>Analytics: 📊 NetworkConnectionBannerUpdateRpcClicked Note right of Analytics: Existing event<br/>Tracks user INTENT Banner->>Form: Navigate to network settings User->>Form: Edit RPC endpoint User->>Form: Click "Save" Form->>Analytics: 📊 NetworkConnectionBannerRpcUpdated Note right of Analytics: NEW event (this PR)<br/>Tracks COMPLETION Form->>User: Network updated ✅ ``` ### Related - Segment schema PR: Consensys/segment-schema#366, Consensys/segment-schema#381 <!-- 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? --> [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/MetaMask/metamask-extension/pull/37751?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: https://consensyssoftware.atlassian.net/browse/WPC-172 ## **Manual testing steps** 1. Go to this page... 2. 3. ## **Screenshots/Recordings** <!-- If applicable, add screenshots and/or recordings to visualize the before and after of your change. --> ### **Before** <!-- [screenshots/recordings] --> ### **After** <!-- [screenshots/recordings] --> ## **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] > Adds `NetworkConnectionBannerRpcUpdated` event and wires a banner->form flag to track completed RPC updates with CAIP chain ID and RPC domain metadata. > > - **Analytics (MetaMetrics)**: > - Add `MetaMetricsEventName.NetworkConnectionBannerRpcUpdated`. > - **State & Actions**: > - Extend `setEditedNetwork`/`editedNetwork` with `trackRpcUpdateFromBanner`. > - Update `getEditedNetwork` selector types accordingly. > - **UI Flow**: > - `network-connection-banner`: on "Update RPC", dispatch `setEditedNetwork({ chainId, trackRpcUpdateFromBanner: true })` and navigate to `NETWORKS_ROUTE`. > - `network-list-menu`: pass `trackRpcUpdateFromBanner` to `NetworksForm`. > - `NetworksForm`: on Save (when editing) and flag is set, track `Network Connection Banner RPC Updated` with: > - `chain_id_caip` (`eip155:<decimal>`), > - `from_rpc_domain`/`to_rpc_domain` (public host or `custom`/`unknown`). > - Utilize `hexToNumber`, `isPublicEndpointUrl`, `onlyKeepHost` for payload. > - **Tests**: > - Update banner tests to assert the new flag in `setEditedNetwork`. > - Add form tests covering event emission, absence when flag not set, custom endpoints, and missing `rpcEndpoints` handling. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0704976. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent b4e970f commit 9f36182

File tree

9 files changed

+313
-7
lines changed

9 files changed

+313
-7
lines changed

shared/constants/metametrics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -764,6 +764,7 @@ export enum MetaMetricsEventName {
764764
NavPermissionsOpened = 'Permissions Opened',
765765
NetworkConnectionBannerShown = 'Network Connection Banner Shown',
766766
NetworkConnectionBannerUpdateRpcClicked = 'Network Connection Banner Update RPC Clicked',
767+
NetworkConnectionBannerRpcUpdated = 'Network Connection Banner RPC Updated',
767768
UpdatePermissionedNetworks = 'Update Permissioned Networks',
768769
UpdatePermissionedAccounts = 'Update Permissioned Accounts',
769770
ViewPermissionedNetworks = 'View Permissioned Networks',

ui/components/app/network-connection-banner/network-connection-banner.test.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,10 @@ describe('NetworkConnectionBanner', () => {
105105
);
106106
fireEvent.click(getByText('Update RPC'));
107107

108-
expect(mockSetEditedNetwork).toHaveBeenCalledWith({ chainId: '0x1' });
108+
expect(mockSetEditedNetwork).toHaveBeenCalledWith({
109+
chainId: '0x1',
110+
trackRpcUpdateFromBanner: true,
111+
});
109112
expect(navigateMock).toHaveBeenCalledWith('/settings/networks');
110113
});
111114

@@ -212,7 +215,10 @@ describe('NetworkConnectionBanner', () => {
212215
);
213216
fireEvent.click(getByText('update RPC'));
214217

215-
expect(mockSetEditedNetwork).toHaveBeenCalledWith({ chainId: '0x1' });
218+
expect(mockSetEditedNetwork).toHaveBeenCalledWith({
219+
chainId: '0x1',
220+
trackRpcUpdateFromBanner: true,
221+
});
216222
expect(navigateMock).toHaveBeenCalledWith('/settings/networks');
217223
});
218224

ui/components/app/network-connection-banner/network-connection-banner.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,12 @@ export const NetworkConnectionBanner = () => {
190190
networkClientId: networkConnectionBanner.networkClientId,
191191
});
192192

193-
dispatch(setEditedNetwork({ chainId: networkConnectionBanner.chainId }));
193+
dispatch(
194+
setEditedNetwork({
195+
chainId: networkConnectionBanner.chainId,
196+
trackRpcUpdateFromBanner: true,
197+
}),
198+
);
194199
navigate(NETWORKS_ROUTE);
195200
}
196201
}, [networkConnectionBanner, dispatch, navigate]);

ui/components/multichain/network-list-menu/network-list-menu.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -184,8 +184,11 @@ export const NetworkListMenu = ({ onClose }: NetworkListMenuProps) => {
184184
getMultichainNetworkConfigurationsByChainId,
185185
);
186186
const currentChainId = useSelector(getSelectedMultichainNetworkChainId);
187-
const { chainId: editingChainId, editCompleted } =
188-
useSelector(getEditedNetwork) ?? {};
187+
const {
188+
chainId: editingChainId,
189+
editCompleted,
190+
trackRpcUpdateFromBanner,
191+
} = useSelector(getEditedNetwork) ?? {};
189192
const permittedChainIds = useSelector((state) =>
190193
getPermittedEVMChainsForSelectedTab(state, selectedTabOrigin),
191194
);
@@ -735,6 +738,7 @@ export const NetworkListMenu = ({ onClose }: NetworkListMenuProps) => {
735738
<NetworksForm
736739
networkFormState={networkFormState}
737740
existingNetwork={editedNetwork}
741+
trackRpcUpdateFromBanner={trackRpcUpdateFromBanner}
738742
onRpcAdd={() => setActionMode(ACTION_MODE.ADD_RPC)}
739743
onBlockExplorerAdd={() => setActionMode(ACTION_MODE.ADD_EXPLORER_URL)}
740744
/>

ui/ducks/app/app.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ type AppState = {
112112
nickname?: string;
113113
editCompleted?: boolean;
114114
newNetwork?: boolean;
115+
trackRpcUpdateFromBanner?: boolean;
115116
}
116117
| undefined;
117118
newNetworkAddedConfigurationId: string;

ui/pages/settings/networks-tab/networks-form/networks-form.test.js

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
setTokenNetworkFilter,
1919
updateNetwork,
2020
} from '../../../../store/actions';
21+
import { MetaMetricsContext } from '../../../../contexts/metametrics';
2122
import { NetworksForm } from './networks-form';
2223

2324
jest.mock('../../../../../ui/store/actions', () => ({
@@ -87,6 +88,8 @@ describe('NetworkForm Component', () => {
8788
});
8889

8990
beforeEach(() => {
91+
jest.clearAllMocks();
92+
9093
nock('https://chainid.network:443', { encodedQueryParams: true })
9194
.get('/chains.json')
9295
.reply(200, [
@@ -457,4 +460,252 @@ describe('NetworkForm Component', () => {
457460
expect(setTokenNetworkFilter).toHaveBeenCalledTimes(1);
458461
});
459462
});
463+
464+
it('should track RPC update event when trackRpcUpdateFromBanner is true', async () => {
465+
const mockTrackEvent = jest.fn();
466+
const store = configureMockStore([thunk])({
467+
metamask: {
468+
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
469+
useSafeChainsListValidation: true,
470+
orderedNetworkList: {
471+
networkId: '0x1',
472+
networkRpcUrl: 'https://mainnet.infura.io/v3/',
473+
},
474+
multichainNetworkConfigurationsByChainId:
475+
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
476+
selectedMultichainNetworkChainId: 'eip155:1',
477+
isEvmSelected: true,
478+
},
479+
});
480+
481+
const { getByText } = renderWithProvider(
482+
<MetaMetricsContext.Provider value={mockTrackEvent}>
483+
<NetworksForm
484+
{...propNetworkDisplay}
485+
networkFormState={{
486+
...propNetworkDisplay.networkFormState,
487+
rpcUrls: {
488+
defaultRpcEndpointIndex: 0,
489+
rpcEndpoints: [
490+
{
491+
url: 'https://monad-mainnet.infura.io/v3/',
492+
type: 'custom',
493+
},
494+
],
495+
},
496+
}}
497+
existingNetwork={{
498+
chainId: '0x64',
499+
name: 'Ethereum',
500+
nativeCurrency: 'ETH',
501+
rpcEndpoints: [
502+
{
503+
url: 'https://mainnet.infura.io/v3/',
504+
},
505+
],
506+
defaultRpcEndpointIndex: 0,
507+
}}
508+
trackRpcUpdateFromBanner
509+
/>
510+
</MetaMetricsContext.Provider>,
511+
store,
512+
);
513+
514+
const saveButton = getByText('Save');
515+
fireEvent.click(saveButton);
516+
517+
await waitFor(() => {
518+
expect(updateNetwork).toHaveBeenCalled();
519+
expect(mockTrackEvent).toHaveBeenCalledWith({
520+
category: 'Network',
521+
event: 'Network Connection Banner RPC Updated',
522+
properties: {
523+
chain_id_caip: 'eip155:100',
524+
from_rpc_domain: 'mainnet.infura.io',
525+
to_rpc_domain: 'monad-mainnet.infura.io',
526+
},
527+
});
528+
});
529+
});
530+
531+
it('should not track RPC update event when trackRpcUpdateFromBanner is not set', async () => {
532+
const mockTrackEvent = jest.fn();
533+
const store = configureMockStore([thunk])({
534+
metamask: {
535+
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
536+
useSafeChainsListValidation: true,
537+
orderedNetworkList: {
538+
networkId: '0x1',
539+
networkRpcUrl: 'https://mainnet.infura.io/v3/',
540+
},
541+
multichainNetworkConfigurationsByChainId:
542+
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
543+
selectedMultichainNetworkChainId: 'eip155:1',
544+
isEvmSelected: true,
545+
},
546+
});
547+
548+
const { getByText } = renderWithProvider(
549+
<MetaMetricsContext.Provider value={mockTrackEvent}>
550+
<NetworksForm
551+
{...propNetworkDisplay}
552+
existingNetwork={{
553+
chainId: '0x64',
554+
name: 'Ethereum',
555+
nativeCurrency: 'ETH',
556+
rpcEndpoints: [
557+
{
558+
url: 'https://mainnet.infura.io/v3/',
559+
},
560+
],
561+
defaultRpcEndpointIndex: 0,
562+
}}
563+
// trackRpcUpdateFromBanner not set
564+
/>
565+
</MetaMetricsContext.Provider>,
566+
store,
567+
);
568+
569+
const saveButton = getByText('Save');
570+
fireEvent.click(saveButton);
571+
572+
await waitFor(() => {
573+
expect(updateNetwork).toHaveBeenCalled();
574+
// Should not have called the banner tracking event
575+
expect(mockTrackEvent).not.toHaveBeenCalledWith(
576+
expect.objectContaining({
577+
event: 'Network Connection Banner RPC Updated',
578+
}),
579+
);
580+
});
581+
});
582+
583+
it('should track custom RPC URL when endpoint is not public', async () => {
584+
const mockTrackEvent = jest.fn();
585+
const store = configureMockStore([thunk])({
586+
metamask: {
587+
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
588+
useSafeChainsListValidation: true,
589+
orderedNetworkList: {
590+
networkId: '0x1',
591+
networkRpcUrl: 'https://mainnet.infura.io/v3/',
592+
},
593+
multichainNetworkConfigurationsByChainId:
594+
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
595+
selectedMultichainNetworkChainId: 'eip155:1',
596+
isEvmSelected: true,
597+
},
598+
});
599+
600+
const { getByText } = renderWithProvider(
601+
<MetaMetricsContext.Provider value={mockTrackEvent}>
602+
<NetworksForm
603+
{...propNetworkDisplay}
604+
networkFormState={{
605+
...propNetworkDisplay.networkFormState,
606+
rpcUrls: {
607+
defaultRpcEndpointIndex: 0,
608+
rpcEndpoints: [
609+
{
610+
url: 'https://custom-rpc.example.com',
611+
type: 'custom',
612+
},
613+
],
614+
},
615+
}}
616+
existingNetwork={{
617+
chainId: '0x64',
618+
name: 'Ethereum',
619+
nativeCurrency: 'ETH',
620+
rpcEndpoints: [
621+
{
622+
url: 'https://custom-rpc.example.com',
623+
},
624+
],
625+
defaultRpcEndpointIndex: 0,
626+
}}
627+
trackRpcUpdateFromBanner
628+
/>
629+
</MetaMetricsContext.Provider>,
630+
store,
631+
);
632+
633+
const saveButton = getByText('Save');
634+
fireEvent.click(saveButton);
635+
636+
await waitFor(() => {
637+
expect(updateNetwork).toHaveBeenCalled();
638+
expect(mockTrackEvent).toHaveBeenCalledWith({
639+
category: 'Network',
640+
event: 'Network Connection Banner RPC Updated',
641+
properties: {
642+
chain_id_caip: 'eip155:100',
643+
from_rpc_domain: 'custom',
644+
to_rpc_domain: 'custom',
645+
},
646+
});
647+
});
648+
});
649+
650+
it('should handle corrupted state with missing rpcEndpoints gracefully', async () => {
651+
const mockTrackEvent = jest.fn();
652+
const store = configureMockStore([thunk])({
653+
metamask: {
654+
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
655+
useSafeChainsListValidation: true,
656+
orderedNetworkList: {
657+
networkId: '0x1',
658+
networkRpcUrl: 'https://mainnet.infura.io/v3/',
659+
},
660+
multichainNetworkConfigurationsByChainId:
661+
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
662+
selectedMultichainNetworkChainId: 'eip155:1',
663+
isEvmSelected: true,
664+
},
665+
});
666+
667+
const { getByText } = renderWithProvider(
668+
<MetaMetricsContext.Provider value={mockTrackEvent}>
669+
<NetworksForm
670+
{...propNetworkDisplay}
671+
networkFormState={{
672+
...propNetworkDisplay.networkFormState,
673+
rpcUrls: {
674+
defaultRpcEndpointIndex: 0,
675+
rpcEndpoints: [
676+
{
677+
url: 'https://monad-mainnet.infura.io/v3/',
678+
type: 'custom',
679+
},
680+
],
681+
},
682+
}}
683+
existingNetwork={{
684+
chainId: '0x64',
685+
name: 'Ethereum',
686+
nativeCurrency: 'ETH',
687+
// rpcEndpoints is undefined (corrupted state)
688+
}}
689+
trackRpcUpdateFromBanner
690+
/>
691+
</MetaMetricsContext.Provider>,
692+
store,
693+
);
694+
695+
const saveButton = getByText('Save');
696+
fireEvent.click(saveButton);
697+
698+
await waitFor(() => {
699+
expect(updateNetwork).toHaveBeenCalled();
700+
expect(mockTrackEvent).toHaveBeenCalledWith({
701+
category: 'Network',
702+
event: 'Network Connection Banner RPC Updated',
703+
properties: {
704+
chain_id_caip: 'eip155:100',
705+
from_rpc_domain: 'unknown', // Corrupted state handled gracefully
706+
to_rpc_domain: 'monad-mainnet.infura.io',
707+
},
708+
});
709+
});
710+
});
460711
});

0 commit comments

Comments
 (0)