# analytics — overview A stateless multi-tenant API proxy exposing four upstream services under one FastAPI app: 1. **Google Analytics 4** — Data API (reporting) + Admin API (read). 2. **Google Search Console** — Search Analytics, Sites, Sitemaps, URL Inspection (read). 3. **Google Ads** — GAQL reporting (search / searchStream). 4. **Sklik** (Seznam) — Drak JSON-RPC API. The three Google services share one OAuth mechanism (token wins over service account) — they differ only in the OAuth *scope* and the header prefix (`X-GA-*`, `X-GSC-*`, `X-GAds-*`). Google Ads additionally needs a developer token. The structure mirrors the sibling `idoklad` / `csob` services (config→env, credentials→headers, client per upstream, routers, central exception handling, Swagger at `/docs`), adapted to Python/FastAPI. ## Design principles - **Stateless / no stored secrets.** Credentials arrive per request in `X-` headers and are used only to call the upstream. Nothing is persisted; the only in-memory state is a short-lived GA access-token cache (see below). - **Thin passthrough.** GA request/response bodies and most Sklik calls are forwarded as-is, so callers keep the full upstream API surface. Only authentication, base URL and error mapping are added. - **No silent failures.** Every error is logged (never the secret values) and surfaced as JSON. Upstream errors preserve the upstream status and body. ## Layout ``` app/ config.py env-driven config (base URLs, scopes, timeout) — no secrets logging_config.py get_logger(); secrets are never logged errors.py MissingCredentialsError, UpstreamError + handlers credentials.py X- header dependencies (GA / GSC / Ads / Sklik) clients/ google.py shared Google client: Bearer/SA token minting + requests sklik_client.py Sklik JSON-RPC client (login + session + report paging) routers/ meta.py /health, /version ga_data.py /ga/data/... (Google Analytics Data) ga_admin.py /ga/admin/... (Google Analytics Admin) gsc.py /gsc/... (Search Console) googleads.py /googleads/... (Google Ads) sklik.py /sklik/... main.py app factory, root_path, router + handler registration ``` ## Reverse proxy `ROOT_PATH` (e.g. `/apps/analytics`) is passed to FastAPI's `root_path`, so the OpenAPI `servers` entry and Swagger "Try it out" use the public prefix. Internal routes are unprefixed (Caddy `handle_path` strips the prefix). ## Authentication summary | Upstream | Header(s) | Behaviour | | --- | --- | --- | | Google Analytics | `X-GA-Access-Token` **or** `X-GA-Credentials` (+ `X-GA-Quota-Project`) | Token used directly; else minted from base64 service-account JSON (scope `analytics.readonly`) and cached in memory until ~60 s before expiry. | | Search Console | `X-GSC-Access-Token` **or** `X-GSC-Credentials` (+ `X-GSC-Quota-Project`) | Same as GA, scope `webmasters.readonly`. | | Google Ads | `X-GAds-Developer-Token` (req) + `X-GAds-Access-Token` **or** `X-GAds-Credentials` (+ `X-GAds-Login-Customer-Id`, `X-GAds-Quota-Project`) | Same OAuth (scope `adwords`) plus `developer-token` / `login-customer-id` headers forwarded upstream. | | Sklik | `X-Sklik-Token` (+ `X-Sklik-User-Id`) | `client.loginByToken` per request → session injected into the call. | ## Deliberately not wired - **Write operations** across the Google services: GA Admin (create/update properties, streams), Search Console (submit/delete sitemaps, add/remove sites), Google Ads mutates (create/update campaigns etc.). All requested scopes are read-only; add the read-write scope + endpoints if management is needed later. Google Ads exposes reporting (GAQL) only for now. - **Sklik header-credential encryption.** Same deferral as `idoklad`/`csob`: header values are plaintext over TLS for now. - **Sklik session reuse across requests** — the chosen model logs in per request; a future `X-Sklik-Session` passthrough could save the login call. ## Verification checklist (per AGENTS.md) - `/health` returns 200. - `/docs` loads; `/openapi.json` `servers` contains the proxy prefix. - New endpoints appear in Swagger with their `X-` headers in "Try it out". - Secrets never appear in logs or source.