This commit is contained in:
Kalle 2024-02-10 12:53:36 +02:00
parent a3757199aa
commit a1dba004ad
7 changed files with 2094 additions and 94 deletions

2
.gitignore vendored
View File

@ -22,3 +22,5 @@ dump
/test-results/
/playwright-report/
/playwright/.cache/
newrelic_agent.log

View File

@ -1,16 +1,20 @@
import { PassThrough } from "stream";
import {
type ActionFunctionArgs,
type LoaderFunctionArgs,
createReadableStreamFromReadable,
type EntryContext,
} from "@remix-run/node";
import { RemixServer } from "@remix-run/react";
import { isbot } from "isbot";
import { renderToPipeableStream } from "react-dom/server";
import cron from "node-cron";
import { updatePatreonData } from "./modules/patreon";
import { i18Instance } from "./modules/i18n";
import { renderToPipeableStream } from "react-dom/server";
import { I18nextProvider } from "react-i18next";
import { getUser } from "./features/auth/core";
import { i18Instance } from "./modules/i18n";
import { updatePatreonData } from "./modules/patreon";
import { noticeError, setTransactionName } from "./utils/newrelic.server";
const ABORT_DELAY = 5000;
@ -21,6 +25,14 @@ const handleRequest = (
remixContext: EntryContext,
) => {
const userAgent = request.headers.get("user-agent");
const lastMatch =
remixContext.staticHandlerContext.matches[
remixContext.staticHandlerContext.matches.length - 1
];
if (lastMatch) setTransactionName(`ssr/${lastMatch.route.id}`);
return userAgent && isbot(userAgent)
? handleBotRequest(
request,
@ -37,6 +49,16 @@ const handleRequest = (
};
export default handleRequest;
export function handleDataRequest(
response: Response,
{ request }: LoaderFunctionArgs | ActionFunctionArgs,
) {
const name = new URL(request.url).searchParams.get("_data");
if (name) setTransactionName(name);
return response;
}
const handleBotRequest = (
request: Request,
responseStatusCode: number,
@ -125,6 +147,23 @@ const handleBrowserRequest = (
});
});
export async function handleError(
error: unknown,
{ request }: LoaderFunctionArgs | ActionFunctionArgs,
) {
const user = await getUser(request);
if (!request.signal.aborted) {
if (error instanceof Error) {
noticeError(error, {
"enduser.id": user?.id,
// TODO: FetchError: Invalid response body while trying to fetch http://localhost:5800/admin?_data=features%2Fadmin%2Froutes%2Fadmin: This stream has already been locked for exclusive reading by another reader
// formData: JSON.stringify(formDataToObject(await request.formData())),
});
}
console.error(error);
}
}
// example from https://github.com/BenMcH/remix-rss/blob/main/app/entry.server.tsx
declare global {
var appStartSignal: undefined | true;

View File

@ -47,6 +47,7 @@ import { CUSTOMIZED_CSS_VARS_NAME } from "./constants";
import NProgress from "nprogress";
import nProgressStyles from "nprogress/nprogress.css";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { browserTimingHeader } from "./utils/newrelic.server";
export const shouldRevalidate: ShouldRevalidateFunction = ({ nextUrl }) => {
// // reload on language change so the selected language gets set into the cookie
@ -97,6 +98,7 @@ export const loader = async ({ request }: LoaderFunctionArgs) => {
publisherId: process.env["PLAYWIRE_PUBLISHER_ID"],
websiteId: process.env["PLAYWIRE_WEBSITE_ID"],
loginDisabled: process.env["LOGIN_DISABLED"] === "true",
browserTimingHeader: browserTimingHeader(),
user: user
? {
discordName: user.discordName,
@ -163,6 +165,12 @@ function Document({
type="text/javascript"
src="https://cdn.jsdelivr.net/npm/brackets-viewer@1.5.1/dist/brackets-viewer.min.js"
/>
{data?.browserTimingHeader ? (
<script
type="text/javascript"
dangerouslySetInnerHTML={{ __html: data?.browserTimingHeader }}
/>
) : null}
<PWALinks />
<Fonts />
</head>

View File

@ -0,0 +1,29 @@
const isEnabled =
process.env["NEW_RELIC_APP_NAME"] && process.env["NEW_RELIC_LICENSE_KEY"];
const newrelic = isEnabled ? require("newrelic") : {};
export const browserTimingHeader = () =>
isEnabled
? newrelic.getBrowserTimingHeader({
hasToRemoveScriptWrapper: true,
})
: null;
export const noticeError = (
error: Error,
attributes?: {
"enduser.id"?: number;
formData?: string;
searchParams?: string;
params?: string;
},
) =>
isEnabled &&
newrelic.noticeError(error, {
...attributes,
"tags.commit": process.env["RENDER_GIT_COMMIT"],
});
export const setTransactionName = (name: string) =>
isEnabled && newrelic.setTransactionName(name);

View File

@ -3,6 +3,7 @@ import type { Params, UIMatch } from "@remix-run/react";
import type navItems from "~/components/layout/nav-items.json";
import { json } from "@remix-run/node";
import type { Namespace, TFunction } from "i18next";
import { noticeError } from "./newrelic.server";
export function notFoundIfFalsy<T>(value: T | null | undefined): T {
if (!value) throw new Response(null, { status: 404 });
@ -18,7 +19,10 @@ export function notFoundIfNullLike<T>(value: T | null | undefined): T {
}
export function badRequestIfFalsy<T>(value: T | null | undefined): T {
if (!value) throw new Response(null, { status: 400 });
if (!value) {
noticeError(new Error("Value is falsy"));
throw new Response(null, { status: 400 });
}
return value;
}
@ -30,11 +34,14 @@ export function parseSearchParams<T extends z.ZodTypeAny>({
request: Request;
schema: T;
}): z.infer<T> {
const url = new URL(request.url);
const searchParams = Object.fromEntries(url.searchParams);
try {
const url = new URL(request.url);
return schema.parse(Object.fromEntries(url.searchParams));
return schema.parse(searchParams);
} catch (e) {
if (e instanceof z.ZodError) {
noticeError(e, { searchParams: JSON.stringify(searchParams) });
console.error(e);
throw new Response(JSON.stringify(e), { status: 400 });
}
@ -64,14 +71,16 @@ export async function parseRequestFormData<T extends z.ZodTypeAny>({
schema: T;
parseAsync?: boolean;
}): Promise<z.infer<T>> {
const formDataObj = formDataToObject(await request.formData());
try {
const parsed = parseAsync
? await schema.parseAsync(formDataToObject(await request.formData()))
: schema.parse(formDataToObject(await request.formData()));
? await schema.parseAsync(formDataObj)
: schema.parse(formDataObj);
return parsed;
} catch (e) {
if (e instanceof z.ZodError) {
noticeError(e, { formData: JSON.stringify(formDataObj) });
console.error(e);
throw new Response(JSON.stringify(e), { status: 400 });
}
@ -88,10 +97,12 @@ export function parseFormData<T extends z.ZodTypeAny>({
formData: FormData;
schema: T;
}): z.infer<T> {
const formDataObj = formDataToObject(formData);
try {
return schema.parse(formDataToObject(formData));
return schema.parse(formDataObj);
} catch (e) {
if (e instanceof z.ZodError) {
noticeError(e, { formData: JSON.stringify(formDataObj) });
console.error(e);
throw new Response(JSON.stringify(e), { status: 400 });
}
@ -112,6 +123,7 @@ export function parseParams<T extends z.ZodTypeAny>({
return schema.parse(params);
} catch (e) {
if (e instanceof z.ZodError) {
noticeError(e, { params: JSON.stringify(params) });
console.error(e);
throw new Response(JSON.stringify(e), { status: 400 });
}
@ -173,6 +185,7 @@ export function validate(
): asserts condition {
if (condition) return;
noticeError(new Error(`Validation error: ${message}`));
throw new Response(
message ? JSON.stringify({ validationError: message }) : undefined,
{

2076
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,6 +10,7 @@
"dev:prod": "cross-env DB_PATH=db-prod.sqlite3 remix dev",
"dev:ci": "cp .env.example .env && npm run migrate up && npm run dev",
"start": "npm run migrate up && remix-serve ./build/index.js",
"start:nr": "npm run migrate up && NODE_OPTIONS='-r dotenv/config -r newrelic' remix-serve ./build/index.js",
"migrate": "ley",
"migrate:reset": "node scripts/delete-db-files.mjs && npm run migrate up",
"add-badge": "node --experimental-specifier-resolution=node -r @swc-node/register -r tsconfig-paths/register scripts/add-badge.ts",
@ -86,6 +87,7 @@
"lru-cache": "10.2.0",
"markdown-to-jsx": "^7.4.1",
"nanoid": "~5.0.5",
"newrelic": "^11.10.3",
"node-cron": "3.0.3",
"nprogress": "^0.2.0",
"openskill": "^3.1.0",
@ -113,6 +115,7 @@
"@swc-node/register": "^1.8.0",
"@types/better-sqlite3": "7.6.9",
"@types/i18next-fs-backend": "^1.1.5",
"@types/newrelic": "^9.14.3",
"@types/node-cron": "^3.0.11",
"@types/nprogress": "^0.2.3",
"@types/prettier": "3.0.0",