Skip to content

Commit 4a8230a

Browse files
committed
metrics: Track when RPC update from network banner is completed
Add NetworkConnectionBannerRpcUpdated event to track when users complete the RPC update flow initiated from the connection banner. Includes chain_id_caip and rpc_endpoint_url properties.
1 parent a37efd8 commit 4a8230a

File tree

9 files changed

+236
-7
lines changed

9 files changed

+236
-7
lines changed

shared/constants/metametrics.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,6 +741,7 @@ export enum MetaMetricsEventName {
741741
NavPermissionsOpened = 'Permissions Opened',
742742
NetworkConnectionBannerShown = 'Network Connection Banner Shown',
743743
NetworkConnectionBannerUpdateRpcClicked = 'Network Connection Banner Update RPC Clicked',
744+
NetworkConnectionBannerRpcUpdated = 'Network Connection Banner RPC Updated',
744745
UpdatePermissionedNetworks = 'Update Permissioned Networks',
745746
UpdatePermissionedAccounts = 'Update Permissioned Accounts',
746747
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
@@ -111,6 +111,7 @@ type AppState = {
111111
nickname?: string;
112112
editCompleted?: boolean;
113113
newNetwork?: boolean;
114+
trackRpcUpdateFromBanner?: boolean;
114115
}
115116
| undefined;
116117
newNetworkAddedConfigurationId: string;

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

Lines changed: 179 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,12 @@ describe('NetworkForm Component', () => {
8788
});
8889

8990
beforeEach(() => {
91+
// Clear mock call history to ensure test isolation and accurate assertions
92+
// Each test verifies these actions are called with specific arguments
93+
updateNetwork.mockClear(); // Reset calls from network update tests
94+
addNetwork.mockClear(); // Reset calls from network creation tests
95+
setTokenNetworkFilter.mockClear(); // Reset calls from token filter tests
96+
9097
nock('https://chainid.network:443', { encodedQueryParams: true })
9198
.get('/chains.json')
9299
.reply(200, [
@@ -457,4 +464,176 @@ describe('NetworkForm Component', () => {
457464
expect(setTokenNetworkFilter).toHaveBeenCalledTimes(1);
458465
});
459466
});
467+
468+
it('should track RPC update event when trackRpcUpdateFromBanner is true', async () => {
469+
const mockTrackEvent = jest.fn();
470+
const store = configureMockStore([thunk])({
471+
metamask: {
472+
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
473+
useSafeChainsListValidation: true,
474+
orderedNetworkList: {
475+
networkId: '0x1',
476+
networkRpcUrl: 'https://mainnet.infura.io/v3/',
477+
},
478+
multichainNetworkConfigurationsByChainId:
479+
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
480+
selectedMultichainNetworkChainId: 'eip155:1',
481+
isEvmSelected: true,
482+
},
483+
});
484+
485+
const { getByText } = renderWithProvider(
486+
<MetaMetricsContext.Provider value={mockTrackEvent}>
487+
<NetworksForm
488+
{...propNetworkDisplay}
489+
existingNetwork={{
490+
chainId: '0x64',
491+
name: 'Ethereum',
492+
nativeCurrency: 'ETH',
493+
rpcEndpoints: [
494+
{
495+
url: 'https://mainnet.infura.io/v3/',
496+
},
497+
],
498+
defaultRpcEndpointIndex: 0,
499+
}}
500+
trackRpcUpdateFromBanner
501+
/>
502+
</MetaMetricsContext.Provider>,
503+
store,
504+
);
505+
506+
const saveButton = getByText('Save');
507+
fireEvent.click(saveButton);
508+
509+
await waitFor(() => {
510+
expect(updateNetwork).toHaveBeenCalled();
511+
expect(mockTrackEvent).toHaveBeenCalledWith({
512+
category: 'Network',
513+
event: 'Network Connection Banner RPC Updated',
514+
properties: {
515+
chain_id_caip: 'eip155:100',
516+
rpc_endpoint_url: 'mainnet.infura.io',
517+
},
518+
});
519+
});
520+
});
521+
522+
it('should not track RPC update event when trackRpcUpdateFromBanner is not set', async () => {
523+
const mockTrackEvent = jest.fn();
524+
const store = configureMockStore([thunk])({
525+
metamask: {
526+
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
527+
useSafeChainsListValidation: true,
528+
orderedNetworkList: {
529+
networkId: '0x1',
530+
networkRpcUrl: 'https://mainnet.infura.io/v3/',
531+
},
532+
multichainNetworkConfigurationsByChainId:
533+
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
534+
selectedMultichainNetworkChainId: 'eip155:1',
535+
isEvmSelected: true,
536+
},
537+
});
538+
539+
const { getByText } = renderWithProvider(
540+
<MetaMetricsContext.Provider value={mockTrackEvent}>
541+
<NetworksForm
542+
{...propNetworkDisplay}
543+
existingNetwork={{
544+
chainId: '0x64',
545+
name: 'Ethereum',
546+
nativeCurrency: 'ETH',
547+
rpcEndpoints: [
548+
{
549+
url: 'https://mainnet.infura.io/v3/',
550+
},
551+
],
552+
defaultRpcEndpointIndex: 0,
553+
}}
554+
// trackRpcUpdateFromBanner not set
555+
/>
556+
</MetaMetricsContext.Provider>,
557+
store,
558+
);
559+
560+
const saveButton = getByText('Save');
561+
fireEvent.click(saveButton);
562+
563+
await waitFor(() => {
564+
expect(updateNetwork).toHaveBeenCalled();
565+
// Should not have called the banner tracking event
566+
expect(mockTrackEvent).not.toHaveBeenCalledWith(
567+
expect.objectContaining({
568+
event: 'Network Connection Banner RPC Updated',
569+
}),
570+
);
571+
});
572+
});
573+
574+
it('should track custom RPC URL when endpoint is not public', async () => {
575+
const mockTrackEvent = jest.fn();
576+
const store = configureMockStore([thunk])({
577+
metamask: {
578+
...mockNetworkState({ chainId: CHAIN_IDS.MAINNET }),
579+
useSafeChainsListValidation: true,
580+
orderedNetworkList: {
581+
networkId: '0x1',
582+
networkRpcUrl: 'https://mainnet.infura.io/v3/',
583+
},
584+
multichainNetworkConfigurationsByChainId:
585+
AVAILABLE_MULTICHAIN_NETWORK_CONFIGURATIONS,
586+
selectedMultichainNetworkChainId: 'eip155:1',
587+
isEvmSelected: true,
588+
},
589+
});
590+
591+
const { getByText } = renderWithProvider(
592+
<MetaMetricsContext.Provider value={mockTrackEvent}>
593+
<NetworksForm
594+
{...propNetworkDisplay}
595+
networkFormState={{
596+
...propNetworkDisplay.networkFormState,
597+
rpcUrls: {
598+
defaultRpcEndpointIndex: 0,
599+
rpcEndpoints: [
600+
{
601+
url: 'https://custom-rpc.example.com',
602+
type: 'custom',
603+
},
604+
],
605+
},
606+
}}
607+
existingNetwork={{
608+
chainId: '0x64',
609+
name: 'Ethereum',
610+
nativeCurrency: 'ETH',
611+
rpcEndpoints: [
612+
{
613+
url: 'https://custom-rpc.example.com',
614+
},
615+
],
616+
defaultRpcEndpointIndex: 0,
617+
}}
618+
trackRpcUpdateFromBanner
619+
/>
620+
</MetaMetricsContext.Provider>,
621+
store,
622+
);
623+
624+
const saveButton = getByText('Save');
625+
fireEvent.click(saveButton);
626+
627+
await waitFor(() => {
628+
expect(updateNetwork).toHaveBeenCalled();
629+
expect(mockTrackEvent).toHaveBeenCalledWith({
630+
category: 'Network',
631+
event: 'Network Connection Banner RPC Updated',
632+
properties: {
633+
chain_id_caip: 'eip155:100',
634+
rpc_endpoint_url: 'custom',
635+
},
636+
});
637+
});
638+
});
460639
});

ui/pages/settings/networks-tab/networks-form/networks-form.tsx

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
type UpdateNetworkFields,
66
RpcEndpointType,
77
} from '@metamask/network-controller';
8-
import { Hex, isStrictHexString } from '@metamask/utils';
8+
import { Hex, isStrictHexString, hexToNumber } from '@metamask/utils';
99
import { NETWORKS_BYPASSING_VALIDATION } from '@metamask/controller-utils';
1010
import {
1111
MetaMetricsEventCategory,
@@ -27,6 +27,7 @@ import {
2727
isSafeChainId,
2828
} from '../../../../../shared/modules/network.utils';
2929
import { jsonRpcRequest } from '../../../../../shared/modules/rpc.utils';
30+
import { isPublicEndpointUrl } from '../../../../../shared/lib/network-utils';
3031
import { MetaMetricsContext } from '../../../../contexts/metametrics';
3132
import { useI18nContext } from '../../../../hooks/useI18nContext';
3233
import { getNetworkConfigurationsByChainId } from '../../../../../shared/modules/selectors/networks';
@@ -81,6 +82,7 @@ import { useNetworkFormState } from './networks-form-state';
8182
export const NetworksForm = ({
8283
networkFormState,
8384
existingNetwork,
85+
trackRpcUpdateFromBanner,
8486
onRpcAdd,
8587
onBlockExplorerAdd,
8688
toggleNetworkMenuAfterSubmit = true,
@@ -89,6 +91,7 @@ export const NetworksForm = ({
8991
}: {
9092
networkFormState: ReturnType<typeof useNetworkFormState>;
9193
existingNetwork?: UpdateNetworkFields;
94+
trackRpcUpdateFromBanner?: boolean;
9295
onRpcAdd: () => void;
9396
onBlockExplorerAdd: () => void;
9497
toggleNetworkMenuAfterSubmit?: boolean;
@@ -309,6 +312,35 @@ export const NetworksForm = ({
309312
);
310313
await dispatch(setEnabledNetworks(existingNetwork.chainId));
311314
}
315+
316+
// Track RPC update from network connection banner
317+
if (trackRpcUpdateFromBanner) {
318+
const selectedRpcEndpoint =
319+
networkPayload.rpcEndpoints?.[
320+
networkPayload.defaultRpcEndpointIndex
321+
];
322+
const chainIdAsDecimal = hexToNumber(chainIdHex);
323+
const sanitizedRpcUrl =
324+
selectedRpcEndpoint &&
325+
isPublicEndpointUrl(
326+
selectedRpcEndpoint.url,
327+
infuraProjectId ?? '',
328+
)
329+
? onlyKeepHost(selectedRpcEndpoint.url)
330+
: 'custom';
331+
332+
trackEvent({
333+
category: MetaMetricsEventCategory.Network,
334+
event: MetaMetricsEventName.NetworkConnectionBannerRpcUpdated,
335+
// The names of Segment properties have a particular case.
336+
/* eslint-disable @typescript-eslint/naming-convention */
337+
properties: {
338+
chain_id_caip: `eip155:${chainIdAsDecimal}`,
339+
rpc_endpoint_url: sanitizedRpcUrl,
340+
},
341+
/* eslint-enable @typescript-eslint/naming-convention */
342+
});
343+
}
312344
} else {
313345
await dispatch(addNetwork(networkPayload));
314346
await dispatch(setEnabledNetworks(networkPayload.chainId));

ui/selectors/selectors.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -223,7 +223,7 @@ export function getNewNetworkAdded(state) {
223223

224224
/**
225225
* @param state
226-
* @returns {{ chainId: import('@metamask/utils').Hex; nickname: string; editCompleted: boolean} | undefined}
226+
* @returns {{ chainId: import('@metamask/utils').Hex; nickname?: string; editCompleted?: boolean; newNetwork?: boolean; trackRpcUpdateFromBanner?: boolean} | undefined}
227227
*/
228228
export function getEditedNetwork(state) {
229229
return state.appState.editedNetwork;

ui/store/actions.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5682,6 +5682,7 @@ export function setEditedNetwork(
56825682
nickname?: string;
56835683
editCompleted?: boolean;
56845684
newNetwork?: boolean;
5685+
trackRpcUpdateFromBanner?: boolean;
56855686
}
56865687
| undefined = undefined,
56875688
): PayloadAction<object> {

0 commit comments

Comments
 (0)