From 35ac3fa0a7b86aab8e1c11cd0bfbdaf83efab1f0 Mon Sep 17 00:00:00 2001
From: Kalle <38327916+Sendouc@users.noreply.github.com>
Date: Tue, 13 Jan 2026 21:02:16 +0200
Subject: [PATCH] Add session ID to server logs for user reporting (#2720)
---
app/components/Catcher.tsx | 17 ++++++++-----
app/entry.client.tsx | 9 +++++++
app/entry.server.tsx | 5 ++++
.../session-id/session-id-context.server.ts | 18 ++++++++++++++
.../session-id-middleware.server.ts | 17 +++++++++++++
app/root.tsx | 2 ++
app/utils/logger.ts | 24 ++++++++++++++++---
app/utils/session-id.ts | 15 ++++++++++++
8 files changed, 98 insertions(+), 9 deletions(-)
create mode 100644 app/features/session-id/session-id-context.server.ts
create mode 100644 app/features/session-id/session-id-middleware.server.ts
create mode 100644 app/utils/session-id.ts
diff --git a/app/components/Catcher.tsx b/app/components/Catcher.tsx
index 1d9bbc2b7..635f9a014 100644
--- a/app/components/Catcher.tsx
+++ b/app/components/Catcher.tsx
@@ -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() {
Error {error.status}
- 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:
- {error.data ? (
- {JSON.stringify(JSON.parse(error.data), null, 2)}
- ) : null}
+
+ Session ID: {getSessionId()}
+ {error.data
+ ? `\n${JSON.stringify(JSON.parse(error.data), null, 2)}`
+ : null}
+
);
}
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
index c6ad81199..563a1553a 100644
--- a/app/entry.client.tsx
+++ b/app/entry.client.tsx
@@ -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", () => {
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index e6d5e79ca..70507f5ce 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -108,3 +108,8 @@ if (!global.appStartSignal && process.env.NODE_ENV === "production") {
process.on("unhandledRejection", (reason: string, p: Promise) => {
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);
+}
diff --git a/app/features/session-id/session-id-context.server.ts b/app/features/session-id/session-id-context.server.ts
new file mode 100644
index 000000000..6a98f5462
--- /dev/null
+++ b/app/features/session-id/session-id-context.server.ts
@@ -0,0 +1,18 @@
+import { AsyncLocalStorage } from "node:async_hooks";
+
+interface SessionIdContext {
+ sessionId: string | undefined;
+}
+
+export const sessionIdAsyncLocalStorage =
+ new AsyncLocalStorage();
+
+function getSessionId(): string | undefined {
+ return sessionIdAsyncLocalStorage.getStore()?.sessionId;
+}
+
+declare global {
+ var __getServerSessionId: (() => string | undefined) | undefined;
+}
+
+globalThis.__getServerSessionId = getSessionId;
diff --git a/app/features/session-id/session-id-middleware.server.ts b/app/features/session-id/session-id-middleware.server.ts
new file mode 100644
index 000000000..b60e2b705
--- /dev/null
+++ b/app/features/session-id/session-id-middleware.server.ts
@@ -0,0 +1,17 @@
+import { sessionIdAsyncLocalStorage } from "./session-id-context.server";
+
+type MiddlewareArgs = {
+ request: Request;
+ context: unknown;
+};
+
+type MiddlewareFn = (
+ args: MiddlewareArgs,
+ next: () => Promise,
+) => Promise;
+
+export const sessionIdMiddleware: MiddlewareFn = async ({ request }, next) => {
+ const sessionId = request.headers.get("Sendou-Session-Id") ?? undefined;
+
+ return sessionIdAsyncLocalStorage.run({ sessionId }, () => next());
+};
diff --git a/app/root.tsx b/app/root.tsx
index 945340717..e7fdd17f3 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -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,
];
diff --git a/app/utils/logger.ts b/app/utils/logger.ts
index 667eb0b83..631261fdb 100644
--- a/app/utils/logger.ts
+++ b/app/utils/logger.ts
@@ -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)),
};
diff --git a/app/utils/session-id.ts b/app/utils/session-id.ts
new file mode 100644
index 000000000..72e38715e
--- /dev/null
+++ b/app/utils/session-id.ts
@@ -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;
+}