Laufzeitphasen: jsPsych-Timelines, Kopfhörertests und externe Aufgaben
Eine jsPsych-8-Timeline, einen serverseitig ausgewerteten Kopfhörertest oder eine oTree- bzw. gehostete Umfrageaufgabe mit signierter Rückkehr als eine Seite einer Potato-Studie ausführen.
Neu in v2.12.0
Eine Laufzeitphase ist eine Seite einer Potato-Studie, die eine Aufgabe ausführt, statt eine Seite mit Fragen anzuzeigen. Potato liefert die Seite aus, übergibt der Aufgabe die ID der teilnehmenden Person, ihre Bedingungen und ausgewählte Umfrageantworten, speichert, was die Aufgabe aufzeichnet, und führt die Person weiter, wenn die Aufgabe endet. Einwilligung, Umfragen, Annotation, die Übergabe an Prolific oder SONA und der Export bleiben in Potato.
Jede Laufzeitphase:
- sitzt im Slot prestudy oder poststudy, festgelegt durch ihre Position in
phases.orderrelativ zuannotation(oder gesetzt mitslot:), und akzeptiertshow_when,exit_whenund Ausbalancierung wie jede andere Phasenseite (Experimentablauf); - läuft unter einer Content-Security-Policy, die Skripte und Netzwerkanfragen nur zum Potato-Server erlaubt;
- lässt sich nicht überspringen, weil der Server das Phasenformular und Next auf der Annotationsseite ablehnt, bis die Daten der Aufgabe gespeichert sind;
- speichert ihre Daten in der Projektdatenbank (
project.sqlite, Tabelleruntime_runs) statt inuser_state.jsonund exportiert sie mit--format runtime, oder mit--format jspsychnur für jsPsych-Seiten.
Diese Schlüssel gelten für jede Laufzeitphase:
| Schlüssel | Standardwert | Bedeutung |
|---|---|---|
extra_scripts | [] | Weitere .js-Dateien aus dem Aufgabenverzeichnis, die vor der Aufgabe geladen werden |
assets_dir | keiner | Ein Ordner mit Dateien, die die Aufgabe laden darf |
expose_answers | [] | Antworten auf frühere Umfragefragen, die an die Aufgabe übergeben werden |
save | on_finish | incremental speichert zusätzlich jeden Trial, sobald er abgeschlossen ist |
allow_rerun | false | Ob die Rückkehr zu einer abgeschlossenen Seite die Aufgabe erneut ausführt |
max_data_mb | 10 | Größter Lauf, den der Server annimmt |
csp.extra_sources | [] | https://-Origins, von denen die Seite Schriften, Bilder oder Skripte laden darf |
jsPsych-Seiten
phases:
order: [consent, stroop, annotation, debrief]
stroop:
type: jspsych
timeline: tasks/stroop.js
plugins: [instructions, html-keyboard-response]Potato liefert jsPsych 8.3.0 und 15 seiner Plugins mit. Eine Studie, die nur diese verwendet, lädt nichts von einem CDN und läuft auf einem Server ohne Internetzugang:
html-keyboard-response, html-button-response, html-slider-response, image-keyboard-response, image-button-response, audio-keyboard-response, audio-button-response, video-keyboard-response, instructions, preload, fullscreen, survey-likert, survey-text, survey-multi-choice und call-function.
Andere Plugins, auch solche aus jspsych-contrib, kommen als Browser-Builds in extra_scripts, also als die Datei index.browser.min.js des npm-Pakets.
| Schlüssel | Standardwert | Bedeutung |
|---|---|---|
timeline | erforderlich | Eine .js-Datei im Aufgabenverzeichnis, die window.potatoTimeline definiert |
plugins | [] | Zu ladende Plugins aus der obigen Liste |
extensions | [] | jsPsych-Erweiterungen, die geladen und an initJsPsych übergeben werden. Potato liefert mouse-tracking mit |
slot | abgeleitet | prestudy vor der Annotationsphase, poststudy danach. Ein expliziter Slot, der der Position der Seite in order widerspricht, hat Vorrang, und eine Warnung wird protokolliert |
Das exit_when einer jsPsych-Seite wird geprüft, wenn die Timeline endet. Die Daten des Laufs sind für eine dadurch ausgeschlossene Person also bereits gespeichert. Um Personen auszuschließen, bevor sie die Aufgabe bearbeiten, stellen Sie den Screener als eigene Seite davor.
Die Timeline-Datei
window.potatoTimeline = function (jsPsych, potato) {
var trial = {
type: jsPsychHtmlKeyboardResponse,
stimulus: '<p style="font-size:48px">RED</p>',
choices: ['r', 'g', 'b']
};
return [trial];
};window.potatoTimeline ist ein Timeline-Array oder eine Funktion, die eines zurückgibt. Potato ruft die Funktion mit der von ihm erzeugten jsPsych-Instanz und einem potato-Objekt auf:
| Eigenschaft | Inhalt |
|---|---|
potato.participant | Der Potato-Benutzername, etwa die Prolific- oder SONA-ID |
potato.conditions | {factor: level} aus conditions |
potato.answers | {question: answer} für die Fragen in expose_answers. Antworten kommen als Strings an, auch Zahlen ("30") |
potato.attempt | 1 beim ersten Lauf, höher, sobald ein Neuladen auf einen Lauf folgt, der bereits Daten gesendet hatte |
potato.asset(path) | Die URL einer Datei in assets_dir |
potato.finish() | Beendet die Aufgabe sofort und speichert, was gesammelt wurde |
Rufen Sie initJsPsych nicht selbst auf, und setzen Sie kein on_finish für das Experiment, denn beides übernimmt Potato. on_finish auf Trial-Ebene und der Rest der jsPsych-API funktionieren wie gewohnt. Jeder Trial erhält potato_user, potato_page, potato_run_id, potato_attempt und eine Spalte condition_<factor> pro Faktor.
Stimuli werden über potato.asset() geladen, und das Plugin preload lädt sie wie in jedem jsPsych-Experiment vor:
{
type: jsPsychImageKeyboardResponse,
stimulus: potato.asset('faces/f01.png')
}Mouse-Tracking
extensions: [mouse-tracking] lädt @jspsych/extension-mouse-tracking 1.2.0 und registriert die Erweiterung. Ein Trial schaltet sie auf die übliche Weise ein, und jeder solche Trial erhält mouse_tracking_data, eine Liste von Messpunkten {x, y, t, event}, sowie mouse_tracking_targets:
{
type: jsPsychHtmlButtonResponse,
stimulus: '<p id="stim">Which is bigger?</p>',
choices: ['3', '8'],
extensions: [{type: jsPsychExtensionMouseTracking, params: {targets: ['#stim']}}]
}Die Eye-Tracking-Erweiterung WebGazer wird nicht mitgeliefert. WebGazer benötigt 'unsafe-eval', was die Content-Security-Policy der Seite nicht erlaubt.
Wenn die Timeline endet
Die Seite sendet die Daten an den Server, zeigt "Saving your responses" an und geht zur nächsten Seite, sobald der Server sie gespeichert hat. Schlägt das Speichern fehl, versucht sie es erneut und zeigt dann eine Schaltfläche Try again. Die teilnehmende Person wird nicht ohne ihre Daten weitergeführt.
Ein Neuladen startet die Timeline von vorn, weil jsPsych eine Timeline nicht mittendrin fortsetzen kann. Mit dem Standardwert save: on_finish gehen Trials verloren, die vor einem Neuladen bearbeitet wurden, und nur der Zähler page_loads der Seite in participants.csv zeigt, dass es passiert ist. Mit save: incremental wird ein Lauf, der Daten gesendet hat, als unvollständig aufbewahrt, und ein Neuladen startet einen neuen Versuch, sodass eine Analyse zweite Versuche ausschließen oder behalten kann. Das Öffnen der Aufgabe in einem zweiten Tab zählt als Neuladen, und der Lauf im ersten Tab kann nicht mehr abgeschlossen werden.
Kopfhörertest
Eine Seite vom Typ headphone_check prüft, ob eine teilnehmende Person in einer Hörstudie Kopfhörer trägt und nicht über Lautsprecher hört.
phases:
order: [consent, headphones, annotation]
headphones:
type: headphone_check
test: huggins
trials: 6
pass_threshold: 5| Schlüssel | Standardwert | Bedeutung |
|---|---|---|
test | huggins | huggins, antiphase oder both |
trials | 6 | Trials pro Test, von 2 bis 20 |
pass_threshold | 5 | Richtige Trials, die zum Bestehen nötig sind, pro Test |
on_fail | screen_out | screen_out beendet die Studie für eine Person, die nicht besteht. continue zeichnet das Ergebnis auf und fährt fort |
message | ein Standardtext | Was eine ausgeschlossene Person zu lesen bekommt |
Die Seite bittet die teilnehmende Person, Kopfhörer aufzusetzen und eine angenehme Lautstärke einzustellen, und führt dann die Trials aus. Jeder Trial spielt drei Klänge einmal ab, und die Person wählt einen davon.
huggins(Milne et al., 2021) spielt drei Rauschstöße. In einem ist ein schmales Band um 600 Hz auf einem Ohr phaseninvertiert, was über Kopfhörer als leiser Ton zu hören ist und über Lautsprecher nicht. Die Person beantwortet "Which sound had a faint tone hidden in the noise?"antiphase(Woods et al., 2017) spielt drei Töne mit 200 Hz. Einer ist 6 dB leiser, und einer liegt zwischen den Ohren in Gegenphase, was sich über Lautsprecher teilweise auslöscht und dort am leisesten klingt. Die Person beantwortet "Which sound was the quietest?"
Der Server erzeugt die Klänge, wenn die Seite sie anfordert, sodass mit Potato keine Audiodateien ausgeliefert werden. Er wählt auch aus, welcher der drei Klänge die Lösung ist, und sendet die drei Klänge jedes Trials als eine WAV-Datei, die nicht verrät, welcher es ist. Der Browser sendet nur die Auswahl. Ein Ziel- oder "correct"-Wert in dem, was er sendet, wird ignoriert, ebenso eine zweite Antwort auf denselben Trial und eine Antwort auf einen Klang, der nie abgerufen wurde. Erfundene Antworten erreichen daher nur Zufallsniveau.
Die Seite beantwortet zwei Fragen, die spätere Seiten prüfen können: <page>_passed (yes oder no) und <page>_correct (eine Zahl, bzw. <page>_correct_huggins und <page>_correct_antiphase bei test: both). Mit on_fail: continue prüfen Sie die Antwort selbst:
phases:
speakers_note:
type: prestudy
file: surveys/speakers.json
show_when:
- {schema: headphones_passed, operator: equals, value: "no"}Bestehen beweist nicht, dass die Person Kopfhörer getragen hat. Wer über Lautsprecher hört, kann bei jedem Trial in einem von drei Fällen richtig raten, und wer Code schreibt, um den heruntergeladenen Klang zu analysieren, kann die Lösung finden, wie bei jedem Kopfhörer-Screening, dessen Klänge den Browser erreichen.
Externe Aufgaben
Eine Seite mit type: external schickt die teilnehmende Person zu einer Aufgabe auf einem anderen Server und holt sie zurück, wenn diese endet. Die Aufgabe läuft auf dem anderen Server. Potato übergibt die Person und zeichnet auf, dass sie zurückgekommen ist.
phases:
order: [consent, game, annotation]
game:
type: external
url: "https://otree.example.org/room/public_goods/?participant_label={participant}&potato_return={return_url}"
return_params: [payoff]
description: The next part is a short game with three other participants.
button_text: Start the game| Schlüssel | Standardwert | Bedeutung |
|---|---|---|
url | erforderlich | Wohin die Person geschickt wird, mit Platzhaltern. Muss https:// sein, oder http für localhost, 127.0.0.1 oder [::1] |
return_params | [] | Query-Parameter, die aus dem Rückkehrlink übernommen werden |
return_secret | keiner | Eine private Zeichenkette mit mindestens 16 Zeichen, die mit dem anderen System geteilt wird. Ist sie gesetzt, muss die Rückkehr signiert sein |
description | keiner | Text über der Schaltfläche |
button_text | "Continue to the task" | Die Beschriftung der Schaltfläche |
new_tab | false | Öffnet die Aufgabe in einem neuen Tab |
Die Platzhalter in url werden URL-kodiert: {participant} (die Potato-Benutzer-ID), {return_url} (die Adresse, die die Person zurückbringt), {run_id} und {condition.<factor>}.
Das andere System schickt die Person zu {return_url}, wenn sie fertig ist, und hängt etwaige Ergebnisse als Query-Parameter an, etwa <return_url>&payoff=4.50. Die Rückkehr muss in derselben Browsersitzung ankommen und den aktuellen Versuch der Person nennen. Ein veralteter Link zeigt "This link is out of date" an und ändert nichts. Die Seite beantwortet dann <page>_returned (yes) und <page>_<param> für jeden aufgeführten Parameter, was spätere Seiten prüfen können, und die Parameter werden mit dem Lauf gespeichert.
Signierte Rückkehr
Der Rückkehrlink steht in der Adresse, zu der die Person geschickt wird. Eine teilnehmende Person kann ihn also lesen, payoff=4.50 in payoff=99 ändern und ihn öffnen, ohne zu spielen. Ohne return_secret sind die zurückgegebenen Parameter Selbstauskünfte und sollten nicht über eine Zahlung entscheiden, es sei denn, Sie gleichen sie mit den Aufzeichnungen des anderen Systems ab.
Mit return_secret fügt das andere System dem Rückkehrlink sig hinzu: den hexadezimalen HMAC-SHA256, mit dem Secret als Schlüssel, über die Lauf-ID, gefolgt von &<param>=<value> für jeden Parameter in return_params, sortiert nach Name. Jeder Name und Wert wird prozentkodiert, wobei jedes reservierte Zeichen maskiert wird, sodass ein Wert mit & oder = nicht als anderer Parameter durchgehen kann. In Python:
import hashlib, hmac
from urllib.parse import quote
message = run_id + "".join(f"&{quote(k, safe='')}={quote(params[k], safe='')}"
for k in sorted(params))
sig = hmac.new(secret.encode(), message.encode(), hashlib.sha256).hexdigest()
# redirect to f"{return_url}&{urlencode({**params, 'sig': sig})}"Eine Rückkehr ohne sig oder mit einer nicht passenden Signatur erhält Status 409 und die Seite "This return could not be confirmed". Die Seite wird nicht als erledigt markiert, sodass eine korrekt signierte Rückkehr danach weiterhin funktioniert. Das Secret muss auf dem Server des anderen Systems bleiben. Eine gehostete Umfrage, die ihre End-Weiterleitung im Browser der teilnehmenden Person zusammensetzt, kann es nicht geheim halten, sodass Signieren dort nicht hilft.
- oTree: Schicken Sie Teilnehmende zu einer Room-URL mit
participant_label={participant}. Die App muss die Rückkehr-URL selbst mitführen: Lesen Sie den Parameterpotato_return, wenn die Person ankommt, legen Sie ihn inparticipant.varsab und leiten Sie von der letzten Seite dorthin weiter. Wie die erste Seite URL-Parameter liest, hängt von der oTree-Version ab. Prüfen Sie daher deren Dokumentation. - Empirica: Übergeben Sie
{participant}als URL-ParameterparticipantKey. - Eine gehostete Umfrage oder ein gehostetes Experiment (Qualtrics, Gorilla, Pavlovia): Tragen Sie
{participant}in einen Embedded-Data- oder URL-Parameter ein und setzen Sie die End-Weiterleitung auf die Rückkehr-URL.
Daten und Export
python -m potato.export --config config.yaml --format runtime --output ./export/Der Export schreibt diese Dateien für jede Laufzeit, die das Projekt verwendet, wobei <kind> jspsych, headphone_check oder external ist:
| Datei | Inhalt |
|---|---|
<kind>_runs.csv | Eine Zeile pro Lauf: teilnehmende Person, Seite, Versuch, Status, Anzahl der Trials und Bedingungen |
<kind>_trials.csv | Eine Zeile pro Trial, mit jeder Spalte, die die Aufgabe aufgezeichnet hat. Verschachtelte Werte sind JSON |
<kind>_interaction.csv | Blur-, Focus- und Vollbild-Ereignisse, sofern welche aufgezeichnet wurden |
<kind>_raw/<user>/<page>-<run>.json | Die Daten jedes Laufs so, wie die Aufgabe sie erzeugt hat |
Nicht abgeschlossene Läufe haben den Status started oder partial. Ein Trial des Kopfhörertests zeichnet test, trial, index (seine Position im Lauf) und response (den gewählten Klang, 1 bis 3) auf, dazu drei Werte, die der Server ergänzt: target (der richtige Klang), correct und scored, das für eine Antwort, die der Server nicht gewertet hat, false ist.
Sicherheit und Timing
Die Timeline ist Code, den Sie geschrieben haben, und sie läuft auf dem Origin des Potato-Servers mit der Sitzung der teilnehmenden Person, mit demselben Vertrauen wie eine eigene HTML-Anweisungsseite. Ihre Content-Security-Policy erlaubt Skripte, Styles und Medien nur vom Potato-Server und aus csp.extra_sources, und Netzwerkanfragen nur zum Potato-Server. Ein Stimulus, der Text von Teilnehmenden anzeigt, kann daher von nichts anderem laden und an nichts anderes senden. Maskieren Sie solchen Text trotzdem, bevor Sie ihn in HTML einsetzen.
Jede Datei, die die Seite nennt, muss im Aufgabenverzeichnis liegen. Die Asset-Route liefert nur Stimulus-Dateitypen aus, nie .js oder .html aus assets_dir, und sendet jede mit einer eigenen Sandboxing-CSP, sodass ein direkt geöffnetes SVG kein Skript ausführt. csp.extra_sources akzeptiert einzelne https://-Origins und lehnt Wildcards ab.
Auf der Seite gilt jsPsychs eigenes Timing, weil Potatos Annotationsskripte dort nicht geladen werden. Die Dokumentation zur Timing-Genauigkeit von jsPsych berichtet, dass in JavaScript gemessene Antwortzeiten tendenziell 10-40 ms länger ausfallen als in Laborsoftware, bei ähnlicher Varianz.
Beispiele
python potato/flask_server.py start examples/advanced/jspsych-stroop/config.yaml -p 8000
python potato/flask_server.py start examples/audio/listening-test-p808/config.yaml -p 8000Das erste ist eine Stroop-Aufgabe zwischen einem Screener und einer Wortbewertungsaufgabe, wobei der Hälfte der Teilnehmenden Feedback angezeigt wird. Das zweite ist ein Kopfhörertest, gefolgt von Sprachclips, die auf der ACR-Skala bewertet werden.
Weiterführende Informationen
- Experimentablauf - Bedingungen,
show_when,exit_whenund Ausbalancierung - Umstieg von jsPsych - jsPsych-Konzepte, zugeordnet zu Potato-Konfigurationsschlüsseln
- Hörtests - MUSHRA, ACR und die anderen Hörtestverfahren
- Zeitgesteuerte Trials - Schnelle Antworten auf Annotationsitems ohne jsPsych-Timeline
Implementierungsdetails finden Sie in der Quelldokumentation zu Laufzeitphasen und zur jsPsych-Phase.