Feedback-Vertrag
Ein kleiner Dialog, den Besucher auf jeder Seite Ihrer Website oder Ihres Produkts öffnen können, um zu sagen, was nicht funktioniert oder fehlt. Fügen Sie ein Script-Tag ein oder rufen Sie den Endpunkt aus Ihrem eigenen Dialog auf. Was ankommt, landet in einem Block im Panel neben der Deinstallations-Umfrage und Rate Us, und der Eigentümer erhält eine Nachricht im Posteingang.
1. Widget einfügen
Ein verzögert geladenes Script-Tag ohne Inline-Code, daher übersteht es eine strenge CSP:
<script src="https://extops.dev/widget.js"
data-extops-widget="wk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX" defer></script>
Erlauben Sie unseren Origin in script-src und connect-src. Die Datei wird von uns ausgeliefert, hat keine Abhängigkeiten und bleibt unter 7 KB gzip; ihre Stile laufen über das CSSOM in einem Shadow Root, daher brauchen Sie kein 'unsafe-inline', und das CSS Ihrer Seite kann den Dialog nicht verändern.
Optionale Attribute: data-locale, data-api, data-position="left" und data-button="false", wenn Sie den Dialog selbst mit window.ExtOpsFeedback.open() öffnen wollen. Außerdem: data-button-style="icon" für eine runde 48-px-Symbolschaltfläche, data-surface="app", um sie auf dem Telefon über die untere Navigation einer App zu heben, und data-color-preset, um die Farbe der Schaltfläche von der Seite aus festzulegen.
Helles und dunkles Design
Jede Farbvorlage wird in einem hellen und einem dunklen Design gezeichnet. Standardmäßig (data-theme="auto") folgt das Widget Ihrer Seite: <html data-theme>, einer Klasse dark oder light auf <html> oder deren color-scheme. Eine Seite ohne eigenes Design erhält das Design des Betriebssystems der Besucher. Wenn Ihr Design-Umschalter eine dieser Angaben ändert, färbt sich das Widget sofort um, ohne Neuladen.
Um das Design festzulegen, setzen Sie data-theme="light" oder data-theme="dark". Wenn Ihre Seite ihr Design an einer Stelle speichert, die das Widget nicht sieht, rufen Sie window.ExtOpsFeedback.setTheme('dark') aus Ihrem Umschalter auf, und setTheme('auto'), um wieder der Seite zu folgen.
2. Oder den Endpunkt selbst aufrufen
Für eine Website, die gar kein fremdes Skript erlaubt, oder ein Produkt, das das Formular im eigenen Designsystem haben will. Derselbe Endpunkt, an den unser Widget sendet: Sie müssen nie zwischen Ihrer Oberfläche und unserer Pipeline wählen.
await fetch('https://extops.dev/api/v1/feedback/widget', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
widget_key: 'wk_XXXXXXXXXXXXXXXXXXXXXXXXXXXX',
topic: 'bug',
comment: userText,
page: location.href,
locale: navigator.language,
event_id: crypto.randomUUID(),
dwell_ms: Date.now() - openedAt,
}),
});
Lesen Sie die vom Inhaber konfigurierten Themen aus GET /api/v1/feedback/widget/config?key=…, damit Ihr Dialog genau das anbietet, was das Panel anbietet. Verwenden Sie bei Wiederholungen dieselbe event_id: Der Server dedupliziert darüber, sodass ein wackeliges Netz aus einer Nachricht keine zwei macht.
3. Der Widget-Schlüssel ist absichtlich öffentlich
Er steht im Quelltext Ihrer Seite, und das ist in Ordnung: Er kann nur eine Feedback-Zeile anlegen. Er kann keine Installations- oder Deinstallationsereignisse schreiben, nichts außer der Dialogkonfiguration lesen und kein Projekt benennen (project_id im Body wird nicht akzeptiert). Deshalb bekommt ein Widget einen eigenen Schlüssel statt des webhook_token Ihrer Quelle, der Metrik-Ereignisse schreiben kann und nie im HTML stehen darf. Sie können den Schlüssel jederzeit neu ausstellen; der alte wird sofort nicht mehr akzeptiert.
4. Was wir speichern und was nicht
Wir speichern das Thema, den Text, die normalisierte Seite, die Sprache, einen optionalen Kontakt, die Zeit, die Art des Feedbacks und welches Widget es erfasst hat.
- Nicht den Query-String oder das Fragment der Seite. Sie werden bei der Annahme verworfen: Sie enthalten oft Zugriffstoken, E-Mail-Adressen und Suchbegriffe, nach denen wir nie gefragt haben und die wir nicht 180 Tage aufbewahren wollen.
- Nicht das Land oder die IP des Absenders. Ein ermitteltes Land steht nur im Tagesaggregat, nie an einem einzelnen Ereignis; die IP ist nur ein flüchtiger Schlüssel für die Ratenbegrenzung.
- Keinen Kontakt, wenn das Widget nicht danach fragt. Ein veraltetes Snippet, das es trotzdem sendet, bekommt keinen Fehler: Das Feld wird verworfen, denn die Datenschutzerklärung verspricht, was die aktuelle Konfiguration des Inhabers vorgibt.
Die Aufbewahrungsdauer beträgt 180 Tage, dieselbe Regel wie für jede andere Art von Feedback.
5. Antworten und Fehlercodes
Ein Erfolg sieht so aus: {"status":"ok","recorded":true}. recorded: false ist ebenfalls ein Erfolg: Die Einsendung war ein Duplikat, oder eine Anti-Bot-Heuristik hat angeschlagen. Zeigen Sie in beiden Fällen denselben Dank: Ein Skript soll nicht erfahren, welche Regel es erwischt hat, und wer nach einem Timeout erneut sendet, soll keinen Fehler für eine Nachricht sehen, die wir bereits haben.
| HTTP | code | Wann |
|---|---|---|
| 400 | validation_error | kein Schlüssel, kein Thema, ein Thema, das das Widget nicht anbietet, ein leerer Kommentar, ein fehlerhafter Body |
| 403 | origin_not_allowed | der Origin der Seite steht nicht auf der Allowlist des Widgets |
| 404 | not_found | unbekannter Schlüssel oder ein deaktiviertes Widget: dieselbe Antwort, damit ein öffentlicher Schlüssel nicht ausloten kann, welche Projekte existieren |
| 429 | rate_limited | 60/Stunde pro Schlüssel, 10/Stunde pro IP; enthält Retry-After |
| 401 | source_token_invalid | nur serverseitiger Weg: falsches Quell-Token |
| 500 | internal_error | our fault |
6. Themen, Arten und Status
Ein Thema besteht aus id und label. Die id ist ein Slug, weil sie in jeder Zeile gespeichert wird und im Panel zum Filterwert wird; das Label formuliert der Inhaber selbst. Ein Widget ohne konfigurierte Themen bietet die Standardthemen an: bug, idea, question, other.
Die Farbe des Dialogs ist eine von sechs Vorgaben, die mit den gehosteten Seiten geteilt werden: amber (Standard), teal, indigo, violet, rose, slate. Bewusst eine gemeinsame Liste: Widget und gehostete Seiten desselben Inhabers sollen gleich aussehen, und eine zweite Palette würde von der ersten abdriften.
Gespeicherte Zeilen haben eine Art: uninstall, rate_us oder widget. Die Art ist ein TAG, keine eigene Pipeline: Panel-Block, Benachrichtigungen und Aufbewahrung sind gemeinsam. Der Bearbeitungsstatus des Inhabers ist new, read oder spam; als Spam markieren nimmt eine Zeile aus der Arbeitsliste und aus den Benachrichtigungen, ohne zu löschen, was jemand geschrieben hat, und lässt sich rückgängig machen.
7. Der Missbrauchsschutz liegt auf dem Server
Weil der Schlüssel öffentlich ist, wäre eine Prüfung im Browser nur ein Rat, keine Regel. Deshalb prüft der Server: die Origin-Allowlist des Widgets (leer heißt jeder Origin, denn ein Kunde kann eine Website über ein Dutzend Subdomains ausliefern), zwei unabhängige Ratenlimits (pro Schlüssel und pro IP), ein Honeypot-Feld und eine Mindestverweildauer sowie Deduplizierung über (widget_id, event_id) in der Datenbank statt einer Vorabprüfung, weil zwei gleichzeitige Wiederholungen beide ein SELECT bestehen würden.
Beide Limiter sind fail-open: Fällt unser Redis aus, erreicht Sie das Feedback trotzdem. Für einen Endpunkt, der einen Absatz schreibt, ist das das kleinere Übel. Ein externes Captcha fügen wir nicht hinzu, es würde Ihre CSP und die Privatsphäre Ihrer Besucher brechen.
8. Der maschinenlesbare Vertrag
Die formale Beschreibung beider Annahme-Endpunkte, des Konfigurationsabrufs, der Enums und der Fehlercodes ist die OpenAPI-3.1-Datei. Sie trägt dieselbe Vertragsversion (1.0.0).