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:
parent
4092f21475
commit
8e576ac578
13 changed files with 160 additions and 64 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
||||
|
|
|
|||
17
packages/sentry-config/package.json
Normal file
17
packages/sentry-config/package.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "@trails-cool/sentry-config",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"scripts": {
|
||||
"lint": "eslint .",
|
||||
"typecheck": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "catalog:"
|
||||
}
|
||||
}
|
||||
100
packages/sentry-config/src/index.ts
Normal file
100
packages/sentry-config/src/index.ts
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
8
packages/sentry-config/tsconfig.json
Normal file
8
packages/sentry-config/tsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
15
pnpm-lock.yaml
generated
15
pnpm-lock.yaml
generated
|
|
@ -230,6 +230,9 @@ importers:
|
|||
'@trails-cool/map':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/map
|
||||
'@trails-cool/sentry-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/sentry-config
|
||||
'@trails-cool/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
|
|
@ -327,6 +330,9 @@ importers:
|
|||
'@trails-cool/map-core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/map-core
|
||||
'@trails-cool/sentry-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/sentry-config
|
||||
'@trails-cool/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
|
|
@ -478,6 +484,9 @@ importers:
|
|||
'@trails-cool/map-core':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/map-core
|
||||
'@trails-cool/sentry-config':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/sentry-config
|
||||
'@trails-cool/types':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/types
|
||||
|
|
@ -635,6 +644,12 @@ importers:
|
|||
|
||||
packages/map-core: {}
|
||||
|
||||
packages/sentry-config:
|
||||
devDependencies:
|
||||
'@types/node':
|
||||
specifier: 'catalog:'
|
||||
version: 22.19.17
|
||||
|
||||
packages/types: {}
|
||||
|
||||
packages/ui: {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue