From f89760811a88bb229e6ca80e15708d3ede976c90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dafydd=20Ll=C5=B7r=20Pearson?= Date: Wed, 28 Feb 2024 11:42:13 +0000 Subject: [PATCH] feat: Setup airbrake logging --- api.planx.uk/airbrake.ts | 15 ---- api.planx.uk/errors/ServerError.ts | 22 +++++- api.planx.uk/errors/airbrake.ts | 20 +++++ api.planx.uk/errors/middleware.ts | 78 +++++++++++++++++++ api.planx.uk/modules/send/utils/helpers.ts | 31 ++------ .../webhooks/service/validateInput/utils.ts | 2 +- api.planx.uk/server.ts | 2 +- 7 files changed, 127 insertions(+), 43 deletions(-) delete mode 100644 api.planx.uk/airbrake.ts create mode 100644 api.planx.uk/errors/airbrake.ts create mode 100644 api.planx.uk/errors/middleware.ts diff --git a/api.planx.uk/airbrake.ts b/api.planx.uk/airbrake.ts deleted file mode 100644 index 2a4a3449f6..0000000000 --- a/api.planx.uk/airbrake.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Notifier } from "@airbrake/node"; -import { isLiveEnv } from "./helpers"; - -const airbrake = - isLiveEnv() && - process.env.AIRBRAKE_PROJECT_ID && - process.env.AIRBRAKE_PROJECT_KEY - ? new Notifier({ - projectId: Number(process.env.AIRBRAKE_PROJECT_ID), - projectKey: process.env.AIRBRAKE_PROJECT_KEY, - environment: process.env.NODE_ENV!, - }) - : undefined; - -export default airbrake; diff --git a/api.planx.uk/errors/ServerError.ts b/api.planx.uk/errors/ServerError.ts index b68b3b62b6..ccaa4be789 100644 --- a/api.planx.uk/errors/ServerError.ts +++ b/api.planx.uk/errors/ServerError.ts @@ -1,23 +1,39 @@ export class ServerError extends Error { + /** + * Message passed to user who triggered error + */ message: string; + /** + * HTTP status code to be returned to user + * @default 500 + */ status: number; + /** + * Original error, to be passed to Airbrake or logged in local dev + */ cause: unknown | undefined; + /** + * Context obejct passed to Airbrake + * Can hold any key-value data which may prove useful for debugging + */ + context: object | undefined; constructor({ message, status, cause, + context, }: { message: string; status?: number; cause?: unknown; + context?: object | undefined; }) { super(message); this.message = message; this.status = status || 500; - if (cause) { - this.cause = cause; - } + if (cause) this.cause = cause; + if (context) this.context = context; // Set the prototype explicitly. Object.setPrototypeOf(this, ServerError.prototype); diff --git a/api.planx.uk/errors/airbrake.ts b/api.planx.uk/errors/airbrake.ts new file mode 100644 index 0000000000..94a3ea8494 --- /dev/null +++ b/api.planx.uk/errors/airbrake.ts @@ -0,0 +1,20 @@ +import { Notifier } from "@airbrake/node"; +import { isLiveEnv } from "../helpers"; + +export const airbrake = + isLiveEnv() && + process.env.AIRBRAKE_PROJECT_ID && + process.env.AIRBRAKE_PROJECT_KEY + ? new Notifier({ + projectId: Number(process.env.AIRBRAKE_PROJECT_ID), + projectKey: process.env.AIRBRAKE_PROJECT_KEY, + environment: process.env.NODE_ENV!, + }) + : undefined; + +/** + * Simple helper function to manually report an error to Airbrake + * To be used when you do not want to throw an error and halt execution + */ +export const reportError = (report: { error: any; context: object }) => + airbrake ? airbrake.notify(report) : console.log(report); diff --git a/api.planx.uk/errors/middleware.ts b/api.planx.uk/errors/middleware.ts new file mode 100644 index 0000000000..71bf58690e --- /dev/null +++ b/api.planx.uk/errors/middleware.ts @@ -0,0 +1,78 @@ +import { ErrorRequestHandler, RequestHandler } from "express"; +import { ServerError } from "./ServerError"; +import { airbrake } from "./airbrake"; +import airbrakeExpress from "@airbrake/node/dist/instrumentation/express"; + +/** + * Sets up Airbrake metrics recording + */ +export const airbrakeMiddleware: RequestHandler = (_req, _res, next) => { + if (!airbrake) return next(); + + return airbrakeExpress.makeMiddleware(airbrake); +}; + +/** + * Log errors to Airbrake + */ +export const errorLogger: ErrorRequestHandler = ( + errorObject, + req, + _res, + next, +) => { + if (!airbrake) { + console.log(errorObject); + return next(errorObject); + } + + // Default Airbrake notice for all errors + // See https://github.com/airbrake/airbrake-js/blob/master/packages/node/src/instrumentation/express.ts + const notice = { + error: errorObject, + context: { + userAddr: req.ip, + userAgent: req.headers["user-agent"], + url: req.protocol + "://" + req.headers.host + req.originalUrl, + httpMethod: req.method, + component: "express", + route: req.route?.path?.toString(), + action: req.route?.stack?.[0]?.name, + referer: req.headers?.referer, + message: "Something went wrong", + }, + }; + + // Append additional information for explicitly caught errors + if (errorObject instanceof ServerError) { + notice.context = { + ...notice.context, + // ...errorObject.context, + message: errorObject.message, + }; + + // Log original error if provided + if (errorObject.cause) notice.error = errorObject.cause; + } + + // Send notice to Airbrake + airbrake.notify(notice); + + return next({ + ...errorObject, + message: errorObject.message.concat(", this error has been logged"), + }); +}; + +export const errorResponder: ErrorRequestHandler = ( + errorObject, + _req, + res, + _next, +) => { + const { status = 500, message = "Something went wrong" } = errorObject; + + res.status(status).send({ + error: message, + }); +}; diff --git a/api.planx.uk/modules/send/utils/helpers.ts b/api.planx.uk/modules/send/utils/helpers.ts index 06db45b327..30c6474ce8 100644 --- a/api.planx.uk/modules/send/utils/helpers.ts +++ b/api.planx.uk/modules/send/utils/helpers.ts @@ -1,6 +1,6 @@ import { gql } from "graphql-request"; -import airbrake from "../../../airbrake"; import { $api } from "../../../client"; +import { ServerError } from "../../../errors"; export async function logPaymentStatus({ sessionId, @@ -20,9 +20,10 @@ export async function logPaymentStatus({ }; }): Promise { if (!flowId || !sessionId) { - reportError({ - error: "Could not log the payment status due to missing context value(s)", - context: { sessionId, flowId, teamSlug }, + throw new ServerError({ + message: + "Could not log the payment status due to missing context value(s)", + // context: { sessionId, flowId, teamSlug }, }); } else { // log payment status response @@ -36,30 +37,14 @@ export async function logPaymentStatus({ amount: govUkResponse.amount, }); } catch (e) { - reportError({ - error: `Failed to insert a payment status: ${e}`, - context: { govUkResponse }, + throw new ServerError({ + message: `Failed to insert a payment status: ${e}`, + // context: { govUkResponse }, }); } } } -// tmp explicit error handling -export function reportError(report: { error: any; context: object }) { - if (airbrake) { - airbrake.notify(report); - return; - } - log(report); -} - -// tmp logger -function log(event: object | string) { - if (!process.env.SUPPRESS_LOGS) { - console.log(event); - } -} - // TODO: this would ideally live in planx-client async function insertPaymentStatus({ flowId, diff --git a/api.planx.uk/modules/webhooks/service/validateInput/utils.ts b/api.planx.uk/modules/webhooks/service/validateInput/utils.ts index 3f8b772598..78e0124d3b 100644 --- a/api.planx.uk/modules/webhooks/service/validateInput/utils.ts +++ b/api.planx.uk/modules/webhooks/service/validateInput/utils.ts @@ -1,8 +1,8 @@ import { isObject } from "lodash"; import { JSDOM } from "jsdom"; import createDOMPurify from "dompurify"; -import { reportError } from "../../../send/utils/helpers"; import { decode } from "he"; +import { reportError } from "../../../../errors/airbrake"; // Setup JSDOM and DOMPurify const window = new JSDOM("").window; diff --git a/api.planx.uk/server.ts b/api.planx.uk/server.ts index 649e615984..6a23ce999a 100644 --- a/api.planx.uk/server.ts +++ b/api.planx.uk/server.ts @@ -12,7 +12,7 @@ import { Server } from "http"; import passport from "passport"; import helmet from "helmet"; import { ServerError } from "./errors"; -import airbrake from "./airbrake"; +import airbrake from "./errors/airbrake"; import { apiLimiter } from "./rateLimit"; import { googleStrategy } from "./modules/auth/strategy/google"; import authRoutes from "./modules/auth/routes";