Tastenanschlag-Logging
Potato kann die Pausen, Schreibschübe, Überarbeitungen und Einfügungen hinter einer Freitextantwort aufzeichnen, ohne eines der Zeichen aufzuzeichnen, die ein Annotator tippt.
Potato kann aufzeichnen, wie eine Freitextantwort entstanden ist, ohne die Antwort selbst aufzuzeichnen. Jedes Ereignis trägt einen Zeitstempel, einen Eingabetyp, eine Tastenklasse und eine Längenänderung; keines davon trägt das getippte Zeichen. Aus diesem Strom berechnet Potato etwa vierzig zusammenfassende Merkmale und speichert sie bei der Annotation.
Der Zweck ist, eine verfasste Antwort, getippt mit den Pausen und zweiten Gedanken von jemandem, der sie sich erarbeitet, von einer abgetippten zu unterscheiden, die aus einem anderen Fenster abgeschrieben wurde, oder von einer eingefügten, die aus einem Chatbot stammt. Liest man die fertigen Antworten, ähneln sie einander. Die Logs tun das nicht.
Tastenanschlag-Logging braucht Potato 2.7.2 oder neuer und ist standardmäßig aus: keystroke_logging.enabled ist false, bis Sie es setzen, ein Upgrade beginnt also nie damit, jemanden aufzuzeichnen. Zu den Regeln, die auf diesen Daten aufbauen, siehe Schreibprozess-Erkennung. Bevor Sie das auf menschliche Teilnehmende richten, lesen Sie Ethik des Tastenanschlag-Loggings.
Schnellstart
keystroke_logging:
enabled: trueDas ist die gesamte Minimalkonfiguration. Jedes Freitextfeld im Projekt erzeugt von da an einen inhaltsblinden Ereignisstrom, eine Zusammenfassung und eine Reihe von Erkennungsmarkierungen.
Ein lauffähiges Beispiel wird mit Potato ausgeliefert:
python potato/flask_server.py start examples/advanced/keystroke-logging/config.yaml -p 8000Warnung:
enabledist standardmäßigfalse. Ein Upgrade von Potato beginnt nie stillschweigend damit, Ihre Annotatoren aufzuzeichnen.
Was erfasst wird
Jedes Ereignis hält einen Zeitstempel, einen Eingabetyp, eine Klasse von Taste, die Cursorposition und die Änderung der Feldlänge fest:
{t_ms: 1240, input_type: "insertText", key_class: "letter", pos: 41, delta: +1}
{t_ms: 1310, input_type: "insertText", key_class: "letter", pos: 42, delta: +1}
{t_ms: 3980, input_type: "deleteContentBackward", key_class: "bksp", pos: 42, delta: -1}
{t_ms: 9120, input_type: "insertFromPaste", key_class: "unknown",pos: 43, delta: +287,
meta: {paste_source: "external", paste_hash: "sekqf3"}}
Was bewusst nicht erfasst wird
| Nicht erfasst | Warum |
|---|---|
| Die getippten Zeichen | Der Strom rekonstruiert den Prozess, nicht den Text |
| Eingefügter Text | Nur eine Länge, ein Quellenlabel und ein gesalzener Hash |
| Zwischenstände des Entwurfs | Aus Längenänderungen allein nicht rekonstruierbar |
| Alles in einem Passwortfeld | getFieldIdentity verweigert type="password" rundweg |
| Zwischenablage-Inhalte allgemein | Beim Einfügen zur Klassifikation gelesen, dann verworfen |
Tastenklassen
Die Taste selbst wird nie gespeichert, nur die Familie, zu der sie gehört:
letter, digit, punct, space, enter, bksp, del, nav, mod, func, unknown
Eingabetypen
Potatos primäres Signal ist InputEvent.inputType bei beforeinput, nicht keydown. Das ist die zentrale technische Entscheidung. Einfügen, Drag-and-drop, IME-Komposition, Diktat, Autofill und Rückgängigmachen verändern ein Feld alle, ohne keydown überhaupt auszulösen, ein Logger, der nur auf keydown hört, ist also blind für genau die Fälle, wegen derer es diese Funktion gibt.
Erfasste Eingabetypen: insertText, insertReplacementText, insertFromPaste, insertFromDrop, insertCompositionText, insertLineBreak, insertParagraph, deleteContentBackward, deleteContentForward, deleteWordBackward, deleteWordForward, deleteByCut, deleteByDrag, historyUndo, historyRedo, dazu die synthetischen focus, blur und keydown.
keydown und keyup werden weiterhin abgehört, aber nur, um körperliche Tastenanschläge zu zählen und die Haltedauer zu messen. Die Lücke zwischen aufgetauchten Zeichen und tatsächlich gedrückten Tasten ist das stärkste einzelne Signal, das erhoben wird. Siehe silent_insert_ratio weiter unten.
Welche Felder instrumentiert werden
Standardmäßig jedes Freitextfeld: das text-Schema, Freitextboxen innerhalb von radio und multiselect sowie die Textbereiche für Begründungen oder Notizen in text_edit, pairwise, trajectory_eval und ähnlichen Schemata.
Felder werden über die Attribute schema und label_name identifiziert, die Potato ohnehin an jedes Annotations-Eingabefeld schreibt; ersatzweise wird das name-Attribut an ::: aufgeteilt.
Den Umfang schränken Sie mit einer der beiden Listen ein:
keystroke_logging:
enabled: true
include_schemas: [rationale] # allowlist; empty = all fields
exclude_schemas: [scratch_notes] # denylistOder Sie nehmen ein einzelnes Element in eigenem HTML aus:
<textarea data-keystroke-logging="off" ...></textarea>Konfigurationsreferenz
keystroke_logging:
enabled: false # master switch
fidelity: events # off | summary | events
include_schemas: [] # empty = every free-text field
exclude_schemas: []
store_events: true # persist raw streams (needs fidelity: events)
classify_paste_source: true # label pastes self/instance_text/ai_suggestion/external
idle_session_ms: 30000 # close a session after this much inactivity
flush_interval_ms: 5000 # how often the browser posts completed sessions
pause_thresholds_ms: [500, 1000, 2000, 5000, 10000]
disclose_to_annotators: true # show a recording notice
detection:
enabled: true
calibrate: false # use project-fitted thresholds
on_external_insert: flag # allow | warn | block | flag
thresholds: {} # per-rule overrides| Schlüssel | Vorgabe | Bedeutung |
|---|---|---|
enabled | false | Hauptschalter. Bei false wird nichts erfasst. |
fidelity | events | off deaktiviert; summary berechnet Merkmale, speichert aber keinen Strom; events speichert beides. |
include_schemas | [] | Positivliste von Schemanamen. Leer bedeutet alle. |
exclude_schemas | [] | Negativliste, angewendet nach der Positivliste. |
store_events | true | Rohströme dauerhaft speichern. Ohne fidelity: events wirkungslos. |
classify_paste_source | true | Einfügungen mit der Passage, KI-Vorschlägen und dem bisherigen Feldinhalt vergleichen. |
idle_session_ms | 30000 | Untätigkeit, nach der eine Sitzung geschlossen und übertragen wird. |
flush_interval_ms | 5000 | Übertragungstakt des Browsers. |
pause_thresholds_ms | [500,1000,2000,5000,10000] | Pausen werden zu jeder dieser Schwellen gezählt. |
disclose_to_annotators | true | Hinweis auf die Aufzeichnung anzeigen. Abschalten erzeugt eine Warnung. |
Die Schlüssel unter detection sind in Schreibprozess-Erkennung dokumentiert.
Eine Erfassungstiefe wählen
| Erfassungstiefe | Strom gespeichert | Neue Metriken später berechenbar? | Wann sinnvoll |
|---|---|---|---|
off | — | — | Funktion für dieses Projekt deaktiviert |
summary | Nein | Nein | Sie wissen sicher, welche Merkmale Sie brauchen, oder Ihre Ethikfreigabe deckt das Aufbewahren von Strömen nicht ab |
events | Ja | Ja | Vorgabe. Etwa 2 Byte pro Tastenanschlag |
events ist die empfohlene Einstellung. Eine Antwort mit 500 Wörtern kostet etwa 5 KB, und eine Metrik, die Ihnen erst nach der Datenerhebung einfällt, lässt sich damit noch berechnen.
Zusammenfassende Merkmale
Eine Zusammenfassung pro (Nutzer, Instanz, Feld). Die Merkmalsfamilien folgen Crossley et al. (2024); siehe die Forschungsgrundlage.
Umfang und Verhältnis von Produkt zu Prozess
| Feld | Bedeutung |
|---|---|
keystrokes | Körperliche keydowns, die Text erzeugt haben |
final_chars | Feldlänge am Sitzungsende |
chars_typed / chars_inserted | Zeichen, die durch Tippen / auf beliebigem Weg eingefügt wurden |
chars_deleted | Entfernte Zeichen |
chars_per_keystroke | Über etwa 1,1 heißt: Text kommt ohne Tastenanschläge herein |
active_ms / wall_ms | Zeit am Feld, ohne / mit der Zeit der Abwesenheit |
Rhythmus
| Feld | Bedeutung |
|---|---|
iki_median_ms, iki_mean_ms | Zentrale Tendenz der Abstände zwischen Tastenanschlägen |
iki_p10/p25/p75/p90_ms | Form der Abstandsverteilung |
iki_log_sd, iki_log_cv | Streuung auf logarithmischer Skala. Niedrig heißt metronomisch, und das heißt Abtippen. |
Logarithmische Skala, weil die Verteilungen der Abstände zwischen Tastenanschlägen stark rechtsschief sind. Abstände über 30 Sekunden bleiben aus diesen Statistiken heraus, damit eine Kaffeepause sie nicht dominiert.
Pausen
| Feld | Bedeutung |
|---|---|
pause_counts | Anzahl zu jeder konfigurierten Schwelle |
pause_total_ms | Gesamte Zeit in Pausen |
pre_word_pause_mean_ms | Mittlere Pause vor dem Beginn eines Wortes |
pre_sentence_pause_mean_ms | Mittlere Pause nach einem Satzzeichen |
intraword_iki_median_ms | Medianabstand innerhalb von Wörtern, ein Näherungsmaß für Tippfertigkeit |
Schreibschübe
| Feld | Bedeutung |
|---|---|
bursts, burst_mean_chars, burst_max_chars | Statistiken zu Produktionsläufen |
p_bursts | Schübe, die durch eine Pause enden |
r_bursts | Schübe, die durch eine Überarbeitung enden |
Überarbeitung
| Feld | Bedeutung |
|---|---|
backspaces, deletes, undo_events | Löschverhalten |
non_terminal_edits | Bearbeitungen hinter dem Textende, die schreibende Person ist also zurückgegangen, um zu überarbeiten |
caret_jumps | Nicht benachbarte Cursorbewegungen |
revision_ratio | chars_deleted / chars_typed |
Externe Einfügung
| Feld | Bedeutung |
|---|---|
paste_events, pasted_chars, largest_paste_chars | Umfang der Einfügungen |
pasted_fraction | Anteil des endgültigen Textes, der eingefügt wurde |
drop_events | Einfügungen per Drag-and-drop |
silent_insert_chars / silent_insert_ratio | Zeichen ohne zugehörigen Tastenanschlag |
external_insert_chars / external_insert_ratio | Wie oben, ohne Selbstzitate und Zitate aus der Passage |
paste_sources, paste_chars_by_source | Anzahl und Zeichen je Quellenlabel |
Verwenden Sie external_insert_ratio für die Erkennung. silent_insert_ratio zählt jede stille Einfügung, auch die legitime.
Aufmerksamkeit
| Feld | Bedeutung |
|---|---|
blur_events, blur_total_ms | Zeit weg von der Seite |
max_blur_before_insert_ms | Längste Abwesenheit unmittelbar vor einer großen Einfügung |
first_keystroke_latency_ms | Nachdenkzeit vor dem ersten Zeichen |
Integrität
| Feld | Bedeutung |
|---|---|
untrusted_events | InputEvent.isTrusted === false, also per Skript oder automatisiert erzeugte Eingabe |
composition_events | IME-Komposition |
virtual_keyboard | Mobile oder Bildschirmtastatur erkannt |
Wo die Daten gespeichert werden
Zwei Ziele, aus zwei verschiedenen Gründen.
Rohströme gehen nach SQLite
<task_dir>/project.sqlite, Tabelle typing_sessions, eine Zeile pro Sitzung, über dieselbe Persistenzschicht wie Memos und Codebuch.
Abfragbare Zusammenfassungsspalten liegen denormalisiert neben einer vollständigen JSON-Zusammenfassung und einem zlib-gepackten Ereignis-Blob:
sqlite3 <task_dir>/project.sqlite "
SELECT user_id, schema_name, keystrokes, final_chars,
pasted_fraction, silent_insert_ratio, iki_log_cv,
json_extract(flags,'\$.level') AS level
FROM typing_sessions;"Der Strom wird als ein gepackter Blob pro Sitzung gespeichert, nicht als eine Zeile pro Tastenanschlag. Er wird ohnehin immer nur als Ganzes zurückgelesen, und bei etwa 2 Byte pro Ereignis brächte ein Schema mit einer Zeile pro Tastenanschlag zig Millionen Zeilen in eine Projektdatei, ohne dass Abfragen davon profitieren.
Phasenseiten
Freitextantworten in der Trainingsphase und in Vor- oder Nachbefragungen werden ebenfalls erfasst. Diese Seiten haben keine Instanz-ID, ihre Sitzungen werden deshalb unter dem Platzhalter __phase_page__ einsortiert, den der Rest des Verhaltenssystems ohnehin verwendet, und stattdessen über ihre Spalten phase und page identifiziert:
SELECT phase, page, count(*) FROM typing_sessions GROUP BY phase, page;Genau das lässt das Kalibrierungsbeispiel funktionieren. Eine Abtipp-Aufgabe in der Trainingsphase liefert Beispiele fürs Abtippen, die sich allein anhand von phase von gewöhnlichen, verfassten Antworten unterscheiden lassen.
Zusammenfassungen gehen nach user_state.json
Die kompakte Skizze wird nach <output_annotation_dir>/<user>/user_state.json unter instance_id_to_behavioral_data.<instance>.typing_summaries gespiegelt, mit dem Schlüssel "{schema}:::{label}", sodass sie mit der Annotation ins Admin-Dashboard und in die Exporte wandert.
Rohströme landen dort bewusst nicht. Diese Datei wird bei jedem Speichern einer Annotation vollständig neu serialisiert und atomar überschrieben, und eine lange Antwort sind Tausende von Ereignissen.
Exportieren
Beide Exporte sind Opt-in, damit Verhaltensdaten nie versehentlich in einer Datensatzveröffentlichung landen.
Zusammenfassende Merkmale neben den Annotationen
export_include_typing_dynamics: trueErzeugt typing_dynamics.csv (oder .tsv) neben annotations.csv, eine Zeile pro (Nutzer, Instanz, Feld), mit den zusammenfassenden Merkmalen und dem Urteil des Detektors.
Rohströme
python -m potato.export.cli <config.yaml> --format keystrokesSchreibt keystroke_sessions.parquet und keystroke_events.parquet, ersatzweise JSONL, wenn pyarrow nicht installiert ist. Zum Exporter insgesamt siehe Parquet-Export.
import pandas as pd
events = pd.read_parquet("keystroke_events.parquet")
# Inter-key intervals for one session
s = events[events.session_id == events.session_id.iloc[0]].sort_values("t_ms")
iki = s.t_ms.diff().dropna()
print(iki.median(), iki.std())
# Every externally-sourced paste in the project
print(events[events.paste_source == "external"])API-Endpunkte
| Methode | Route | Zweck |
|---|---|---|
POST | /api/track_typing | Abgeschlossene Sitzungen vom Browser entgegennehmen |
GET | /api/typing_summary/<instance_id> | Zusammenfassungen für eine Instanz, aktueller Nutzer |
GET | /admin/api/writing_process | Auswertung pro Annotator (Admin-Schlüssel erforderlich) |
Sitzungen werden serverseitig zusammengefasst. Der Browser schickt nie eine fertig berechnete Zusammenfassung, die Zahlen lassen sich also nicht mit einem manipulierten Client fälschen, und eine später hinzugefügte Metrik lässt sich aus den gespeicherten Strömen nachberechnen.
Wie Sitzungen funktionieren
Eine Sitzung beginnt, wenn ein Feld den Fokus erhält, und endet mit dem, was zuerst eintritt: Fokusverlust, Navigation zu einer anderen Instanz, idle_session_ms ohne Aktivität oder das Entladen der Seite. Abgeschlossene Sitzungen werden alle flush_interval_ms übertragen, beim Entladen zusätzlich über navigator.sendBeacon, damit eine laufende Sitzung nicht verloren geht.
Mehrere Sitzungen am selben Feld werden zusammengeführt, bevor die Zusammenfassung in den Nutzerzustand geschrieben wird, das Verlassen und Zurückkehren zu einem Feld erscheint also als eine Antwort statt als mehrere verdächtig kurze. Anzahlen und Dauern addieren sich. Verteilungsstatistiken sind mit Tastenanschlägen gewichtete Näherungen, greifen Sie also auf die Rohströme zurück, wenn Sie eine exakte gepoolte Verteilung brauchen.
Fehlerbehebung
Es werden keine Daten aufgezeichnet
Prüfen Sie keystroke_logging.enabled: true und dass fidelity nicht off ist. In der Browser-Konsole sollte window.keystrokeTracker existieren, mit isInitialized === true. Ist es undefined, hat die Konfiguration das Template nie erreicht.
Der Tracker existiert, aber es erscheinen keine Sitzungen
Prüfen Sie die Feldidentifikation:
const el = document.querySelector('textarea');
window.keystrokeTracker.getFieldIdentity(el); // null means it is not trackednull heißt, dass das Element weder schema- noch label_name-Attribute und kein mit ::: getrenntes name hat, oder dass es per Konfiguration ausgeschlossen ist.
silent_insertion markiert jeden mobilen Annotator
Sollte es nicht, denn die Regel wird unterdrückt, wenn virtual_keyboard wahr ist. Wenn die Erkennung danebengreift, prüfen Sie, ob der Client dieses Flag gesetzt hat. Siehe die Tabelle der Falsch-Positiven.
project.sqlite wächst
Etwa 2 Byte pro Tastenanschlag. Setzen Sie fidelity: summary, um die Merkmale zu behalten und die Ströme fallen zu lassen, oder verwenden Sie typing_store.delete_for_user(), um die Daten einer teilnehmenden Person zu entfernen.
Die Zahlen sehen bei automatisierten Tests falsch aus
Browser-Automatisierung tippt mit Abständen nahe null, und das löst implausible_speed tatsächlich aus. Das ist die Markierung bei der Arbeit und kein Fehler.
Weiterführende Informationen
- Schreibprozess-Erkennung - die sechs Regeln und die drei Erkennungsstufen
- Ethik des Tastenanschlag-Loggings - Einwilligung, IRB, Aufbewahrung, Rechte der Teilnehmenden
- Verhaltens-Tracking - das umfassendere Interaktions-Tracking-System, in dem das steckt
- Qualitätskontrolle - Aufmerksamkeitschecks und Gold-Standards
- Admin-Dashboard - wo das Schreibprozess-Panel liegt
Implementierungsdetails finden Sie in der Quelldokumentation.