Skip to content

Commit 0e40a5b

Browse files
authored
Add @metamask/error-reporting-service (#5849)
This package makes it possible for any module to report an error to an error reporting app (such as Sentry). The exact mechanism to do so is customizable, and the service object exposes a messenger action so that modules can report the error without needing direct access to the service object itself.
1 parent 1f9c597 commit 0e40a5b

17 files changed

+644
-4
lines changed

.github/CODEOWNERS

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@
6363
/packages/build-utils @MetaMask/wallet-framework-engineers
6464
/packages/composable-controller @MetaMask/wallet-framework-engineers
6565
/packages/controller-utils @MetaMask/wallet-framework-engineers
66-
/packages/sample-controllers @MetaMask/wallet-framework-engineers
66+
/packages/error-reporting-service @MetaMask/wallet-framework-engineers
67+
/packages/sample-controllers @MetaMask/wallet-framework-engineers
6768
/packages/polling-controller @MetaMask/wallet-framework-engineers
6869
/packages/preferences-controller @MetaMask/wallet-framework-engineers
6970

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,11 @@ Each package in this repository has its own README where you can find installati
3333
- [`@metamask/chain-agnostic-permission`](packages/chain-agnostic-permission)
3434
- [`@metamask/composable-controller`](packages/composable-controller)
3535
- [`@metamask/controller-utils`](packages/controller-utils)
36+
- [`@metamask/delegation-controller`](packages/delegation-controller)
3637
- [`@metamask/earn-controller`](packages/earn-controller)
3738
- [`@metamask/eip1193-permission-middleware`](packages/eip1193-permission-middleware)
3839
- [`@metamask/ens-controller`](packages/ens-controller)
40+
- [`@metamask/error-reporting-service`](packages/error-reporting-service)
3941
- [`@metamask/eth-json-rpc-provider`](packages/eth-json-rpc-provider)
4042
- [`@metamask/gas-fee-controller`](packages/gas-fee-controller)
4143
- [`@metamask/json-rpc-engine`](packages/json-rpc-engine)
@@ -87,9 +89,11 @@ linkStyle default opacity:0.5
8789
chain_agnostic_permission(["@metamask/chain-agnostic-permission"]);
8890
composable_controller(["@metamask/composable-controller"]);
8991
controller_utils(["@metamask/controller-utils"]);
92+
delegation_controller(["@metamask/delegation-controller"]);
9093
earn_controller(["@metamask/earn-controller"]);
9194
eip1193_permission_middleware(["@metamask/eip1193-permission-middleware"]);
9295
ens_controller(["@metamask/ens-controller"]);
96+
error_reporting_service(["@metamask/error-reporting-service"]);
9397
eth_json_rpc_provider(["@metamask/eth-json-rpc-provider"]);
9498
gas_fee_controller(["@metamask/gas-fee-controller"]);
9599
json_rpc_engine(["@metamask/json-rpc-engine"]);
@@ -136,30 +140,42 @@ linkStyle default opacity:0.5
136140
assets_controllers --> network_controller;
137141
assets_controllers --> permission_controller;
138142
assets_controllers --> preferences_controller;
143+
assets_controllers --> transaction_controller;
139144
base_controller --> json_rpc_engine;
140145
bridge_controller --> base_controller;
141146
bridge_controller --> controller_utils;
147+
bridge_controller --> gas_fee_controller;
148+
bridge_controller --> multichain_network_controller;
142149
bridge_controller --> polling_controller;
143150
bridge_controller --> accounts_controller;
151+
bridge_controller --> assets_controllers;
144152
bridge_controller --> eth_json_rpc_provider;
145153
bridge_controller --> network_controller;
154+
bridge_controller --> remote_feature_flag_controller;
146155
bridge_controller --> transaction_controller;
147156
bridge_status_controller --> base_controller;
148-
bridge_status_controller --> bridge_controller;
149157
bridge_status_controller --> controller_utils;
150158
bridge_status_controller --> polling_controller;
159+
bridge_status_controller --> user_operation_controller;
151160
bridge_status_controller --> accounts_controller;
161+
bridge_status_controller --> bridge_controller;
162+
bridge_status_controller --> gas_fee_controller;
163+
bridge_status_controller --> multichain_transactions_controller;
152164
bridge_status_controller --> network_controller;
153165
bridge_status_controller --> transaction_controller;
154166
chain_agnostic_permission --> controller_utils;
155167
chain_agnostic_permission --> network_controller;
156168
chain_agnostic_permission --> permission_controller;
157169
composable_controller --> base_controller;
158170
composable_controller --> json_rpc_engine;
171+
delegation_controller --> base_controller;
172+
delegation_controller --> accounts_controller;
173+
delegation_controller --> keyring_controller;
159174
earn_controller --> base_controller;
160175
earn_controller --> controller_utils;
161176
earn_controller --> accounts_controller;
162177
earn_controller --> network_controller;
178+
earn_controller --> transaction_controller;
163179
eip1193_permission_middleware --> chain_agnostic_permission;
164180
eip1193_permission_middleware --> controller_utils;
165181
eip1193_permission_middleware --> json_rpc_engine;
@@ -183,10 +199,13 @@ linkStyle default opacity:0.5
183199
multichain --> network_controller;
184200
multichain --> permission_controller;
185201
multichain_api_middleware --> chain_agnostic_permission;
202+
multichain_api_middleware --> controller_utils;
186203
multichain_api_middleware --> json_rpc_engine;
187204
multichain_api_middleware --> network_controller;
188205
multichain_api_middleware --> permission_controller;
206+
multichain_api_middleware --> multichain_transactions_controller;
189207
multichain_network_controller --> base_controller;
208+
multichain_network_controller --> controller_utils;
190209
multichain_network_controller --> accounts_controller;
191210
multichain_network_controller --> keyring_controller;
192211
multichain_network_controller --> network_controller;
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
### Added
11+
12+
- Initial release ([#5849](https://github.com/MetaMask/core/pull/5849))
13+
14+
[Unreleased]: https://github.com/MetaMask/core/
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
MIT License
2+
3+
Copyright (c) 2025 MetaMask
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# `@metamask/error-reporting-service`
2+
3+
Reports errors to an external app such as Sentry but in an agnostic fashion.
4+
5+
## Installation
6+
7+
`yarn add @metamask/error-reporting-service`
8+
9+
or
10+
11+
`npm install @metamask/error-reporting-service`
12+
13+
## Usage
14+
15+
This package is designed to be used in another module via a messenger, but can also be used on its own if needed.
16+
17+
### Using the service via a messenger
18+
19+
In most cases, you will want to use the error reporting service in your module via a messenger object.
20+
21+
In this example, we have a controller, and something bad happens, but we want to report an error instead of throwing it.
22+
23+
#### 1. Controller file
24+
25+
```typescript
26+
// We need to get the type for the `ErrorReportingService:captureException`
27+
// action.
28+
import type { ErrorReportingServiceCaptureExceptionAction } from '@metamask/error-reporting-service';
29+
30+
// Now let's set up our controller, starting with the messenger.
31+
// Note that we grant the `ErrorReportingService:captureException` action to the
32+
// messenger.
33+
type AllowedActions = ErrorReportingServiceCaptureExceptionAction;
34+
type ExampleControllerMessenger = RestrictedMessenger<
35+
'ExampleController',
36+
AllowedActions,
37+
never,
38+
AllowedActions['type'],
39+
never
40+
>;
41+
42+
// Finally, we define our controller.
43+
class ExampleController extends BaseController<
44+
'ExampleController',
45+
ExampleControllerState,
46+
ExampleControllerMessenger
47+
> {
48+
doSomething() {
49+
// Now imagine that we do something that produces an error and we want to
50+
// report the error.
51+
this.messagingSystem.call(
52+
'ErrorReportingService:captureException',
53+
new Error('Something went wrong'),
54+
);
55+
}
56+
}
57+
```
58+
59+
#### 2A. Initialization file (browser)
60+
61+
```typescript
62+
// We need a version of `captureException` from somewhere. Here, we are getting
63+
// it from `@sentry/browser`.
64+
import { captureException } from '@sentry/browser';
65+
66+
// We also need to get the ErrorReportingService.
67+
import { ErrorReportingService } from '@metamask/error-reporting-service';
68+
69+
// And we need our controller.
70+
import { ExampleController } from './example-controller';
71+
72+
// We need to have a global messenger.
73+
const globalMessenger = new Messenger();
74+
75+
// We need to create a restricted messenger for the ErrorReportingService, and
76+
// then we can create the service itself.
77+
const errorReportingServiceMessenger = globalMessenger.getRestricted({
78+
allowedActions: [],
79+
allowedEvents: [],
80+
});
81+
const errorReportingService = new ErrorReportingService({
82+
messenger: errorReportingServiceMessenger,
83+
captureException,
84+
});
85+
86+
// Now we can create a restricted messenger for our controller, and then
87+
// we can create the controller too.
88+
// Note that we grant the `ErrorReportingService:captureException` action to the
89+
// messenger.
90+
const exampleControllerMessenger = globalMessenger.getRestricted({
91+
allowedActions: ['ErrorReportingService:captureException'],
92+
allowedEvents: [],
93+
});
94+
const exampleController = new ExampleController({
95+
messenger: exampleControllerMessenger,
96+
});
97+
```
98+
99+
#### 2B. Initialization file (React Native)
100+
101+
```typescript
102+
// We need a version of `captureException` from somewhere. Here, we are getting
103+
// it from `@sentry/react-native`.
104+
import { captureException } from '@sentry/react-native';
105+
106+
// We also need to get the ErrorReportingService.
107+
import { ErrorReportingService } from '@metamask/error-reporting-service';
108+
109+
// And we need our controller.
110+
import { ExampleController } from './example-controller';
111+
112+
// We need to have a global messenger.
113+
const globalMessenger = new Messenger();
114+
115+
// We need to create a restricted messenger for the ErrorReportingService, and
116+
// then we can create the service itself.
117+
const errorReportingServiceMessenger = globalMessenger.getRestricted({
118+
allowedActions: [],
119+
allowedEvents: [],
120+
});
121+
const errorReportingService = new ErrorReportingService({
122+
messenger: errorReportingServiceMessenger,
123+
captureException,
124+
});
125+
126+
// Now we can create a restricted messenger for our controller, and then
127+
// we can create the controller too.
128+
// Note that we grant the `ErrorReportingService:captureException` action to the
129+
// messenger.
130+
const exampleControllerMessenger = globalMessenger.getRestricted({
131+
allowedActions: ['ErrorReportingService:captureException'],
132+
allowedEvents: [],
133+
});
134+
const exampleController = new ExampleController({
135+
messenger: exampleControllerMessenger,
136+
});
137+
```
138+
139+
#### 3. Using the controller
140+
141+
```typescript
142+
// Now this will report an error without throwing it.
143+
exampleController.doSomething();
144+
```
145+
146+
### Using the service directly
147+
148+
You probably don't need to use the service directly, but if you do, here's how.
149+
150+
In this example, we have a function, and we use the error reporting service there.
151+
152+
#### 1. Function file
153+
154+
```typescript
155+
export function doSomething(
156+
errorReportingService: AbstractErrorReportingService,
157+
) {
158+
errorReportingService.captureException(new Error('Something went wrong'));
159+
}
160+
```
161+
162+
#### 2A. Calling file (browser)
163+
164+
```typescript
165+
// We need a version of `captureException` from somewhere. Here, we are getting
166+
it from `@sentry/browser`.
167+
import { captureException } from '@sentry/browser';
168+
169+
// We also need to get the ErrorReportingService.
170+
import { ErrorReportingService } from '@metamask/error-reporting-service';
171+
172+
// We also bring in our function.
173+
import { doSomething } from './do-something';
174+
175+
// We create a new instance of the ErrorReportingService.
176+
const errorReportingService = new ErrorReportingService({ captureException });
177+
178+
// Now we call our function, and it will report the error in Sentry.
179+
doSomething(errorReportingService);
180+
```
181+
182+
#### 2A. Calling file (React Native)
183+
184+
```typescript
185+
// We need a version of `captureException` from somewhere. Here, we are getting
186+
it from `@sentry/react-native`.
187+
import { captureException } from '@sentry/react-native';
188+
189+
// We also need to get the ErrorReportingService.
190+
import { ErrorReportingService } from '@metamask/error-reporting-service';
191+
192+
// We also bring in our function.
193+
import { doSomething } from './do-something';
194+
195+
// We create a new instance of the ErrorReportingService.
196+
const errorReportingService = new ErrorReportingService({ captureException });
197+
198+
// Now we call our function, and it will report the error in Sentry.
199+
doSomething(errorReportingService);
200+
```
201+
202+
## Contributing
203+
204+
This package is part of a monorepo. Instructions for contributing can be found in the [monorepo README](https://github.com/MetaMask/core#readme).
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/*
2+
* For a detailed explanation regarding each configuration property and type check, visit:
3+
* https://jestjs.io/docs/configuration
4+
*/
5+
6+
const merge = require('deepmerge');
7+
const path = require('path');
8+
9+
const baseConfig = require('../../jest.config.packages');
10+
11+
const displayName = path.basename(__dirname);
12+
13+
module.exports = merge(baseConfig, {
14+
// The display name when running multiple projects
15+
displayName,
16+
17+
// An object that configures minimum threshold enforcement for coverage results
18+
coverageThreshold: {
19+
global: {
20+
branches: 100,
21+
functions: 100,
22+
lines: 100,
23+
statements: 100,
24+
},
25+
},
26+
});

0 commit comments

Comments
 (0)