зеркало из https://github.com/mozilla/fxa.git
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:
Родитель
895b0eba2e
Коммит
d89aae2caa
|
@ -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: ['*'],
|
||||
|
|
Загрузка…
Ссылка в новой задаче