Skip to content

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/sveltekit SDK installation + a thin src/lib/observability/sentry.ts init module.
  • sentry.client.config.ts (or equivalent SvelteKit-side hook) that initialises Sentry only if PUBLIC_SENTRY_DSN is set at build time. Empty DSN = no-op, no network requests, no console noise.
  • A beforeSend PII scrubber that strips URL query params, replaces full URLs with route-only, nulls request.headers, drops any breadcrumb whose category is ui.input (form fields).
  • sendDefaultPii: false. tracesSampleRate: 0. Errors only — no performance tracing, no Web Vitals (per RFC-025 §scope agreed during design).
  • grafana-agent service added to docker-compose.yml under a new observability compose profile. Default off; only spins up when explicitly invoked via docker compose --profile observability up -d grafana-agent.
  • ops/observability/grafana-agent.yaml config — scrapes nothing today (no metrics endpoint), tails the web + pipeline-runner Docker container logs via the Docker socket discovery pattern from podcast_scraper RFC-081. Ships to Grafana Cloud Loki only if GRAFANA_CLOUD_LOKI_URL + GRAFANA_CLOUD_API_KEY are 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.example at repo root — already committed, already documents ANTHROPIC_API_KEY + LL2_PATREON_KEY. Append a new "Observability (RFC-025)" section with PUBLIC_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 .gitignore already covers .env (line 6) + .env.local (line 7). Add .env.production, .env.development, .env.*.local for completeness (vite reads each variant on the matching mode). .env.example stays 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.md operator 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 /metrics endpoint today (no Node backend, nginx default doesn't expose Prometheus). When the future VPS RFC adds nginx-prometheus-exporter, this RFC's grafana-agent.yaml gets 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 documents ANTHROPIC_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 .env and .env.local. Add .env.production, .env.development, .env.*.local for vite-mode-specific variants. .env.example stays tracked.
  • scripts/check-no-secrets.ts — runs in npm run preflight AND 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)
  • Fail-closed: any match → exit code non-zero → the commit / push is rejected. No --no-verify recommended path; if the scanner flags a false positive, add it to the allowlist file.

The env vars themselves (per environment):

ini
# 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 for handleError (server) and the client init.
  • That's it. No @sentry/node (no Node server), no @sentry/vite-plugin for 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

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

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 dev reads .env (gitignored); operator can leave PUBLIC_SENTRY_DSN= empty and Sentry is silent.
  • Production build (GH Pages deploy): .github/workflows/preview.yml sets PUBLIC_SENTRY_DSN: ${‌{ secrets.SENTRY_DSN_WEB }} in the env: 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

yaml
# 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:

bash
docker compose --profile observability up -d

When 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:

yaml
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.io initialise the Sentry SDK if PUBLIC_SENTRY_DSN is set at build time — captures unhandled JavaScript exceptions only (no performance tracing, no session replay, no Web Vitals). sendDefaultPii: false is set explicitly; a beforeSend hook strips URL query parameters, nulls request headers, and sets IP to 0.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, write src/lib/observability/sentry.ts, wire src/hooks.client.ts, expand existing .env.example with the Sentry + Grafana env-var rows, extend existing .gitignore with .env.production / .env.development / .env.*.local, add scripts/check-no-secrets.ts to preflight. Update README Privacy section. Update preview.yml to 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-agent service, ops/observability/grafana-agent.yaml, the two dashboard JSONs, a one-page ops/observability/dashboards/README.md operator 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. Update AGENTS.md pointer (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, name orrery-web. Copy the DSN string from the project's Client Keys page. Set it in GitHub repo settings → Secrets → Actions as SENTRY_DSN_WEB. Optionally set it in your local .env for 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:write scope 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 .env for now (the VPS RFC will move them to the VPS host's environment).
  • Verify silence in dev. npm run dev should show no Sentry network requests in the browser devtools, and docker compose --profile observability up -d should show the agent running but no outbound traffic in docker logs grafana-agent.

9 · Risks + mitigations

  • A future contributor adds Sentry.captureMessage('user typed: ' + input) somewhere. Captures PII despite the SDK config. Mitigation: the beforeSend hook scrubs known PII surfaces (URL, headers, IP) but cannot scrub free-text message arguments. Add a lint rule (or at least a docs/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_DSN IS 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. The check-no-secrets scanner 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_DSN is 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: 0 keeps 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_bytes cap so a misconfigured app can't blow the budget overnight.
  • Compose-profile drift between local + CI + VPS. Mitigation: the observability profile is opt-in everywhere. CI doesn't run with it (the docker-e2e workflow per ADR-066 only brings up web). 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, with request.url stripped of query params, request.headers undefined, user.ip_address recorded as 0.0.0.0.
  • docker compose --profile observability up -d grafana-agent with all GRAFANA_CLOUD_* empty → agent runs, tails container logs to stdout, makes zero outbound network requests (verified via docker exec grafana-agent ss -tn).
  • docker compose --profile observability up -d grafana-agent with all GRAFANA_CLOUD_* populated → log lines from orrery-web and any running pipeline-runner appear in Grafana Cloud Loki under {env: 'local-dev', app: 'orrery', container: 'orrery-web'}.
  • scripts/check-no-secrets.ts with a manually-injected DSN in the staged diff → script exits non-zero, blocks the commit.
  • npm run preflight continues to pass on main. The docker-e2e CI workflow continues to pass.

11 · Future work

  • Sentry release tracking + source-map upload. @sentry/vite-plugin integration in vite.config.ts. Auth token via SENTRY_AUTH_TOKEN GH Actions secret. CI workflow step that uploads source maps + tags the release with the git 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-observability URL parameter for operator debugging — ?orrery_telemetry=off short-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-parse at 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

Orrery — architecture documentation · MIT · No tracking