Merge pull request #434 from trails-cool/fix/sentry-dsn-no-hardcoded-fallback

fix(sentry): remove hardcoded DSN fallbacks; supply via env in CI
This commit is contained in:
Ullrich Schäfer 2026-05-25 23:12:55 +02:00 committed by GitHub
commit 1263372eef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 55 additions and 21 deletions

View file

@ -13,6 +13,15 @@ concurrency:
group: deploy-apps
cancel-in-progress: true
# Public Sentry DSNs for the trails.cool flagship instance. Public by
# design — Sentry DSNs are transmitted unencrypted from the client JS
# bundle, embedding them in this workflow is no worse than embedding
# them in the runtime env. Self-hosted forks should either replace
# these with their own DSNs or remove the lines to ship without Sentry.
env:
SENTRY_DSN_JOURNAL: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728"
SENTRY_DSN_PLANNER: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208"
jobs:
build-images:
name: Build & Push Docker Images
@ -48,8 +57,12 @@ jobs:
tags: |
ghcr.io/trails-cool/${{ matrix.app }}:latest
ghcr.io/trails-cool/${{ matrix.app }}:${{ github.sha }}
# VITE_SENTRY_DSN bakes the client-side DSN into the journal's
# built bundle. Only journal has a client Sentry init; planner
# ignores the build-arg if present.
build-args: |
SENTRY_RELEASE=${{ github.sha }}
VITE_SENTRY_DSN=${{ matrix.app == 'journal' && env.SENTRY_DSN_JOURNAL || '' }}
secrets: |
SENTRY_AUTH_TOKEN=/tmp/sentry_token
@ -70,6 +83,9 @@ jobs:
echo "DOMAIN=trails.cool" >> infrastructure/app.env
# Flagship marker — see cd-infra.yml for what this gates.
echo "IS_FLAGSHIP=true" >> infrastructure/app.env
# Sentry DSNs (public — see workflow top-level env for context).
echo "SENTRY_DSN_JOURNAL=$SENTRY_DSN_JOURNAL" >> infrastructure/app.env
echo "SENTRY_DSN_PLANNER=$SENTRY_DSN_PLANNER" >> infrastructure/app.env
- name: Copy files to server
uses: appleboy/scp-action@v1

View file

@ -30,6 +30,12 @@ concurrency:
group: staging-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.number) || 'main' }}
cancel-in-progress: true
# Public Sentry DSNs (same as cd-apps.yml). See that workflow for the
# "public by design" rationale.
env:
SENTRY_DSN_JOURNAL: "https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728"
SENTRY_DSN_PLANNER: "https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208"
jobs:
# ── Build ─────────────────────────────────────────────────────────────
# Tags:
@ -89,6 +95,7 @@ jobs:
ghcr.io/trails-cool/${{ matrix.app }}:${{ steps.tags.outputs.sha }}
build-args: |
SENTRY_RELEASE=${{ steps.tags.outputs.sha }}
VITE_SENTRY_DSN=${{ matrix.app == 'journal' && env.SENTRY_DSN_JOURNAL || '' }}
secrets: |
SENTRY_AUTH_TOKEN=/tmp/sentry_token
@ -118,6 +125,8 @@ jobs:
echo "JOURNAL_IMAGE_TAG=staging"
echo "PLANNER_IMAGE_TAG=staging"
echo "SENTRY_RELEASE=${{ github.sha }}"
echo "SENTRY_DSN_JOURNAL=$SENTRY_DSN_JOURNAL"
echo "SENTRY_DSN_PLANNER=$SENTRY_DSN_PLANNER"
} >> infrastructure/staging.env
- name: Copy compose file + env to server
@ -220,6 +229,8 @@ jobs:
# PR-preview journals all share the persistent staging planner.
echo "PLANNER_URL=https://planner.staging.trails.cool"
echo "SENTRY_RELEASE=${{ github.event.pull_request.head.sha }}"
echo "SENTRY_DSN_JOURNAL=$SENTRY_DSN_JOURNAL"
echo "SENTRY_DSN_PLANNER=$SENTRY_DSN_PLANNER"
} >> infrastructure/staging-pr-${{ steps.ports.outputs.pr }}.env
- name: Generate per-PR Caddyfile snippet

View file

@ -23,11 +23,16 @@ RUN pnpm install --frozen-lockfile
FROM base AS build
ARG SENTRY_RELEASE
# Client-side Sentry DSN baked into the bundle at build time. Empty (or
# unset) produces a Sentry-free client. Public-by-design: the DSN
# appears in the shipped client JS regardless.
ARG VITE_SENTRY_DSN=""
COPY --from=deps /app/ ./
COPY . .
RUN --mount=type=secret,id=SENTRY_AUTH_TOKEN \
SENTRY_AUTH_TOKEN="$(cat /run/secrets/SENTRY_AUTH_TOKEN 2>/dev/null | tr -d '\n\r')" \
SENTRY_RELEASE="$SENTRY_RELEASE" \
VITE_SENTRY_DSN="$VITE_SENTRY_DSN" \
pnpm --filter @trails-cool/journal build
FROM base AS runtime

View file

@ -5,15 +5,12 @@ import { browserSentryConfig } from "@trails-cool/sentry-config";
let initialized = false;
// Build-time DSN injection: `VITE_SENTRY_DSN` (if set during `pnpm build`)
// overrides the flagship default so self-hosters can ship their own
// Sentry project. Set it to `""` (empty string) to disable Sentry on
// the client entirely.
const FLAGSHIP_JOURNAL_DSN =
"https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728";
const CLIENT_DSN =
(import.meta.env as Record<string, string | undefined>).VITE_SENTRY_DSN ??
FLAGSHIP_JOURNAL_DSN;
// Build-time DSN injection. `VITE_SENTRY_DSN` (set during `pnpm build`,
// see apps/journal/Dockerfile + cd-apps.yml) determines whether the
// client emits Sentry events at all. Self-hosted builds without the
// env var produce a Sentry-free client bundle.
const CLIENT_DSN = (import.meta.env as Record<string, string | undefined>)
.VITE_SENTRY_DSN;
export function initSentryClient() {
if (initialized) return;

View file

@ -11,14 +11,13 @@ import { createBoss, startWorker } from "@trails-cool/jobs";
import { getDatabaseUrl } from "@trails-cool/db";
import postgres, { type Sql } from "postgres";
// Sentry DSN is read from env so self-hosted instances don't ship their
// errors to the trails.cool flagship Sentry by default. The flagship
// keeps its DSN as the fallback; setting SENTRY_DSN="" (or any other
// truthy value) overrides. SENTRY_DISABLED=true skips init entirely.
const FLAGSHIP_JOURNAL_SENTRY_DSN =
"https://a32ffcc575d34be072e91b20f247eeee@o4509530546634752.ingest.de.sentry.io/4509530555547728";
const sentryDsn = process.env.SENTRY_DSN ?? FLAGSHIP_JOURNAL_SENTRY_DSN;
if (process.env.SENTRY_DISABLED !== "true" && sentryDsn !== "") {
// Sentry DSN is supplied via env. No hardcoded fallback — self-hosted
// instances default to no Sentry. Flagship sets SENTRY_DSN via
// docker-compose env (see infrastructure/docker-compose.yml).
// SENTRY_DISABLED=true is an explicit opt-out even when a DSN is set
// (useful for local prod-mode debugging).
const sentryDsn = process.env.SENTRY_DSN;
if (process.env.SENTRY_DISABLED !== "true" && sentryDsn) {
Sentry.init({
dsn: sentryDsn,
...nodeSentryConfig("journal server"),

View file

@ -14,10 +14,8 @@ import { getDatabaseUrl } from "@trails-cool/db";
import postgres, { type Sql } from "postgres";
// See apps/journal/server.ts for the SENTRY_DSN / SENTRY_DISABLED contract.
const FLAGSHIP_PLANNER_SENTRY_DSN =
"https://5215134cd78d5e6c199e29300b8425af@o4509530546634752.ingest.de.sentry.io/4511102546608208";
const sentryDsn = process.env.SENTRY_DSN ?? FLAGSHIP_PLANNER_SENTRY_DSN;
if (process.env.SENTRY_DISABLED !== "true" && sentryDsn !== "") {
const sentryDsn = process.env.SENTRY_DSN;
if (process.env.SENTRY_DISABLED !== "true" && sentryDsn) {
Sentry.init({
dsn: sentryDsn,
...nodeSentryConfig("planner server"),

View file

@ -53,6 +53,7 @@ services:
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set}
NODE_ENV: production
PORT: 3000
SENTRY_DSN: ${SENTRY_DSN_JOURNAL:-}
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
SMTP_URL: ""
SMTP_FROM: trails.cool staging <noreply@staging.trails.cool>
@ -89,6 +90,7 @@ services:
DATABASE_URL: postgres://trails:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in SOPS secrets.app.env}@postgres:5432/${STAGING_DATABASE}
NODE_ENV: production
PORT: 3001
SENTRY_DSN: ${SENTRY_DSN_PLANNER:-}
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:3001/health || exit 1"]

View file

@ -42,6 +42,11 @@ services:
SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET must be set in SOPS secrets.app.env}
NODE_ENV: production
PORT: 3000
# SENTRY_DSN per service (journal + planner have separate Sentry
# projects on the same org). Empty = no Sentry init (correct
# default for self-hosted instances). See cd-apps.yml for how the
# flagship populates this.
SENTRY_DSN: ${SENTRY_DSN_JOURNAL:-}
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
SMTP_URL: ${SMTP_URL:-}
SMTP_FROM: ${SMTP_FROM:-trails.cool <noreply@trails.cool>}
@ -87,6 +92,7 @@ services:
DATABASE_URL: postgres://trails:${POSTGRES_PASSWORD:?POSTGRES_PASSWORD must be set in SOPS secrets.app.env}@postgres:5432/trails
NODE_ENV: production
PORT: 3001
SENTRY_DSN: ${SENTRY_DSN_PLANNER:-}
SENTRY_RELEASE: ${SENTRY_RELEASE:-}
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:3001/health || exit 1"]