fix(deploy): wire VITE_SENTRY_DSN as Docker build arg so frontend errors reach GlitchTip #646

Merged
marcel merged 1 commits from fix/issue-645-vite-sentry-dsn-build-arg into main 2026-05-20 10:59:26 +02:00
Owner

Problem

The frontend Sentry SDK (hooks.client.ts, hooks.server.ts) reads import.meta.env.VITE_SENTRY_DSN, which is a Vite build-time variable — baked into the JS bundle at npm run build. The frontend Dockerfile build stage had no ARG VITE_SENTRY_DSN, so Vite never saw the value and the built bundle had enabled: false hardcoded. No frontend errors ever reached GlitchTip project #1.

Discovered during #641 (backend Sentry + JSON logging fix).

Changes

File Change
frontend/Dockerfile ARG VITE_SENTRY_DSN + ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN before npm run build
docker-compose.prod.yml build.args.VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-} on the frontend service
.gitea/workflows/nightly.yml VITE_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }} added to env file writer

The :- empty fallback ensures deploys succeed before the secret is configured.

Security note

VITE_SENTRY_DSN is a write-only ingest key — it can POST events to GlitchTip but cannot read them. It is safe to include in the client JS bundle per the Sentry security model. It is passed as a Docker build arg (not a runtime env var), so it is absent from the production image's environment layer.

Required action after merge

Set Gitea repository secret VITE_SENTRY_DSN = https://758169b5be8e4d799d09aaca4215036d@glitchtip.archiv.raddatz.cloud/1

Then trigger a manual nightly deploy (or wait for the scheduled run) to pick up the new build arg.

Closes #645

## Problem The frontend Sentry SDK (`hooks.client.ts`, `hooks.server.ts`) reads `import.meta.env.VITE_SENTRY_DSN`, which is a **Vite build-time variable** — baked into the JS bundle at `npm run build`. The frontend `Dockerfile` build stage had no `ARG VITE_SENTRY_DSN`, so Vite never saw the value and the built bundle had `enabled: false` hardcoded. No frontend errors ever reached GlitchTip project #1. Discovered during #641 (backend Sentry + JSON logging fix). ## Changes | File | Change | |---|---| | `frontend/Dockerfile` | `ARG VITE_SENTRY_DSN` + `ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN` before `npm run build` | | `docker-compose.prod.yml` | `build.args.VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-}` on the frontend service | | `.gitea/workflows/nightly.yml` | `VITE_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }}` added to env file writer | The `:-` empty fallback ensures deploys succeed before the secret is configured. ## Security note `VITE_SENTRY_DSN` is a write-only ingest key — it can POST events to GlitchTip but cannot read them. It is safe to include in the client JS bundle per the Sentry security model. It is passed as a Docker **build arg** (not a runtime env var), so it is absent from the production image's environment layer. ## Required action after merge Set Gitea repository secret **`VITE_SENTRY_DSN`** = `https://758169b5be8e4d799d09aaca4215036d@glitchtip.archiv.raddatz.cloud/1` Then trigger a manual nightly deploy (or wait for the scheduled run) to pick up the new build arg. Closes #645
marcel added 1 commit 2026-05-20 09:54:45 +02:00
fix(deploy): wire VITE_SENTRY_DSN as Docker build arg for frontend GlitchTip (#645)
All checks were successful
CI / Backend Unit Tests (pull_request) Successful in 3m18s
CI / fail2ban Regex (pull_request) Successful in 42s
CI / Semgrep Security Scan (pull_request) Successful in 20s
CI / Compose Bucket Idempotency (pull_request) Successful in 1m0s
CI / Unit & Component Tests (push) Successful in 3m19s
CI / OCR Service Tests (push) Successful in 19s
CI / Backend Unit Tests (push) Successful in 3m26s
CI / fail2ban Regex (push) Successful in 43s
CI / Semgrep Security Scan (push) Successful in 18s
CI / Compose Bucket Idempotency (push) Successful in 1m0s
CI / Unit & Component Tests (pull_request) Successful in 3m29s
CI / OCR Service Tests (pull_request) Successful in 19s
cdc3e2e4c8
VITE_SENTRY_DSN is a Vite build-time variable baked into the JS bundle.
Without an ARG/ENV in the Dockerfile build stage and a build.args entry in
docker-compose.prod.yml, the SDK initialised with enabled=false regardless
of the Gitea secret value.

- frontend/Dockerfile: add ARG VITE_SENTRY_DSN + ENV before npm run build
- docker-compose.prod.yml: add build.args.VITE_SENTRY_DSN with empty fallback
- nightly.yml: write VITE_SENTRY_DSN secret into .env.staging

Requires Gitea secret VITE_SENTRY_DSN to be set to the GlitchTip project #1 DSN.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Author
Owner

🏛️ Markus Keller — Software Architect

Verdict: Approved

This PR correctly mirrors the pattern from #641 for the frontend. The root cause analysis is accurate: VITE_SENTRY_DSN is a Vite build-time variable, so the value must reach the npm run build process — a runtime env var would do nothing.

The three-layer fix is correct:

  • ARG VITE_SENTRY_DSN declares the variable receivable from compose
  • ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN promotes it to process environment so Vite reads it via import.meta.env
  • build.args in compose wires it through from the env file
  • nightly.yml writes the secret into the env file

Multi-stage isolation is correct: The production image (FROM node:20.19.0-alpine3.21 AS production) inherits nothing from the build stage's ENV. The DSN is baked into /app/build (the JS bundle), which is copied over — this is the intended behavior for a client-side SDK.

One nuance worth documenting (non-blocking): The DSN is baked into the client JS bundle and therefore visible to any user who inspects network traffic or the bundle source. The PR description already explains why this is acceptable (write-only ingest key, Sentry security model). Good to have this in the PR body for future auditors.

No architectural concerns.

## 🏛️ Markus Keller — Software Architect **Verdict: ✅ Approved** This PR correctly mirrors the pattern from #641 for the frontend. The root cause analysis is accurate: `VITE_SENTRY_DSN` is a Vite build-time variable, so the value must reach the `npm run build` process — a runtime env var would do nothing. **The three-layer fix is correct:** - `ARG VITE_SENTRY_DSN` declares the variable receivable from compose - `ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN` promotes it to process environment so Vite reads it via `import.meta.env` - `build.args` in compose wires it through from the env file - nightly.yml writes the secret into the env file **Multi-stage isolation is correct:** The production image (`FROM node:20.19.0-alpine3.21 AS production`) inherits nothing from the `build` stage's `ENV`. The DSN is baked into `/app/build` (the JS bundle), which is copied over — this is the intended behavior for a client-side SDK. **One nuance worth documenting** (non-blocking): The DSN is baked into the client JS bundle and therefore visible to any user who inspects network traffic or the bundle source. The PR description already explains why this is acceptable (write-only ingest key, Sentry security model). Good to have this in the PR body for future auditors. No architectural concerns.
Author
Owner

👨‍💻 Felix Brandt — Senior Fullstack Developer

Verdict: Approved

Pure config — no application code changed. The fix is correct at every layer.

What I verified in the Dockerfile:

ARG VITE_SENTRY_DSN
ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN

The ARG alone is not enough — Docker ARG values are not available as shell env vars unless explicitly promoted with ENV. Vite reads VITE_* variables from process.env at build time, so the ENV line is load-bearing. This is the correct pattern and it's placed before COPY . . and RUN npm run build as required.

Alignment with existing SDK init (hooks.client.ts, hooks.server.ts):

enabled: !!import.meta.env.VITE_SENTRY_DSN

The !! guard means an empty string (when the secret is not yet set) correctly disables the SDK. Matches the :- empty fallback in compose. Consistent defensive pattern end to end.

Nothing to block on.

## 👨‍💻 Felix Brandt — Senior Fullstack Developer **Verdict: ✅ Approved** Pure config — no application code changed. The fix is correct at every layer. **What I verified in the Dockerfile:** ```dockerfile ARG VITE_SENTRY_DSN ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSN ``` The `ARG` alone is not enough — Docker `ARG` values are not available as shell env vars unless explicitly promoted with `ENV`. Vite reads `VITE_*` variables from `process.env` at build time, so the `ENV` line is load-bearing. This is the correct pattern and it's placed before `COPY . .` and `RUN npm run build` as required. **Alignment with existing SDK init (`hooks.client.ts`, `hooks.server.ts`):** ```typescript enabled: !!import.meta.env.VITE_SENTRY_DSN ``` The `!!` guard means an empty string (when the secret is not yet set) correctly disables the SDK. Matches the `:-` empty fallback in compose. Consistent defensive pattern end to end. Nothing to block on.
Author
Owner

🛠️ Tobias Wendt — DevOps / Infrastructure

Verdict: Approved

This is directly in my area. Reviewed carefully.

frontend/Dockerfile — build stage
The ARG + ENV ordering is correct: declared after FROM node:20.19.0-alpine3.21 AS build, before COPY and RUN npm run build. Vite reads VITE_* from process environment at build time — the ENV promotion is required and correct.

Multi-stage build isolation works in our favour here: the production stage (FROM node:20.19.0-alpine3.21 AS production) has no ENV VITE_SENTRY_DSN. The DSN is baked into the /app/build bundle (copied via COPY --from=build), which is exactly what we want. The runtime image environment is clean.

docker-compose.prod.yml — build.args
VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-} is consistent with the existing compose variable style. The :- empty fallback ensures docker compose build succeeds before the secret is configured — no broken staging deploy.

nightly.yml
VITE_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }} added in the same block as SENTRY_DSN. Consistent placement. The DOCKER_BUILDKIT: "1" env var already present on the job ensures --mount=type=cache is honoured; build args work the same under BuildKit.

Required follow-up before the next nightly run:
Set Gitea secret VITE_SENTRY_DSN = https://758169b5be8e4d799d09aaca4215036d@glitchtip.archiv.raddatz.cloud/1

LGTM.

## 🛠️ Tobias Wendt — DevOps / Infrastructure **Verdict: ✅ Approved** This is directly in my area. Reviewed carefully. **`frontend/Dockerfile` — build stage** The `ARG` + `ENV` ordering is correct: declared after `FROM node:20.19.0-alpine3.21 AS build`, before `COPY` and `RUN npm run build`. Vite reads `VITE_*` from process environment at build time — the `ENV` promotion is required and correct. Multi-stage build isolation works in our favour here: the `production` stage (`FROM node:20.19.0-alpine3.21 AS production`) has no `ENV VITE_SENTRY_DSN`. The DSN is baked into the `/app/build` bundle (copied via `COPY --from=build`), which is exactly what we want. The runtime image environment is clean. **`docker-compose.prod.yml` — build.args** `VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-}` is consistent with the existing compose variable style. The `:-` empty fallback ensures `docker compose build` succeeds before the secret is configured — no broken staging deploy. **`nightly.yml`** `VITE_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }}` added in the same block as `SENTRY_DSN`. Consistent placement. The `DOCKER_BUILDKIT: "1"` env var already present on the job ensures `--mount=type=cache` is honoured; build args work the same under BuildKit. **Required follow-up before the next nightly run:** Set Gitea secret `VITE_SENTRY_DSN` = `https://758169b5be8e4d799d09aaca4215036d@glitchtip.archiv.raddatz.cloud/1` LGTM.
Author
Owner

📋 Elicit — Requirements Engineer

Verdict: Approved

Checking against issue #645 requirements:

Requirement Addressed?
VITE_SENTRY_DSN reaches Vite at build time ARG + ENV in Dockerfile build stage before npm run build
Frontend JS errors surface in GlitchTip project #1 Unblocked — SDK enabled condition now resolves to true when secret is set
Deploy succeeds when VITE_SENTRY_DSN is not yet configured ${VITE_SENTRY_DSN:-} empty fallback; SDK initialises with enabled: false
DSN absent from production image runtime env Multi-stage build — ENV only exists in the build stage
No hardcoded secrets Uses ${{ secrets.VITE_SENTRY_DSN }} Gitea secret

All three acceptance criteria from the issue are addressed. Fix is minimal and precisely scoped.

Open action (outside PR scope, documented in PR description): Set Gitea secret VITE_SENTRY_DSN before the next nightly deploy.

## 📋 Elicit — Requirements Engineer **Verdict: ✅ Approved** Checking against issue #645 requirements: | Requirement | Addressed? | |---|---| | `VITE_SENTRY_DSN` reaches Vite at build time | ✅ `ARG` + `ENV` in Dockerfile build stage before `npm run build` | | Frontend JS errors surface in GlitchTip project #1 | ✅ Unblocked — SDK `enabled` condition now resolves to `true` when secret is set | | Deploy succeeds when `VITE_SENTRY_DSN` is not yet configured | ✅ `${VITE_SENTRY_DSN:-}` empty fallback; SDK initialises with `enabled: false` | | DSN absent from production image runtime env | ✅ Multi-stage build — `ENV` only exists in the `build` stage | | No hardcoded secrets | ✅ Uses `${{ secrets.VITE_SENTRY_DSN }}` Gitea secret | All three acceptance criteria from the issue are addressed. Fix is minimal and precisely scoped. **Open action** (outside PR scope, documented in PR description): Set Gitea secret `VITE_SENTRY_DSN` before the next nightly deploy.
Author
Owner

🔒 Nora "NullX" Steiner — Security Expert

Verdict: Approved

Reviewed with focus on secret handling and the "DSN in the bundle" exposure model.

DSN exposure in the client JS bundle
VITE_SENTRY_DSN is baked into the client-side bundle at build time. This means anyone who inspects the network traffic or the JS source can extract the DSN value. The PR description correctly frames this:

"VITE_SENTRY_DSN is a write-only ingest key — it can POST events to GlitchTip but cannot read them."

This is the standard GlitchTip/Sentry security model for client-side SDKs. The DSN format https://<ingest-key>@host/<project> gives an attacker the ability to submit fake error events (noise, not data exfiltration). This is a known, accepted trade-off by design. No concern.

Docker build arg leakage

  • The ENV VITE_SENTRY_DSN=... in the build stage could theoretically appear in docker history --no-trunc for the intermediate build image. However:
    1. The intermediate build image is not pushed or published
    2. The final production image (production stage) inherits no ENV from build
    3. The DSN value is already public in the bundle, so intermediate image leakage is moot
  • No concern.

Secret injection

  • ${{ secrets.VITE_SENTRY_DSN }} — correct, not hardcoded
  • ${VITE_SENTRY_DSN:-} — safe empty fallback, no crash on missing value
  • sendDefaultPii: false already in hooks.client.ts — good, PII is not sent with events

No security blockers.

## 🔒 Nora "NullX" Steiner — Security Expert **Verdict: ✅ Approved** Reviewed with focus on secret handling and the "DSN in the bundle" exposure model. **DSN exposure in the client JS bundle** `VITE_SENTRY_DSN` is baked into the client-side bundle at build time. This means anyone who inspects the network traffic or the JS source can extract the DSN value. The PR description correctly frames this: > "VITE_SENTRY_DSN is a write-only ingest key — it can POST events to GlitchTip but cannot read them." This is the standard GlitchTip/Sentry security model for client-side SDKs. The DSN format `https://<ingest-key>@host/<project>` gives an attacker the ability to submit fake error events (noise, not data exfiltration). This is a known, accepted trade-off by design. No concern. **Docker build arg leakage** - The `ENV VITE_SENTRY_DSN=...` in the `build` stage could theoretically appear in `docker history --no-trunc` for the intermediate build image. However: 1. The intermediate `build` image is not pushed or published 2. The final production image (`production` stage) inherits no ENV from `build` 3. The DSN value is already public in the bundle, so intermediate image leakage is moot - No concern. **Secret injection** - `${{ secrets.VITE_SENTRY_DSN }}` — correct, not hardcoded - `${VITE_SENTRY_DSN:-}` — safe empty fallback, no crash on missing value - `sendDefaultPii: false` already in `hooks.client.ts` — good, PII is not sent with events No security blockers.
Author
Owner

🧪 Sara Holt — QA / Tester

Verdict: Approved

Infrastructure config change — no unit/integration tests applicable. Validation is deployment-time.

Test plan for after merge + nightly deploy:

  1. Verify DSN is baked into the frontend bundle

    docker run --rm familienarchiv/frontend:nightly \
      sh -c "grep -r 'glitchtip.archiv.raddatz.cloud' /app/build" | head -3
    

    Expected: one or more matches in the minified JS files

  2. Verify SDK is active (not disabled)

    • Open staging in browser → DevTools → Network tab
    • Trigger a known frontend error (e.g., navigate to a broken route)
    • Look for a POST to https://glitchtip.archiv.raddatz.cloud/api/...
  3. Verify GlitchTip project #1 receives events

    • Check https://glitchtip.archiv.raddatz.cloud → Project 1 (frontend)
    • After any client-side error occurs in staging, an event should appear within ~30 seconds
  4. Verify deploy succeeds when secret is absent (already proven by :- fallback, but worth confirming in a clean run)

Note: The VITE_SENTRY_DSN Gitea secret must be set before step 1 will produce a match.

No test coverage gap — this is config wiring, not business logic. The existing hooks.client.test.ts and hooks.server.test.ts test the SDK integration at the logic level.

## 🧪 Sara Holt — QA / Tester **Verdict: ✅ Approved** Infrastructure config change — no unit/integration tests applicable. Validation is deployment-time. **Test plan for after merge + nightly deploy:** 1. **Verify DSN is baked into the frontend bundle** ```bash docker run --rm familienarchiv/frontend:nightly \ sh -c "grep -r 'glitchtip.archiv.raddatz.cloud' /app/build" | head -3 ``` Expected: one or more matches in the minified JS files 2. **Verify SDK is active (not disabled)** - Open staging in browser → DevTools → Network tab - Trigger a known frontend error (e.g., navigate to a broken route) - Look for a POST to `https://glitchtip.archiv.raddatz.cloud/api/...` 3. **Verify GlitchTip project #1 receives events** - Check `https://glitchtip.archiv.raddatz.cloud` → Project 1 (frontend) - After any client-side error occurs in staging, an event should appear within ~30 seconds 4. **Verify deploy succeeds when secret is absent** (already proven by `:-` fallback, but worth confirming in a clean run) **Note:** The `VITE_SENTRY_DSN` Gitea secret must be set before step 1 will produce a match. No test coverage gap — this is config wiring, not business logic. The existing `hooks.client.test.ts` and `hooks.server.test.ts` test the SDK integration at the logic level.
Author
Owner

🎨 Leonie Voss — UI / UX Expert

Verdict: Approved

Pure Dockerfile and CI config change — no frontend components, no Svelte files, no UI behavior affected. Users will not see any visible change from this PR.

The indirect benefit is that frontend JavaScript errors (unhandled exceptions, SvelteKit handleError events) will now surface in GlitchTip, giving faster feedback loops when client-side regressions occur. That improves the overall experience quality for users indirectly.

Nothing to flag from a UI/UX perspective. LGTM.

## 🎨 Leonie Voss — UI / UX Expert **Verdict: ✅ Approved** Pure Dockerfile and CI config change — no frontend components, no Svelte files, no UI behavior affected. Users will not see any visible change from this PR. The indirect benefit is that frontend JavaScript errors (unhandled exceptions, SvelteKit `handleError` events) will now surface in GlitchTip, giving faster feedback loops when client-side regressions occur. That improves the overall experience quality for users indirectly. Nothing to flag from a UI/UX perspective. LGTM.
marcel merged commit cdc3e2e4c8 into main 2026-05-20 10:59:26 +02:00
marcel deleted branch fix/issue-645-vite-sentry-dsn-build-arg 2026-05-20 10:59:27 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: marcel/familienarchiv#646