Skip to content

Commit d4d4dfa

Browse files
authored
feat: forward duration and traceId to RPC Service Degraded segment events (#29964)
## **Description** Bumps `@metamask/network-controller` to `^31.0.0` and `@metamask/controller-utils` to `^12.0.0`, which introduce two new optional payload fields on `NetworkController:rpcEndpointDegraded` (and `…ChainDegraded`): - `duration` (`number | undefined`): the policy execution time in milliseconds when the request succeeded but exceeded the degraded threshold. `undefined` when retries were exhausted. - `traceId` (`string | undefined`): the value of the `X-Trace-Id` response header from the last request attempt. `undefined` when no response was received or the header was absent. This PR threads both fields through `onRpcEndpointDegraded` → `trackRpcEndpointEvent` and emits them on the `RPC Service Degraded` Segment event as `duration_ms` and `trace_id` (snake_case, to match existing properties such as `rpc_method_name` and `retry_reason`). Both keys are conditionally omitted from the event when the upstream value is `undefined`, so we don't pollute Segment with empty values. A `resolutions` entry pins `@metamask/network-controller` to `31.0.0` repo-wide to dedupe the nested copy that `@metamask/multichain-network-controller@^3.1.0` still pulls in at `^30.1.0`. Without this dedupe, TypeScript reports nominal-type mismatches across packages that consume `NetworkConfiguration` / `NetworkState` (e.g. `transaction-controller-init.ts`, `assets-list.ts`). The runtime change in 31.0.0 is additive only (two optional payload fields), so forcing the multichain dep onto the newer version is safe. This enables correlating degraded RPC events with backend traces for debugging RPC health, and surfaces the actual slow-success latency. See core PR [MetaMask/core#8455](MetaMask/core#8455) for the upstream change. ## **Changelog** CHANGELOG entry: null ## **Related issues** Fixes: [WPC-963](https://consensyssoftware.atlassian.net/browse/WPC-963) ## **Manual testing steps** \`\`\`gherkin Feature: RPC degraded events carry duration and trace ID Scenario: slow but successful RPC request emits duration_ms and trace_id Given a network whose RPC endpoint responds slowly (>5s) and returns an X-Trace-Id header And MetaMetrics is enabled When the user performs an action that issues a JSON-RPC call (e.g. switching chains, refreshing balances) Then the outgoing "RPC Service Degraded" Segment event contains numeric duration_ms And the event contains trace_id equal to the response's X-Trace-Id header Scenario: retries-exhausted RPC request omits duration_ms Given a network whose RPC endpoint returns retriable errors (e.g. 503) for every attempt And MetaMetrics is enabled When the user performs an action that issues a JSON-RPC call Then the outgoing "RPC Service Degraded" Segment event omits the duration_ms property And the event includes trace_id only if a response with X-Trace-Id was received before failing \`\`\` ## **Screenshots/Recordings** N/A — no UI change. ### **Before** ### **After** ## **Pre-merge author checklist** - [x] I've followed [MetaMask Contributor Docs](https://github.com/MetaMask/contributor-docs) and [MetaMask Mobile Coding Standards](https://github.com/MetaMask/metamask-mobile/blob/main/.github/guidelines/CODING_GUIDELINES.md). - [x] I've completed the PR template to the best of my ability - [x] I've included tests if applicable - [x] 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-mobile/blob/main/.github/guidelines/LABELING_GUIDELINES.md)). Not required for external contributors. #### Performance checks (if applicable) - [ ] I've tested on Android - [ ] I've tested with a power user scenario - [ ] I've instrumented key operations with Sentry traces for production performance metrics ## **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. [WPC-963]: https://consensyssoftware.atlassian.net/browse/WPC-963?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Touches network telemetry wiring and analytics event payloads; low functional impact, but mistakes could break/alter Segment schema and degrade monitoring quality. > > **Overview** > **RPC degraded telemetry now includes more context.** The `NetworkController:rpcEndpointDegraded` subscription forwards optional `duration` and `traceId` into `onRpcEndpointDegraded`/`trackRpcEndpointEvent`, emitting them to Segment as `duration_ms` and `trace_id` only when defined, with new unit tests covering both presence and omission. > > **Dependency + fixture updates.** Bumps/pins `@metamask/network-controller` to `31.0.0` (plus `controller-utils` and `remote-feature-flag-controller`), adds new default network entries (Avalanche, Monad Mainnet, MegaETH Mainnet, ZKsync Era) to test background state and updates Infura API mocks and AddressSelector sorting expectations accordingly. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit 73534c4. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 520fa22 commit d4d4dfa

8 files changed

Lines changed: 169 additions & 43 deletions

File tree

app/components/Views/AddressSelector/AddressSelector.test.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,10 +140,14 @@ describe('AccountSelector', () => {
140140
MAINNET_DISPLAY_NAME,
141141
LINEA_MAINNET_DISPLAY_NAME,
142142
ARBITRUM_DISPLAY_NAME,
143+
'Avalanche Mainnet',
143144
BASE_DISPLAY_NAME,
144145
BNB_DISPLAY_NAME,
146+
'MegaETH Mainnet',
147+
'Monad Mainnet',
145148
OPTIMISM_DISPLAY_NAME,
146149
POLYGON_DISPLAY_NAME,
150+
'ZKsync Era',
147151
]);
148152
expect(networkNames).not.toContain('Solana');
149153
});

app/core/Engine/controllers/network-controller-init.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,19 +219,23 @@ export const networkControllerInit: MessengerClientInitFunction<
219219
'NetworkController:rpcEndpointDegraded',
220220
async ({
221221
chainId,
222+
duration,
222223
endpointUrl,
223224
error,
224225
rpcMethodName,
226+
traceId,
225227
type,
226228
retryReason,
227229
}) => {
228230
onRpcEndpointDegraded({
229231
chainId,
232+
duration,
230233
endpointUrl,
231234
error,
232235
infuraProjectId,
233236
retryReason,
234237
rpcMethodName,
238+
traceId,
235239
trackEvent: ({ event, properties }) => {
236240
buildAndTrackEvent(initMessenger, event, properties);
237241
},

app/core/Engine/controllers/network-controller/messenger-action-handlers.test.ts

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,67 @@ describe('onRpcEndpointDegraded', () => {
322322
});
323323
/* eslint-enable @typescript-eslint/naming-convention */
324324
});
325+
326+
it('includes duration_ms and trace_id when present in the payload', () => {
327+
shouldCreateRpcServiceEventsMock.mockReturnValue(true);
328+
isPublicEndpointUrlMock.mockReturnValue(true);
329+
const trackEvent = jest.fn();
330+
331+
onRpcEndpointDegraded({
332+
chainId: '0xaa36a7',
333+
duration: 5123,
334+
endpointUrl: 'https://example.com',
335+
error: undefined,
336+
infuraProjectId: 'the-infura-project-id',
337+
rpcMethodName: 'eth_blockNumber',
338+
traceId: 'abc-123-trace',
339+
trackEvent,
340+
type: 'slow_success',
341+
metaMetricsId:
342+
'0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420',
343+
});
344+
345+
// The names of Segment properties have a particular case.
346+
/* eslint-disable @typescript-eslint/naming-convention */
347+
expect(trackEvent).toHaveBeenCalledWith({
348+
event: expect.objectContaining({
349+
category: 'RPC Service Degraded',
350+
}),
351+
properties: {
352+
chain_id_caip: 'eip155:11155111',
353+
type: 'slow_success',
354+
rpc_endpoint_url: 'example.com',
355+
rpc_domain: 'example.com',
356+
rpc_method_name: 'eth_blockNumber',
357+
duration_ms: 5123,
358+
trace_id: 'abc-123-trace',
359+
},
360+
});
361+
/* eslint-enable @typescript-eslint/naming-convention */
362+
});
363+
364+
it('omits duration_ms and trace_id when they are undefined in the payload', () => {
365+
shouldCreateRpcServiceEventsMock.mockReturnValue(true);
366+
isPublicEndpointUrlMock.mockReturnValue(true);
367+
const trackEvent = jest.fn();
368+
369+
onRpcEndpointDegraded({
370+
chainId: '0xaa36a7',
371+
endpointUrl: 'https://example.com',
372+
error: new HttpError(420),
373+
infuraProjectId: 'the-infura-project-id',
374+
rpcMethodName: 'eth_blockNumber',
375+
trackEvent,
376+
type: 'retries_exhausted',
377+
retryReason: 'non_successful_http_status',
378+
metaMetricsId:
379+
'0x86bacb9b2bf9a7e8d2b147eadb95ac9aaa26842327cd24afc8bd4b3c1d136420',
380+
});
381+
382+
const [[call]] = trackEvent.mock.calls;
383+
expect(call.properties).not.toHaveProperty('duration_ms');
384+
expect(call.properties).not.toHaveProperty('trace_id');
385+
});
325386
});
326387

327388
describe('if the Segment event should not be created', () => {

app/core/Engine/controllers/network-controller/messenger-action-handlers.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export function onRpcEndpointUnavailable({
7171
*
7272
* @param args - The arguments.
7373
* @param args.chainId - The chain ID that the endpoint represents.
74+
* @param args.duration - The policy execution time in milliseconds when the
75+
* request succeeded but was slow. `undefined` when retries were exhausted.
7476
* @param args.endpointUrl - The URL of the endpoint.
7577
* @param args.error - The connection or response error encountered after making
7678
* a request to the RPC endpoint.
@@ -79,28 +81,34 @@ export function onRpcEndpointUnavailable({
7981
* @param args.retryReason - The category of error that was retried (only
8082
* present when `type` is `'retries_exhausted'`).
8183
* @param args.rpcMethodName - The JSON-RPC method that was being executed.
84+
* @param args.traceId - The value of the `X-Trace-Id` response header from the
85+
* last request attempt, or `undefined` if the header was not present.
8286
* @param args.trackEvent - The function that will create the Segment event.
8387
* @param args.type - Why the endpoint became degraded (`'slow_success'` or
8488
* `'retries_exhausted'`).
8589
*/
8690
export function onRpcEndpointDegraded({
8791
chainId,
92+
duration,
8893
endpointUrl,
8994
error,
9095
infuraProjectId,
9196
metaMetricsId,
9297
retryReason,
9398
rpcMethodName,
99+
traceId,
94100
trackEvent,
95101
type,
96102
}: {
97103
chainId: Hex;
104+
duration?: number;
98105
endpointUrl: string;
99106
error: unknown;
100107
infuraProjectId: string;
101108
metaMetricsId: string | null | undefined;
102109
retryReason?: RetryReason;
103110
rpcMethodName: string;
111+
traceId?: string;
104112
trackEvent: (options: {
105113
event: IMetaMetricsEvent | ITrackingEvent;
106114
properties: JsonMap;
@@ -109,12 +117,14 @@ export function onRpcEndpointDegraded({
109117
}): void {
110118
trackRpcEndpointEvent(MetaMetricsEvents.RPC_SERVICE_DEGRADED, {
111119
chainId,
120+
duration,
112121
endpointUrl,
113122
error,
114123
infuraProjectId,
115124
metaMetricsId,
116125
retryReason,
117126
rpcMethodName,
127+
traceId,
118128
trackEvent,
119129
type,
120130
});
@@ -127,6 +137,9 @@ export function onRpcEndpointDegraded({
127137
* @param event - The Segment event to create.
128138
* @param args - The remaining arguments.
129139
* @param args.chainId - The chain ID that the endpoint represents.
140+
* @param args.duration - The policy execution time in milliseconds when the
141+
* request succeeded but was slow (only present for degraded events from a
142+
* slow success).
130143
* @param args.endpointUrl - The URL of the endpoint.
131144
* @param args.error - The connection or response error encountered after making
132145
* a request to the RPC endpoint.
@@ -136,6 +149,8 @@ export function onRpcEndpointDegraded({
136149
* present for degraded events when `type` is `'retries_exhausted'`).
137150
* @param args.rpcMethodName - The JSON-RPC method that was being executed
138151
* (only present for degraded events).
152+
* @param args.traceId - The value of the `X-Trace-Id` response header from the
153+
* last request attempt (only present for degraded events).
139154
* @param args.trackEvent - The function that will create the Segment event.
140155
* @param args.type - Why the endpoint became degraded (only present for
141156
* degraded events).
@@ -144,21 +159,25 @@ export function trackRpcEndpointEvent(
144159
event: (typeof MetaMetricsEvents)[keyof typeof MetaMetricsEvents],
145160
{
146161
chainId,
162+
duration,
147163
endpointUrl,
148164
error,
149165
infuraProjectId,
150166
retryReason,
151167
rpcMethodName,
168+
traceId,
152169
trackEvent,
153170
type,
154171
metaMetricsId,
155172
}: {
156173
chainId: Hex;
174+
duration?: number;
157175
endpointUrl: string;
158176
error: unknown;
159177
infuraProjectId: string;
160178
retryReason?: RetryReason;
161179
rpcMethodName?: string;
180+
traceId?: string;
162181
trackEvent: (options: {
163182
event: IMetaMetricsEvent | ITrackingEvent;
164183
properties: JsonMap;
@@ -188,6 +207,8 @@ export function trackRpcEndpointEvent(
188207
...(rpcMethodName ? { rpc_method_name: rpcMethodName } : {}),
189208
...(type ? { type } : {}),
190209
...(retryReason ? { retry_reason: retryReason } : {}),
210+
...(duration === undefined ? {} : { duration_ms: duration }),
211+
...(traceId === undefined ? {} : { trace_id: traceId }),
191212
...(isObject(error) &&
192213
'httpStatus' in error &&
193214
isValidJson(error.httpStatus)

app/util/test/initial-background-state.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,66 @@
237237
"url": "https://base-mainnet.infura.io/v3/{infuraProjectId}"
238238
}
239239
]
240+
},
241+
"0x8f": {
242+
"blockExplorerUrls": [],
243+
"chainId": "0x8f",
244+
"defaultRpcEndpointIndex": 0,
245+
"name": "Monad Mainnet",
246+
"nativeCurrency": "MON",
247+
"rpcEndpoints": [
248+
{
249+
"failoverUrls": [],
250+
"networkClientId": "monad-mainnet",
251+
"type": "infura",
252+
"url": "https://monad-mainnet.infura.io/v3/{infuraProjectId}"
253+
}
254+
]
255+
},
256+
"0x10e6": {
257+
"blockExplorerUrls": [],
258+
"chainId": "0x10e6",
259+
"defaultRpcEndpointIndex": 0,
260+
"name": "MegaETH Mainnet",
261+
"nativeCurrency": "ETH",
262+
"rpcEndpoints": [
263+
{
264+
"failoverUrls": [],
265+
"networkClientId": "megaeth-mainnet",
266+
"type": "infura",
267+
"url": "https://megaeth-mainnet.infura.io/v3/{infuraProjectId}"
268+
}
269+
]
270+
},
271+
"0x144": {
272+
"blockExplorerUrls": [],
273+
"chainId": "0x144",
274+
"defaultRpcEndpointIndex": 0,
275+
"name": "ZKsync Era",
276+
"nativeCurrency": "ETH",
277+
"rpcEndpoints": [
278+
{
279+
"failoverUrls": [],
280+
"networkClientId": "zksync-mainnet",
281+
"type": "infura",
282+
"url": "https://zksync-mainnet.infura.io/v3/{infuraProjectId}"
283+
}
284+
]
285+
},
286+
"0xa86a": {
287+
"blockExplorerUrls": [],
288+
"chainId": "0xa86a",
289+
"defaultRpcEndpointIndex": 0,
290+
"name": "Avalanche Mainnet",
291+
"nativeCurrency": "AVAX",
292+
"rpcEndpoints": [
293+
{
294+
"failoverUrls": [],
295+
"networkClientId": "avalanche-mainnet",
296+
"type": "infura",
297+
"url": "https://avalanche-mainnet.infura.io/v3/{infuraProjectId}"
298+
}
299+
]
240300
}
241301
}
242302
},

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@
165165
]
166166
},
167167
"resolutions": {
168+
"@metamask/network-controller": "31.0.0",
168169
"@react-native-community/viewpager": "patch:@react-native-community/viewpager@npm%3A3.3.0#~/.yarn/patches/@react-native-community-viewpager-npm-3.3.0.patch",
169170
"@solana-mobile/mobile-wallet-adapter-protocol": "^2.2.5",
170171
"@appium/schema/json-schema": "^0.4.0",
@@ -247,7 +248,7 @@
247248
"@metamask/client-controller": "^1.0.1",
248249
"@metamask/compliance-controller": "2.0.0",
249250
"@metamask/connectivity-controller": "^0.1.0",
250-
"@metamask/controller-utils": "^11.18.0",
251+
"@metamask/controller-utils": "^12.0.0",
251252
"@metamask/core-backend": "^6.2.0",
252253
"@metamask/delegation-controller": "^2.0.2",
253254
"@metamask/delegation-deployments": "^1.0.0",
@@ -299,7 +300,7 @@
299300
"@metamask/multichain-network-controller": "^3.1.0",
300301
"@metamask/multichain-transactions-controller": "^7.1.0",
301302
"@metamask/native-utils": "^0.8.0",
302-
"@metamask/network-controller": "^30.0.0",
303+
"@metamask/network-controller": "^31.0.0",
303304
"@metamask/network-enablement-controller": "^5.1.0",
304305
"@metamask/notification-services-controller": "^23.1.0",
305306
"@metamask/permission-controller": "^13.1.1",
@@ -317,7 +318,7 @@
317318
"@metamask/react-native-payments": "patch:@metamask/react-native-payments@npm%3A2.0.2#~/.yarn/patches/@metamask-react-native-payments-npm-2.0.2-4ddc5f9862.patch",
318319
"@metamask/react-native-search-api": "1.0.1",
319320
"@metamask/react-native-webview": "patch:@metamask/react-native-webview@npm%3A14.6.0#~/.yarn/patches/@metamask-react-native-webview-npm-14.6.0-f12ccc06ff.patch",
320-
"@metamask/remote-feature-flag-controller": "^4.1.0",
321+
"@metamask/remote-feature-flag-controller": "^4.2.1",
321322
"@metamask/rpc-errors": "^7.0.2",
322323
"@metamask/sample-controllers": "^3.0.0",
323324
"@metamask/scure-bip39": "^2.1.0",

tests/api-mocking/mock-responses/infura-mocks.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ const createInfuraMocks = () => {
6666
'starknet-sepolia.infura.io',
6767
'ipfs.infura.io',
6868
'sei-mainnet.infura.io',
69+
'monad-mainnet.infura.io',
70+
'megaeth-mainnet.infura.io',
71+
'zksync-mainnet.infura.io',
6972
];
7073

7174
endpoints.forEach((endpoint) => {

0 commit comments

Comments
 (0)