duxt meldet, was Leser mit einer Dokumentationsseite tun — suchen, einen Befehl
kopieren, einen Endpoint ausprobieren, „War diese Seite hilfreich?“ beantworten
— an **eine einzige Funktion, die du schreibst**. Sie liefert keinen Anbieter,
kein SDK und kein Skript aus, und eine Seite, die diese Funktion nicht
geschrieben hat, meldet nichts und lädt nichts zusätzlich herunter.

::callout{type="info" title="Aus, bis du es einschaltest"}
Es gibt kein Standardziel, denn eine Dokumentationsebene kann keines für die
Leser eines anderen aussuchen. Ohne `duxt.analytics.track` erreicht jede
Aufrufstelle unten eine fehlende Funktion und hält an.
::

## Schritte

::steps

1. **Schreibe die Callback-Funktion.** Sie lebt in `app.config.ts`, neben dem
   Rest deiner duxt-Konfiguration, und bekommt ein Ereignis pro Interaktion
   übergeben:

   ```ts [app/app.config.ts]
   export default defineAppConfig({
     duxt: {
       analytics: {
         track(event) {
           // `event.name` ist der Diskriminator; `event.context` sagt, auf
           // welcher Seite, in welcher Locale, aus welcher Quelle und in
           // welcher Version es passiert ist.
           console.log(event.name, event.context.path);
         }
       }
     }
   });
   ```

   Die Callback-Funktion läuft **nur im Browser**. Ein Build und ein
   Vorab-Rendering rufen sie nie auf, ein statisches Rendering füllt deine
   Analytik also nicht mit Seiten, die ein Crawler geöffnet hat.

2. **Sende es dorthin, wo deine Analytik ohnehin liegt.** Was auch immer dein
   Anbieter anbietet — ein SDK, das ohnehin auf der Seite liegt, oder eine
   eigene Route, damit der Browser des Lesers nie mit einem Dritten spricht,
   den du nicht gewählt hast:

   ```ts [app/app.config.ts]
   export default defineAppConfig({
     duxt: {
       analytics: {
         track(event) {
           const { name, context, ...properties } = event;

           // Dein Anbieter, deine Eigenschaftsnamen, deine
           // Einwilligungsschranke.
           window.plausible?.(name, { props: { ...context, ...properties } });
         }
       }
     }
   });
   ```

   ::callout{type="warning" title="Die Einwilligung ist deine, nicht duxts"}
   duxt setzt kein Cookie, speichert nichts, puffert nichts und zeigt kein
   Einwilligungsbanner — es hat nichts zu fragen, denn nichts verlässt die Seite
   außer durch deine Funktion. Das heißt auch: **Die Schranke gehört dir.**
   Wenn deine Datenschutzerklärung eine Einwilligung verlangt, bevor das einen
   Anbieter erreicht, prüfe sie in `track` und kehre früh zurück.
   ::

3. **Wähle die Ereignisse, die du willst, und lass den Rest fallen.** Die Union
   ist geschlossen, ein `switch` deckt sie also erschöpfend ab, und was du nicht
   nennst, wird schlicht nicht gesendet:

   ```ts [app/app.config.ts]
   track(event) {
     if (event.name === 'search' && event.results === 0) {
       // Der eine Bericht, den eine Dokumentationsseite fast immer will: ein
       // Begriff, nach dem jemand gesucht hat und den die Seite nicht
       // beantworten konnte.
       report('docs-search-miss', { query: event.query, ...event.context });
     }
   }
   ```

4. **Prüfe, dass ein Ereignis ankommt**, mit dem Kontext, den du erwartet hast —
   unter einem Versions- und Locale-Präfix, nicht nur an der nackten URL.

::

## Die Ereignisse

Jedes Ereignis hat einen `name`, eigene Felder und einen `context`.

| `name` | Wird gesendet, wenn | Trägt |
| :--- | :--- | :--- |
| `search` | Eine entprellte, nicht leere Suchanfrage zur Ruhe kommt — auch eine, die nichts gefunden hat | `query`, `results`, `approximate` |
| `search-result` | Ein Leser einen Treffer öffnet | `query`, `rank`, `to`, `collection` |
| `copy` | Ein Codebeispiel der Ebene kopiert wird | `kind`, `language`, `manager`, `sample` |
| `api-request` | Eine Try-it-Anfrage zur Ruhe kommt | `operation`, `method`, `path`, `outcome`, `statusClass`, `duration` |
| `feedback` | Ein Leser „War diese Seite hilfreich?“ beantwortet | `helpful` |

`search` feuert **einmal pro abgeschlossener Suchanfrage**, nicht pro
Tastenanschlag: Die Entprellung verwirft, was niemand zu Ende getippt hat, und
ein Ergebnis, das ein neueres ablöst, wird nie gemeldet, weil der Leser es nie
gesehen hat.

`copy` deckt die eigenen Codebeispiele der Ebene ab — einen Codeblock, einen
`::code-group`-Block, einen `::package-managers`-Block, ein Request-Sample im
Try-it-Client — und `kind` sagt, welches davon. **„Seite als Markdown kopieren“
gehört nicht zu dieser Familie** und sendet nichts: Es ist die Seite, kein
Beispiel darin.

`api-request` beschreibt die Operation **so, wie das OpenAPI-Dokument sie
deklariert**. `path` ist die Vorlage (`/pets/{id}`), nie die URL, die aufgerufen
wurde.

### Der Kontext

| Feld | Hinweise |
| :--- | :--- |
| `path` | Der Dokumentationspfad, ohne Locale-Präfix, Query-String oder Fragment |
| `locale` | Die Locale, in der die Seite gelesen wurde |
| `source` | Aus welcher Quelle die Seite stammt — fehlt auf einer Seite mit nur einer Quelle |
| `version` | Welche Version — fehlt, wo die Quelle nur eine hat |

## Was nie gesendet wird

Diese Liste ist der Sinn der Funktion, deshalb lohnt es sich, sie deutlich
auszusprechen. Kein Ereignis trägt:

- ein API-Credential, einen Header oder ein Token;
- einen Request- oder Response-Body oder eine konkrete Request-URL;
- den Text, der kopiert wurde;
- den gerenderten Inhalt eines Suchtreffers;
- irgendeine Browser-, Sitzungs- oder Leserkennung;
- den Query-String oder das Fragment aus dem Pfad der Seite selbst.

duxt **überträgt, puffert und speichert** auch selbst nie etwas. Es gibt in der
Ebene keine Warteschlange, keinen Wiederholungsversuch, keinen Speicher und
keinen Netzwerkaufruf: Deine Funktion ist das Einzige, was die Seite je
verlässt.

Deine Callback-Funktion wird nie abgewartet, und was sie wirft oder womit sie
ablehnt, wird verschluckt — ein Anbieter, der ausfällt, darf nicht die Suche,
das Kopieren, das Feedback oder die Anfrage mit sich reißen.

## Seiten-Feedback, so oder so

`feedback` ist dieselbe Antwort, die `DuxtPageFeedback` schon immer gesendet
hat. Das `@feedback`-Ereignis der Komponente und ihr Standard-Slot sind
unverändert, [sie zu umhüllen](/guides/collect-page-feedback) funktioniert also
weiterhin, und keiner der beiden Wege braucht den anderen — nimm das
Analytik-Callback, wenn du ein Ziel für alles willst, und die Hülle, wenn das
Feedback allein irgendwo eigenes landen soll.

## Checkliste

- [ ] `duxt.analytics.track` ist gesetzt, und mehr war nicht nötig, um es
      einzuschalten
- [ ] Das Ziel gehört dir — ein Anbieter deiner Wahl oder eine eigene Route
- [ ] Die Einwilligung wird in `track` geprüft, falls deine Richtlinie danach
      fragt
- [ ] Ein Ereignis kam mit dem Pfad, der Locale, der Quelle und der Version an,
      die du erwartet hast
- [ ] Nichts in dem, was du weiterleitest, identifiziert den Leser