feat(payments): add sentry reporting to apiClient

Because:

- Unknown errors increased a few months ago, without any logging or
  reporting to Sentry.

This commit:

- For unknown API errors report an issue to Sentry.
- Update fxa-profile-server to allow additional Sentry headers, similar
  to auth-server

Closes #FXA-10157
This commit is contained in:
Reino Muhl 2024-08-05 12:38:30 -04:00
Родитель 895b0eba2e
Коммит d89aae2caa
Не найден ключ, соответствующий данной подписи
3 изменённых файлов: 83 добавлений и 20 удалений

Просмотреть файл

@ -20,6 +20,7 @@ import {
updateSubscriptionPlan_FULFILLED,
updateSubscriptionPlan_PENDING,
updateSubscriptionPlan_REJECTED,
getErrorId,
} from './amplitude';
import {
apiCancelSubscription,
@ -50,6 +51,7 @@ import {
updateAPIClientConfig,
updateAPIClientToken,
} from './apiClient';
import sentryMetrics from './sentry';
import { Config, defaultConfig } from './config';
import { PaymentProvider } from './PaymentProvider';
import {
@ -146,6 +148,54 @@ describe('Authorization header', () => {
});
});
describe('apiFetch error', () => {
let config: Config;
beforeEach(() => {
config = defaultConfig();
config.servers.profile.url = PROFILE_BASE_URL;
updateAPIClientConfig(config);
mockOptionsResponses(PROFILE_BASE_URL);
});
afterEach(() => {
noc.cleanAll();
});
it('throw APIError', async () => {
let error: any;
(getErrorId as jest.Mock).mockReturnValue('error-id-123');
nock(PROFILE_BASE_URL).get('/v1/profile').reply(500, {
code: '999',
statusCode: 500,
errno: 999,
error: 'boofed it',
message: 'alert: boofed it',
});
try {
await apiFetchProfile();
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(APIError);
expect(error.errno).toBe(999);
expect(sentryMetrics.captureException as jest.Mock).not.toBeCalled();
});
it('should throw and report to Sentry for unknown_error', async () => {
let error: any;
(getErrorId as jest.Mock).mockReturnValue('unknown_error');
nock(PROFILE_BASE_URL).get('/v1/profile').reply(500, {});
try {
await apiFetchProfile();
} catch (err) {
error = err;
}
expect(error).toBeInstanceOf(APIError);
expect(sentryMetrics.captureException as jest.Mock).toBeCalledWith(error);
});
});
describe('API requests', () => {
let config: Config;

Просмотреть файл

@ -17,6 +17,7 @@ import {
} from 'fxa-shared/dto/auth/payments/invoice';
import { CouponDetails } from 'fxa-shared/dto/auth/payments/coupon';
import { CheckoutType } from 'fxa-shared/subscriptions/types';
import sentryMetrics from './sentry';
// TODO: Use a better type here
export interface APIFetchOptions {
@ -62,6 +63,7 @@ export class APIError extends Error {
...params: Array<any>
) {
super(...params);
this.name = 'APIError';
this.response = response;
this.body = body || null;
this.code = code || null;
@ -92,28 +94,38 @@ async function apiFetch(
path: string,
options: APIFetchOptions = {}
) {
const response = await fetch(path, {
mode: 'cors',
credentials: 'omit',
method,
...options,
headers: {
'Content-Type': 'application/json',
Authorization: accessToken ? `Bearer ${accessToken}` : undefined,
...(options.headers || {}),
},
});
if (response.status >= 400) {
let body = {};
try {
// Parse the body as JSON, but will fail if things have really gone wrong
body = await response.json();
} catch (_) {
// No-op
try {
const response = await fetch(path, {
mode: 'cors',
credentials: 'omit',
method,
...options,
headers: {
'Content-Type': 'application/json',
Authorization: accessToken ? `Bearer ${accessToken}` : undefined,
...(options.headers || {}),
},
});
if (response.status >= 400) {
let body = {};
try {
// Parse the body as JSON, but will fail if things have really gone wrong
body = await response.json();
} catch (_) {
// No-op
}
throw new APIError(body, response);
}
throw new APIError(body, response);
return response.json();
} catch (error) {
const errorId = Amplitude.getErrorId(error);
// Only capture unknown errors to Sentry regardless of if its a 4XX or 5XX
if (errorId === 'unknown_error') {
sentryMetrics.captureException(error);
}
throw error;
}
return response.json();
}
export async function apiFetchAccountStatus(

Просмотреть файл

@ -59,6 +59,7 @@ exports.create = async function createServer() {
routes: {
cors: {
additionalExposedHeaders: ['Timestamp', 'Accept-Language'],
additionalHeaders: ['sentry-trace', 'baggage'],
// If we're accepting CORS from any origin then use Hapi's "ignore" mode,
// which is more forgiving of missing Origin header.
origin: ['*'],