# Produkte berichten über ihre API

URL: https://www.mikebild.dev/de/blog/produkte-berichten-ueber-ihre-api/

Eine Statistik-API ist eine Produktfunktion. Dieser Satz klingt selbstverständlich, wurde in der Arbeit am [Mission Cockpit](https://www.mikebild.dev/de/blog/ein-cockpit-das-stuendlich-schaut/) aber erst nach zwei schlechteren Entwürfen klar. Zunächst sollte ein zentraler Sammler direkt auf mehrere PostgreSQL-Datenbanken zugreifen. Danach standen besondere private Reporting-Endpunkte und eigene Schlüssel im Raum. Beides hätte das Cockpit tiefer in die Produkte eingebaut, als es für einen Report nötig ist.

Übrig blieb ein kleinerer Vertrag. ContentKit, WikiKit und die Orchestrierungssoftware verdichten ihre Daten in der jeweils eigenen PostgreSQL-Datenbank. Auch Deck-Pläne, Renderings, SVG-/PNG-Komponenten und asynchrone Builds werden von ContentKit gezählt. Alle drei bieten ihre Sicht über normale, authentifizierte HTTP-Endpunkte an. Der Sammler sieht weder Tabellen noch Markdown, Namen, Schlüssel oder interne IDs.

Diese Aufteilung ist kein spezieller Kniff für Dashboards. Jeder autorisierte Verbraucher kann dieselbe API verwenden: ein Kapazitätsreport, eine Qualitätsprüfung, eine lokale Auswertung oder ein späteres Exportwerkzeug.

## Datenbesitz ist mehr als Speicherort

Wer eine Tabelle besitzt, kennt ihre Bedeutung. ContentKit weiß, wann eine Revision erstellt, ein Deck vollständig gerendert und ein Release tatsächlich aktiviert wurde. WikiKit unterscheidet einen gestarteten Ingest von einem geprüften Wissensstand. Die Ausführungsschicht kennt den Unterschied zwischen einem vorbereiteten und einem abgeschlossenen Lauf. Ein zentraler Sammler müsste diese Bedeutungen nachbauen und bei jeder Produktänderung mitziehen.

Direktes SQL macht diesen Nachbau besonders leicht – und damit gefährlich. Eine Abfrage funktioniert, obwohl sie vielleicht einen Zwischenstatus mitzählt, den das Produkt selbst niemals als abgeschlossen anzeigen würde. Eine neue Migration kann den Report brechen, ohne dass sich die öffentliche Produkt-API geändert hat. Der Datenbankzugriff umgeht außerdem die normale Rechteprüfung und zieht Credentials mehrerer Systeme an eine Stelle.

Mit einer Produkt-API liegt die fachliche Verdichtung dort, wo auch die Zustandsänderung implementiert ist:

:::process{title="Fachliche Verdichtung bleibt im Produkt" role="primary" preferredPattern="connected-process"}
1. Produktaktion · erzeugt einen fachlich definierten Zustandsübergang
2. Eigener Datenspeicher · bewahrt die interne Wahrheit des Produkts
3. Aggregation · übersetzt interne Zustände in begrenzte fachliche Kennzahlen
4. Statistik-API · liefert authentifizierte Werte mit Zeitraum und Bedeutung
5. Collector · führt kompatible Antworten zusammen, ohne sie neu zu definieren
6. Report · erklärt den eingefrorenen gemeinsamen Stand
:::

Der Sammler darf die Antworten zusammenführen, aber nicht neu deuten, was ein erfolgreicher Release oder ein verifizierter Claim ist. Diese Begriffe gehören dem Produkt.

## Einheitlich ist die Zeit, nicht die Fachlichkeit

Ein gemeinsamer Vertrag bedeutet nicht, dass alle Produkte dieselben Kennzahlen liefern. Das wäre eine künstliche Gleichmacherei. Gemeinsam sind nur die Eigenschaften, die ein Collector für zuverlässige Zeitfenster benötigt:

- `bucket` bestimmt die Auflösung: Stunde, Tag, Monat oder Jahr.
- `from` ist der eingeschlossene Anfang des Fensters.
- `to` ist das ausgeschlossene Ende.
- `tz` macht die Zeitzone ausdrücklich, hier durchgehend UTC.
- `buckets` enthält eine dichte Reihe einschließlich leerer Intervalle.
- `totals` fasst genau das angefragte Fenster zusammen.

Die Nutzlast bleibt fachlich. ContentKit berichtet über Releases, Content, Reader, Webhooks, Audio, Engagement und Decks samt Renderdauer, Cache und asynchronen Jobs. WikiKit berichtet über Ingests, Wissen, Modellnutzung und Webhooks. Die Ausführungsschicht kennt Läufe, Connector-Aufrufe, Ereignisklassen, Modellnutzung und Qualität.

```json
{
  "bucket": "hour",
  "tz": "UTC",
  "from": "2026-07-18T10:00:00.000Z",
  "to": "2026-07-18T12:00:00.000Z",
  "buckets": [
    { "ts": "2026-07-18T10:00:00.000Z", "items_created": 2 },
    { "ts": "2026-07-18T11:00:00.000Z", "items_created": 0 }
  ],
  "totals": { "items_created": 2 }
}
```

Die halboffene Schreibweise `[from,to)` verhindert Doppelzählungen. Der Lauf für 10 bis 11 Uhr enthält ein Ereignis um 10:59:59, aber keines um 11:00:00. Das nächste Fenster beginnt genau dort. Ein Retry fragt dasselbe Fenster erneut ab und erhält dieselbe Bedeutung.

## Null, unbekannt und unvollständig

Eine dichte Zeitreihe enthält auch leere Buckets. Das ist praktisch, weil der Collector keine fehlenden Stunden erraten oder auffüllen muss. Eine Null ist dabei ein gemessener Wert: Im Fenster gab es kein entsprechendes Ereignis.

Davon zu unterscheiden sind zwei andere Zustände. „Unbekannt“ bedeutet, dass eine Quelle oder Kennzahl noch nicht angeschlossen ist. „Unvollständig“ bedeutet, dass ein erwarteter Snapshot fehlt oder das Fenster noch nicht abgeschlossen war. Diese drei Fälle dürfen im Report nicht gleich aussehen.

```ts
type ObservedValue =
  | { state: "measured"; value: number }
  | { state: "missing_source"; connector: string }
  | { state: "incomplete_window"; expected: number; observed: number };
```

Für jemanden, der einen Report liest, ist diese Unterscheidung wichtiger als eine weitere Nachkommastelle. Null technische Fehler sind beruhigend. Null Seitenaufrufe können stimmen. Fehlende Analytics-Daten sind keine Null und sollten als offene Verbindung erscheinen.

## Datenschutz beginnt bei der Antwortform

Aggregate sollten Informationen weglassen, die der Zweck nicht benötigt. ContentKit zählt erfolgreiche, fehlgeschlagene und rate-limitierte Reader-Anmeldungen sowie Deck-Builds, speichert dafür aber weder Benutzernamen noch IP-Adresse, Session-ID, Decknamen, Markdown-Inhalte, URLs, Job-IDs oder Credentials in der Statistikstabelle. WikiKit gibt keine Quellentexte über die Statistik-API aus.

Eine solche Negativliste gehört in die Dokumentation und in Vertragstests. „Wir geben aktuell keine Namen zurück“ ist schwächer als ein Schema, in dem es für Namen gar kein Feld gibt. Der Collector kann dann keinen sensiblen Wert versehentlich in ein JSON-Artefakt oder eine Reportseite übernehmen.

Auch Trace-Daten bleiben auf ihren Zweck begrenzt. W3C Trace Context standardisiert `traceparent` und `tracestate`, damit ein Aufruf über mehrere Dienste korreliert werden kann. Die W3C-Empfehlung warnt ausdrücklich davor, personenbezogene oder andere sensible Informationen in diesen Headern zu transportieren. Eine Trace-ID verbindet technische Schritte; sie beschreibt keinen Menschen.

```http
GET /v1/sites/example/stats/releases?bucket=hour&from=2026-07-18T10:00:00Z&to=2026-07-18T11:00:00Z
Authorization: Bearer <site-read-key>
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01
```

## Zero-config heißt: vorhandene Begriffe verwenden

Vollständig konfigurationslos ist Telemetrie selten. Ein Dienst braucht zumindest eine dauerhafte Ablage und eine Information darüber, ob er in Entwicklung, Test oder Produktion läuft. „Zero-config“ ist hier enger gemeint: Für das Cockpit wird im Produkt kein Cockpit-Modus aktiviert, kein besonderer Schlüssel erzeugt und kein Reportziel eingetragen.

Die Statistik-Endpunkte verwenden vorhandene Produktgrenzen. ContentKit nutzt den Site-Slug und einen normalen `content:read`-Key – auch für Deck-Statistiken. WikiKit nutzt den Space und `knowledge:read`. Die Orchestrierungssoftware nutzt ihre gewöhnlichen mandantenbezogenen Leserechte.

OpenTelemetry liefert dafür ein brauchbares gemeinsames Vokabular. `service.name` benennt den logischen Dienst. `deployment.environment.name` unterscheidet `development`, `staging`, `production` und `test`. Der ältere Schlüssel `deployment.environment` ist inzwischen als veraltet markiert. Solche standardisierten Felder verhindern, dass ein Dienst `prod`, ein anderer `live` und ein dritter `production-eu` als dasselbe Konzept behandelt.

```yaml
resource:
  service.name: contentkit
  service.version: 1.14.5
  deployment.environment.name: production
```

Das Produkt setzt diese Werte einmal für seine Laufzeit. Jeder strukturierte Logeintrag und jeder weitergereichte Trace kann sie anschließend verwenden. Der Reporting-Collector muss keine Produktetiketten reparieren.

Die gemeinsame Bezeichnung allein genügt allerdings nicht. Im ersten produktiven
Lauf las der Connector-Aggregator `status_code` und `latency_ms`, während ein älterer
Egress-Pfad noch `status` und `duration_ms` schrieb. 37 erfolgreiche HTTP-Aufrufe
erschienen dadurch als `network_error`. Der Übergang wurde absichtlich in beide
Richtungen kompatibel gemacht: Der Leser versteht alte und neue Felder, der Schreiber
liefert vorerst beide, und neue Tests verwenden echte Auditzeilen statt handgebauter
Idealobjekte. Ein erfolgreicher S3-Aufruf ohne HTTP-Status heißt nun `no_status` und
nicht Netzwerkfehler.

Eine zweite Lücke betraf die Umgebung selbst. Ein stündlicher Schedule verwendete
PROD-Connectoren, sein Execution-Datensatz stand jedoch auf `dev`; die Laufzeit musste
bei jedem Schritt auf PROD ausweichen und warnte zu Recht. Seitdem trägt der Schedule
`execution_mode` als persistierten Teil seines allgemeinen Vertrags. Neue
Automatisierungen verwenden `prod`, ein kurzer Entwicklungsloop muss `dev` ausdrücklich
wählen. Damit stimmen Queue, Ausführung, Audit und Statistik über denselben Begriff
überein.

## Begrenzte Abfragen statt versteckter Last

Eine allgemeine Statistik-API braucht harte Grenzen. ContentKit erlaubt stündliche Fenster bis 31 Tage, tägliche bis 366 Tage, monatliche bis fünf Jahre und jährliche bis zehn Jahre. Das gilt auch für die Site-gebundenen Deck-Statistiken. Ungültige oder zu große Fenster liefern `422`, statt die Produktdatenbank mit einer unerwarteten Vollauswertung zu belasten.

Diese Grenzen gehören zum Produktvertrag, weil nur das Produkt seine Kosten kennt. Ein Collector sollte nicht versuchen, sie mit vielen überlappenden Abfragen zu umgehen. Für einen langen Zeitraum verwendet er die gröbere Auflösung oder arbeitet in eindeutigen, nicht überlappenden Seiten.

Dasselbe gilt für Aufbewahrung. ContentKit entfernt seine anonymen Reader-Ereignisse standardmäßig nach 400 Tagen. Ein Reportartefakt kann länger bestehen, wenn sein verdichteter Inhalt dafür vorgesehen ist. Rohereignis, Produktaggregat und veröffentlichter Report haben verschiedene Lebenszyklen; sie müssen nicht in einem Speicher enden.

## Bestehende Schlüssel statt einer Hintertür

Ein eigener „Reporting-Key“ wirkt zunächst ordentlich, schafft aber eine zweite Sicherheitslogik. Was darf dieser Schlüssel lesen? Gilt er für eine Site, einen Space oder das ganze Produkt? Wer rotiert ihn? Sobald jedes Produkt darauf eine andere Antwort gibt, wird aus dem vermeintlich einfachen Collector ein Sonderfall mit weitreichenden Rechten.

Die Statistik-APIs verwenden deshalb vorhandene Scopes. Ein ContentKit-Key mit `content:read` darf einschließlich der Deck-Werte genau die Statistiken der Site lesen, für die er ausgestellt wurde. Ein WikiKit-Key benötigt `knowledge:read` und bleibt auf seinen Space begrenzt. Ohne passenden Schlüssel bleibt jeder dieser Endpunkte geschlossen.

Für den Collector gilt damit dieselbe Regel wie für andere API-Nutzer: pro Produkt ein eigener, möglichst enger Schlüssel; Secrets ausschließlich in der Credential-Ablage; keine Schlüssel in Workflowdefinition, JSON-Artefakt, Log oder Report. Eine Rotation verändert die Credential-Referenz, nicht den fachlichen Ablauf.

```yaml
connectors:
  - name: content-publishing-stats
    base_url: https://content.example.test
    credential_ref: content-read-key
  - name: knowledge-stats
    base_url: https://knowledge.example.test
    credential_ref: knowledge-read-key
```

Der Workflow kennt nur `credential_ref`. Der tatsächliche Wert wird beim HTTP-Aufruf eingesetzt und erscheint weder im gespeicherten Ablauf noch in dessen Ergebnis. Diese Trennung lässt sich testen: Ein Aufruf ohne Schlüssel muss `401` liefern, ein Schlüssel ohne passenden Scope `403`, ein korrekter Schlüssel `200`.

## PostgreSQL ist intern, nicht verboten

Die Ablehnung direkter Datenbankabfragen bedeutet nicht, dass Aggregate außerhalb der Produktdatenbank liegen müssen. Für ContentKit und WikiKit ist PostgreSQL gerade der passende Ort. Ereignis und fachlicher Zustand lassen sich dort transaktional erfassen, nach Site oder Space begrenzen und mit denselben Migrationen weiterentwickeln wie der Rest des Produkts.

Wichtig ist die Richtung der Abhängigkeit. Das Produkt darf seine eigene Datenbank kennen. Ein fremder Collector darf sie nicht kennen müssen. Ob eine Antwort aus einer vorberechneten Tabelle, einer begrenzten SQL-Aggregation oder einem Cache entsteht, bleibt eine interne Entscheidung.

Deck-Rendering zeigt, warum Datenbesitz nicht mit einem eigenen Dienst gleichzusetzen ist. Planung, synchrone und asynchrone Builds, Cache-Treffer, erzeugte Slides sowie SVG-/PNG-Komponenten werden innerhalb der Site-Grenze von ContentKit erfasst. Die Statistik enthält keine Quelldokumente und nutzt denselben Zeitvertrag wie die übrigen ContentKit-Werte.

„Alles in PostgreSQL“ und „alles in Dateien“ wären beides technische Gleichmacherei. Entscheidend ist, dass jedes Produkt eine zu seinem Betriebsmodell passende dauerhafte Ablage wählt und nach außen denselben verlässlichen Zeitvertrag erfüllt.

## Dokumentation gehört zum Vertrag

Ein neuer Endpunkt ist erst fertig, wenn OpenAPI, `llms.txt`, `llms-full.txt`, README und die ausführliche Produktdokumentation dasselbe erzählen. Sonst findet ein Mensch einen Scope in der README, ein Client generiert aus einer älteren OpenAPI-Datei und ein Agent liest eine dritte Variante.

Darum prüfen die Repositories ihre generierten Dokumente auf Drift. Vertragstests schicken gültige, ungültige, zu große und nicht autorisierte Fenster an die Handler. Integrationstests verwenden die echte Produktablage. Ein Binärtest stellt schließlich sicher, dass die ausgelieferte Datei dieselben Routen und dieselbe Versionsnummer enthält wie der Quellstand.

Diese Sorgfalt wirkt unspektakulär, verhindert aber genau jene Kopplung, die eine API eigentlich auflösen sollte. Der Collector wird gegen den veröffentlichten Vertrag gebaut, nicht gegen eine Erinnerung daran, wie die Antwort gestern aussah.

## APIs machen Austausch möglich

Der unmittelbare Effekt dieser Trennung zeigt sich beim Testen. Jede Produkt-API lässt sich mit einem festen Fenster gegen ihr eigenes Schema prüfen. Der Collector erhält Fixtures ohne Zugriff auf die Datenbank. Ein Produktrelease kann unabhängig zurückgerollt werden, solange der veröffentlichte Vertrag erhalten bleibt.

```mermaid
flowchart TD
    P["Produktrelease"] --> T["eigene Unit- und Integrationstests"]
    T --> O["OpenAPI- und Doku-Driftprüfung"]
    O --> L["Live /stats Contract"]
    L --> C["Collector-Contract"]
    C --> R["Report-Artefakt"]
    L -. unverändert .-> X["Collector bleibt unverändert"]
```

Ein neuer Verbraucher braucht keine neue Tabelle im Produkt. Ein anderes Reporting-Werkzeug kann dieselbe API ebenso aufrufen. ContentKit darf seine interne Ablage ändern, ohne dass `/v1/sites/{site}/stats/decks` neu erfunden wird.

Das hält auch die Produkte verständlich. ContentKit bleibt ein Publishing-System für Seiten, Artikel, Reports und Präsentationen, WikiKit eine kuratierte Wissensbasis und die Ausführungsschicht ein Orchestrator. Ihre Statistik-APIs beschreiben die eigene Arbeit. Alles Weitere – Zusammenführung, Aufbewahrung, Darstellung und fachübergreifende Analyse – geschieht außerhalb ihrer Grenzen.

## Was sich seit damals geändert hat

Der Beitrag [HTTP APIs: klein, stabil, lesbar](https://www.mikebild.dev/de/blog/http-apis-klein-stabil-lesbar/) formulierte 2014 einen Endpunkt als langfristiges Versprechen an unbekannte Clients. Kleine Ressourcen, eindeutige Zeitstempel, stabile Bedeutungen und lesbare Fehler sollten verhindern, dass jeder Client eine eigene Abwehrschicht baut. Diese Anforderungen gelten weiterhin.

Neu ist hier der Blick auf interne Produktstatistiken. Ein Statistikpfad wird nicht für ein bestimmtes Cockpit gebaut, sondern als begrenzte Projektion der eigenen Produktdaten. Er verwendet bestehende Schlüssel und Scopes, liefert Zeitgrenzen und Datenqualität mit und veröffentlicht nur Aggregate. OpenAPI, README und maschinenlesbare Dokumentation entstehen aus demselben Vertrag. Damit bleibt die API klein, obwohl inzwischen Menschen, Workflows und Agenten zu ihren Clients gehören.

## Weiterführende Quellen

- [ContentKit Product Analytics](https://github.com/MikeBild/contentkit/blob/main/docs/PRODUCT_ANALYTICS.md)
- [ContentKit OpenAPI](https://github.com/MikeBild/contentkit/blob/main/docs/openapi.json)
- [WikiKit Architektur](https://github.com/MikeBild/wikikit/blob/main/docs/ARCHITECTURE.md)
- [ContentKit Semantic Slide Decks](https://github.com/MikeBild/contentkit/blob/main/docs/SLIDE_DECKS.md)
- [W3C Trace Context](https://www.w3.org/TR/trace-context/)
- [OpenTelemetry Service Semantic Conventions](https://opentelemetry.io/docs/specs/semconv/resource/service/)
- [OpenTelemetry Deployment Attributes](https://opentelemetry.io/docs/specs/semconv/registry/attributes/deployment/)
- [HTTP APIs: klein, stabil, lesbar](https://www.mikebild.dev/de/blog/http-apis-klein-stabil-lesbar/)
