Files
2026-07-20 09:02:42 +02:00

5.5 KiB

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)
  idempotency.py       in-memory idempotency store for writes (opt-in per request)
  sklik_guards.py      write guard rails: forced paused status, optional budget caps
  sklik_models.py      typed request bodies for Sklik writes (Swagger schema; extra fields pass through)
  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-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.