trails/packages/sentry-config/src/index.ts
Ullrich Schäfer 8e576ac578
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>
2026-04-18 00:30:09 +02:00

100 lines
3.3 KiB
TypeScript

/**
* Shared Sentry configuration helpers for all trails.cool apps.
*
* Each app still calls its own SDK-specific `Sentry.init(...)` (they use
* three different SDKs: `@sentry/node`, `@sentry/react`, `@sentry/react-native`),
* but the common options are centralised here so privacy defaults (`sendDefaultPii: false`,
* replay off) and enable logic stay consistent.
*/
/**
* Drop 404 errors — they come from scanners/typos and aren't bugs.
* Pass as `beforeSend` to `Sentry.init(...)` on server-side init sites.
*
* Typed loosely on purpose: the `@sentry/node` and `@sentry/react` SDKs
* each have their own `ErrorEvent` / `Event` types, and we don't want this
* package to depend on either SDK.
*/
export function drop404s<E extends { extra?: Record<string, unknown> }>(event: E): E | null {
const serialized = event.extra?.__serialized__ as Record<string, unknown> | undefined;
if (serialized?.status === 404) return null;
return event;
}
/**
* Shared Sentry options for Node-side runtimes (the HTTP servers).
* Spread into `Sentry.init({...nodeSentryConfig("journal server"), dsn, beforeSend: drop404s, integrations})`.
*
* Logs a debug message at init time when the config is inert (local dev)
* so developers can see that Sentry would send in production.
*/
export function nodeSentryConfig(appContext: string) {
const environment = process.env.CI ? "ci" : (process.env.NODE_ENV ?? "development");
const enabled = process.env.NODE_ENV === "production" && !process.env.CI;
if (!enabled && !process.env.CI) {
console.debug(
`[sentry] ${appContext} init inert (env=${environment}); would send to Sentry in production`,
);
}
return {
environment,
enabled,
release: process.env.SENTRY_RELEASE,
tracesSampleRate: 1.0,
sendDefaultPii: false as const,
};
}
interface BrowserEnv {
PROD: boolean;
VITE_SENTRY_ENVIRONMENT?: string;
}
/**
* Shared Sentry options for Vite/browser runtimes.
* Spread into `Sentry.init({...browserSentryConfig("journal client", import.meta.env), dsn, integrations})`.
*
* Replay sample rates are explicitly 0 — replay integration is not installed,
* but the options document the intent and prevent accidental enablement.
*/
export function browserSentryConfig(appContext: string, env: BrowserEnv) {
const environment = env.VITE_SENTRY_ENVIRONMENT ?? (env.PROD ? "production" : "development");
const enabled = env.PROD && environment !== "ci";
if (!enabled && !env.PROD) {
console.debug(
`[sentry] ${appContext} init inert (env=${environment}); would send to Sentry in production`,
);
}
return {
environment,
enabled,
tracesSampleRate: environment === "ci" ? 0 : 1.0,
sendDefaultPii: false as const,
replaysSessionSampleRate: 0 as const,
replaysOnErrorSampleRate: 0 as const,
};
}
/**
* Shared Sentry options for React Native.
* Spread into `Sentry.init({...mobileSentryConfig(__DEV__), dsn})`.
*/
export function mobileSentryConfig(dev: boolean) {
const enabled = !dev;
if (dev) {
console.debug("[sentry] mobile init inert in dev; would send to Sentry in production");
}
return {
enabled,
tracesSampleRate: dev ? 0 : 1.0,
sendDefaultPii: false as const,
replaysSessionSampleRate: 0 as const,
replaysOnErrorSampleRate: 0 as const,
};
}