108 lines
5.5 KiB
Markdown
108 lines
5.5 KiB
Markdown
# 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.
|