Skip to content

Commit a4ad0b9

Browse files
authored
feat(observability): auto-wire SENTRY_DSN + VITE_SENTRY_DSN from GlitchTip (#56)
`dev.sh up -d` on the dev stack now spawns scripts/glitchtip-fetch-dsn.sh in the background. The script waits for glitchtip-web to be ready, pulls the DSN for the auto-created API and Frontend projects via manage.py shell (no API auth dance — straight to the Django ORM), writes them to compose/.env, and recreates api-dev + ui-dev so the new env values land in the running containers. Idempotent: only writes when the .env value is empty. A non-empty existing value is treated as deliberate operator intent (hosted Sentry, external GlitchTip) and left alone. To re-wire after a Postgres volume wipe, clear the two lines in compose/.env and re-run the script. Removes the manual "open GlitchTip, copy DSN, paste into env, restart" ritual from first boot. Sentry init no-ops on empty DSN, so a partial or failed wire still leaves the stack bootable — the failure mode is "errors don't ship", same as before. Other touches: - docker-compose.yml api-dev gets SENTRY_DSN env passthrough; ui-dev gets VITE_SENTRY_DSN — neither was previously plumbed from compose/.env. - compose/.env.example documents both vars and the auto-wire behaviour. - infra/compose/docs/glitchtip.md and the docs site error-tracking page drop the manual paste step; the SDK integration section now reflects that init is already wired in the templates (not a snippet the operator copies in). - Script uses Docker label filters (project + service) rather than `docker compose ps -q` because glitchtip-web is defined in the overlay, not the base docker-compose.yml — label lookup is overlay-agnostic and project-scoped.
1 parent 137c28b commit a4ad0b9

6 files changed

Lines changed: 269 additions & 16 deletions

File tree

apps/docs/src/content/docs/topics/error-tracking.mdx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,11 @@ The SDKs don't know which one they're talking to. Choosing is a one-env-var chan
4444
app already uses. `WITH_GLITCHTIP=0` opts out.
4545
</FaqItem>
4646
<FaqItem title="Empty DSN: SDK is a no-op">
47-
Tests do not ship error reports. Once you copy a project DSN from
48-
the GlitchTip UI into `SENTRY_DSN` / `VITE_SENTRY_DSN`, error
49-
capture starts.
47+
Tests do not ship error reports. The dev stack auto-wires
48+
`SENTRY_DSN` and `VITE_SENTRY_DSN` from the bundled GlitchTip
49+
(`scripts/glitchtip-fetch-dsn.sh`, called by `dev.sh up -d`), so
50+
first boot lands ready-to-ship; pre-set values are left alone so
51+
you can point at hosted Sentry instead.
5052
</FaqItem>
5153
<FaqItem title="Replay-on-error on, full-session replays off">
5254
Captures the broken flow without storing healthy sessions.
@@ -143,7 +145,7 @@ GlitchTip is Apache-licensed and Sentry-API-compatible. It runs as part of the d
143145
WITH_GLITCHTIP=0 ./dev.sh up -d # opt out if you'd rather not run it
144146
```
145147

146-
First boot bootstraps a superuser (`admin@localhost` / `admin123456`), a default org, and two projects (`API` and `Frontend`). Visit `http://glitchtip.localhost`, grab each project's DSN, and drop them into the matching env vars.
148+
First boot bootstraps a superuser (`admin@localhost` / `admin123456`), a default org, and two projects (`API` and `Frontend`). `dev.sh up -d` then runs `scripts/glitchtip-fetch-dsn.sh` in the background — it pulls the DSNs out of GlitchTip's Django ORM, writes them into `compose/.env` as `SENTRY_DSN` / `VITE_SENTRY_DSN`, and restarts `api-dev` + `ui-dev`. Visit `http://glitchtip.localhost` to browse events; the DSN paste step is gone.
147149

148150
The overlay reuses the base stack's Postgres (in a separate `glitchtip` database) and Valkey (DB 1). Adding GlitchTip costs two extra containers, not a separate database server.
149151

infra/compose/compose/.env.example

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,16 @@ GRAFANA_ADMIN_PASSWORD=change-me
128128
# GLITCHTIP_SUPERUSER_PASSWORD=admin123456
129129
# GLITCHTIP_DEFAULT_ORG_NAME=Local
130130
# GLITCHTIP_DEFAULT_PROJECTS=API,Frontend
131+
#
132+
# DSNs — auto-wired by `scripts/glitchtip-fetch-dsn.sh`, which `dev.sh up -d`
133+
# runs in the background on the dev stack. The script reads the DSNs from
134+
# GlitchTip's Django ORM and writes them here, then restarts api-dev / ui-dev
135+
# so they pick up the values. DSNs are public keys (they ship in the browser
136+
# bundle), not secrets — safe to keep in this file even if it's committed.
137+
# Set them by hand if you'd rather skip the auto-wire (or are pointing at
138+
# hosted Sentry / a different GlitchTip):
139+
# SENTRY_DSN= # consumed by api-dev (and api in prod)
140+
# VITE_SENTRY_DSN= # consumed by ui-dev (and ui in prod build)
131141

132142
# --- Mailpit (optional, dev-only, WITH_MAILPIT=1) -----------------------
133143
# Local SMTP catcher. Captures emails sent by the api when EMAIL_PROVIDER=smtp

infra/compose/compose/dev.sh

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,25 @@ if [[ $# -eq 0 ]]; then
117117
set -- up -d
118118
fi
119119

120-
exec docker compose "${COMPOSE_FILES[@]}" "${PROFILE_ARGS[@]}" "$@"
120+
# Auto-wire GlitchTip → API/UI DSNs after a detached `up` on the dev stack.
121+
# Skipped for prod (operator owns those DSNs), smoke (no GlitchTip), and
122+
# non-up actions (down/logs/ps/etc.) which run a pass-through compose call.
123+
WIRE_GLITCHTIP=0
124+
if [[ "$STACK" == "dev" && "$WITH_GLITCHTIP" == "1" && "${1:-}" == "up" ]]; then
125+
for arg in "$@"; do
126+
if [[ "$arg" == "-d" || "$arg" == "--detach" ]]; then
127+
WIRE_GLITCHTIP=1
128+
break
129+
fi
130+
done
131+
fi
132+
133+
if [[ "$WIRE_GLITCHTIP" == "1" ]]; then
134+
docker compose "${COMPOSE_FILES[@]}" "${PROFILE_ARGS[@]}" "$@"
135+
# Background — GlitchTip's first-boot bootstrap can take a minute and
136+
# we don't want to hold the dev loop. The fetch script is idempotent
137+
# and silent when wiring is already done.
138+
( "$ROOT/../scripts/glitchtip-fetch-dsn.sh" --quiet & ) >/dev/null 2>&1
139+
else
140+
exec docker compose "${COMPOSE_FILES[@]}" "${PROFILE_ARGS[@]}" "$@"
141+
fi

infra/compose/compose/docker-compose.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,10 @@ services:
242242
# `tempo` service over the shared `backend` network.
243243
OTEL_EXPORTER_OTLP_ENDPOINT: ${API_DEV_OTEL_EXPORTER_OTLP_ENDPOINT:-http://tempo:4318}
244244
OTEL_SERVICE_NAME: ${API_DEV_OTEL_SERVICE_NAME:-boringstack-api-dev}
245+
# Auto-wired by scripts/glitchtip-fetch-dsn.sh after first up; empty
246+
# until that script writes a value into compose/.env. Sentry init
247+
# is a no-op when empty, so an unwired stack still boots cleanly.
248+
SENTRY_DSN: ${SENTRY_DSN:-}
245249
depends_on:
246250
postgres:
247251
condition: service_healthy
@@ -293,6 +297,10 @@ services:
293297
CI: "true"
294298
VITE_API_URL: ""
295299
VITE_API_PROXY_TARGET: http://api-dev:7330
300+
# Auto-wired by scripts/glitchtip-fetch-dsn.sh after first up; empty
301+
# until that script writes a value into compose/.env. Vite picks
302+
# this up at dev-server startup and exposes it as import.meta.env.
303+
VITE_SENTRY_DSN: ${VITE_SENTRY_DSN:-}
296304
depends_on:
297305
api-dev:
298306
condition: service_healthy

infra/compose/docs/glitchtip.md

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@ That's it. `dev.sh` auto-seeds a dev-only `GLITCHTIP_SECRET_KEY` if you don't se
1717
- creates the superuser `admin@localhost` / `admin123456` (override via `GLITCHTIP_SUPERUSER_*` in `.env`)
1818
- creates a default organization (`Local`) with two projects (`API`, `Frontend`)
1919

20-
Visit **http://glitchtip.localhost** and log in. Each project has a DSN under `Settings → Client Keys`. Copy them into the the API app's `.env` (as `SENTRY_DSN`) and the the UI app's `.env.local` (as `VITE_SENTRY_DSN`).
20+
**DSNs are auto-wired.** `dev.sh up -d` runs `scripts/glitchtip-fetch-dsn.sh` in the background once GlitchTip is up. The script reads the DSNs for the `API` and `Frontend` projects from GlitchTip's Django ORM, writes them to `compose/.env` as `SENTRY_DSN` and `VITE_SENTRY_DSN`, and restarts `api-dev` + `ui-dev` so they pick the values up. Re-runs are idempotent — the script exits silently when the wiring already matches.
21+
22+
Visit **http://glitchtip.localhost** to log in if you want to browse events, configure alerts, or rotate the superuser password. The DSN copy-paste step from older docs is gone.
23+
24+
If you'd rather point at hosted Sentry or a different GlitchTip, set `SENTRY_DSN` / `VITE_SENTRY_DSN` manually in `compose/.env` before `up`. The script only writes when the .env value is empty — any non-empty value is treated as deliberate and left alone. To rewire after wiping the GlitchTip Postgres volume, clear those two lines and re-run `./scripts/glitchtip-fetch-dsn.sh`.
2125

2226
## How it's wired
2327

@@ -57,18 +61,11 @@ The `/api/*` router has **no Basic Auth** so client SDKs can POST events without
5761

5862
## SDK integration
5963

60-
**API (apps/api)**`bun add @sentry/bun` (or use the the API app's existing Sentry middleware):
61-
```ts
62-
import * as Sentry from "@sentry/bun";
64+
The SDKs are already wired in both apps; the auto-wire above gives them DSNs to talk to.
6365

64-
Sentry.init({
65-
dsn: process.env.SENTRY_DSN, // from GlitchTip → Project → Client Keys
66-
tracesSampleRate: 1.0,
67-
environment: process.env.NODE_ENV,
68-
});
69-
```
66+
**API (apps/api)**`@sentry/bun` is initialised in `apps/api/src/config/sentry/sentry.ts`. Reads `SENTRY_DSN` from env; no-op when empty. `tracesSampleRate` defaults to `0` (env-tunable via `SENTRY_TRACES_SAMPLE_RATE`) — OTel ships traces to Tempo, Sentry stays error-capture-only.
7067

71-
**UI (apps/ui)**uses `@sentry/react` and reads `VITE_SENTRY_DSN` from `src/lib/env`. Just set the DSN; the existing wiring sends events to whichever endpoint that DSN points at.
68+
**UI (apps/ui)**`@sentry/react` is initialised in `apps/ui/src/app/main.tsx`. Reads `VITE_SENTRY_DSN` at dev-server start (or build time for the prod bundle); no-op when empty. Same posture: `tracesSampleRate: 0`, error capture only, trace IDs propagate via `browserTracingIntegration` so server-side spans land in Tempo with the same ID.
7269

7370
## Verifying ingestion
7471

Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
#!/usr/bin/env bash
2+
# Auto-wire SENTRY_DSN + VITE_SENTRY_DSN from GlitchTip into compose/.env.
3+
#
4+
# What it does:
5+
# 1. Waits for the `glitchtip-web` container to be healthy.
6+
# 2. Pulls the DSN for the auto-created `API` and `Frontend` projects
7+
# via `manage.py shell` (no API auth dance — talks to Django direct).
8+
# 3. Writes / updates SENTRY_DSN and VITE_SENTRY_DSN in compose/.env.
9+
# 4. Restarts api-dev + ui-dev so they pick up the new env on next request.
10+
#
11+
# Idempotent: skips writing if the .env values already match the live DSNs.
12+
# Safe to invoke from dev.sh on every `up` — does nothing on subsequent
13+
# runs once .env is wired.
14+
#
15+
# Usage:
16+
# ./scripts/glitchtip-fetch-dsn.sh # foreground, verbose
17+
# ./scripts/glitchtip-fetch-dsn.sh --quiet # background-friendly, less output
18+
#
19+
# DSNs are public keys (they ship in the browser bundle), not secrets — safe
20+
# to write to compose/.env even if that file is committed.
21+
22+
set -euo pipefail
23+
24+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
25+
COMPOSE_DIR="$ROOT/compose"
26+
ENV_FILE="$COMPOSE_DIR/.env"
27+
28+
QUIET=0
29+
if [[ "${1:-}" == "--quiet" ]]; then
30+
QUIET=1
31+
fi
32+
33+
log() {
34+
if [[ "$QUIET" == "0" ]]; then
35+
echo "[glitchtip-dsn] $*"
36+
fi
37+
}
38+
39+
warn() {
40+
echo "[glitchtip-dsn] $*" >&2
41+
}
42+
43+
cd "$COMPOSE_DIR"
44+
45+
# Project name is fixed via docker-compose.yml's top-level `name:` directive.
46+
# Don't hard-code that string — `docker compose config` is the source of
47+
# truth (also handles COMPOSE_PROJECT_NAME overrides). Falls back to the
48+
# directory name if config is unparseable for some reason.
49+
PROJECT_NAME="$(docker compose config --format json 2>/dev/null \
50+
| grep -o '"name":[[:space:]]*"[^"]*"' | head -n1 \
51+
| sed 's/.*"name":[[:space:]]*"\([^"]*\)".*/\1/' \
52+
|| true)"
53+
PROJECT_NAME="${PROJECT_NAME:-$(basename "$COMPOSE_DIR")}"
54+
55+
# Resolve glitchtip-web by Docker compose labels rather than `docker compose
56+
# ps -q`, because `ps` validates the service against the *loaded* compose
57+
# files and the base docker-compose.yml doesn't define glitchtip-web — the
58+
# overlay does. Label lookup is project-scoped and overlay-agnostic.
59+
WEB_CONTAINER="$(docker ps -q \
60+
--filter "label=com.docker.compose.project=${PROJECT_NAME}" \
61+
--filter "label=com.docker.compose.service=glitchtip-web" \
62+
| head -n1)"
63+
64+
if [[ -z "$WEB_CONTAINER" ]]; then
65+
log "glitchtip-web is not running — skipping auto-wire."
66+
exit 0
67+
fi
68+
69+
# GlitchTip Celery/Django boot takes ~30–60s on a cold start. Poll the
70+
# container's `manage.py check --database default` until it succeeds — that
71+
# proves migrations have applied and the ORM is reachable.
72+
log "Waiting for glitchtip-web to be ready..."
73+
DEADLINE=$(( $(date +%s) + 180 ))
74+
while (( $(date +%s) < DEADLINE )); do
75+
if docker exec "$WEB_CONTAINER" ./manage.py check --database default >/dev/null 2>&1; then
76+
break
77+
fi
78+
sleep 3
79+
done
80+
81+
if ! docker exec "$WEB_CONTAINER" ./manage.py check --database default >/dev/null 2>&1; then
82+
warn "glitchtip-web didn't become ready within 180s — skipping auto-wire."
83+
warn "Re-run later with: ./scripts/glitchtip-fetch-dsn.sh"
84+
exit 0
85+
fi
86+
87+
# Fetch DSNs via the Django ORM. Two import paths are tried because GlitchTip
88+
# moved its `projects` app between releases. The shell script prints
89+
# `<ProjectName>=<dsn>` lines or `__missing__=<ProjectName>` if a project
90+
# doesn't exist yet (it usually does — GLITCHTIP_DEFAULT_PROJECTS creates
91+
# `API` and `Frontend` on first boot of an empty DB).
92+
log "Fetching project DSNs from GlitchTip..."
93+
PYTHON_PROBE=$(cat <<'PYEOF'
94+
import sys
95+
try:
96+
from apps.projects.models import Project
97+
except Exception:
98+
try:
99+
from glitchtip.projects.models import Project
100+
except Exception:
101+
try:
102+
from projects.models import Project
103+
except Exception as e:
104+
sys.stderr.write(f"could not import Project model: {e}\n")
105+
sys.exit(2)
106+
107+
wanted = ["API", "Frontend"]
108+
for name in wanted:
109+
p = Project.objects.filter(name=name).first()
110+
if not p:
111+
print(f"__missing__={name}")
112+
continue
113+
key = p.projectkey_set.first()
114+
if not key:
115+
print(f"__nokey__={name}")
116+
continue
117+
print(f"{name}={key.get_dsn()}")
118+
PYEOF
119+
)
120+
121+
OUTPUT="$(docker exec "$WEB_CONTAINER" ./manage.py shell -c "$PYTHON_PROBE" 2>/dev/null || true)"
122+
123+
if [[ -z "$OUTPUT" ]]; then
124+
warn "manage.py shell returned no output — model import probably failed."
125+
warn "Open an issue or run manually:"
126+
warn " docker compose exec glitchtip-web ./manage.py shell"
127+
exit 0
128+
fi
129+
130+
API_DSN=""
131+
UI_DSN=""
132+
while IFS= read -r line; do
133+
case "$line" in
134+
API=*) API_DSN="${line#API=}" ;;
135+
Frontend=*) UI_DSN="${line#Frontend=}" ;;
136+
__missing__=*) warn "GlitchTip project '${line#__missing__=}' not found — bootstrap may not have completed." ;;
137+
__nokey__=*) warn "GlitchTip project '${line#__nokey__=}' exists but has no key — odd, inspect manually." ;;
138+
esac
139+
done <<< "$OUTPUT"
140+
141+
if [[ -z "$API_DSN" || -z "$UI_DSN" ]]; then
142+
warn "Could not resolve both DSNs (API=${API_DSN:-<missing>} UI=${UI_DSN:-<missing>})."
143+
warn "Re-run later once GlitchTip has finished its first-boot bootstrap."
144+
exit 0
145+
fi
146+
147+
# Policy: only write when the .env value is currently empty / missing. A
148+
# non-empty existing value is the operator's choice — they may have pointed
149+
# the stack at hosted Sentry or a different GlitchTip instance, and the
150+
# auto-wire shouldn't silently overwrite that. The trade-off: once a DSN is
151+
# in place, this script never touches it again, even if it has gone stale
152+
# (e.g. the GlitchTip Postgres volume was wiped). In that case clear the
153+
# line in compose/.env and re-run; the script will repopulate it.
154+
touch "$ENV_FILE"
155+
CUR_API_DSN="$(grep -E '^SENTRY_DSN=' "$ENV_FILE" | tail -n1 | sed 's/^SENTRY_DSN=//' || true)"
156+
CUR_UI_DSN="$(grep -E '^VITE_SENTRY_DSN=' "$ENV_FILE" | tail -n1 | sed 's/^VITE_SENTRY_DSN=//' || true)"
157+
158+
WROTE_ANY=0
159+
upsert_env_if_empty() {
160+
local key="$1" value="$2" cur="$3" file="$4"
161+
if [[ -n "$cur" ]]; then
162+
log "$key already set — leaving as-is (operator override)."
163+
return
164+
fi
165+
if grep -qE "^${key}=" "$file"; then
166+
# Key is present but empty — replace the empty value in-place.
167+
awk -v k="$key" -v v="$value" 'BEGIN{FS=OFS="="} $1==k {$0=k"="v} {print}' "$file" > "${file}.tmp" \
168+
&& mv "${file}.tmp" "$file"
169+
else
170+
printf '\n%s=%s\n' "$key" "$value" >> "$file"
171+
fi
172+
WROTE_ANY=1
173+
}
174+
175+
log "Wiring DSNs in ${ENV_FILE#"$ROOT"/}"
176+
upsert_env_if_empty SENTRY_DSN "$API_DSN" "$CUR_API_DSN" "$ENV_FILE"
177+
upsert_env_if_empty VITE_SENTRY_DSN "$UI_DSN" "$CUR_UI_DSN" "$ENV_FILE"
178+
179+
if [[ "$WROTE_ANY" == "0" ]]; then
180+
log "Both DSNs already set — nothing to do."
181+
exit 0
182+
fi
183+
184+
# Recreate api-dev + ui-dev so they pick up the new env interpolation.
185+
# `docker compose restart` is NOT enough: it stops + starts the existing
186+
# container with the env block that was interpolated from .env when the
187+
# container was first `up`-ed. Only `up -d --force-recreate` re-resolves
188+
# the env at recreate time.
189+
#
190+
# Both services live in the base docker-compose.yml (profiles [dev] /
191+
# [dev, smoke]), so no overlay is needed — `--profile dev` is enough.
192+
RECREATABLE=()
193+
for svc in api-dev ui-dev; do
194+
cid="$(docker ps -q \
195+
--filter "label=com.docker.compose.project=${PROJECT_NAME}" \
196+
--filter "label=com.docker.compose.service=${svc}" \
197+
| head -n1)"
198+
if [[ -n "$cid" ]]; then
199+
RECREATABLE+=("$svc")
200+
fi
201+
done
202+
203+
if (( ${#RECREATABLE[@]} > 0 )); then
204+
log "Recreating ${RECREATABLE[*]} to pick up the new DSNs..."
205+
if docker compose --profile dev up -d --no-deps --force-recreate "${RECREATABLE[@]}" >/dev/null 2>&1; then
206+
log "Recreated: ${RECREATABLE[*]}"
207+
else
208+
warn "Recreate failed — run manually:"
209+
warn " docker compose --profile dev up -d --no-deps --force-recreate ${RECREATABLE[*]}"
210+
fi
211+
else
212+
log "(api-dev / ui-dev not running — they'll read the new env on next up)"
213+
fi
214+
215+
log "Done. Sentry / GlitchTip wiring is live."

0 commit comments

Comments
 (0)