Skip to content

Phases d'exécution : timelines jsPsych, tests de casque et tâches externes

Exécutez une timeline jsPsych 8, un test de casque noté par le serveur, ou une tâche oTree ou un questionnaire hébergé avec retours signés, comme une page d'une étude Potato.

Nouveau dans la v2.12.0

Une phase d'exécution est une page d'une étude Potato qui exécute une tâche au lieu d'afficher une page de questions. Potato sert la page, transmet à la tâche l'identifiant du participant, ses conditions et les réponses de questionnaire choisies, enregistre ce que la tâche consigne, et fait avancer le participant quand elle se termine. Le consentement, les questionnaires, l'annotation, le retour vers Prolific ou SONA et l'export restent dans Potato.

typeExécute
jspsychUne timeline jsPsych 8
headphone_checkLe test de dépistage du casque de Potato
externalUne tâche sur un autre serveur, comme oTree, Empirica ou un questionnaire hébergé, avec un lien de retour

Toute phase d'exécution :

  • occupe l'emplacement prestudy ou poststudy, déterminé par sa place dans phases.order par rapport à annotation (ou fixé avec slot:), et accepte show_when, exit_when et le contrebalancement comme n'importe quelle autre page de phase (Déroulement de l'expérience) ;
  • s'exécute sous une Content-Security-Policy qui n'autorise les scripts et les requêtes réseau que vers le serveur Potato ;
  • ne peut pas être sautée, car tant que les données de la tâche ne sont pas enregistrées, le serveur refuse le formulaire de la phase et le Next de la page d'annotation ;
  • enregistre ses données dans la base de données du projet (project.sqlite, table runtime_runs) au lieu de user_state.json, et les exporte avec --format runtime, ou --format jspsych pour les seules pages jsPsych.

Ces clés s'appliquent à toute phase d'exécution :

CléValeur par défautSignification
extra_scripts[]Fichiers .js supplémentaires du répertoire de la tâche, chargés avant la tâche
assets_diraucuneUn dossier de fichiers que la tâche peut charger
expose_answers[]Réponses à des questions de questionnaire antérieures transmises à la tâche
saveon_finishincremental enregistre aussi chaque essai dès qu'il se termine
allow_rerunfalseIndique si revenir sur une page terminée relance la tâche
max_data_mb10Taille maximale d'une exécution acceptée par le serveur
csp.extra_sources[]Origines https:// depuis lesquelles la page peut charger des polices, des images ou des scripts

Pages jsPsych

yaml
phases:
  order: [consent, stroop, annotation, debrief]
  stroop:
    type: jspsych
    timeline: tasks/stroop.js
    plugins: [instructions, html-keyboard-response]

Potato embarque jsPsych 8.3.0 et 15 de ses plugins ; une étude qui n'utilise que ceux-ci ne charge donc rien depuis un CDN et fonctionne sur un serveur sans accès à Internet :

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 et call-function.

Les autres plugins, y compris ceux de jspsych-contrib, se placent dans extra_scripts sous forme de builds pour navigateur, c'est-à-dire le fichier index.browser.min.js du paquet npm.

CléValeur par défautSignification
timelineobligatoireUn fichier .js du répertoire de la tâche qui définit window.potatoTimeline
plugins[]Plugins à charger, parmi la liste ci-dessus
extensions[]Extensions jsPsych à charger et à transmettre à initJsPsych. Potato embarque mouse-tracking
slotdéduitprestudy avant la phase d'annotation, poststudy après. Un emplacement explicite qui contredit la place de la page dans order l'emporte, et un avertissement est journalisé

L'exit_when d'une page jsPsych est évalué à la fin de la timeline ; les données de l'exécution sont donc déjà enregistrées pour un participant qu'il exclut. Pour exclure des personnes avant qu'elles ne fassent la tâche, placez le questionnaire de sélection sur sa propre page, devant elle.

Le fichier de timeline

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 est un tableau de timeline, ou une fonction qui en renvoie un. Potato appelle la fonction avec l'instance jsPsych qu'il a créée et un objet potato :

PropriétéContenu
potato.participantLe nom d'utilisateur Potato, par exemple l'identifiant Prolific ou SONA
potato.conditions{factor: level} issu de conditions
potato.answers{question: answer} pour les questions de expose_answers. Les réponses arrivent sous forme de chaînes, nombres compris ("30")
potato.attempt1 à la première exécution, davantage dès qu'un rechargement suit une exécution qui avait envoyé des données
potato.asset(path)L'URL d'un fichier de assets_dir
potato.finish()Termine la tâche immédiatement et enregistre ce qui a été recueilli

N'appelez pas initJsPsych vous-même et ne définissez pas on_finish sur l'expérience, car Potato fait les deux. Le on_finish au niveau des essais et le reste de l'API jsPsych fonctionnent comme d'habitude. Chaque essai reçoit potato_user, potato_page, potato_run_id, potato_attempt et une colonne condition_<factor> par facteur.

Les stimuli se chargent par potato.asset(), et le plugin preload les précharge comme dans toute expérience jsPsych :

javascript
{
    type: jsPsychImageKeyboardResponse,
    stimulus: potato.asset('faces/f01.png')
}

Suivi de la souris

extensions: [mouse-tracking] charge @jspsych/extension-mouse-tracking 1.2.0 et l'enregistre. Un essai l'active de la manière habituelle, et chacun de ces essais reçoit mouse_tracking_data, une liste d'échantillons {x, y, t, event}, ainsi que mouse_tracking_targets :

javascript
{
    type: jsPsychHtmlButtonResponse,
    stimulus: '<p id="stim">Which is bigger?</p>',
    choices: ['3', '8'],
    extensions: [{type: jsPsychExtensionMouseTracking, params: {targets: ['#stim']}}]
}

L'extension d'oculométrie WebGazer n'est pas fournie. WebGazer a besoin de 'unsafe-eval', que la Content-Security-Policy de la page n'autorise pas.

À la fin de la timeline

La page envoie les données au serveur, affiche « Saving your responses » et passe à la page suivante une fois que le serveur les a enregistrées. Si l'enregistrement échoue, elle réessaie puis affiche un bouton Try again, et le participant n'avance pas sans ses données.

Un rechargement relance la timeline depuis le début, car jsPsych ne peut pas reprendre une timeline en cours de route. Avec la valeur par défaut save: on_finish, les essais effectués avant un rechargement sont perdus, et seul le compteur page_loads de la page dans participants.csv montre que c'est arrivé. Avec save: incremental, une exécution qui a envoyé des données est conservée comme incomplète et un rechargement lance une nouvelle tentative ; une analyse peut alors exclure les deuxièmes tentatives ou les garder. Ouvrir la tâche dans un deuxième onglet compte comme un rechargement, et l'exécution du premier onglet ne peut plus se terminer.

Test de casque

Une page headphone_check vérifie qu'un participant à une étude d'écoute porte un casque et n'écoute pas sur des haut-parleurs.

yaml
phases:
  order: [consent, headphones, annotation]
  headphones:
    type: headphone_check
    test: huggins
    trials: 6
    pass_threshold: 5
CléValeur par défautSignification
testhugginshuggins, antiphase ou both
trials6Essais par test, de 2 à 20
pass_threshold5Essais corrects nécessaires pour réussir, par test
on_failscreen_outscreen_out met fin à l'étude pour un participant qui échoue. continue enregistre le résultat et poursuit
messageun texte par défautCe que lit un participant exclu

La page demande au participant de mettre son casque et de régler un volume confortable, puis lance les essais. Chaque essai joue trois sons une seule fois, et le participant en choisit un.

  • huggins (Milne et al., 2021) joue trois salves de bruit. Dans l'une d'elles, une bande étroite autour de 600 Hz est inversée en phase dans une oreille, ce qui s'entend comme un son faible au casque et pas sur des haut-parleurs. Le participant répond à « Which sound had a faint tone hidden in the noise? »
  • antiphase (Woods et al., 2017) joue trois sons purs de 200 Hz. L'un est plus faible de 6 dB, et un autre est en opposition de phase entre les oreilles, ce qui l'atténue en partie sur des haut-parleurs et le fait paraître le plus faible dans ce cas. Le participant répond à « Which sound was the quietest? »

Le serveur produit les sons quand la page les demande ; aucun fichier audio n'est donc livré avec Potato. Il choisit aussi lequel des trois sons est la bonne réponse, et envoie les trois sons de chaque essai dans un seul WAV qui n'indique pas lequel c'est. Le navigateur n'envoie que le choix. Une valeur de cible ou de « correct » dans ce qu'il envoie est ignorée, tout comme une deuxième réponse au même essai et une réponse à un son qui n'a jamais été récupéré ; des réponses inventées obtiennent donc un score de niveau hasard.

La page répond à deux questions que les pages suivantes peuvent tester : <page>_passed (yes ou no) et <page>_correct (un nombre, ou <page>_correct_huggins et <page>_correct_antiphase pour test: both). Avec on_fail: continue, testez la réponse vous-même :

yaml
phases:
  speakers_note:
    type: prestudy
    file: surveys/speakers.json
    show_when:
      - {schema: headphones_passed, operator: equals, value: "no"}

Réussir le test ne prouve pas que le participant portait un casque. Une personne sur haut-parleurs peut deviner juste une fois sur trois à chaque essai, et un participant qui écrit du code pour analyser le son téléchargé peut trouver la réponse, comme avec tout test de casque dont les sons parviennent au navigateur.

Tâches externes

Une page type: external envoie le participant vers une tâche sur un autre serveur et le ramène quand elle se termine. La tâche s'exécute sur l'autre serveur ; Potato transmet le participant et enregistre son retour.

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
CléValeur par défautSignification
urlobligatoireOù envoyer le participant, avec des espaces réservés. Elle doit être en https://, ou en http pour localhost, 127.0.0.1 ou [::1]
return_params[]Paramètres de requête à conserver depuis le lien de retour
return_secretaucuneUne chaîne privée d'au moins 16 caractères, partagée avec l'autre système. Quand elle est définie, le retour doit être signé
descriptionaucuneTexte au-dessus du bouton
button_text« Continue to the task »Le libellé du bouton
new_tabfalseOuvre la tâche dans un nouvel onglet

Les espaces réservés de url sont encodés pour URL : {participant} (l'identifiant utilisateur Potato), {return_url} (l'adresse qui ramène le participant), {run_id} et {condition.<factor>}.

L'autre système envoie le participant vers {return_url} quand il a terminé, en ajoutant les résultats éventuels comme paramètres de requête, par exemple <return_url>&payoff=4.50. Le retour doit arriver dans la même session de navigateur et désigner la tentative en cours du participant ; un lien périmé affiche « This link is out of date » et ne change rien. La page répond ensuite <page>_returned (yes) et <page>_<param> pour chaque paramètre listé, que les pages suivantes peuvent tester, et les paramètres sont enregistrés avec l'exécution.

Retours signés

Le lien de retour figure dans l'adresse vers laquelle le participant est envoyé ; un participant peut donc le lire, remplacer payoff=4.50 par payoff=99 et l'ouvrir sans jouer. Sans return_secret, les paramètres retournés sont déclaratifs, et ils ne devraient pas déterminer un paiement à moins de les vérifier par rapport aux enregistrements de l'autre système.

Avec return_secret, l'autre système ajoute sig au lien de retour : le HMAC-SHA256 en hexadécimal, avec le secret pour clé, de l'identifiant d'exécution suivi de &<param>=<value> pour chaque paramètre de return_params, triés par nom. Chaque nom et chaque valeur est encodé en pourcentage avec tous les caractères réservés échappés, de sorte qu'une valeur contenant & ou = ne peut pas se faire passer pour un autre paramètre. En 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})}"

Un retour sans sig, ou avec une signature qui ne correspond pas, reçoit le statut 409 et la page « This return could not be confirmed ». La page n'est pas marquée comme terminée ; un retour correctement signé fonctionne donc encore ensuite. Le secret doit rester sur le serveur de l'autre système. Un questionnaire hébergé qui construit sa redirection finale dans le navigateur du participant ne peut pas le garder privé ; la signature n'y sert donc à rien.

  • oTree : envoyez les participants vers une URL de salle avec participant_label={participant}. L'application doit transporter elle-même l'URL de retour : lisez le paramètre potato_return à l'arrivée du participant, conservez-le dans participant.vars, et redirigez vers lui depuis la dernière page. La façon dont la première page lit les paramètres d'URL dépend de la version d'oTree ; consultez donc sa documentation.
  • Empirica : transmettez {participant} comme paramètre d'URL participantKey.
  • Un questionnaire ou une expérience hébergés (Qualtrics, Gorilla, Pavlovia) : placez {participant} dans une donnée intégrée ou un paramètre d'URL, et définissez la redirection finale vers l'URL de retour.

Données et export

bash
python -m potato.export --config config.yaml --format runtime --output ./export/

L'export écrit ces fichiers pour chaque type d'exécution utilisé par le projet, où <kind> vaut jspsych, headphone_check ou external :

FichierContenu
<kind>_runs.csvUne ligne par exécution : participant, page, tentative, statut, nombre d'essais et conditions
<kind>_trials.csvUne ligne par essai, avec chaque colonne enregistrée par la tâche. Les valeurs imbriquées sont en JSON
<kind>_interaction.csvÉvénements de perte de focus, de focus et de plein écran, quand il y en a eu
<kind>_raw/<user>/<page>-<run>.jsonLes données de chaque exécution telles que la tâche les a produites

Les exécutions qui ne se sont pas terminées ont le statut started ou partial. Un essai de test de casque enregistre test, trial, index (sa position dans l'exécution) et response (le son choisi, de 1 à 3), plus trois valeurs ajoutées par le serveur : target (le bon son), correct, et scored, qui vaut false pour une réponse que le serveur n'a pas comptée.

Sécurité et chronométrage

La timeline est du code que vous avez écrit, et elle s'exécute sur l'origine du serveur Potato avec la session du participant, avec le même niveau de confiance qu'une page de consignes HTML personnalisée. Sa Content-Security-Policy n'autorise les scripts, les styles et les médias que depuis le serveur Potato et les éventuelles csp.extra_sources, et les requêtes réseau que vers le serveur Potato ; un stimulus qui affiche du texte de participant ne peut donc ni charger depuis autre chose ni y envoyer quoi que ce soit. Échappez tout de même ce texte avant de le placer dans du HTML.

Chaque fichier nommé par la page doit se trouver dans le répertoire de la tâche. La route des ressources ne sert que des types de fichiers de stimuli, jamais de .js ni de .html depuis assets_dir, et envoie chacun avec sa propre CSP de sandbox ; un SVG ouvert directement n'exécute donc aucun script. csp.extra_sources accepte des origines https:// uniques et refuse les caractères génériques.

Le chronométrage propre à jsPsych s'applique sur la page, car les scripts d'annotation de Potato n'y sont pas chargés. La documentation de jsPsych sur la précision temporelle indique que les temps de réponse mesurés en JavaScript tendent à être de 10 à 40 ms plus longs que dans les logiciels de laboratoire, avec une variance comparable.

Exemples

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

Le premier est une tâche de Stroop placée entre un questionnaire de sélection et une tâche de notation de mots, avec un retour montré à la moitié des participants. Le second est un test de casque suivi d'extraits de parole notés sur l'échelle ACR.

Pour aller plus loin

Pour les détails d'implémentation, consultez la documentation source sur les phases d'exécution et la phase jsPsych.