Skip to content

Fases de ejecución: líneas de tiempo de jsPsych, comprobaciones de auriculares y tareas externas

Ejecuta una línea de tiempo de jsPsych 8, una comprobación de auriculares puntuada en el servidor o una tarea de oTree o de una encuesta alojada con retornos firmados como una página de un estudio de Potato.

Nuevo en v2.12.0

Una fase de ejecución es una página de un estudio de Potato que ejecuta una tarea en lugar de mostrar una página de preguntas. Potato sirve la página, pasa a la tarea el ID del participante, sus condiciones y las respuestas de encuesta elegidas, guarda lo que la tarea registra y hace avanzar al participante cuando termina. El consentimiento, las encuestas, la anotación, el traspaso a Prolific o SONA y la exportación siguen en Potato.

typeEjecuta
jspsychUna línea de tiempo de jsPsych 8
headphone_checkLa prueba de detección de auriculares de Potato
externalUna tarea en otro servidor, como oTree, Empirica o una encuesta alojada, con un enlace de retorno

Toda fase de ejecución:

  • ocupa la posición prestudy o poststudy, que se decide por su lugar en phases.order respecto a annotation (o se fija con slot:), y admite show_when, exit_when y contrabalanceo como cualquier otra página de fase (Flujo del experimento);
  • se ejecuta bajo una Content-Security-Policy que permite scripts y peticiones de red solo hacia el servidor de Potato;
  • no se puede saltar, porque hasta que se guardan los datos de la tarea el servidor rechaza el formulario de la fase y el botón Next de la página de anotación;
  • guarda sus datos en la base de datos del proyecto (project.sqlite, tabla runtime_runs) en lugar de en user_state.json, y los exporta con --format runtime, o con --format jspsych solo para las páginas de jsPsych.

Estas claves se aplican a toda fase de ejecución:

ClaveValor predeterminadoSignificado
extra_scripts[]Más archivos .js del directorio de la tarea, cargados antes que la tarea
assets_dirningunoUna carpeta de archivos que la tarea puede cargar
expose_answers[]Respuestas a preguntas de encuesta anteriores que se pasan a la tarea
saveon_finishincremental guarda además cada ensayo en cuanto termina
allow_rerunfalseSi volver a una página terminada ejecuta la tarea de nuevo
max_data_mb10La ejecución más grande que acepta el servidor
csp.extra_sources[]Orígenes https:// desde los que la página puede cargar fuentes, imágenes o scripts

Páginas de jsPsych

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

Potato incluye jsPsych 8.3.0 y 15 de sus plugins, así que un estudio que use solo esos no carga nada desde una CDN y funciona en un servidor sin acceso a 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 y call-function.

Los demás plugins, incluidos los de jspsych-contrib, van en extra_scripts como compilaciones para navegador, es decir, el archivo index.browser.min.js del paquete npm.

ClaveValor predeterminadoSignificado
timelineobligatorioUn archivo .js del directorio de la tarea que define window.potatoTimeline
plugins[]Plugins que cargar, de la lista anterior
extensions[]Extensiones de jsPsych que cargar y pasar a initJsPsych. Potato incluye mouse-tracking
slotinferidoprestudy antes de la fase de anotación, poststudy después. Una posición explícita que contradice el lugar de la página en order prevalece, y se registra una advertencia

El exit_when de una página de jsPsych se comprueba cuando termina la línea de tiempo, así que los datos de la ejecución ya están guardados para el participante al que excluye. Para excluir a las personas antes de que hagan la tarea, pon el cuestionario de selección en su propia página delante de ella.

El archivo de la línea de tiempo

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 es un array de línea de tiempo, o una función que devuelve uno. Potato llama a la función con la instancia de jsPsych que ha creado y un objeto potato:

PropiedadContenido
potato.participantEl nombre de usuario de Potato, como el ID de Prolific o SONA
potato.conditions{factor: level} a partir de conditions
potato.answers{question: answer} para las preguntas de expose_answers. Las respuestas llegan como cadenas, también los números ("30")
potato.attempt1 en la primera ejecución, un valor mayor cuando una recarga sigue a una ejecución que ya había enviado datos
potato.asset(path)La URL de un archivo de assets_dir
potato.finish()Termina la tarea en ese momento y guarda lo recogido

No llames tú a initJsPsych ni definas on_finish en el experimento, porque Potato hace ambas cosas. El on_finish a nivel de ensayo y el resto de la API de jsPsych funcionan como siempre. Cada ensayo recibe potato_user, potato_page, potato_run_id, potato_attempt y una columna condition_<factor> por cada factor.

Los estímulos se cargan mediante potato.asset(), y el plugin preload los precarga como en cualquier experimento de jsPsych:

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

Seguimiento del ratón

extensions: [mouse-tracking] carga @jspsych/extension-mouse-tracking 1.2.0 y lo registra. Un ensayo lo activa de la forma habitual, y cada uno de esos ensayos recibe mouse_tracking_data, una lista de muestras {x, y, t, event}, y mouse_tracking_targets:

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

La extensión de seguimiento ocular WebGazer no se incluye. WebGazer necesita 'unsafe-eval', que la Content-Security-Policy de la página no permite.

Cuando termina la línea de tiempo

La página envía los datos al servidor, muestra "Saving your responses" y pasa a la página siguiente en cuanto el servidor los ha guardado. Si el guardado falla, lo reintenta y después muestra un botón Try again, y el participante no avanza sin sus datos.

Una recarga reinicia la línea de tiempo desde el principio, porque jsPsych no puede reanudar una línea de tiempo a medias. Con el valor predeterminado save: on_finish, los ensayos hechos antes de una recarga se pierden, y solo el recuento page_loads de la página en participants.csv muestra que ocurrió. Con save: incremental, una ejecución que ha enviado datos se conserva como incompleta y una recarga inicia un nuevo intento, de modo que un análisis puede excluir los segundos intentos o conservarlos. Abrir la tarea en una segunda pestaña cuenta como una recarga, y la ejecución de la primera pestaña ya no puede terminar.

Comprobación de auriculares

Una página headphone_check comprueba que un participante de un estudio de escucha lleva auriculares y no escucha por altavoces.

yaml
phases:
  order: [consent, headphones, annotation]
  headphones:
    type: headphone_check
    test: huggins
    trials: 6
    pass_threshold: 5
ClaveValor predeterminadoSignificado
testhugginshuggins, antiphase o both
trials6Ensayos por prueba, de 2 a 20
pass_threshold5Ensayos correctos necesarios para aprobar, por prueba
on_failscreen_outscreen_out termina el estudio para un participante que no aprueba. continue registra el resultado y sigue adelante
messageuno predeterminadoLo que lee un participante excluido

La página pide al participante que se ponga los auriculares y ajuste un volumen cómodo, y después ejecuta los ensayos. Cada ensayo reproduce tres sonidos una sola vez, y el participante elige uno.

  • huggins (Milne et al., 2021) reproduce tres ráfagas de ruido. En una de ellas, una banda estrecha alrededor de 600 Hz tiene la fase invertida en un oído, lo que con auriculares se oye como un tono tenue y con altavoces no. El participante responde a "Which sound had a faint tone hidden in the noise?"
  • antiphase (Woods et al., 2017) reproduce tres tonos de 200 Hz. Uno es 6 dB más bajo, y otro está en antifase entre los oídos, lo que con altavoces se cancela en parte y ahí suena como el más bajo. El participante responde a "Which sound was the quietest?"

El servidor genera los sonidos cuando la página los pide, así que Potato no incluye archivos de audio. También elige cuál de los tres sonidos es la respuesta, y envía los tres sonidos de cada ensayo como un único WAV que no indica cuál es. El navegador envía solo la elección. Un valor de objetivo o de "correcto" en lo que envía se ignora, igual que una segunda respuesta al mismo ensayo y una respuesta a un sonido que nunca se descargó, así que las respuestas inventadas aciertan al nivel del azar.

La página responde dos preguntas que las páginas posteriores pueden comprobar: <page>_passed (yes o no) y <page>_correct (un número, o <page>_correct_huggins y <page>_correct_antiphase con test: both). Con on_fail: continue, comprueba tú mismo la respuesta:

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

Aprobar no demuestra que el participante llevara auriculares. Alguien con altavoces puede acertar por azar una vez de cada tres en cada ensayo, y un participante que escriba código para analizar el sonido descargado puede encontrar la respuesta, como ocurre con cualquier prueba de auriculares cuyos sonidos llegan al navegador.

Tareas externas

Una página type: external envía al participante a una tarea en otro servidor y lo trae de vuelta cuando termina. La tarea se ejecuta en el otro servidor, y Potato le traspasa el participante y registra que ha vuelto.

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
ClaveValor predeterminadoSignificado
urlobligatorioAdónde enviar al participante, con marcadores de posición. Tiene que ser https://, o http para localhost, 127.0.0.1 o [::1]
return_params[]Parámetros de consulta que conservar del enlace de retorno
return_secretningunoUna cadena privada de al menos 16 caracteres, compartida con el otro sistema. Si se define, el retorno debe estar firmado
descriptionningunoTexto encima del botón
button_text"Continue to the task"La etiqueta del botón
new_tabfalseAbre la tarea en una pestaña nueva

Los marcadores de posición de url se codifican para URL: {participant} (el ID de usuario de Potato), {return_url} (la dirección que trae de vuelta al participante), {run_id} y {condition.<factor>}.

El otro sistema envía al participante a {return_url} cuando termina, y añade los resultados como parámetros de consulta, como en <return_url>&payoff=4.50. El retorno debe llegar en la misma sesión del navegador y nombrar el intento actual del participante, y un enlace caducado muestra "This link is out of date" y no cambia nada. La página responde entonces <page>_returned (yes) y <page>_<param> para cada parámetro de la lista, que las páginas posteriores pueden comprobar, y los parámetros se guardan con la ejecución.

Retornos firmados

El enlace de retorno está en la dirección a la que se envía al participante, así que un participante puede leerlo, cambiar payoff=4.50 por payoff=99 y abrirlo sin jugar. Sin return_secret, los parámetros devueltos son autodeclarados, y no deberían decidir un pago salvo que los contrastes con los registros del otro sistema.

Con return_secret, el otro sistema añade sig al enlace de retorno: el HMAC-SHA256 en hexadecimal, con el secreto como clave, del ID de ejecución seguido de &<param>=<value> para cada parámetro de return_params, ordenados por nombre. Cada nombre y cada valor se codifica con porcentajes escapando todos los caracteres reservados, de modo que un valor que contenga & o = no puede hacerse pasar por otro parámetro. 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 retorno sin sig, o con uno que no coincide, recibe el estado 409 y la página "This return could not be confirmed". La página no se marca como terminada, así que un retorno firmado correctamente sigue funcionando después. El secreto tiene que quedarse en el servidor del otro sistema. Una encuesta alojada que construye su redirección final en el navegador del participante no puede mantenerlo privado, así que en ese caso la firma no sirve de nada.

  • oTree: envía a los participantes a la URL de una sala con participant_label={participant}. La aplicación tiene que llevar ella misma la URL de retorno: lee el parámetro potato_return cuando llega el participante, guárdalo en participant.vars y redirige a él desde la última página. Cómo lee la primera página los parámetros de la URL depende de la versión de oTree, así que consulta su documentación.
  • Empirica: pasa {participant} como parámetro de URL participantKey.
  • Una encuesta o un experimento alojados (Qualtrics, Gorilla, Pavlovia): pon {participant} en un parámetro de datos incrustados o de URL, y fija la redirección final en la URL de retorno.

Datos y exportación

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

La exportación escribe estos archivos para cada tipo de ejecución que usa el proyecto, donde <kind> es jspsych, headphone_check o external:

ArchivoContenido
<kind>_runs.csvUna fila por ejecución: participante, página, intento, estado, número de ensayos y condiciones
<kind>_trials.csvUna fila por ensayo, con todas las columnas que registró la tarea. Los valores anidados van en JSON
<kind>_interaction.csvEventos de pérdida de foco, foco y pantalla completa, cuando se registró alguno
<kind>_raw/<user>/<page>-<run>.jsonLos datos de cada ejecución tal como los produjo la tarea

Las ejecuciones que no terminaron tienen el estado started o partial. Un ensayo de la comprobación de auriculares registra test, trial, index (su posición en la ejecución) y response (el sonido elegido, de 1 a 3), más tres valores que añade el servidor: target (el sonido correcto), correct y scored, que es false para una respuesta que el servidor no contó.

Seguridad y temporización

La línea de tiempo es código que has escrito tú, y se ejecuta en el origen del servidor de Potato con la sesión del participante, con la misma confianza que una página de instrucciones en HTML personalizada. Su Content-Security-Policy permite scripts, estilos y medios solo desde el servidor de Potato y desde cualquier csp.extra_sources, y peticiones de red solo hacia el servidor de Potato, así que un estímulo que muestra texto de un participante no puede cargar desde nada más ni enviar nada a otro sitio. Aun así, escapa ese texto antes de ponerlo en HTML.

Todo archivo que nombre la página debe estar dentro del directorio de la tarea. La ruta de recursos solo sirve tipos de archivo de estímulos, nunca .js ni .html de assets_dir, y envía cada uno con su propia CSP de aislamiento, así que un SVG abierto directamente no ejecuta ningún script. csp.extra_sources admite orígenes https:// individuales y rechaza los comodines.

En la página rige la temporización propia de jsPsych, porque los scripts de anotación de Potato no se cargan ahí. La documentación sobre la precisión de la temporización de jsPsych indica que los tiempos de respuesta medidos en JavaScript suelen ser entre 10 y 40 ms más largos que en el software de laboratorio, con una varianza similar.

Ejemplos

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

El primero es una tarea de Stroop entre un cuestionario de selección y una tarea de valoración de palabras, con retroalimentación mostrada a la mitad de los participantes. El segundo es una comprobación de auriculares seguida de fragmentos de habla valorados en la escala ACR.

Lecturas adicionales

Para detalles de implementación, consulta la documentación fuente sobre las fases de ejecución y la fase de jsPsych.