RFC-025 · Observability — Sentry for client-side JS errors + Grafana Cloud Agent (env-var-gated, no committed secrets)
Status: Closed (v0.7.x) · 2026-05-22 · Closes into: ADR-067 / ADR-068 · Reuses operator credentials from podcast_scraper RFC-081
Why this is an RFC. Five things bind. (1) Sentry's DSN is fundamentally a build-time public string (it gets baked into the bundle and shipped to every browser), but it is still a credential that ties errors to a specific Sentry project + organisation, and the project's stated posture in the README's Privacy section ("no PII", "aggregate only", "zero analytics on forks by default") has to remain true after Sentry lands or we ship a regression on a load-bearing user promise. (2) The Sentry initialisation has to scrub PII (URLs with query params, IP addresses, breadcrumbs that reach into form fields) before the SDK serialises the event — once an event leaves the browser, the redaction is no longer in our hands. (3) Grafana Cloud Agent has nothing to ship from Orrery today (static SPA on GH Pages → no logs, no metrics surface), but the future VPS RFC (RFC-024 §10) will want it wired the moment we have nginx access logs + scheduled pipeline-runner runs to observe; landing the compose scaffolding now under env-var gates means the VPS RFC inherits a working integration rather than re-litigating it. (4) Every credential in this RFC — Sentry DSN, Grafana Cloud Prometheus / Loki URLs, Grafana Cloud API key — must NEVER be committed to the repo; the integration shape has to make accidental commits structurally hard, not just discouraged. (5) The pattern follows the operator's podcast_scraper observability stack (RFC-081 / Grafana Cloud free tier, same account, same credentials), so the operational burden across both projects is one Sentry org + one Grafana Cloud org + one set of access policies, not two.
1 · Scope + non-scope
In scope (this RFC)
@sentry/sveltekitSDK installation + a thinsrc/lib/observability/sentry.tsinit module.sentry.client.config.ts(or equivalent SvelteKit-side hook) that initialises Sentry only ifPUBLIC_SENTRY_DSNis set at build time. Empty DSN = no-op, no network requests, no console noise.- A
beforeSendPII scrubber that strips URL query params, replaces full URLs with route-only, nullsrequest.headers, drops any breadcrumb whose category isui.input(form fields). sendDefaultPii: false.tracesSampleRate: 0. Errors only — no performance tracing, no Web Vitals (per RFC-025 §scope agreed during design).grafana-agentservice added todocker-compose.ymlunder a newobservabilitycompose profile. Default off; only spins up when explicitly invoked viadocker compose --profile observability up -d grafana-agent.ops/observability/grafana-agent.yamlconfig — scrapes nothing today (no metrics endpoint), tails theweb+pipeline-runnerDocker container logs via the Docker socket discovery pattern from podcast_scraper RFC-081. Ships to Grafana Cloud Loki only ifGRAFANA_CLOUD_LOKI_URL+GRAFANA_CLOUD_API_KEYare set.- Provisioned Grafana dashboards as JSON in
ops/observability/dashboards/— committed (no secrets), imported into the operator's Grafana Cloud org via the dashboards-as-code pattern. - Expand the existing
.env.exampleat repo root — already committed, already documentsANTHROPIC_API_KEY+LL2_PATREON_KEY. Append a new "Observability (RFC-025)" section withPUBLIC_SENTRY_DSN,PUBLIC_SENTRY_ENVIRONMENT,PUBLIC_SENTRY_RELEASE,GRAFANA_CLOUD_LOKI_URL,GRAFANA_CLOUD_LOKI_USER,GRAFANA_CLOUD_API_KEY,GRAFANA_AGENT_ENV— variable names + comments only, no values, no DSNs. - Existing
.gitignorealready covers.env(line 6) +.env.local(line 7). Add.env.production,.env.development,.env.*.localfor completeness (vite reads each variant on the matching mode)..env.examplestays tracked so the template stays committed. scripts/check-no-secrets.ts— pre-commit / preflight scanner that greps the staged diff for Sentry DSN patterns + Grafana Cloud token patterns and fails the commit if any leaked through. Fast and dumb on purpose.- README Privacy section update — one new paragraph documenting Sentry's role + scrubbing + the fork-silent default, mirroring the existing Umami paragraph's honesty.
docs/guides/observability.mdoperator guide — what to set up in Sentry + Grafana Cloud consoles, which env vars to populate in which environment, how to verify the integration is silent in local dev.
Explicitly out of scope (deferred)
- Sentry release tracking + source-map upload — adds a CI step + a Sentry CLI auth token. Useful for production-debugging stack traces, but a follow-up once the base SDK + scrubbing land cleanly. Tracked as §11.
- Sentry performance tracing / Web Vitals. User chose "errors only" during the design pass; revisit only if real-user performance regressions become a recurring problem.
- Cookie-based opt-in consent banner. With
sendDefaultPii: false+ URL-scrubbing + IP-null, Sentry collects no PII — the standard GDPR carve-out for "legitimate interest in service stability" applies. Adding a banner is a separate UX decision that doesn't belong in this RFC. - Grafana metrics scraping. Orrery has no
/metricsendpoint today (no Node backend, nginx default doesn't expose Prometheus). When the future VPS RFC adds nginx-prometheus-exporter, this RFC'sgrafana-agent.yamlgets a scrape_configs block added then. - Synthetic monitoring (uptime probes from Grafana Cloud Synthetic Monitoring). Costs nothing on free tier but configures one production hostname; lives more naturally in the future VPS RFC than here.
- OpenTelemetry / OTLP exporter. Grafana Agent supports OTLP, but Orrery has no traceable workload right now (static SPA, no server). Reconsider when the VPS adds nginx + the pipeline-runner has a daemon mode.
- Self-hosted Grafana / Loki / Prometheus. Container overhead + ops burden, and the operator already uses Grafana Cloud for podcast_scraper. Same posture for Orrery.
2 · Architecture overview
┌────────────────────── browser (chipi.github.io) ──────────────────────┐
│ │
│ SvelteKit app │
│ ↓ │
│ Sentry SDK (@sentry/sveltekit) │
│ if PUBLIC_SENTRY_DSN is "" → no-op (default for forks) │
│ else → init with beforeSend scrubber + sendDefaultPii: false │
│ ↓ HTTPS │
│ Sentry Cloud (saas.sentry.io · operator's org / orrery project) │
│ │
└───────────────────────────────────────────────────────────────────────┘
┌──────────────── docker-compose stack (local dev + future VPS) ────────┐
│ │
│ ┌──────────────┐ ┌─────────────────┐ ┌──────────────────────┐ │
│ │ web │ │ pipeline-runner │ │ grafana-agent │ │
│ │ nginx │ │ (on-demand) │ │ (observability │ │
│ │ │ │ │ │ profile, default │ │
│ │ stdout + │ │ stdout + │ │ off in `up`) │ │
│ │ /var/log │ │ /var/log │ │ │ │
│ └──────┬───────┘ └────────┬────────┘ └──────────┬───────────┘ │
│ │ │ │ │
│ └──── docker logs ────┴──── docker_sd ─────────┘ │
│ ↓ │
│ scrape labels: orrery_* │
│ ↓ │
│ if all GRAFANA_CLOUD_* set: │
│ remote_write → Grafana Cloud │
│ Loki (operator's org) │
│ else: tail only, no ship │
└───────────────────────────────────────────────────────────────────────┘Two independent integrations. Sentry covers the client browser surface (every visitor, immediately useful on GH Pages today). Grafana Agent covers the docker stack (silent today because GH Pages isn't docker-served, will be useful the moment the VPS RFC ships nginx + cron'd pipeline runners). Neither writes anything to disk in the repo; both fail closed when env vars are unset.
3 · The non-negotiable contract: no committed secrets
Everything credential-shaped lives behind an env var. The repo contains:
.env.example— already exists and committed (currently documentsANTHROPIC_API_KEY+LL2_PATREON_KEY). Observability adds an "Observability (RFC-025)" section to it — variable names + comments only. No values, no DSNs, no URLs..gitignore— already excludes.envand.env.local. Add.env.production,.env.development,.env.*.localfor vite-mode-specific variants..env.examplestays tracked.scripts/check-no-secrets.ts— runs innpm run preflightAND in.husky/pre-commit. Greps the staged diff for:- Sentry DSN pattern:
https://[a-f0-9]{32}@[a-z0-9.]+\.ingest\.sentry\.io/[0-9]+ - Sentry public DSN pattern:
https://[a-zA-Z0-9]{32}@o[0-9]+\.ingest\.sentry\.io/[0-9]+ - Grafana Cloud API key pattern:
glc_[A-Za-z0-9+/=]{40,} - Generic high-entropy strings adjacent to
DSN,API_KEY,TOKEN,SECRET(heuristic, with a configurable false-positive allowlist)
- Sentry DSN pattern:
- Fail-closed: any match → exit code non-zero → the commit / push is rejected. No
--no-verifyrecommended path; if the scanner flags a false positive, add it to the allowlist file.
The env vars themselves (per environment):
# Sentry — client-side error reporting
PUBLIC_SENTRY_DSN= # Empty in dev/CI, set in production build only.
PUBLIC_SENTRY_ENVIRONMENT= # 'development' | 'staging' | 'production'
PUBLIC_SENTRY_RELEASE= # Optional: sha-<short> or pkg-version
# Grafana Cloud — log shipping from docker stack
GRAFANA_CLOUD_LOKI_URL= # https://logs-prod-XXX.grafana.net/loki/api/v1/push
GRAFANA_CLOUD_LOKI_USER= # Numeric instance ID
GRAFANA_CLOUD_API_KEY= # glc_...
GRAFANA_AGENT_ENV= # 'local-dev' | 'staging' | 'production-vps'PUBLIC_ prefix on the Sentry vars is the SvelteKit convention for "this gets exposed to the client at build time, treat as public." The Sentry DSN is public by Sentry's own docs (it doesn't authenticate, it identifies — the security model is project-side rate limits + IP allowlists), but documenting it with the PUBLIC_ prefix surfaces the fact that this string ships to every browser.
4 · Sentry — the client-side integration
4.1 · Dependencies
@sentry/sveltekit— adds the SDK + SvelteKit-specific hooks forhandleError(server) and the client init.- That's it. No
@sentry/node(no Node server), no@sentry/vite-pluginfor now (defer source-map upload to §11), no@sentry/replay(session replay collects user UI — would violate the privacy posture).
4.2 · Init module — src/lib/observability/sentry.ts
import * as Sentry from '@sentry/sveltekit';
import { env } from '$env/dynamic/public';
import { dev } from '$app/environment';
/**
* Initialise Sentry client-side. No-op if PUBLIC_SENTRY_DSN is empty —
* which is the default for forks, local dev, CI, and any environment
* the operator hasn't explicitly opted in. Errors only — no
* performance tracing, no Web Vitals, no session replay.
*
* Privacy: sendDefaultPii: false explicitly, beforeSend scrubs URL
* query params and IP, drops UI input breadcrumbs.
*/
export function initSentry(): void {
const dsn = env.PUBLIC_SENTRY_DSN;
if (!dsn) return; // Fork-silent default.
if (dev) return; // Vite dev never reports.
Sentry.init({
dsn,
environment: env.PUBLIC_SENTRY_ENVIRONMENT || 'production',
release: env.PUBLIC_SENTRY_RELEASE || undefined,
tracesSampleRate: 0, // Errors only, no perf tracing.
sendDefaultPii: false, // No IP, no cookies, no user data.
beforeSend(event) {
// Strip URL query string — bookmarkable mission deep-links could
// carry user-typed search text in the future. Route-only is
// enough for triage.
if (event.request?.url) {
try {
const u = new URL(event.request.url);
event.request.url = u.origin + u.pathname;
} catch { /* leave as-is */ }
}
// Null any auto-populated headers (Sentry SDK sometimes attaches
// referer + cookie even with sendDefaultPii: false).
event.request = event.request ? { ...event.request, headers: undefined, cookies: undefined } : undefined;
// Server-side IP scrubbing: Sentry derives IP from the connecting
// TCP socket; setting user.ip_address to '0.0.0.0' tells the
// server to discard it (per Sentry's PII discard rules).
event.user = { ...event.user, ip_address: '0.0.0.0' };
return event;
},
beforeBreadcrumb(crumb) {
// ui.input breadcrumbs contain the typed value — drop them.
if (crumb.category === 'ui.input') return null;
// Navigation breadcrumbs also carry full URLs; strip query.
if (crumb.category === 'navigation' && typeof crumb.data?.to === 'string') {
crumb.data = { ...crumb.data, to: stripQuery(crumb.data.to) };
}
return crumb;
},
});
}
function stripQuery(s: string): string {
try { const u = new URL(s); return u.pathname; } catch { return s; }
}4.3 · Wire-up — src/hooks.client.ts
import { initSentry } from '$lib/observability/sentry';
import { handleErrorWithSentry } from '@sentry/sveltekit';
initSentry();
export const handleError = handleErrorWithSentry();The SvelteKit framework calls handleError on any unhandled error during client-side hydration, route navigation, or load function. handleErrorWithSentry is the SDK's drop-in wrapper that forwards through to Sentry.
4.4 · Why no hooks.server.ts
Orrery uses adapter-static. There is no server runtime. Adding a server hook would be dead code; the SDK's server-side init never executes because the SPA is pre-rendered + served as static files.
4.5 · Build-time DSN injection
@sveltejs/kit reads PUBLIC_* env vars from .env files at build time and inlines them into the client bundle. So:
- Local dev:
npm run devreads.env(gitignored); operator can leavePUBLIC_SENTRY_DSN=empty and Sentry is silent. - Production build (GH Pages deploy):
.github/workflows/preview.ymlsetsPUBLIC_SENTRY_DSN: ${{ secrets.SENTRY_DSN_WEB }}in theenv:block of the build step. The bundle that lands on GH Pages has the DSN baked in. - Fork builds: the secret doesn't exist in their fork → empty env → silent Sentry. The pattern matches Umami exactly.
5 · Grafana Cloud Agent — the docker-side integration
5.1 · New compose service
# docker-compose.yml — new service, profile-gated.
grafana-agent:
image: grafana/agent:v0.43.4
profiles: ['observability']
volumes:
- ./ops/observability/grafana-agent.yaml:/etc/grafana-agent.yaml:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- GRAFANA_CLOUD_LOKI_URL=${GRAFANA_CLOUD_LOKI_URL:-}
- GRAFANA_CLOUD_LOKI_USER=${GRAFANA_CLOUD_LOKI_USER:-}
- GRAFANA_CLOUD_API_KEY=${GRAFANA_CLOUD_API_KEY:-}
- GRAFANA_AGENT_ENV=${GRAFANA_AGENT_ENV:-local-dev}
command: ['-config.file=/etc/grafana-agent.yaml']profiles: ['observability'] means docker compose up -d web does NOT start it. To run with observability locally:
docker compose --profile observability up -dWhen GRAFANA_CLOUD_* vars are empty (the local-dev default), the agent stays running but the Loki client config short-circuits (no remote_write target) and the agent tails logs to its own stdout only — useful for "is my logging output structured the way I expect?" but no external traffic.
5.2 · ops/observability/grafana-agent.yaml
Mirrors the podcast_scraper compose/grafana-agent.yaml pattern. Loki block:
logs:
configs:
- name: orrery
clients:
- url: ${GRAFANA_CLOUD_LOKI_URL}
basic_auth:
username: ${GRAFANA_CLOUD_LOKI_USER}
password: ${GRAFANA_CLOUD_API_KEY}
external_labels:
env: ${GRAFANA_AGENT_ENV}
app: orrery
positions:
filename: /tmp/positions.yaml
scrape_configs:
- job_name: orrery-docker
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 30s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(orrery-web|orrery-pipeline-runner-.*)'
action: keep
- source_labels: ['__meta_docker_container_name']
target_label: container
regex: '/(.*)'
replacement: '$1'Promtail's docker_sd_configs discovers containers via the docker socket; the relabel_configs filters to Orrery-specific names. Logs land in Grafana Cloud Loki keyed by {env, app, container}.
5.3 · Dashboards as code
ops/observability/dashboards/*.json — committed, no secrets. Two dashboards to start:
orrery-web-access.json— nginx access log volume + status code breakdown over time. Useful when nginx is actually serving traffic (i.e. on the VPS); silent today.orrery-pipelines.json— pipeline-runner invocation log lines, errors, duration. Useful when pipelines run on a host cron (i.e. on the VPS); silent today.
Operator imports them into Grafana Cloud via grafana-cli dashboard import or the dashboards-as-code script in ops/observability/dashboards/import.sh. None of this runs from the app.
5.4 · Why land this now if it's silent today?
Two reasons. First, getting the env-var contract + compose profile + agent config right is the easy 80% — leaving it until the VPS RFC means re-litigating the shape under deploy-stress, which is the worst time to negotiate it. Second, the dashboards-as-code files committed today form the operator playbook for the VPS bring-up; they ARE the implementation of "what does production observability look like" and reviewing them now (when nothing is live) is the cheap moment to get the shape right.
6 · README Privacy section update
Adds one paragraph after the existing Umami paragraph:
Error tracking — Sentry, errors only, no PII, fork-silent. Production builds on
chipi.github.ioinitialise the Sentry SDK ifPUBLIC_SENTRY_DSNis set at build time — captures unhandled JavaScript exceptions only (no performance tracing, no session replay, no Web Vitals).sendDefaultPii: falseis set explicitly; abeforeSendhook strips URL query parameters, nulls request headers, and sets IP to0.0.0.0(Sentry's discard signal). Self-hosters that clone Orrery and build their own fork get zero Sentry traffic by default — the env var isn't in their CI secrets, so the SDK init returns early before any network request. The Sentry DSN, like Umami's website ID, is public-by-design (the security model is project-side rate limits, not DSN secrecy), but it's still injected via build-time env var so that forks don't accidentally report into Orrery's project.
The Grafana side stays unmentioned in the user-facing Privacy section — it observes the docker stack (local + future VPS), never the browser, so it doesn't intersect with the user's privacy contract.
7 · Implementation slices
- Slice 1 — Sentry SDK + scrubbing + env-var wiring + secret guard. Add
@sentry/sveltekit, writesrc/lib/observability/sentry.ts, wiresrc/hooks.client.ts, expand existing.env.examplewith the Sentry + Grafana env-var rows, extend existing.gitignorewith.env.production/.env.development/.env.*.local, addscripts/check-no-secrets.tstopreflight. Update README Privacy section. Updatepreview.ymlto thread${{ secrets.SENTRY_DSN_WEB }}into the build env (operator sets this in repo settings once). Closes ADR-067. - Slice 2 — Grafana Agent compose service + config + dashboards. Add the
grafana-agentservice,ops/observability/grafana-agent.yaml, the two dashboard JSONs, a one-pageops/observability/dashboards/README.mdoperator import flow. Closes ADR-068. - Slice 3 — operator guide + flip ADRs.
docs/guides/observability.md(set up Sentry org, set up Grafana Cloud, populate env vars, verify silence locally). Flip ADRs 067-068 Draft → Accepted. UpdateAGENTS.mdpointer (per the established RFC-024 pattern).
Total: ~2-3 days of focused work. No data migration. Zero behaviour change for users on chipi.github.io until the operator populates the SENTRY_DSN_WEB GitHub Actions secret. Zero behaviour change for the docker stack until the operator runs docker compose --profile observability up -d with GRAFANA_CLOUD_* env vars set.
8 · Operator setup (once, outside the repo)
These steps live in docs/guides/observability.md; documented here for completeness so the RFC is self-contained.
- Sentry account / org. Operator already has a Sentry org (per podcast_scraper RFC-081). Create a new project under that org: type
JavaScript / SvelteKit, nameorrery-web. Copy the DSN string from the project's Client Keys page. Set it in GitHub repo settings → Secrets → Actions asSENTRY_DSN_WEB. Optionally set it in your local.envfor testing. - Grafana Cloud account / org. Operator already has a Grafana Cloud free-tier stack (per podcast_scraper RFC-081). Create a new Access Policy with
logs:writescope only; export the API key (one-time view). Note the Loki ingest URL from the stack's Loki integration page and the numeric instance ID. Put those into local.envfor now (the VPS RFC will move them to the VPS host's environment). - Verify silence in dev.
npm run devshould show no Sentry network requests in the browser devtools, anddocker compose --profile observability up -dshould show the agent running but no outbound traffic indocker logs grafana-agent.
9 · Risks + mitigations
- A future contributor adds
Sentry.captureMessage('user typed: ' + input)somewhere. Captures PII despite the SDK config. Mitigation: thebeforeSendhook scrubs known PII surfaces (URL, headers, IP) but cannot scrub free-text message arguments. Add a lint rule (or at least adocs/guides/observability.md§don't-do-this section) explicitly calling this out. Code review is the real defense. - CI accidentally exposes the DSN. Mitigation:
PUBLIC_SENTRY_DSNIS public-by-design, so the worst case is a fork builds with the operator's DSN and starts reporting into the operator's project. Not catastrophic — Sentry's project-side rate limits absorb the noise — but worth a follow-up "what's our reaction" runbook. Thecheck-no-secretsscanner catches the backend Grafana Cloud API key (which IS genuinely secret) in the staged diff. - Forks build and ship Sentry without realising. Mitigation: the SDK is no-op when
PUBLIC_SENTRY_DSNis empty. Forks would have to actively set the env var. This is the same posture as Umami's hostname allowlist; the user is in control. - Sentry quota exhaustion on a viral spike. Free tier is 5k errors/month. If Orrery goes viral and starts erroring, we'd run out within hours. Mitigation:
tracesSampleRate: 0keeps perf events out; for errors-only the volume tends to be bounded by unique bugs, not unique users. Add a follow-up to RFC-025 if quota becomes a real ceiling. - Grafana Cloud free tier limits. 50 GB logs / month. The docker stack only logs in production-mode; nginx access logs at ~200 B / request, pipeline runners produce ~5-50 KB / invocation. At 100k req/day + 4 pipeline runs/day, we're at ~700 MB / month. Comfortable. The VPS RFC should add a
loki.client.batch_size_bytescap so a misconfigured app can't blow the budget overnight. - Compose-profile drift between local + CI + VPS. Mitigation: the
observabilityprofile is opt-in everywhere. CI doesn't run with it (the docker-e2e workflow per ADR-066 only brings upweb). VPS bring-up runs it explicitly. Local dev runs it explicitly when the operator wants to test.
10 · Validation plan
PUBLIC_SENTRY_DSN= npm run dev && curl logs from dev tools→ no Sentry requests fire.PUBLIC_SENTRY_DSN=<dev DSN> npm run dev && trigger an unhandled error in /science→ one event in Sentry's dev project's issue list within ~30 s, withrequest.urlstripped of query params,request.headersundefined,user.ip_addressrecorded as0.0.0.0.docker compose --profile observability up -d grafana-agentwith allGRAFANA_CLOUD_*empty → agent runs, tails container logs to stdout, makes zero outbound network requests (verified viadocker exec grafana-agent ss -tn).docker compose --profile observability up -d grafana-agentwith allGRAFANA_CLOUD_*populated → log lines fromorrery-weband any running pipeline-runner appear in Grafana Cloud Loki under{env: 'local-dev', app: 'orrery', container: 'orrery-web'}.scripts/check-no-secrets.tswith a manually-injected DSN in the staged diff → script exits non-zero, blocks the commit.npm run preflightcontinues to pass onmain. The docker-e2e CI workflow continues to pass.
11 · Future work
- Sentry release tracking + source-map upload.
@sentry/vite-pluginintegration invite.config.ts. Auth token viaSENTRY_AUTH_TOKENGH Actions secret. CI workflow step that uploads source maps + tags the release with thegit rev-parse --short HEAD. Materially improves the readability of production stack traces — defer until enough real errors warrant the operator setup. Own RFC if scope grows. - Sentry performance tracing + Web Vitals.
tracesSampleRate: 0.1,browserTracingIntegration. Adds page-load + route-transition timings to the issue list. Defer until a real-user performance regression motivates the bandwidth + free-tier cost. - OpenTelemetry / OTLP exporter. Grafana Agent supports OTLP; once any Node-side workload exists (the VPS RFC's pipeline-runner cron, for example), structured traces could ship via OTLP. Cleaner than parsing log lines; defer until the VPS RFC.
- Synthetic uptime probe. Grafana Cloud Synthetic Monitoring free tier supports one production hostname. Configure for
chipi.github.io+ the future VPS hostname. Drops nicely into the VPS RFC's checklist. - Privacy banner / cookie-based consent. If the project ever needs to surface Sentry collection to users (regulatory shift, new data type added), drop in a banner. With current scope (errors only + scrubbed), GDPR legitimate-interest covers it.
- A
disable-observabilityURL parameter for operator debugging —?orrery_telemetry=offshort-circuits Sentry init for the session. Trivial to add; punted because it's a tool you only need when you're actively debugging Sentry itself.
12 · Open questions
- Should the Sentry release-tag include the GH Pages deploy SHA or the upstream Orrery release tag? Probably both —
release: vX.Y.Z+gha-<sha>. Decided during Slice 1 when the CI env block is written. - Should the Grafana Cloud Loki labels include the Orrery git SHA? Yes if it's available cheaply via
git rev-parseat compose bring-up; punt to runtime label injection (vs static config) if it's not. - Should pipeline-runner logs use a structured logger (pino, winston) to make Loki queries cleaner, or do we trust unstructured stdout for now? Punt to a follow-up — pipelines today print human-readable lines and that's fine for a first dashboard pass.
RFC-025 · 2026-05-22 · Marko + Claude · v0.7.x slice gate · ADRs 067-068 · reuses podcast_scraper RFC-081's Grafana Cloud account