From 95aed6104c90ca8e1450a4c8b6799676da3850ee Mon Sep 17 00:00:00 2001 From: PhilippTheServer Date: Wed, 26 Aug 2026 17:32:28 +0200 Subject: [PATCH] docs: document observability, frontend errors and the mail API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refreshing the snapshot surfaced eleven undocumented paths: the frontend error endpoints, and the whole admin mail API, which had never been written up. Documents all of them, plus the OpenTelemetry settings, and carries across the parts a reader cannot recover from a schema: Why the OTLP endpoint is the seam — no application code imports a vendor SDK, so pointing it elsewhere is the entire change needed to use a different backend. That the user agent is reduced to a family and major version rather than stored, and that the reduction is also a filter. That frontend errors are grouped by message rather than by stack, because the same fault reached from two routes produces two stacks and is one bug. That the list shows only what browsers managed to send, so an error that breaks a page badly enough to stop the reporter never arrives — a quiet list is no news, not proof of no errors. For mail: that a uid is only meaningful alongside its folder, that /status is the endpoint to check first when the others return nothing, and that transactional order mail does not pass through this API at all. Closes #11 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01YY1ekLLeFLkAU2kvdQ8Ey4 --- API/Architecture.md | 61 +++++++++++++++++++++++++++++++++++++++++ Configuration.md | 22 +++++++++++++++ home.md | 1 + openapi.snapshot.json | 64 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 148 insertions(+) diff --git a/API/Architecture.md b/API/Architecture.md index f4f0e6d..e5dc4fe 100644 --- a/API/Architecture.md +++ b/API/Architecture.md @@ -34,6 +34,8 @@ persistence: | 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) | +| Frontend errors | `/v1/telemetry` | Uncaught browser error reports (public ingest) | +| Admin mail | `/v1/admin/mail` | Mailbox configuration and folders | | Returns | `/v1/admin/returns` | Admin decisions on return requests | | Webhooks | `/v1/webhooks` | Inbound payment provider callbacks | | Health | `/health` | Liveness and readiness | @@ -245,6 +247,65 @@ Browser timestamps outside ±24 hours of server time are discarded and counted i `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. +### Frontend errors — `/v1/telemetry` and `/v1/admin/telemetry` + +| Method | Path | Purpose | Auth | +|---|---|---|---| +| `POST` | `/v1/telemetry/errors` | Report uncaught browser errors | — | +| `GET` | `/v1/admin/telemetry/errors` | Read them, grouped by frequency | admin | + +Traces and metrics see nothing that happens in a browser. A component that throws leaves the +server returning `200` with healthy metrics while the shop is broken for a real customer — +and for a shop, by the time someone reports it the sale is gone. + +Reporting is **public**, because storefront visitors are not signed in and an error before +login is exactly the one worth catching. Off unless `FRONTEND_ERRORS_ENABLED` is set, +returning `404` while off. + +**The user agent is reduced, never stored.** A raw agent string is a fingerprint, but "which +browser?" is genuinely diagnostic, so it is reduced at the boundary to a family and major +version — `Safari 18` reproduces a bug and does not recognise anyone. The reduction doubles +as a filter: whatever a client sends, the output is a known family name and an integer. + +Errors are grouped by application, error class and message, **not by stack**: the same fault +reached from two routes produces two stacks and is one bug. `affected_paths` carries the +spread instead. + +> **This shows only what browsers managed to send.** An error that breaks a page badly +> enough to stop the reporter is precisely the one that will not appear. Read a quiet list as +> *no news*, never as *no errors*. + +Being public, it is rate limited harder than the analytics ingest — a component throwing in +a render loop reports as fast as the browser can loop — with a batch cap, a closed `app` +vocabulary, bounded fields and truncated stacks. + +### Admin mail — `/v1/admin/mail` + +Provider-neutral mailbox access for the back office, over IMAP and SMTP. + +| Method | Path | Purpose | Auth | +|---|---|---|---| +| `GET` | `/v1/admin/mail/status` | Mailbox configuration status | admin | +| `GET` | `/v1/admin/mail/folders` | List mail folders | admin | +| `POST` | `/v1/admin/mail/folders` | Create a folder | admin | +| `PATCH` | `/v1/admin/mail/folders/{folder}` | Rename a folder | admin | +| `DELETE` | `/v1/admin/mail/folders/{folder}` | Delete a folder | admin | +| `GET` | `/v1/admin/mail/folders/{folder}/messages` | List messages | admin | +| `GET` | `/v1/admin/mail/folders/{folder}/messages/{uid}` | Read a message | admin | +| `DELETE` | `/v1/admin/mail/folders/{folder}/messages/{uid}` | Permanently delete a message | admin | +| `PATCH` | `/v1/admin/mail/folders/{folder}/messages/{uid}/flags` | Update message flags | admin | +| `POST` | `/v1/admin/mail/folders/{folder}/messages/{uid}/move` | Move a message | admin | +| `GET` | `/v1/admin/mail/folders/{folder}/messages/{uid}/attachments/{part_id}` | Download an attachment | admin | +| `POST` | `/v1/admin/mail/messages` | Send a message | admin | + +Messages are addressed by IMAP `uid` within a folder, so a `uid` is only meaningful +alongside the folder it came from. `GET /status` reports whether a mailbox is configured at +all, which is the endpoint to check first when the others return nothing. + +This is separate from the transactional mail the API sends itself — order tracking +notifications go out over the `SMTP_*` settings in [Configuration](/Configuration) and do +not pass through here. + ### Webhooks and health | Method | Path | Purpose | Auth | diff --git a/Configuration.md b/Configuration.md index b5c954b..0635081 100644 --- a/Configuration.md +++ b/Configuration.md @@ -163,6 +163,7 @@ hold stock nobody can buy. |---|---|---| | `SHOP_TIMEZONE` | `Europe/Berlin` | IANA timezone the shop trades in | | `STOREFRONT_ANALYTICS_ENABLED` | `false` | Accept anonymous shopper events from the storefront | +| `FRONTEND_ERRORS_ENABLED` | `false` | Accept uncaught error reports from the frontends | 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 — @@ -174,6 +175,27 @@ silently start collecting anything, even something that identifies nobody. While ingest endpoint returns `404`. The storefront has a matching switch in `storefront.config.ts`; both must be on. +## OpenTelemetry + +| Setting | Default | Description | +|---|---|---| +| `OTEL_ENABLED` | `false` | Export traces and metrics over OTLP | +| `OTEL_EXPORTER_OTLP_ENDPOINT` | `http://opentaberna-otel-collector:4318` | Where telemetry goes | +| `OTEL_SERVICE_NAME` | `opentaberna-api` | `service.name` on every span and metric | +| `OTEL_METRIC_EXPORT_INTERVAL_SECONDS` | `30` | Seconds between metric exports | + +**The endpoint is the seam.** No application code imports a vendor SDK, so pointing this at +Datadog or Grafana Cloud is the whole change required to use one. The development compose +file runs a collector, Prometheus and Grafana; production can keep them or replace all three +here. + +Off by default, like everything else that sends data anywhere. While off, no exporter is +created and no connection is opened. + +The wiring is defensive throughout: an absent or misconfigured collector produces a warning +and a running API. Observability that can cause the outage it exists to diagnose is a bad +trade. + ## Object storage — MinIO / S3 | Setting | Default | Description | diff --git a/home.md b/home.md index cede818..47b14b0 100644 --- a/home.md +++ b/home.md @@ -129,6 +129,7 @@ system. It is one possible shop UI, not *the* shop UI — that is the point of t | Shipments | Tracking numbers and label files | | Returns | Customer return requests and admin decisions | | Analytics | Revenue, product performance, and the order and shopper funnels | +| Telemetry | Uncaught frontend errors, and OpenTelemetry traces and metrics | | 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 aafdee7..6a8266e 100644 --- a/openapi.snapshot.json +++ b/openapi.snapshot.json @@ -63,6 +63,60 @@ "summary": "Update inventory stock (admin)" } }, + "/v1/admin/mail/folders": { + "get": { + "summary": "List mail folders" + }, + "post": { + "summary": "Create a mail folder" + } + }, + "/v1/admin/mail/folders/{folder}": { + "delete": { + "summary": "Delete a mail folder" + }, + "patch": { + "summary": "Rename a mail folder" + } + }, + "/v1/admin/mail/folders/{folder}/messages": { + "get": { + "summary": "List messages" + } + }, + "/v1/admin/mail/folders/{folder}/messages/{uid}": { + "delete": { + "summary": "Permanently delete a message" + }, + "get": { + "summary": "Read a message" + } + }, + "/v1/admin/mail/folders/{folder}/messages/{uid}/attachments/{part_id}": { + "get": { + "summary": "Download an attachment" + } + }, + "/v1/admin/mail/folders/{folder}/messages/{uid}/flags": { + "patch": { + "summary": "Update message flags" + } + }, + "/v1/admin/mail/folders/{folder}/messages/{uid}/move": { + "post": { + "summary": "Move a message" + } + }, + "/v1/admin/mail/messages": { + "post": { + "summary": "Send a message" + } + }, + "/v1/admin/mail/status": { + "get": { + "summary": "Get mailbox configuration status" + } + }, "/v1/admin/orders/": { "get": { "summary": "List all orders (admin)" @@ -111,6 +165,11 @@ "summary": "Update return request (admin)" } }, + "/v1/admin/telemetry/errors": { + "get": { + "summary": "Frontend errors, grouped (admin)" + } + }, "/v1/analytics/events": { "post": { "summary": "Record anonymous storefront events" @@ -195,6 +254,11 @@ "summary": "Request a return" } }, + "/v1/telemetry/errors": { + "post": { + "summary": "Report uncaught frontend errors" + } + }, "/v1/webhooks/stripe": { "post": { "summary": "Stripe payment webhook"