Add session ID to server logs for user reporting (#2720)

This commit is contained in:
Kalle 2026-01-13 21:02:16 +02:00 committed by GitHub
parent cfce363fa0
commit 35ac3fa0a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 98 additions and 9 deletions

View File

@ -6,6 +6,7 @@ import {
} from "react-router";
import { useLocation } from "react-use";
import { useUser } from "~/features/auth/core/user";
import { getSessionId } from "~/utils/session-id";
import {
ERROR_GIRL_IMAGE_PATH,
LOG_IN_URL,
@ -52,10 +53,11 @@ export function Catcher() {
}
if (!isRouteErrorResponse(error)) {
const sessionId = getSessionId();
const errorText = (() => {
if (!(error instanceof Error)) return;
return `Time: ${new Date().toISOString()}\nURL: ${location.href}\nUser ID: ${user?.id ?? "Not logged in"}\n${error.stack ?? error.message}`;
return `Session ID: ${sessionId}\nTime: ${new Date().toISOString()}\nURL: ${location.href}\nUser ID: ${user?.id ?? "Not logged in"}\n${error.stack ?? error.message}`;
})();
return (
@ -124,12 +126,15 @@ export function Catcher() {
<h2>Error {error.status}</h2>
<GetHelp />
<div className="text-sm text-lighter font-semi-bold">
Please include the message below if any and an explanation on what
you were doing:
Please include the session ID and message below if any and an
explanation on what you were doing:
</div>
{error.data ? (
<pre>{JSON.stringify(JSON.parse(error.data), null, 2)}</pre>
) : null}
<pre>
Session ID: {getSessionId()}
{error.data
? `\n${JSON.stringify(JSON.parse(error.data), null, 2)}`
: null}
</pre>
</Main>
);
}

View File

@ -4,6 +4,15 @@ import { I18nextProvider } from "react-i18next";
import { HydratedRouter } from "react-router/dom";
import { i18nLoader } from "./modules/i18n/loader";
import { logger } from "./utils/logger";
import { getSessionId } from "./utils/session-id";
const originalFetch = window.fetch;
window.fetch = (input, init) => {
const sessionId = getSessionId();
const headers = new Headers(init?.headers);
headers.set("Sendou-Session-Id", sessionId);
return originalFetch(input, { ...init, headers });
};
if ("serviceWorker" in navigator) {
window.addEventListener("load", () => {

View File

@ -108,3 +108,8 @@ if (!global.appStartSignal && process.env.NODE_ENV === "production") {
process.on("unhandledRejection", (reason: string, p: Promise<any>) => {
logger.error("Unhandled Rejection at:", p, "reason:", reason);
});
// wrapper so we get request id shown in the server logs
export function handleError(error: unknown) {
logger.error(error);
}

View File

@ -0,0 +1,18 @@
import { AsyncLocalStorage } from "node:async_hooks";
interface SessionIdContext {
sessionId: string | undefined;
}
export const sessionIdAsyncLocalStorage =
new AsyncLocalStorage<SessionIdContext>();
function getSessionId(): string | undefined {
return sessionIdAsyncLocalStorage.getStore()?.sessionId;
}
declare global {
var __getServerSessionId: (() => string | undefined) | undefined;
}
globalThis.__getServerSessionId = getSessionId;

View File

@ -0,0 +1,17 @@
import { sessionIdAsyncLocalStorage } from "./session-id-context.server";
type MiddlewareArgs = {
request: Request;
context: unknown;
};
type MiddlewareFn = (
args: MiddlewareArgs,
next: () => Promise<Response>,
) => Promise<Response>;
export const sessionIdMiddleware: MiddlewareFn = async ({ request }, next) => {
const sessionId = request.headers.get("Sendou-Session-Id") ?? undefined;
return sessionIdAsyncLocalStorage.run({ sessionId }, () => next());
};

View File

@ -37,6 +37,7 @@ import { Ramp } from "./components/ramp/Ramp";
import { apiCorsMiddleware } from "./features/api-public/api-cors-middleware.server";
import { getUser } from "./features/auth/core/user.server";
import { userMiddleware } from "./features/auth/core/user-middleware.server";
import { sessionIdMiddleware } from "./features/session-id/session-id-middleware.server";
import {
isTheme,
Theme,
@ -53,6 +54,7 @@ import { allI18nNamespaces } from "./utils/i18n";
import { isRevalidation, metaTags, type SerializeFrom } from "./utils/remix";
export const middleware: Route.MiddlewareFunction[] = [
sessionIdMiddleware,
apiCorsMiddleware,
userMiddleware,
];

View File

@ -1,7 +1,25 @@
/** biome-ignore-all lint/suspicious/noConsole: stub file to enable different solution later */
import { getSessionId as getClientSessionId } from "./session-id";
declare global {
var __getServerSessionId: (() => string | undefined) | undefined;
}
function getSessionIdForLog(): string {
if (typeof window !== "undefined") {
return getClientSessionId();
}
return globalThis.__getServerSessionId?.() ?? "no-session";
}
function formatLog(...args: unknown[]) {
const sessionId = getSessionIdForLog();
return [`[${sessionId}]`, ...args];
}
export const logger = {
info: console.log,
error: console.error,
warn: console.warn,
info: (...args: unknown[]) => console.log(...formatLog(...args)),
error: (...args: unknown[]) => console.error(...formatLog(...args)),
warn: (...args: unknown[]) => console.warn(...formatLog(...args)),
};

15
app/utils/session-id.ts Normal file
View File

@ -0,0 +1,15 @@
import { customAlphabet } from "nanoid";
const nanoid = customAlphabet(
// avoid 1/I and 0/O
"23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
);
let sessionId: string | undefined;
export function getSessionId(): string {
if (!sessionId) {
sessionId = nanoid(10);
}
return sessionId;
}