Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ linkStyle default opacity:0.5
name_controller --> base_controller;
name_controller --> controller_utils;
name_controller --> messenger;
network_controller --> analytics_controller;
network_controller --> base_controller;
network_controller --> connectivity_controller;
network_controller --> controller_utils;
Expand Down
12 changes: 12 additions & 0 deletions packages/network-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add optional `analytics` constructor option that makes `NetworkController` emit `RPC Service Unavailable` and `RPC Service Degraded` analytics events via the `AnalyticsController:trackEvent` action when an RPC endpoint becomes unavailable or degraded ([#9270](https://github.com/MetaMask/core/pull/9270))
- The option takes `isRpcEndpointUrlPublic` (decides whether an endpoint URL is safe to report verbatim or as `'custom'`) and `rpcServiceEventsSampleRate` (the proportion of events to emit, between `0` and `1`).
- No analytics are emitted when the option is omitted.
- Adds the `NetworkControllerAnalyticsOptions` and `RpcServiceEventName` types.

### Changed

- The `NetworkControllerMessenger` now allows the `AnalyticsController:getState` and `AnalyticsController:trackEvent` actions ([#9270](https://github.com/MetaMask/core/pull/9270))
- Consumers that pass the `analytics` option must delegate these actions to the network controller messenger.

## [33.0.0]

### Added
Expand Down
1 change: 1 addition & 0 deletions packages/network-controller/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
"test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watch"
},
"dependencies": {
"@metamask/analytics-controller": "^1.2.0",
"@metamask/base-controller": "^9.1.0",
"@metamask/connectivity-controller": "^0.2.0",
"@metamask/controller-utils": "^12.3.0",
Expand Down
143 changes: 142 additions & 1 deletion packages/network-controller/src/NetworkController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ import type {
ControllerStateChangeEvent,
} from '@metamask/base-controller';
import { BaseController } from '@metamask/base-controller';
import type {
AnalyticsControllerGetStateAction,
AnalyticsControllerTrackEventAction,
} from '@metamask/analytics-controller';
import type { ConnectivityControllerGetStateAction } from '@metamask/connectivity-controller';
import type { Partialize } from '@metamask/controller-utils';
import {
Expand All @@ -22,6 +26,7 @@ import type { PollingBlockTrackerOptions } from '@metamask/eth-block-tracker';
import EthQuery from '@metamask/eth-query';
import type { Messenger } from '@metamask/messenger';
import {
generateDeterministicRandomNumber,
RemoteFeatureFlagControllerGetStateAction,
RemoteFeatureFlagControllerStateChangeEvent,
} from '@metamask/remote-feature-flag-controller';
Expand Down Expand Up @@ -55,6 +60,15 @@ import { createAutoManagedNetworkClient } from './create-auto-managed-network-cl
import type { DegradedEventType, RetryReason } from './create-network-client';
import { projectLogger, createModuleLogger } from './logger';
import type { NetworkControllerMethodActions } from './NetworkController-method-action-types';
import {
buildRpcServiceEventProperties,
toAnalyticsTrackingEvent,
} from './rpc-service-events';
import type {
NetworkControllerAnalyticsOptions,
RpcServiceEventName,
} from './rpc-service-events';
import { isConnectionError } from './rpc-service/rpc-service';
import type { RpcServiceOptionsWithDefaults } from './rpc-service/rpc-service';
import { getIsRpcFailoverEnabled } from './selectors';
import { NetworkClientType } from './types';
Expand Down Expand Up @@ -710,7 +724,9 @@ export type NetworkControllerActions =
*/
type AllowedActions =
| ConnectivityControllerGetStateAction
| RemoteFeatureFlagControllerGetStateAction;
| RemoteFeatureFlagControllerGetStateAction
| AnalyticsControllerGetStateAction
| AnalyticsControllerTrackEventAction;

export type NetworkControllerMessenger = Messenger<
typeof controllerName,
Expand Down Expand Up @@ -763,6 +779,15 @@ export type NetworkControllerOptions = {
getBlockTrackerOptions?: (
rpcEndpointUrl: string,
) => Omit<PollingBlockTrackerOptions, 'provider'>;
/**
* Optional configuration that, when provided, makes the controller emit
* "RPC Service Unavailable" and "RPC Service Degraded" analytics events via
* the `AnalyticsController:trackEvent` action whenever an RPC endpoint becomes
* unavailable or degraded. When omitted, no analytics are emitted and the
* `AnalyticsController` actions are never called. When provided, the messenger
* must allow `AnalyticsController:getState` and `AnalyticsController:trackEvent`.
*/
analytics?: NetworkControllerAnalyticsOptions;
};

/**
Expand Down Expand Up @@ -1268,6 +1293,8 @@ export class NetworkController extends BaseController<

readonly #getBlockTrackerOptions: NetworkControllerOptions['getBlockTrackerOptions'];

readonly #analytics: NetworkControllerAnalyticsOptions | undefined;

#networkConfigurationsByNetworkClientId: Map<
NetworkClientId,
NetworkConfiguration
Expand All @@ -1289,6 +1316,7 @@ export class NetworkController extends BaseController<
log,
getRpcServiceOptions,
getBlockTrackerOptions,
analytics,
} = options;
const initialState = {
...getDefaultNetworkControllerState(),
Expand Down Expand Up @@ -1332,6 +1360,7 @@ export class NetworkController extends BaseController<
this.#log = log;
this.#getRpcServiceOptions = getRpcServiceOptions;
this.#getBlockTrackerOptions = getBlockTrackerOptions;
this.#analytics = analytics;

this.#previouslySelectedNetworkClientId =
this.state.selectedNetworkClientId;
Expand Down Expand Up @@ -1370,6 +1399,44 @@ export class NetworkController extends BaseController<
},
);

if (this.#analytics) {
const analyticsOptions = this.#analytics;
this.messenger.subscribe(
`${this.name}:rpcEndpointUnavailable`,
({ chainId, endpointUrl, error }) => {
this.#trackRpcServiceEvent(analyticsOptions, 'RPC Service Unavailable', {
chainId,
endpointUrl,
error,
});
},
);
this.messenger.subscribe(
`${this.name}:rpcEndpointDegraded`,
({
chainId,
duration,
endpointUrl,
error,
retryReason,
rpcMethodName,
traceId,
type,
}) => {
this.#trackRpcServiceEvent(analyticsOptions, 'RPC Service Degraded', {
chainId,
duration,
endpointUrl,
error,
retryReason,
rpcMethodName,
traceId,
type,
});
},
);
}

this.messenger.subscribe(
// eslint-disable-next-line no-restricted-syntax
'RemoteFeatureFlagController:stateChange',
Expand All @@ -1380,6 +1447,80 @@ export class NetworkController extends BaseController<
);
}

/**
* Emits an "RPC Service Unavailable" or "RPC Service Degraded" analytics event
* via the `AnalyticsController:trackEvent` action.
*
* Does nothing when analytics are not configured, when the error indicates a
* local connectivity issue, when there is no analytics ID, or when the event
* falls outside the configured sample.
*
* @param analytics - The analytics configuration.
* @param name - The analytics event name.
* @param payload - The relevant fields from the originating event.
* @param payload.chainId - The chain ID that the endpoint represents.
* @param payload.endpointUrl - The URL of the endpoint.
* @param payload.error - The connection or response error encountered.
* @param payload.duration - The policy execution time in milliseconds (degraded only).
* @param payload.retryReason - The category of error that was retried (degraded only).
* @param payload.rpcMethodName - The JSON-RPC method being executed (degraded only).
* @param payload.traceId - The `X-Trace-Id` response header value (degraded only).
* @param payload.type - Why the endpoint became degraded (degraded only).
*/
#trackRpcServiceEvent(
analytics: NetworkControllerAnalyticsOptions,
name: RpcServiceEventName,
payload: {
chainId: Hex;
endpointUrl: string;
error: unknown;
duration?: number;
retryReason?: RetryReason;
rpcMethodName?: string;
traceId?: string;
type?: DegradedEventType;
},
): void {
try {
if (isConnectionError(payload.error)) {
return;
}

const { analyticsId } = this.messenger.call(
'AnalyticsController:getState',
);
if (!analyticsId) {
return;
}

if (
generateDeterministicRandomNumber(analyticsId) >=
analytics.rpcServiceEventsSampleRate
) {
return;
}

const properties = buildRpcServiceEventProperties({
chainId: payload.chainId,
endpointUrl: payload.endpointUrl,
error: payload.error,
isRpcEndpointUrlPublic: analytics.isRpcEndpointUrlPublic,
duration: payload.duration,
retryReason: payload.retryReason,
rpcMethodName: payload.rpcMethodName,
traceId: payload.traceId,
type: payload.type,
});

this.messenger.call(
'AnalyticsController:trackEvent',
toAnalyticsTrackingEvent(name, properties),
);
} catch (error) {
this.messenger.captureException?.(error as Error);
}
}

/**
* Returns the EthQuery instance for the currently selected network.
*
Expand Down
4 changes: 4 additions & 0 deletions packages/network-controller/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ export type { AbstractRpcService } from './rpc-service/abstract-rpc-service';
export type { RpcServiceRequestable } from './rpc-service/rpc-service-requestable';
export type { DegradedEventType, RetryReason } from './create-network-client';
export { classifyRetryReason } from './create-network-client';
export type {
NetworkControllerAnalyticsOptions,
RpcServiceEventName,
} from './rpc-service-events';
export { isConnectionError } from './rpc-service/rpc-service';
export type {
NetworkControllerGetEthQueryAction,
Expand Down
Loading
Loading