Skip to content

Commit 18a2ce0

Browse files
authored
feat: captureException with claims messenger (#7553)
## Explanation <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> This PR uses `captureException` function from the base-messenger to capture the claims errors and report to sentry (from the extension). ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs) - [x] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Add Sentry-backed error reporting and robust HTTP error handling to claims service** > > - Wrap `fetchClaimsConfigurations`, `getClaims`, `getClaimById`, and `generateMessageForClaimSignature` in try/catch; on failures call `messenger.captureException(createSentryError(...))` and rethrow user-facing errors. > - Use new `utils.getErrorFromResponse` to normalize non-OK HTTP responses (JSON/text/unknown) and `createSentryError` to attach original causes; add comprehensive unit tests for both utilities. > - Update service tests to inject `captureException` via mocked messenger, assert it’s called on failures, and verify error messages (including status codes); add specific tests for rejected fetch and 500 responses. > - Extend test messenger to support `captureException`; update CHANGELOG with new behavior. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit cd6eae6. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 58afc71 commit 18a2ce0

File tree

6 files changed

+305
-51
lines changed

6 files changed

+305
-51
lines changed

packages/claims-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Capture claims error and report to sentry using `Messenger.captureException` method from `@metamask/messenger`. ([#7553](https://github.com/MetaMask/core/pull/7553))
13+
1014
### Changed
1115

1216
- Upgrade `@metamask/utils` from `^11.8.1` to `^11.9.0` ([#7511](https://github.com/MetaMask/core/pull/7511))

packages/claims-controller/src/ClaimsService.test.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,22 @@ import type {
1010
ClaimsConfigurationsResponse,
1111
GenerateSignatureMessageResponse,
1212
} from './types';
13+
import { createSentryError } from './utils';
1314
import { createMockClaimsServiceMessenger } from '../tests/mocks/messenger';
1415

1516
const mockAuthenticationControllerGetBearerToken = jest.fn();
1617
const mockFetchFunction = jest.fn();
17-
18+
const mockCaptureException = jest.fn();
1819
/**
1920
* Create a mock claims service.
2021
*
2122
* @param env - The environment to use for the mock claims service. Defaults to Env.DEV.
22-
* @returns A mock claims service.
23+
* @returns A mock claims service and its messenger.
2324
*/
2425
function createMockClaimsService(env: Env = Env.DEV): ClaimsService {
2526
const { messenger } = createMockClaimsServiceMessenger(
2627
mockAuthenticationControllerGetBearerToken,
28+
mockCaptureException,
2729
);
2830
return new ClaimsService({
2931
env,
@@ -68,7 +70,10 @@ describe('ClaimsService', () => {
6870
});
6971

7072
it('should create instance with valid config', () => {
71-
const { messenger } = createMockClaimsServiceMessenger(jest.fn());
73+
const { messenger } = createMockClaimsServiceMessenger(
74+
jest.fn(),
75+
jest.fn(),
76+
);
7277
const service = new ClaimsService({
7378
env: Env.DEV,
7479
messenger,
@@ -231,6 +236,25 @@ describe('ClaimsService', () => {
231236
ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID,
232237
);
233238
});
239+
240+
it('should handle fetch error and capture exception', async () => {
241+
mockFetchFunction.mockRestore();
242+
243+
mockFetchFunction.mockRejectedValueOnce(new Error('Fetch error'));
244+
245+
const service = createMockClaimsService();
246+
247+
await expect(service.getClaimById('1')).rejects.toThrow(
248+
ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID,
249+
);
250+
251+
expect(mockCaptureException).toHaveBeenCalledWith(
252+
createSentryError(
253+
ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID,
254+
new Error('Fetch error'),
255+
),
256+
);
257+
});
234258
});
235259

236260
describe('generateMessageForClaimSignature', () => {
@@ -289,6 +313,7 @@ describe('ClaimsService', () => {
289313

290314
mockFetchFunction.mockResolvedValueOnce({
291315
ok: false,
316+
status: 500,
292317
json: jest.fn().mockResolvedValueOnce(null),
293318
});
294319

@@ -299,6 +324,13 @@ describe('ClaimsService', () => {
299324
).rejects.toThrow(
300325
ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED,
301326
);
327+
328+
expect(mockCaptureException).toHaveBeenCalledWith(
329+
createSentryError(
330+
ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED,
331+
new Error('error: Unknown error, statusCode: 500'),
332+
),
333+
);
302334
});
303335
});
304336
});

packages/claims-controller/src/ClaimsService.ts

Lines changed: 97 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
ClaimsConfigurationsResponse,
1414
GenerateSignatureMessageResponse,
1515
} from './types';
16+
import { createSentryError, getErrorFromResponse } from './utils';
1617

1718
export type ClaimsServiceFetchClaimsConfigurationsAction = {
1819
type: `${typeof SERVICE_NAME}:fetchClaimsConfigurations`;
@@ -114,20 +115,32 @@ export class ClaimsService {
114115
* @returns The required configurations for the claims service.
115116
*/
116117
async fetchClaimsConfigurations(): Promise<ClaimsConfigurationsResponse> {
117-
const headers = await this.getRequestHeaders();
118-
const url = `${this.getClaimsApiUrl()}/configurations`;
119-
const response = await this.#fetch(url, {
120-
headers,
121-
});
122-
123-
if (!response.ok) {
118+
try {
119+
const headers = await this.getRequestHeaders();
120+
const url = `${this.getClaimsApiUrl()}/configurations`;
121+
const response = await this.#fetch(url, {
122+
headers,
123+
});
124+
125+
if (!response.ok) {
126+
const error = await getErrorFromResponse(response);
127+
throw error;
128+
}
129+
130+
const configurations = await response.json();
131+
return configurations;
132+
} catch (error) {
133+
console.error('fetchClaimsConfigurations', error);
134+
this.#messenger.captureException?.(
135+
createSentryError(
136+
ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS,
137+
error as Error,
138+
),
139+
);
124140
throw new Error(
125141
ClaimsServiceErrorMessages.FAILED_TO_FETCH_CONFIGURATIONS,
126142
);
127143
}
128-
129-
const configurations = await response.json();
130-
return configurations;
131144
}
132145

133146
/**
@@ -136,18 +149,30 @@ export class ClaimsService {
136149
* @returns The claims for the current user.
137150
*/
138151
async getClaims(): Promise<Claim[]> {
139-
const headers = await this.getRequestHeaders();
140-
const url = `${this.getClaimsApiUrl()}/claims`;
141-
const response = await this.#fetch(url, {
142-
headers,
143-
});
144-
145-
if (!response.ok) {
152+
try {
153+
const headers = await this.getRequestHeaders();
154+
const url = `${this.getClaimsApiUrl()}/claims`;
155+
const response = await this.#fetch(url, {
156+
headers,
157+
});
158+
159+
if (!response.ok) {
160+
const error = await getErrorFromResponse(response);
161+
throw error;
162+
}
163+
164+
const claims = await response.json();
165+
return claims;
166+
} catch (error) {
167+
console.error('getClaims', error);
168+
this.#messenger.captureException?.(
169+
createSentryError(
170+
ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS,
171+
error as Error,
172+
),
173+
);
146174
throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIMS);
147175
}
148-
149-
const claims = await response.json();
150-
return claims;
151176
}
152177

153178
/**
@@ -157,18 +182,30 @@ export class ClaimsService {
157182
* @returns The claim by id.
158183
*/
159184
async getClaimById(id: string): Promise<Claim> {
160-
const headers = await this.getRequestHeaders();
161-
const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`;
162-
const response = await this.#fetch(url, {
163-
headers,
164-
});
165-
166-
if (!response.ok) {
185+
try {
186+
const headers = await this.getRequestHeaders();
187+
const url = `${this.getClaimsApiUrl()}/claims/byId/${id}`;
188+
const response = await this.#fetch(url, {
189+
headers,
190+
});
191+
192+
if (!response.ok) {
193+
const error = await getErrorFromResponse(response);
194+
throw error;
195+
}
196+
197+
const claim = await response.json();
198+
return claim;
199+
} catch (error) {
200+
console.error('getClaimById', error);
201+
this.#messenger.captureException?.(
202+
createSentryError(
203+
ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID,
204+
error as Error,
205+
),
206+
);
167207
throw new Error(ClaimsServiceErrorMessages.FAILED_TO_GET_CLAIM_BY_ID);
168208
}
169-
170-
const claim = await response.json();
171-
return claim;
172209
}
173210

174211
/**
@@ -182,28 +219,40 @@ export class ClaimsService {
182219
chainId: number,
183220
walletAddress: Hex,
184221
): Promise<GenerateSignatureMessageResponse> {
185-
const headers = await this.getRequestHeaders();
186-
const url = `${this.getClaimsApiUrl()}/signature/generateMessage`;
187-
const response = await this.#fetch(url, {
188-
method: 'POST',
189-
headers: {
190-
...headers,
191-
'Content-Type': 'application/json',
192-
},
193-
body: JSON.stringify({
194-
chainId,
195-
walletAddress,
196-
}),
197-
});
198-
199-
if (!response.ok) {
222+
try {
223+
const headers = await this.getRequestHeaders();
224+
const url = `${this.getClaimsApiUrl()}/signature/generateMessage`;
225+
const response = await this.#fetch(url, {
226+
method: 'POST',
227+
headers: {
228+
...headers,
229+
'Content-Type': 'application/json',
230+
},
231+
body: JSON.stringify({
232+
chainId,
233+
walletAddress,
234+
}),
235+
});
236+
237+
if (!response.ok) {
238+
const error = await getErrorFromResponse(response);
239+
throw error;
240+
}
241+
242+
const message = await response.json();
243+
return message;
244+
} catch (error) {
245+
console.error('generateMessageForClaimSignature', error);
246+
this.#messenger.captureException?.(
247+
createSentryError(
248+
ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED,
249+
error as Error,
250+
),
251+
);
200252
throw new Error(
201253
ClaimsServiceErrorMessages.SIGNATURE_MESSAGE_GENERATION_FAILED,
202254
);
203255
}
204-
205-
const message = await response.json();
206-
return message;
207256
}
208257

209258
/**

0 commit comments

Comments
 (0)