first
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
# Google Analytics (GA4)
|
||||
|
||||
Proxy over the GA4 **Data API** (`analyticsdata.googleapis.com/v1beta`) and
|
||||
**Admin API** (`analyticsadmin.googleapis.com/v1beta`).
|
||||
|
||||
## Credentials
|
||||
|
||||
Token has precedence over the service account:
|
||||
|
||||
| Header | Meaning |
|
||||
| --- | --- |
|
||||
| `X-GA-Access-Token` | Ready OAuth2 access token, used directly as `Authorization: Bearer`. |
|
||||
| `X-GA-Credentials` | **Base64** of a Google service-account JSON key. The proxy mints a short-lived token (scope `https://www.googleapis.com/auth/analytics.readonly`) via `google-auth` and caches it in memory until ~60 s before expiry. |
|
||||
| `X-GA-Quota-Project` | Optional GCP project id → upstream `x-goog-user-project`. |
|
||||
|
||||
At least one of `X-GA-Access-Token` / `X-GA-Credentials` is required (otherwise
|
||||
`401 missing_credentials`).
|
||||
|
||||
The service account (or token) must have access to the GA4 property — add its
|
||||
`client_email` as a viewer in GA Admin → Property Access Management.
|
||||
|
||||
> Encoding the key: `base64 -w0 service-account.json` (Linux) or
|
||||
> `[Convert]::ToBase64String([IO.File]::ReadAllBytes("service-account.json"))`
|
||||
> (PowerShell).
|
||||
|
||||
## Data API endpoints
|
||||
|
||||
`property_id` may be the bare number (`123456789`) or `properties/123456789`.
|
||||
|
||||
| Method | Path | Upstream |
|
||||
| --- | --- | --- |
|
||||
| POST | `/ga/data/properties/{id}/runReport` | `:runReport` |
|
||||
| POST | `/ga/data/properties/{id}/runPivotReport` | `:runPivotReport` |
|
||||
| POST | `/ga/data/properties/{id}/batchRunReports` | `:batchRunReports` |
|
||||
| POST | `/ga/data/properties/{id}/batchRunPivotReports` | `:batchRunPivotReports` |
|
||||
| POST | `/ga/data/properties/{id}/runRealtimeReport` | `:runRealtimeReport` |
|
||||
| POST | `/ga/data/properties/{id}/checkCompatibility` | `:checkCompatibility` |
|
||||
| GET | `/ga/data/properties/{id}/metadata` | `/metadata` |
|
||||
|
||||
The POST body is the GA4 request object, forwarded unchanged. Example
|
||||
`runReport` body:
|
||||
|
||||
```json
|
||||
{
|
||||
"dateRanges": [{ "startDate": "7daysAgo", "endDate": "today" }],
|
||||
"dimensions": [{ "name": "country" }],
|
||||
"metrics": [{ "name": "activeUsers" }]
|
||||
}
|
||||
```
|
||||
|
||||
## Admin API endpoints (read)
|
||||
|
||||
| Method | Path | Notes |
|
||||
| --- | --- | --- |
|
||||
| GET | `/ga/admin/accounts` | `pageSize`, `pageToken` |
|
||||
| GET | `/ga/admin/accountSummaries` | accounts + their properties |
|
||||
| GET | `/ga/admin/properties?accountId=123` | builds `filter=parent:accounts/123` |
|
||||
| GET | `/ga/admin/properties/{id}` | single property |
|
||||
| GET | `/ga/admin/properties/{id}/dataStreams` | data streams |
|
||||
|
||||
## Errors
|
||||
|
||||
`UpstreamError` is returned as JSON with the upstream status and body:
|
||||
|
||||
```json
|
||||
{
|
||||
"error": "upstream_error",
|
||||
"status": 403,
|
||||
"detail": "User does not have sufficient permissions for this property.",
|
||||
"upstream_status": 403,
|
||||
"upstream_body": { "error": { "code": 403, "status": "PERMISSION_DENIED" } }
|
||||
}
|
||||
```
|
||||
|
||||
Timeouts → `504`, unreachable/transport → `502`, token minting failure → `401`.
|
||||
|
||||
## curl example
|
||||
|
||||
```bash
|
||||
curl -X POST "https://services.csbot.cz/apps/analytics/ga/data/properties/123456789/runReport" \
|
||||
-H "X-GA-Access-Token: ya29...." \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"dateRanges":[{"startDate":"7daysAgo","endDate":"today"}],"metrics":[{"name":"activeUsers"}]}'
|
||||
```
|
||||
@@ -0,0 +1,71 @@
|
||||
# analytics — overview
|
||||
|
||||
A stateless multi-tenant API proxy exposing two upstream services under one
|
||||
FastAPI app:
|
||||
|
||||
1. **Google Analytics 4** — Data API (reporting) + Admin API (read).
|
||||
2. **Sklik** (Seznam) — Drak JSON-RPC API.
|
||||
|
||||
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, scope, timeout) — no secrets
|
||||
logging_config.py get_logger(); secrets are never logged
|
||||
errors.py MissingCredentialsError, UpstreamError + handlers
|
||||
credentials.py X- header dependencies (GA + Sklik)
|
||||
clients/
|
||||
ga_client.py GA Data/Admin HTTP client + service-account token minting
|
||||
sklik_client.py Sklik JSON-RPC client (login + session + report paging)
|
||||
routers/
|
||||
meta.py /health, /version
|
||||
ga_data.py /ga/data/...
|
||||
ga_admin.py /ga/admin/...
|
||||
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 a token is minted from the base64 service-account JSON (scope `analytics.readonly`) and cached in memory until ~60 s before expiry. |
|
||||
| Sklik | `X-Sklik-Token` (+ `X-Sklik-User-Id`) | `client.loginByToken` per request → session injected into the call. |
|
||||
|
||||
## Deliberately not wired
|
||||
|
||||
- **GA Admin write operations** (create/update/delete properties, streams). The
|
||||
requested scope is read-only (`analytics.readonly`); add `analytics.edit` and
|
||||
endpoints if management is needed later.
|
||||
- **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.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Sklik (Seznam)
|
||||
|
||||
Proxy over the Sklik **Drak JSON API**
|
||||
(`https://api.sklik.cz/drak/json/v5/{method}`).
|
||||
|
||||
## Protocol (verified against seznam/api-examples)
|
||||
|
||||
- HTTP `POST` to the base URL with the **method name appended** to the path.
|
||||
- Body is a **JSON array** of positional arguments.
|
||||
- `client.loginByToken` takes the API token and returns
|
||||
`{"status":200,"session":"...","statusMessage":"OK"}`.
|
||||
- Every authenticated method takes the user struct `{"session": ...}` (optionally
|
||||
`"userId"`) as its **first** argument, followed by the method's own arguments.
|
||||
- Every response is an object with `status` (HTTP-style), `statusMessage`, a
|
||||
refreshed `session`, and method-specific data. `200`, `206` and `301` are
|
||||
treated as success.
|
||||
|
||||
The proxy performs `client.loginByToken` per request from `X-Sklik-Token` and
|
||||
injects the session — callers never handle the session.
|
||||
|
||||
## Credentials
|
||||
|
||||
| Header | Required | Meaning |
|
||||
| --- | --- | --- |
|
||||
| `X-Sklik-Token` | yes | API token from Sklik → account settings → API. |
|
||||
| `X-Sklik-User-Id` | no | Managed account `userId` (agency/MCC access). |
|
||||
|
||||
Missing token → `401 missing_credentials`. Sklik business errors (invalid token,
|
||||
access denied, bad arguments) are surfaced as `upstream_error` with the Sklik
|
||||
`status` and full body.
|
||||
|
||||
## Endpoints
|
||||
|
||||
| Method | Path | Purpose |
|
||||
| --- | --- | --- |
|
||||
| POST | `/sklik/login` | Verify the token. Returns `{valid, status, statusMessage}` (no session). |
|
||||
| GET | `/sklik/limits` | `api.limits` — quotas and the `statsDataLimit`. |
|
||||
| POST | `/sklik/report/{entity}` | `createReport` + paged `readReport` for an entity. |
|
||||
| POST | `/sklik/rpc/{method}` | Generic authenticated call to any method. |
|
||||
|
||||
### Report helper
|
||||
|
||||
`entity` ∈ `campaigns, groups, ads, keywords, queries, sitelinks, productSets,
|
||||
banners`. Body = the arguments for `{entity}.createReport` (restriction filter +
|
||||
optional display options). The proxy creates the report then pages through
|
||||
`{entity}.readReport` (100 rows/page) and returns:
|
||||
|
||||
```json
|
||||
{ "reportId": "...", "totalCount": 1234, "returnedCount": 1234, "truncated": false, "report": [ ... ] }
|
||||
```
|
||||
|
||||
Example body for `POST /sklik/report/campaigns`:
|
||||
|
||||
```json
|
||||
[
|
||||
{ "dateFrom": "2026-06-01", "dateTo": "2026-06-18", "statGranularity": "daily" },
|
||||
{ "statGranularity": "daily" }
|
||||
]
|
||||
```
|
||||
|
||||
### Generic RPC
|
||||
|
||||
`POST /sklik/rpc/{method}` with a JSON-array body of the arguments **after** the
|
||||
session struct (which the proxy injects). Examples:
|
||||
|
||||
```bash
|
||||
# List campaigns
|
||||
curl -X POST ".../apps/analytics/sklik/rpc/campaigns.list" \
|
||||
-H "X-Sklik-Token: <TOKEN>" -H "Content-Type: application/json" \
|
||||
-d '[{"statuses":["active"]}, {"displayColumns":["id","name","status"]}]'
|
||||
|
||||
# Account info
|
||||
curl -X POST ".../apps/analytics/sklik/rpc/client.get" \
|
||||
-H "X-Sklik-Token: <TOKEN>" -H "Content-Type: application/json" -d '[]'
|
||||
```
|
||||
|
||||
`client.loginByToken` cannot be called via `/sklik/rpc` — the proxy manages the
|
||||
session (returns `400`).
|
||||
|
||||
## Method reference
|
||||
|
||||
Full method list: <https://api.sklik.cz/drak/>. Common ones: `client.get`,
|
||||
`api.limits`, `campaigns.list`, `groups.list`, `ads.list`, `keywords.list`,
|
||||
`*.createReport` / `*.readReport`.
|
||||
Reference in New Issue
Block a user