Consolidate Sentry config into shared package; fix inconsistencies

Introduces @trails-cool/sentry-config with three helpers that encode
the common Sentry.init options:
- nodeSentryConfig(appContext) for the two HTTP servers
- browserSentryConfig(appContext, env) for the Journal client
- mobileSentryConfig(__DEV__) for the React Native app
- drop404s beforeSend for servers

Also fixes three inconsistencies found in the audit:
- Planner server was missing sendDefaultPii: false (IPs could leak)
- Planner entry.server.tsx didn't call Sentry.captureException on SSR
  render errors; Journal's SSR did
- Sentry.init location now matches across apps (both in server.ts;
  Journal previously had it in app/entry.server.tsx, which meant init
  ran lazily on first request rather than at server startup)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ullrich Schäfer 2026-04-18 00:30:09 +02:00
parent 4092f21475
commit 8e576ac578
No known key found for this signature in database
GPG key ID: A32FF691A0F752D9
13 changed files with 160 additions and 64 deletions

View file

@ -8,28 +8,6 @@ import type { RenderToPipeableStreamOptions } from "react-dom/server";
import { renderToPipeableStream } from "react-dom/server";
import { initI18nServer, detectLanguage } from "@trails-cool/i18n";
const sentryEnvironment = process.env.CI ? "ci" : (process.env.NODE_ENV ?? "development");
const sentryEnabled = process.env.NODE_ENV === "production" && !process.env.CI;
Sentry.init({
dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728",
release: process.env.SENTRY_RELEASE,
environment: sentryEnvironment,
tracesSampleRate: 1.0,
enabled: sentryEnabled,
sendDefaultPii: false,
beforeSend(event) {
// Drop 404s — they're expected (scanners, typos), not bugs
const serialized = event.extra?.__serialized__ as Record<string, unknown> | undefined;
if (serialized?.status === 404) return null;
return event;
},
});
if (!sentryEnabled && !process.env.CI) {
console.debug(`[sentry] journal SSR init inert (env=${sentryEnvironment}); would send to Sentry in production`);
}
export const streamTimeout = 5_000;
export default function handleRequest(

View file

@ -1,6 +1,7 @@
import * as Sentry from "@sentry/react";
import { useEffect } from "react";
import { useLocation, useNavigationType, createRoutesFromChildren, matchRoutes } from "react-router";
import { browserSentryConfig } from "@trails-cool/sentry-config";
let initialized = false;
@ -8,10 +9,6 @@ export function initSentryClient() {
if (initialized) return;
initialized = true;
const sentryEnvironment = import.meta.env.VITE_SENTRY_ENVIRONMENT ??
(import.meta.env.PROD ? "production" : "development");
const enabled = import.meta.env.PROD && sentryEnvironment !== "ci";
Sentry.init({
dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728",
integrations: [
@ -23,16 +20,6 @@ export function initSentryClient() {
matchRoutes,
}),
],
environment: sentryEnvironment,
tracesSampleRate: sentryEnvironment === "ci" ? 0 : 1.0,
enabled,
sendDefaultPii: false,
// Session Replay is not installed; document the intent explicitly.
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
...browserSentryConfig("journal client", import.meta.env),
});
if (!enabled && !import.meta.env.PROD) {
console.debug(`[sentry] journal client init inert (env=${sentryEnvironment}); would send to Sentry in production`);
}
}

View file

@ -24,6 +24,7 @@
"@trails-cool/gpx": "workspace:*",
"@trails-cool/i18n": "workspace:*",
"@trails-cool/map": "workspace:*",
"@trails-cool/sentry-config": "workspace:*",
"@trails-cool/types": "workspace:*",
"@trails-cool/ui": "workspace:*",
"drizzle-orm": "catalog:",

View file

@ -1,3 +1,5 @@
import * as Sentry from "@sentry/node";
import { nodeSentryConfig, drop404s } from "@trails-cool/sentry-config";
import { createRequestListener } from "@react-router/node";
import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
import { createReadStream, statSync } from "node:fs";
@ -7,6 +9,12 @@ import { httpRequestDuration, registry } from "./app/lib/metrics.server.ts";
import { createBoss, startWorker } from "@trails-cool/jobs";
import postgres from "postgres";
Sentry.init({
dsn: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728",
...nodeSentryConfig("journal server"),
beforeSend: drop404s,
});
const port = Number(process.env.PORT ?? 3000);
const CLIENT_DIR = resolve(import.meta.dirname, "build", "client");

View file

@ -1,22 +1,13 @@
import * as Sentry from "@sentry/react-native";
import { mobileSentryConfig } from "@trails-cool/sentry-config";
const SENTRY_DSN = "https://af50ac7aea803216adbe21e9dd4b896b@o4509530546634752.ingest.de.sentry.io/4511209742532688";
export function initSentry() {
const enabled = !__DEV__;
Sentry.init({
dsn: SENTRY_DSN,
tracesSampleRate: __DEV__ ? 0 : 1.0,
enabled,
sendDefaultPii: false,
// Session Replay is not installed; document the intent explicitly.
replaysSessionSampleRate: 0,
replaysOnErrorSampleRate: 0,
...mobileSentryConfig(__DEV__),
});
if (!enabled) {
console.debug("[sentry] mobile init inert in dev; would send to Sentry in production");
}
}
export { Sentry };

View file

@ -38,6 +38,7 @@
"@trails-cool/gpx": "workspace:*",
"@trails-cool/i18n": "workspace:*",
"@trails-cool/map-core": "workspace:*",
"@trails-cool/sentry-config": "workspace:*",
"@trails-cool/types": "workspace:*",
"expo": "~55.0.15",
"expo-constants": "~55.0.14",

View file

@ -1,3 +1,4 @@
import * as Sentry from "@sentry/node";
import { PassThrough } from "node:stream";
import type { AppLoadContext, EntryContext } from "react-router";
import { createReadableStreamFromReadable } from "@react-router/node";
@ -71,6 +72,7 @@ export default function handleRequest(
responseStatusCode = 500;
if (shellRendered) {
console.error(error);
Sentry.captureException(error);
}
},
},

View file

@ -27,6 +27,7 @@
"@trails-cool/i18n": "workspace:*",
"@trails-cool/map": "workspace:*",
"@trails-cool/map-core": "workspace:*",
"@trails-cool/sentry-config": "workspace:*",
"@trails-cool/types": "workspace:*",
"@trails-cool/ui": "workspace:*",
"codemirror": "^6.0.2",

View file

@ -1,4 +1,5 @@
import * as Sentry from "@sentry/node";
import { nodeSentryConfig, drop404s } from "@trails-cool/sentry-config";
import { logger } from "./app/lib/logger.server.ts";
import { httpRequestDuration } from "./app/lib/metrics.server.ts";
import { createRequestListener } from "@react-router/node";
@ -10,26 +11,12 @@ import { createBoss, startWorker } from "@trails-cool/jobs";
import { expireSessionsJob } from "./app/jobs/expire-sessions.ts";
import postgres from "postgres";
const sentryEnvironment = process.env.CI ? "ci" : (process.env.NODE_ENV ?? "development");
const sentryEnabled = process.env.NODE_ENV === "production" && !process.env.CI;
Sentry.init({
dsn: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208",
release: process.env.SENTRY_RELEASE,
environment: sentryEnvironment,
tracesSampleRate: 1.0,
enabled: sentryEnabled,
beforeSend(event) {
const serialized = event.extra?.__serialized__ as Record<string, unknown> | undefined;
if (serialized?.status === 404) return null;
return event;
},
...nodeSentryConfig("planner server"),
beforeSend: drop404s,
});
if (!sentryEnabled && !process.env.CI) {
console.debug(`[sentry] planner server init inert (env=${sentryEnvironment}); would send to Sentry in production`);
}
const port = Number(process.env.PORT ?? 3001);
const CLIENT_DIR = resolve(import.meta.dirname, "build", "client");