From 65ad392d74c14dde80047570ab69fcdb2f3053f4 Mon Sep 17 00:00:00 2001 From: PhilippTheServer Date: Wed, 26 Aug 2026 16:23:23 +0200 Subject: [PATCH] docs: document storefront analytics The API gained a public ingest endpoint and an admin shopper funnel (OpenTaberna/fastapi#47), and refreshing the snapshot made the drift check fail with two undocumented paths. Documents both, and the parts a reader cannot recover from the schema: that the pre-order steps are a floor rather than a count while the paid step is exact, why the ingest endpoint is public, what it refuses to store, and why none of it needs a consent banner. Also records why the endpoint returns 404 rather than 403 when collection is off, and adds STOREFRONT_ANALYTICS_ENABLED to the configuration reference. Closes #9 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YY1ekLLeFLkAU2kvdQ8Ey4 --- API/Architecture.md | 52 +++++++++++++++++++++++++++++++++++++++++++ Configuration.md | 6 +++++ home.md | 2 +- openapi.snapshot.json | 10 +++++++++ 4 files changed, 69 insertions(+), 1 deletion(-) diff --git a/API/Architecture.md b/API/Architecture.md index e2351b1..f4f0e6d 100644 --- a/API/Architecture.md +++ b/API/Architecture.md @@ -33,6 +33,7 @@ persistence: | Inventory | `/v1/admin/inventory` | Stock levels and reservations | | Admin | `/v1/admin/orders` | Back-office fulfillment | | Analytics | `/v1/admin/analytics` | Commercial reporting over the order history | +| Storefront analytics | `/v1/analytics` | Anonymous shopper telemetry ingest (public) | | Returns | `/v1/admin/returns` | Admin decisions on return requests | | Webhooks | `/v1/webhooks` | Inbound payment provider callbacks | | Health | `/health` | Liveness and readiness | @@ -159,6 +160,27 @@ UTC belongs to the next day in Berlin, and bucketing on UTC would file a day's t against the wrong day — an error that looks like a data problem for weeks. See [Configuration](/Configuration). +#### The shopper funnel + +| Method | Path | Purpose | Auth | +|---|---|---|---| +| `GET` | `/v1/admin/analytics/storefront` | Sessions → product views → carts → checkouts → paid | admin | + +What happens *before* an order exists — which the order funnel above cannot see, because +nothing has happened in the order tables yet. Fed by the public ingest endpoint below. + +Sessions are counted distinctly: ten product views from one shopper is one person +considering a purchase, not ten. + +**The pre-order steps are a floor, not a count.** Blocked scripts, a tab closed before the +batch flushed and disabled JavaScript all lose events. The `paid` step is read from the +orders table and is exact. A funnel that undercounts its first step but not its last +overstates the drop, so the two are labelled differently rather than presented as one +continuous measurement. Real conversion is never worse than reported. + +Returns `enabled: false` with zeroes when the deployment is not collecting — "nobody +visited" and "we are not counting" would otherwise look identical. + #### What these numbers cannot tell you Stated plainly, because a reader will otherwise infer something stronger: @@ -193,6 +215,36 @@ reserves against. | `PATCH` | `/v1/admin/inventory/{inventory_id}` | Update stock | admin | | `DELETE` | `/v1/admin/inventory/{inventory_id}` | Delete inventory record | admin | +### Storefront ingest — `/v1/analytics` + +| Method | Path | Purpose | Auth | +|---|---|---|---| +| `POST` | `/v1/analytics/events` | Record anonymous shopper events | — | + +**Public by design**, because a shopper who has not signed in is exactly who this measures. +Off unless `STOREFRONT_ANALYTICS_ENABLED` is set, returning `404` while off so a deployment +that has not opted in does not advertise the capability. Returns `202`: the browser must +neither wait on the result nor retry. + +Nothing stored identifies a person. There is no column for an IP address, a user agent, an +email or a customer id, and query strings are stripped before storage — that is where +personal data arrives by accident, in a share link or a redirect. The request schema +forbids unknown fields, so a client sending `email` gets a `422` rather than having it +quietly dropped. + +Because nothing identifies anyone and the browser stores only a per-tab session id, this +requires no consent banner in the EU. That is the reason for the shape rather than a happy +accident: a banner costs 40–60% of sessions to opt-outs, which would make the funnel it +feeds mostly fiction. + +Being public, it is guarded accordingly — rate limited, batch capped at 50 events, a closed +event vocabulary, and every field length-bounded. The worst an abusive client achieves is +noise in a report. + +Browser timestamps outside ±24 hours of server time are discarded and counted in +`rejected`. Clocks are wrong often enough that rejecting all skew would lose real data, and +trusting all of it would let anyone write into a period already reported on. + ### Webhooks and health | Method | Path | Purpose | Auth | diff --git a/Configuration.md b/Configuration.md index e6f7746..b5c954b 100644 --- a/Configuration.md +++ b/Configuration.md @@ -162,12 +162,18 @@ hold stock nobody can buy. | Setting | Default | Description | |---|---|---| | `SHOP_TIMEZONE` | `Europe/Berlin` | IANA timezone the shop trades in | +| `STOREFRONT_ANALYTICS_ENABLED` | `false` | Accept anonymous shopper events from the storefront | Analytics buckets days in this zone rather than UTC, so "today" matches the operator's day. Leave it wrong and evening orders land on the following day for any shop east of Greenwich — the figures stay internally consistent, which is what makes it hard to notice. An unknown zone is rejected at request time with a `422` naming the value, rather than a `500`. +`STOREFRONT_ANALYTICS_ENABLED` is off by default because cloning OpenTaberna must not +silently start collecting anything, even something that identifies nobody. While off, the +ingest endpoint returns `404`. The storefront has a matching switch in +`storefront.config.ts`; both must be on. + ## Object storage — MinIO / S3 | Setting | Default | Description | diff --git a/home.md b/home.md index 99eaa6b..cede818 100644 --- a/home.md +++ b/home.md @@ -128,7 +128,7 @@ system. It is one possible shop UI, not *the* shop UI — that is the point of t | Fulfillment | Carrier labels, packing slips, the job queue and outbox | | Shipments | Tracking numbers and label files | | Returns | Customer return requests and admin decisions | -| Analytics | Revenue, product performance and the order funnel | +| Analytics | Revenue, product performance, and the order and shopper funnels | | Health | Liveness and readiness probes | The full endpoint list is on the [API Architecture](/API/Architecture) page, and a live, diff --git a/openapi.snapshot.json b/openapi.snapshot.json index ab862e4..aafdee7 100644 --- a/openapi.snapshot.json +++ b/openapi.snapshot.json @@ -24,6 +24,11 @@ "summary": "Product performance (admin)" } }, + "/v1/admin/analytics/storefront": { + "get": { + "summary": "Shopper funnel (admin)" + } + }, "/v1/admin/analytics/summary": { "get": { "summary": "Commercial summary (admin)" @@ -106,6 +111,11 @@ "summary": "Update return request (admin)" } }, + "/v1/analytics/events": { + "post": { + "summary": "Record anonymous storefront events" + } + }, "/v1/customers/me": { "get": { "summary": "Get my profile"