report errors to sentry
This commit is contained in:
106
server/index.ts
Normal file
106
server/index.ts
Normal file
@ -0,0 +1,106 @@
|
||||
import express from "express";
|
||||
import compression from "compression";
|
||||
import morgan from "morgan";
|
||||
import { createRequestHandler } from "@remix-run/express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
|
||||
import config from "~/config/config.server";
|
||||
import logger from "~/utils/logger.server";
|
||||
import { adminMiddleware, setupBullBoard } from "./queues";
|
||||
import { registerSentry, sentryLoadContext } from "./sentry-remix";
|
||||
|
||||
Sentry.init({
|
||||
dsn: config.sentry.dsn,
|
||||
integrations: [new Sentry.Integrations.Http({ tracing: true })],
|
||||
tracesSampleRate: 1.0,
|
||||
environment: process.env.NODE_ENV,
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.use((req, res, next) => {
|
||||
res.set("X-Fly-Region", process.env.FLY_REGION ?? "unknown");
|
||||
res.set("Strict-Transport-Security", `max-age=31536000; preload`);
|
||||
next();
|
||||
});
|
||||
|
||||
// replay non-GET/HEAD/OPTIONS requests to the primary Fly.io region rather than read-only Postgres instances
|
||||
// learn more: https://fly.io/docs/getting-started/multi-region-databases/#replay-the-request
|
||||
app.all("*", (req, res, next) => {
|
||||
const { method, path: pathname } = req;
|
||||
const { PRIMARY_REGION, FLY_REGION } = process.env;
|
||||
const isMethodReplayable = !["GET", "OPTIONS", "HEAD"].includes(method);
|
||||
const isReadOnlyRegion = FLY_REGION && PRIMARY_REGION && FLY_REGION !== PRIMARY_REGION;
|
||||
const shouldReplay = isMethodReplayable && isReadOnlyRegion;
|
||||
|
||||
if (!shouldReplay) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const logInfo = {
|
||||
pathname,
|
||||
method,
|
||||
PRIMARY_REGION,
|
||||
FLY_REGION,
|
||||
};
|
||||
logger.info("Replaying:", logInfo);
|
||||
res.set("fly-replay", `region=${PRIMARY_REGION}`);
|
||||
return res.sendStatus(409);
|
||||
});
|
||||
|
||||
app.disable("x-powered-by");
|
||||
app.use(
|
||||
compression({
|
||||
filter(req, res) {
|
||||
const contentTypeHeader = res.getHeader("Content-Type");
|
||||
let contentType = "";
|
||||
if (contentTypeHeader) {
|
||||
if (Array.isArray(contentTypeHeader)) {
|
||||
contentType = contentTypeHeader.join(" ");
|
||||
} else {
|
||||
contentType = String(contentTypeHeader);
|
||||
}
|
||||
}
|
||||
|
||||
if (contentType.includes("text/event-stream")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// cache static and immutable assets
|
||||
app.use(express.static("public", { immutable: true, maxAge: "1y" }));
|
||||
|
||||
// setup background queues and cron jobs
|
||||
app.use("/admin", adminMiddleware);
|
||||
app.use("/admin/queues", setupBullBoard().getRouter());
|
||||
|
||||
app.use(morgan("tiny"));
|
||||
|
||||
app.all("*", (req, res, next) => {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
purgeRequireCache();
|
||||
}
|
||||
|
||||
return createRequestHandler({
|
||||
build: registerSentry(require("../build")),
|
||||
mode: process.env.NODE_ENV,
|
||||
getLoadContext: sentryLoadContext,
|
||||
})(req, res, next);
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
app.listen(port, () => {
|
||||
logger.info(`Server listening on port ${port}`);
|
||||
});
|
||||
|
||||
function purgeRequireCache() {
|
||||
const resolved = require.resolve("../build");
|
||||
for (const key in require.cache) {
|
||||
if (key.startsWith(resolved)) {
|
||||
delete require.cache[key];
|
||||
}
|
||||
}
|
||||
}
|
35
server/queues.ts
Normal file
35
server/queues.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import { ExpressAdapter } from "@bull-board/express";
|
||||
import { BullMQAdapter } from "@bull-board/api/bullMQAdapter";
|
||||
import { createBullBoard } from "@bull-board/api";
|
||||
import { GlobalRole } from "@prisma/client";
|
||||
|
||||
import { __getSession } from "~/utils/session.server";
|
||||
import type { SessionData } from "~/utils/auth.server";
|
||||
import queues from "~/queues";
|
||||
import cronJobs from "~/cron-jobs";
|
||||
|
||||
export async function adminMiddleware(req: Request, res: Response, next: NextFunction) {
|
||||
const session = await __getSession(req.headers.cookie);
|
||||
const sessionData: SessionData | undefined = session.data.user;
|
||||
if (!sessionData || sessionData.user.role !== GlobalRole.SUPERADMIN) {
|
||||
return res.setHeader("Location", "/sign-in").status(302).end();
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
export function setupBullBoard() {
|
||||
const serverAdapter = new ExpressAdapter();
|
||||
const cronJobsQueues = registerCronJobs();
|
||||
createBullBoard({
|
||||
queues: [...queues, ...cronJobsQueues].map((queue) => new BullMQAdapter(queue)),
|
||||
serverAdapter,
|
||||
});
|
||||
serverAdapter.setBasePath("/admin/queues");
|
||||
return serverAdapter;
|
||||
}
|
||||
|
||||
function registerCronJobs() {
|
||||
return cronJobs.map((registerCronJob) => registerCronJob());
|
||||
}
|
109
server/sentry-remix.ts
Normal file
109
server/sentry-remix.ts
Normal file
@ -0,0 +1,109 @@
|
||||
import type { Request, Response } from "express";
|
||||
import type { ActionFunction, DataFunctionArgs, LoaderFunction, ServerBuild } from "@remix-run/node";
|
||||
import { isResponse } from "@remix-run/server-runtime/responses";
|
||||
import type { Transaction } from "@sentry/types";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { __getSession } from "~/utils/session.server";
|
||||
import type { SessionData } from "~/utils/auth.server";
|
||||
|
||||
function wrapDataFunc(func: ActionFunction | LoaderFunction, routeId: string, method: string) {
|
||||
const ogFunc = func;
|
||||
|
||||
return async (args: DataFunctionArgs) => {
|
||||
const session = await __getSession(args.request.headers.get("Cookie"));
|
||||
const sessionData: SessionData | undefined = session.data.user;
|
||||
if (sessionData) {
|
||||
Sentry.setUser({
|
||||
id: sessionData.user.id,
|
||||
email: sessionData.user.email,
|
||||
role: sessionData.user.role,
|
||||
});
|
||||
} else {
|
||||
Sentry.configureScope((scope) => scope.setUser(null));
|
||||
}
|
||||
|
||||
const parentTransaction: Transaction | undefined = args.context && args.context.__sentry_transaction;
|
||||
const transaction = parentTransaction?.startChild({
|
||||
op: `${method}:${routeId}`,
|
||||
description: `${method}: ${routeId}`,
|
||||
});
|
||||
if (transaction) {
|
||||
transaction.setStatus("ok");
|
||||
transaction.transaction = parentTransaction;
|
||||
}
|
||||
|
||||
try {
|
||||
return await ogFunc(args);
|
||||
} catch (error) {
|
||||
if (isResponse(error)) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
Sentry.captureException(error, {
|
||||
tags: {
|
||||
global_id: parentTransaction && parentTransaction.tags["global_id"],
|
||||
},
|
||||
});
|
||||
transaction?.setStatus("internal_error");
|
||||
throw error;
|
||||
} finally {
|
||||
transaction?.finish();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Register Sentry across your entire remix build.
|
||||
export function registerSentry(build: ServerBuild) {
|
||||
type Route = ServerBuild["routes"][string];
|
||||
|
||||
const routes: Record<string, Route> = {};
|
||||
|
||||
for (const [id, route] of Object.entries(build.routes)) {
|
||||
const newRoute: Route = { ...route, module: { ...route.module } };
|
||||
|
||||
if (route.module.action) {
|
||||
newRoute.module.action = wrapDataFunc(route.module.action, id, "action");
|
||||
}
|
||||
|
||||
if (route.module.loader) {
|
||||
newRoute.module.loader = wrapDataFunc(route.module.loader, id, "loader");
|
||||
}
|
||||
|
||||
routes[id] = newRoute;
|
||||
}
|
||||
|
||||
return {
|
||||
...build,
|
||||
routes,
|
||||
};
|
||||
}
|
||||
|
||||
export function sentryLoadContext(req: Request, res: Response) {
|
||||
const transaction = Sentry.getCurrentHub().startTransaction({
|
||||
op: "request",
|
||||
name: `${req.method}: ${req.url}`,
|
||||
description: `${req.method}: ${req.url}`,
|
||||
metadata: {
|
||||
requestPath: req.url,
|
||||
},
|
||||
tags: {
|
||||
global_id: uuid(),
|
||||
},
|
||||
});
|
||||
transaction && transaction.setStatus("internal_error");
|
||||
|
||||
res.once("finish", () => {
|
||||
if (transaction) {
|
||||
transaction.setHttpStatus(res.statusCode);
|
||||
transaction.setTag("http.status_code", res.statusCode);
|
||||
transaction.setTag("http.method", req.method);
|
||||
transaction.finish();
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
__sentry_transaction: transaction,
|
||||
};
|
||||
}
|
Reference in New Issue
Block a user