Skip to content

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.

typeFührt aus
jspsychEine jsPsych-8-Timeline
headphone_checkPotatos Kopfhörer-Screeningtest
externalEine Aufgabe auf einem anderen Server, etwa oTree, Empirica oder eine gehostete Umfrage, mit einem Rückkehrlink

Jede Laufzeitphase:

  • sitzt im Slot prestudy oder poststudy, festgelegt durch ihre Position in phases.order relativ zu annotation (oder gesetzt mit slot:), und akzeptiert show_when, exit_when und 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, Tabelle runtime_runs) statt in user_state.json und exportiert sie mit --format runtime, oder mit --format jspsych nur für jsPsych-Seiten.

Diese Schlüssel gelten für jede Laufzeitphase:

SchlüsselStandardwertBedeutung
extra_scripts[]Weitere .js-Dateien aus dem Aufgabenverzeichnis, die vor der Aufgabe geladen werden
assets_dirkeinerEin Ordner mit Dateien, die die Aufgabe laden darf
expose_answers[]Antworten auf frühere Umfragefragen, die an die Aufgabe übergeben werden
saveon_finishincremental speichert zusätzlich jeden Trial, sobald er abgeschlossen ist
allow_rerunfalseOb die Rückkehr zu einer abgeschlossenen Seite die Aufgabe erneut ausführt
max_data_mb10Größter Lauf, den der Server annimmt
csp.extra_sources[]https://-Origins, von denen die Seite Schriften, Bilder oder Skripte laden darf

jsPsych-Seiten

yaml
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üsselStandardwertBedeutung
timelineerforderlichEine .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
slotabgeleitetprestudy 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

javascript
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:

EigenschaftInhalt
potato.participantDer 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.attempt1 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:

javascript
{
    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:

javascript
{
    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.

yaml
phases:
  order: [consent, headphones, annotation]
  headphones:
    type: headphone_check
    test: huggins
    trials: 6
    pass_threshold: 5
SchlüsselStandardwertBedeutung
testhugginshuggins, antiphase oder both
trials6Trials pro Test, von 2 bis 20
pass_threshold5Richtige Trials, die zum Bestehen nötig sind, pro Test
on_failscreen_outscreen_out beendet die Studie für eine Person, die nicht besteht. continue zeichnet das Ergebnis auf und fährt fort
messageein StandardtextWas 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:

yaml
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.

yaml
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üsselStandardwertBedeutung
urlerforderlichWohin 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_secretkeinerEine private Zeichenkette mit mindestens 16 Zeichen, die mit dem anderen System geteilt wird. Ist sie gesetzt, muss die Rückkehr signiert sein
descriptionkeinerText über der Schaltfläche
button_text"Continue to the task"Die Beschriftung der Schaltfläche
new_tabfalseÖ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:

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 Parameter potato_return, wenn die Person ankommt, legen Sie ihn in participant.vars ab 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-Parameter participantKey.
  • 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

bash
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:

DateiInhalt
<kind>_runs.csvEine Zeile pro Lauf: teilnehmende Person, Seite, Versuch, Status, Anzahl der Trials und Bedingungen
<kind>_trials.csvEine Zeile pro Trial, mit jeder Spalte, die die Aufgabe aufgezeichnet hat. Verschachtelte Werte sind JSON
<kind>_interaction.csvBlur-, Focus- und Vollbild-Ereignisse, sofern welche aufgezeichnet wurden
<kind>_raw/<user>/<page>-<run>.jsonDie 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

bash
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 8000

Das 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

Implementierungsdetails finden Sie in der Quelldokumentation zu Laufzeitphasen und zur jsPsych-Phase.