5.4 KiB
analytics — overview
A stateless multi-tenant API proxy exposing four upstream services under one FastAPI app:
- Google Analytics 4 — Data API (reporting) + Admin API (read).
- Google Search Console — Search Analytics, Sites, Sitemaps, URL Inspection (read).
- Google Ads — GAQL reporting (search / searchStream).
- 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)
idempotency.py in-memory idempotency store for writes (opt-in per request)
sklik_guards.py write guard rails: forced paused status, optional budget caps
clients/
google.py shared Google client: Bearer/SA token minting + requests
sklik_client.py Sklik JSON-RPC client (login + session + list/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. |
Writes
Sklik is the only upstream with write endpoints — full CRUD over campaigns,
groups and ads (create / update / remove / restore). The pattern established
there — create paused, optional budget ceilings, optional idempotency key — is
the template for any future write support (Google Ads mutates, Meta campaign
management in the sibling meta service).
The one non-negotiable rule: entities are always created paused. Sklik
defaults status to active, so this must be forced server-side. Update does
not force a status (that is how you pause or resume a campaign), but an
operator can refuse activation entirely with SKLIK_BLOCK_ACTIVATION. Budget
ceilings and idempotency are opt-in, since the approval flow lives on the
caller's side. Removal is Sklik's own soft delete and is reversible.
See sklik.md for details.
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 keyword / sitelink / product-set management. Only campaigns, groups
and ads have typed CRUD; the rest of the Sklik API remains reachable through
POST /sklik/rpc/{method}without guard rails. - 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-Sessionpassthrough could save the login call.
Verification checklist (per AGENTS.md)
/healthreturns 200./docsloads;/openapi.jsonserverscontains the proxy prefix.- New endpoints appear in Swagger with their
X-headers in "Try it out". - Secrets never appear in logs or source.