Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions API/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |
Expand Down
22 changes: 22 additions & 0 deletions Configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 —
Expand All @@ -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 |
Expand Down
1 change: 1 addition & 0 deletions home.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
64 changes: 64 additions & 0 deletions openapi.snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -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)"
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
Loading