From 09680b8ac489483b223320004c9272ae1d820864 Mon Sep 17 00:00:00 2001 From: Pavlov Alexandr Date: Mon, 15 Jun 2026 21:18:01 +0700 Subject: [PATCH 1/2] feat(secrets): per-app Sentry DSN injection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each app now reports to its own GlitchTip project (org `staging`): server-2 → project 2, replays-fetcher → 3, replay-parser-2 → 4. The renderer reads three distinct env secrets (SENTRY_DSN_SERVER_2 / _REPLAYS_FETCHER / _REPLAY_PARSER_2) and injects each as SENTRY_DSN into the matching -runtime Secret, replacing the single shared SENTRY_DSN. The stale single secret is removed. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/render-staging-secrets.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/scripts/render-staging-secrets.py b/scripts/render-staging-secrets.py index aa95106..e107281 100755 --- a/scripts/render-staging-secrets.py +++ b/scripts/render-staging-secrets.py @@ -42,13 +42,15 @@ def secret(name: str, values: dict[str, str], secret_type: str = "Opaque") -> st s3_access_key_id = required("S3_ACCESS_KEY_ID") s3_secret_access_key = required("S3_SECRET_ACCESS_KEY") -# SENTRY_DSN: GlitchTip project DSN for the app-side error SDK (Phase 18). Optional — -# an empty DSN makes the Sentry SDK a no-op, so app deploys never break before an app -# wires the SDK. Value is the public-URL form https://@errors.solid-stats.ru/1 -# (apps egress to the public edge; the edge->glitchtip ingress is already allowed by the -# Phase 17 netpol). Injected into every app runtime Secret via envFrom. See +# SENTRY_DSN (per-app): each app reports to its OWN GlitchTip project, so each app gets a +# distinct DSN injected as SENTRY_DSN into its runtime Secret. Optional — an empty DSN makes +# the Sentry SDK a no-op, so app deploys never break before an app wires the SDK. Public-URL +# form https://@errors.solid-stats.ru/ (apps egress to the public +# edge; the edge->glitchtip ingress is allowed by the Phase 17 netpol). See # docs/error-sdk-handoff.md and ../plans//SENTRY-WIRE-BRIEF.md. -sentry_dsn = os.environ.get("SENTRY_DSN", "") +sentry_dsn_server2 = os.environ.get("SENTRY_DSN_SERVER_2", "") +sentry_dsn_parser = os.environ.get("SENTRY_DSN_REPLAY_PARSER_2", "") +sentry_dsn_fetcher = os.environ.get("SENTRY_DSN_REPLAYS_FETCHER", "") fetcher_replay_source_url = required("REPLAYS_FETCHER_REPLAY_SOURCE_URL") fetcher_replay_source_transport = os.environ.get("REPLAYS_FETCHER_REPLAY_SOURCE_TRANSPORT", "direct") @@ -104,7 +106,7 @@ def secret(name: str, values: dict[str, str], secret_type: str = "Opaque") -> st "S3_BUCKET": s3_bucket, "S3_ACCESS_KEY_ID": s3_access_key_id, "S3_SECRET_ACCESS_KEY": s3_secret_access_key, - "SENTRY_DSN": sentry_dsn, + "SENTRY_DSN": sentry_dsn_server2, }, ), secret( @@ -114,10 +116,10 @@ def secret(name: str, values: dict[str, str], secret_type: str = "Opaque") -> st "REPLAY_PARSER_S3_BUCKET": s3_bucket, "AWS_ACCESS_KEY_ID": s3_access_key_id, "AWS_SECRET_ACCESS_KEY": s3_secret_access_key, - "SENTRY_DSN": sentry_dsn, + "SENTRY_DSN": sentry_dsn_parser, }, ), - secret("replays-fetcher-runtime", {**fetcher_runtime, "SENTRY_DSN": sentry_dsn}), + secret("replays-fetcher-runtime", {**fetcher_runtime, "SENTRY_DSN": sentry_dsn_fetcher}), ] print("\n---\n".join(documents)) From afced3b787bf44938d30b9a001b9dddd57971d94 Mon Sep 17 00:00:00 2001 From: Pavlov Alexandr Date: Mon, 15 Jun 2026 21:28:46 +0700 Subject: [PATCH 2/2] feat(secrets): per-app Sentry DSN injection (staging) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each app reports to its own GlitchTip project. The staging renderer now reads three per-app DSN secrets (SENTRY_DSN_SERVER_2 / _REPLAYS_FETCHER / _REPLAY_PARSER_2) and injects each as SENTRY_DSN into the matching -runtime Secret, replacing the single shared SENTRY_DSN. Updates error-sdk-handoff.md to the per-app, one-org-per-env model (6 projects: staging 2/3/4, production 5/6/7). Production wiring deferred — no render-production-secrets.py / prod namespace yet; the three production projects exist and reuse the same secret names. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/error-sdk-handoff.md | 71 +++++++++++++++++++++------------------ 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/docs/error-sdk-handoff.md b/docs/error-sdk-handoff.md index 36f6501..26c4283 100644 --- a/docs/error-sdk-handoff.md +++ b/docs/error-sdk-handoff.md @@ -1,55 +1,62 @@ # App-side Error SDK — DSN Handoff (GlitchTip) -How the GlitchTip project DSN reaches the app workloads so an errors-only Sentry SDK can report +How each GlitchTip project DSN reaches its app workload so an errors-only Sentry SDK can report to GlitchTip. Infra owns the wiring; the per-app SDK code is owned in the app repos (wire briefs: `../plans//SENTRY-WIRE-BRIEF.md`). -## The DSN +## Model: one project per app, one org per environment -| Field | Value | -|-------|-------| -| GlitchTip org / project | `solidstats` / `staging` (PROJECT_ID `1`) | -| Public key (non-secret client key) | `e771bce6-706f-4deb-b308-0e4ba12fb233` | -| **DSN the apps use** | `https://e771bce6-706f-4deb-b308-0e4ba12fb233@errors.solid-stats.ru/1` | +Each app reports to its **own** GlitchTip project, so issue lists / DSNs / quotas are separated per +service. Environments are separated at the **organization** level (`staging`, `production`). -The **public-URL** form is used on purpose: app pods in `solid-stats-staging` egress freely to the -public edge, and the edge → glitchtip-web ingress source is the cni0 gateway `10.42.0.1`, which the -Phase 17 NetworkPolicy already allows. So no new NetworkPolicy is required. +| App | Env | GlitchTip org / project (id) | DSN the app uses | +|-----|-----|------------------------------|------------------| +| `server-2` | staging | `staging` / `server-2` (2) | `https://32d59bfbb7ea43e0bba7d91d79405c77@errors.solid-stats.ru/2` | +| `replays-fetcher` | staging | `staging` / `replays-fetcher` (3) | `https://b8337da501bf411c8e8cb454e66e9f28@errors.solid-stats.ru/3` | +| `replay-parser-2` | staging | `staging` / `replay-parser-2` (4) | `https://1c0d52572d6d4813b25329e46f5ea83b@errors.solid-stats.ru/4` | +| `server-2` | production | `production` / `server-2` (5) | `https://5b4b2f26b87c4fafbbeb1d199df077b2@errors.solid-stats.ru/5` | +| `replays-fetcher` | production | `production` / `replays-fetcher` (6) | `https://8d1bd54c772e45f2b0deb916e6d9a9fc@errors.solid-stats.ru/6` | +| `replay-parser-2` | production | `production` / `replay-parser-2` (7) | `https://a78e1215080149bb9e3b216318ae98ee@errors.solid-stats.ru/7` | -> In-cluster alternative (NOT used): `http://e771bce6-…@glitchtip-web.error-tracking.svc:8000/1`. -> This direct path is blocked by the Phase 17 `error-tracking` default-deny ingress (only the edge + -> intra-ns are allowed). Using it would require adding a `from: solid-stats-staging` selector to -> `allow-glitchtip-web-ingress` in `k8s/observability/96-netpol-error-tracking.yaml` (the Phase 18 -> note in that file). Prefer the public URL until there's a reason to switch. +The DSN public key is a non-secret client ingest key. The **public-URL** form is used on purpose: +app pods egress freely to the public edge, and the edge → glitchtip-web ingress source is the cni0 +gateway `10.42.0.1`, which the Phase 17 NetworkPolicy already allows. No new NetworkPolicy is required. -## Handoff chain +## Handoff chain (per app) ``` -GlitchTip project DSN - → GitHub `staging` environment secret SENTRY_DSN - → scripts/render-staging-secrets.py (injects SENTRY_DSN into each -runtime k8s Secret) - → k8s Secret server-2-runtime / replay-parser-2-runtime / replays-fetcher-runtime +GlitchTip project DSN (per app, per env) + → GitHub environment secret SENTRY_DSN_SERVER_2 | SENTRY_DSN_REPLAYS_FETCHER | SENTRY_DSN_REPLAY_PARSER_2 + → scripts/render--secrets.py (injects each into the matching -runtime Secret as SENTRY_DSN) + → k8s Secret server-2-runtime / replays-fetcher-runtime / replay-parser-2-runtime → Deployment/CronJob envFrom: secretRef → env var SENTRY_DSN → app SDK init (Sentry.init / sentry::init reads SENTRY_DSN) ``` -`SENTRY_DSN` is **optional** in the renderer. An empty value makes the Sentry SDK a no-op, so an app -can ship the SDK code before the secret is set (and the secret can be set before the SDK ships) -without breaking deploys. No manifest change is needed — every app workload already pulls its env -via `envFrom: secretRef: -runtime`. +The in-pod env var name stays `SENTRY_DSN` for every app — only the value differs per app. Each DSN +is **optional** in the renderer: an empty value makes the SDK a no-op, so an app can ship the SDK +code before its secret is set (and vice versa) without breaking deploys. No manifest change is +needed — every app workload already pulls its env via `envFrom: secretRef: -runtime`. -## Operator steps (infra side — done in Phase 18) +## Operator steps -```bash -# 1. Set the GitHub staging secret (value = the public-URL DSN above) -gh secret set SENTRY_DSN --env staging --body 'https://e771bce6-…@errors.solid-stats.ru/1' +**Staging — done:** -# 2. Render + apply the runtime Secrets (CI does this on deploy; or live): -SENTRY_DSN=… python3 scripts/render-staging-secrets.py | kubectl apply -f - +```bash +# Three per-app DSN secrets in the GitHub `staging` environment: +gh secret set SENTRY_DSN_SERVER_2 --env staging -R solid-stats/infrastructure --body '…/2' +gh secret set SENTRY_DSN_REPLAYS_FETCHER --env staging -R solid-stats/infrastructure --body '…/3' +gh secret set SENTRY_DSN_REPLAY_PARSER_2 --env staging -R solid-stats/infrastructure --body '…/4' +# render-staging-secrets.py reads them; CI renders + applies the runtime Secrets on deploy. ``` -The env var reaches the pods on their next rollout — no forced restart is needed just to carry an -unused var; it becomes live when an app actually wires the SDK and redeploys. +**Production — pending prod infra.** The three production projects exist (ids 5/6/7) but there is no +`render-production-secrets.py` / production namespace yet. When prod is stood up, set the **same three +secret names** in the GitHub `production` environment (values = the production DSNs above); the prod +renderer reuses the names, so no further change here. + +The env var reaches the pods on their next rollout — it becomes live when an app wires the SDK and +redeploys. ## Errors-only policy (all apps)