Add Sentry

This commit is contained in:
Kalle
2026-05-09 15:41:20 +03:00
parent 84f4757409
commit 863608b8a3
10 changed files with 1189 additions and 36 deletions

View File

@@ -43,3 +43,10 @@ VITE_SHOW_LUTI_NAV_ITEM=false
VITE_VAPID_PUBLIC_KEY=
VAPID_PRIVATE_KEY=
VAPID_EMAIL=
// Sentry build-time config (used for sourcemap upload)
SENTRY_ORG=
SENTRY_PROJECT=
SENTRY_AUTH_TOKEN=
VITE_SENTRY_DSN=

View File

@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/react-router";
import "@formatjs/intl-durationformat/polyfill.js";
import i18next from "i18next";
import { hydrateRoot } from "react-dom/client";
@@ -7,8 +8,35 @@ import { i18nLoader } from "./modules/i18n/loader";
import { logger } from "./utils/logger";
import { getSessionId } from "./utils/session-id";
const tracing = Sentry.reactRouterTracingIntegration({
useInstrumentationAPI: true,
});
Sentry.init({
dsn: import.meta.env.VITE_SENTRY_DSN,
sendDefaultPii: false,
integrations: [tracing],
enableLogs: true,
tracesSampleRate: 0.1,
tracePropagationTargets: [/^\//],
});
const originalFetch = window.fetch;
window.fetch = (input, init) => {
const url =
typeof input === "string"
? input
: input instanceof URL
? input.href
: input.url;
const isSameOrigin =
url.startsWith("/") ||
new URL(url, window.location.origin).origin === window.location.origin;
if (!isSameOrigin) {
return originalFetch(input, init);
}
const sessionId = getSessionId();
const headers = new Headers(init?.headers);
headers.set("Sendou-Session-Id", sessionId);
@@ -27,7 +55,14 @@ i18nLoader()
hydrateRoot(
document,
<I18nextProvider i18n={i18next}>
<HydratedRouter />
<HydratedRouter
unstable_instrumentations={[tracing.clientInstrumentation]}
onError={(error) => {
if (error && error instanceof Error) {
Sentry.captureException(error);
}
}}
/>
</I18nextProvider>,
),
)

View File

@@ -1,3 +1,4 @@
import * as Sentry from "@sentry/react-router";
import "@formatjs/intl-durationformat/polyfill.js";
import { PassThrough } from "node:stream";
import { createReadableStreamFromReadable } from "@react-router/node";
@@ -6,7 +7,11 @@ import { isbot } from "isbot";
import cron from "node-cron";
import { renderToPipeableStream } from "react-dom/server";
import { I18nextProvider, initReactI18next } from "react-i18next";
import { type EntryContext, ServerRouter } from "react-router";
import {
type EntryContext,
type HandleErrorFunction,
ServerRouter,
} from "react-router";
import { config } from "~/modules/i18n/config"; // your i18n configuration file
import { i18next } from "~/modules/i18n/i18next.server";
import { resources } from "./modules/i18n/resources.server";
@@ -21,7 +26,7 @@ import { logger } from "./utils/logger";
// Reject/cancel all pending promises after 5 seconds
export const streamTimeout = 5000;
export default async function handleRequest(
async function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
@@ -64,7 +69,7 @@ export default async function handleRequest(
}),
);
pipe(body);
pipe(Sentry.getMetaTagTransformer(body));
},
onShellError(error: unknown) {
reject(error);
@@ -122,6 +127,13 @@ process.on("unhandledRejection", (reason: string, p: Promise<any>) => {
});
// wrapper so we get request id shown in the server logs
export function handleError(error: unknown) {
export const handleError: HandleErrorFunction = (error, { request }) => {
if (!request.signal.aborted) {
Sentry.captureException(error);
}
logger.error(error);
}
};
export default Sentry.wrapSentryHandleRequest(handleRequest);
export const unstable_instrumentations = [
Sentry.createSentryServerInstrumentation(),
];

23
instrument.server.mjs Normal file
View File

@@ -0,0 +1,23 @@
import * as Sentry from "@sentry/react-router";
Sentry.init({
dsn: process.env.VITE_SENTRY_DSN,
sendDefaultPii: false,
enableLogs: true,
tracesSampleRate: 0.1, // Capture 10% of the transactions
// Set up performance monitoring
beforeSend(event) {
// Filter out 404s from error reporting
if (event.exception) {
const error = event.exception.values?.[0];
if (
error?.type === "NotFoundException" ||
error?.value?.includes("404")
) {
return null;
}
}
return event;
},
});

View File

@@ -12,8 +12,6 @@ const config = {
"public/sw-2.js",
"ley.config.cjs",
"ley-driver.cjs",
"vitest.browser.config.ts",
"app/browser-test-setup.ts",
],
compilers: {
css: (text: string, path: string) => {

View File

@@ -9,8 +9,9 @@
"deploy": "pnpm install --frozen-lockfile && pnpm run build",
"build": "react-router build",
"dev": "cross-env DB_PATH=db.sqlite3 pnpm run migrate up && pnpm run setup && react-router dev --host",
"dev:sentry": "NODE_OPTIONS='--import ./instrument.server.mjs' pnpm dev",
"dev:prod": "cross-env DB_PATH=db-prod.sqlite3 VITE_PROD_MODE=true react-router dev --host",
"start": "pnpm run migrate up && react-router-serve build/server/index.js",
"start": "NODE_ENV=production NODE_OPTIONS='--import ./instrument.server.mjs' react-router-serve ./build/server/index.js",
"migrate": "ley",
"migrate:prod": "cross-env DB_PATH=db-prod.sqlite3 pnpm run migrate up",
"check-translation-jsons": "node --experimental-strip-types scripts/check-translation-jsons.ts",
@@ -51,6 +52,7 @@
"@react-router/node": "7.14.2",
"@react-router/serve": "7.14.2",
"@remix-run/form-data-parser": "0.16.0",
"@sentry/react-router": "^10.52.0",
"@tldraw/tldraw": "3.12.1",
"@zumer/snapdom": "2.9.0",
"aws-sdk": "2.1693.0",

1067
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
import type { Config } from "@react-router/dev/config";
import { sentryOnBuildEnd } from "@sentry/react-router";
export default {
// Upfront cost vs. lazy loading trade-off
@@ -8,4 +9,11 @@ export default {
future: {
v8_middleware: true,
},
buildEnd: async ({ viteConfig, reactRouterConfig, buildManifest }) => {
await sentryOnBuildEnd({
viteConfig: viteConfig,
reactRouterConfig: reactRouterConfig,
buildManifest: buildManifest,
});
},
} satisfies Config;

View File

@@ -1,10 +1,10 @@
import { reactRouter } from "@react-router/dev/vite";
import { sentryReactRouter } from "@sentry/react-router";
import { defineConfig, loadEnv } from "vite";
import babel from "vite-plugin-babel";
import { configDefaults } from "vitest/config";
export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), "");
export default defineConfig((config) => {
const env = loadEnv(config.mode, process.cwd(), "");
return {
server: {
port: Number(env.PORT) || 5173,
@@ -40,26 +40,19 @@ export default defineConfig(({ mode }) => {
plugins: [["babel-plugin-react-compiler", {}]],
},
}),
sentryReactRouter(
{
org: process.env.SENTRY_ORG,
project: process.env.SENTRY_PROJECT,
authToken: process.env.SENTRY_AUTH_TOKEN,
telemetry: false,
},
config,
),
],
test: {
projects: [
{
extends: true,
test: {
name: "unit",
include: ["**/*.test.{ts,tsx}"],
exclude: [
...configDefaults.exclude,
"e2e/**",
"**/*.browser.test.{ts,tsx}",
],
setupFiles: ["./app/test-setup.ts"],
},
},
{
extends: "./vitest.browser.config.ts",
},
],
projects: ["./vitest.unit.config.ts", "./vitest.browser.config.ts"],
},
define: {
__GIT_COMMIT__: JSON.stringify(process.env.RENDER_GIT_COMMIT ?? ""),
@@ -75,5 +68,8 @@ export default defineConfig(({ mode }) => {
resolve: {
tsconfigPaths: true,
},
optimizeDeps: {
exclude: ["@sentry/react-router"],
},
};
});

17
vitest.unit.config.ts Normal file
View File

@@ -0,0 +1,17 @@
import { configDefaults, defineConfig } from "vitest/config";
export default defineConfig({
test: {
name: "unit",
include: ["**/*.test.{ts,tsx}"],
exclude: [
...configDefaults.exclude,
"e2e/**",
"**/*.browser.test.{ts,tsx}",
],
setupFiles: ["./app/test-setup.ts"],
},
resolve: {
tsconfigPaths: true,
},
});