fix(deploy): wire VITE_SENTRY_DSN as Docker build arg so frontend errors reach GlitchTip #646
Reference in New Issue
Block a user
Delete Branch "fix/issue-645-vite-sentry-dsn-build-arg"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The frontend Sentry SDK (
hooks.client.ts,hooks.server.ts) readsimport.meta.env.VITE_SENTRY_DSN, which is a Vite build-time variable — baked into the JS bundle atnpm run build. The frontendDockerfilebuild stage had noARG VITE_SENTRY_DSN, so Vite never saw the value and the built bundle hadenabled: falsehardcoded. No frontend errors ever reached GlitchTip project #1.Discovered during #641 (backend Sentry + JSON logging fix).
Changes
frontend/DockerfileARG VITE_SENTRY_DSN+ENV VITE_SENTRY_DSN=$VITE_SENTRY_DSNbeforenpm run builddocker-compose.prod.ymlbuild.args.VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-}on the frontend service.gitea/workflows/nightly.ymlVITE_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }}added to env file writerThe
:-empty fallback ensures deploys succeed before the secret is configured.Security note
VITE_SENTRY_DSNis 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/1Then trigger a manual nightly deploy (or wait for the scheduled run) to pick up the new build arg.
Closes #645
🏛️ 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_DSNis a Vite build-time variable, so the value must reach thenpm run buildprocess — a runtime env var would do nothing.The three-layer fix is correct:
ARG VITE_SENTRY_DSNdeclares the variable receivable from composeENV VITE_SENTRY_DSN=$VITE_SENTRY_DSNpromotes it to process environment so Vite reads it viaimport.meta.envbuild.argsin compose wires it through from the env fileMulti-stage isolation is correct: The production image (
FROM node:20.19.0-alpine3.21 AS production) inherits nothing from thebuildstage'sENV. 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.
👨💻 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:
The
ARGalone is not enough — DockerARGvalues are not available as shell env vars unless explicitly promoted withENV. Vite readsVITE_*variables fromprocess.envat build time, so theENVline is load-bearing. This is the correct pattern and it's placed beforeCOPY . .andRUN npm run buildas required.Alignment with existing SDK init (
hooks.client.ts,hooks.server.ts):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.
🛠️ Tobias Wendt — DevOps / Infrastructure
Verdict: ✅ Approved
This is directly in my area. Reviewed carefully.
frontend/Dockerfile— build stageThe
ARG+ENVordering is correct: declared afterFROM node:20.19.0-alpine3.21 AS build, beforeCOPYandRUN npm run build. Vite readsVITE_*from process environment at build time — theENVpromotion is required and correct.Multi-stage build isolation works in our favour here: the
productionstage (FROM node:20.19.0-alpine3.21 AS production) has noENV VITE_SENTRY_DSN. The DSN is baked into the/app/buildbundle (copied viaCOPY --from=build), which is exactly what we want. The runtime image environment is clean.docker-compose.prod.yml— build.argsVITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-}is consistent with the existing compose variable style. The:-empty fallback ensuresdocker compose buildsucceeds before the secret is configured — no broken staging deploy.nightly.ymlVITE_SENTRY_DSN=${{ secrets.VITE_SENTRY_DSN }}added in the same block asSENTRY_DSN. Consistent placement. TheDOCKER_BUILDKIT: "1"env var already present on the job ensures--mount=type=cacheis 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/1LGTM.
📋 Elicit — Requirements Engineer
Verdict: ✅ Approved
Checking against issue #645 requirements:
VITE_SENTRY_DSNreaches Vite at build timeARG+ENVin Dockerfile build stage beforenpm run buildenabledcondition now resolves totruewhen secret is setVITE_SENTRY_DSNis not yet configured${VITE_SENTRY_DSN:-}empty fallback; SDK initialises withenabled: falseENVonly exists in thebuildstage${{ secrets.VITE_SENTRY_DSN }}Gitea secretAll 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_DSNbefore the next nightly deploy.🔒 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_DSNis 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: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
ENV VITE_SENTRY_DSN=...in thebuildstage could theoretically appear indocker history --no-truncfor the intermediate build image. However:buildimage is not pushed or publishedproductionstage) inherits no ENV frombuildSecret injection
${{ secrets.VITE_SENTRY_DSN }}— correct, not hardcoded${VITE_SENTRY_DSN:-}— safe empty fallback, no crash on missing valuesendDefaultPii: falsealready inhooks.client.ts— good, PII is not sent with eventsNo security blockers.
🧪 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:
Verify DSN is baked into the frontend bundle
Expected: one or more matches in the minified JS files
Verify SDK is active (not disabled)
https://glitchtip.archiv.raddatz.cloud/api/...Verify GlitchTip project #1 receives events
https://glitchtip.archiv.raddatz.cloud→ Project 1 (frontend)Verify deploy succeeds when secret is absent (already proven by
:-fallback, but worth confirming in a clean run)Note: The
VITE_SENTRY_DSNGitea 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.tsandhooks.server.test.tstest the SDK integration at the logic level.🎨 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
handleErrorevents) 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.