3  jsPsychMaker


jsPsychMaker: Create experiments with jsPsych, randomize participants, etc.


In brief

Using jsPsychMaker to build experimental protocols helps you with a few things:

  • Create full protocols using:

    • tasks already implemented

    • task that will be created reading a csv or excel file

  • Configure your protocol by editing a simple config.js file

  • Select order of tasks, randomize blocks of tasks, etc.

  • Randomizes participants to groups, making sure the balance between the groups is maintained

  • Allow participants to continue in the task where they left in the protocol

  • Set time limits to complete the protocol

    • Automatically discard participants over the time limit, freeing the slots for new participants
  • Run online and offline protocols

  • Simulate participants with jsPsychMonkeys

  • Automagically get your data prepared with jsPsychHelpeR


See QuickGuide for basic instructions.


3.1 Available tasks

In 2024-05-01 we have 68 tasks implemented, and 34 in development. The full details about the available tasks can be checked in this document. You can always check the full list of tasks in the Github repo.

To list the available tasks in the jsPsychMaker R package in you console, you can use the list_available_tasks() function. If you don’t have the jsPsychMaker package, install it first.

jsPsychMaker::list_available_tasks()$tasks
  [1] "AIM"                 "AntiBots"            "APrioriDiagnostic"  
  [4] "APrioriScreening"    "Bank"                "BART"               
  [7] "Bayesian39"          "Bayesian40"          "BDI"                
 [10] "BNT"                 "bRCOPE"              "CAS"                
 [13] "CEL"                 "CIT"                 "CMApost"            
 [16] "CMApre"              "Consent"             "ConsentAudio"       
 [19] "ConsentHTML"         "Cov19Q"              "COVIDCONTROL"       
 [22] "CRQ"                 "CRS"                 "CRT7"               
 [25] "CRTMCQ4_ES-es"       "CRTMCQ4"             "CRTv"               
 [28] "CS"                  "CTT"                 "DASS21"             
 [31] "DEBRIEF"             "DEBRIEF39"           "DEMOGR"             
 [34] "DMW"                 "EAR"                 "EmpaTom"            
 [37] "EQ"                  "ERQ"                 "ESM"                
 [40] "ESV"                 "ESZ"                 "fauxPasEv"          
 [43] "FDMQ"                "FKEA"                "GBS"                
 [46] "GHQ12"               "Goodbye"             "HRPVB"              
 [49] "HRPVBpost"           "IBT"                 "ICvsID"             
 [52] "IDQ"                 "IEC"                 "INFCONS"            
 [55] "IRI"                 "IRS"                 "LoB"                
 [58] "LOT"                 "LSNS"                "MCQ30"              
 [61] "MDDF"                "MDMQ"                "MIS"                
 [64] "NARS"                "OBJNUM"              "OTRASRELIG"         
 [67] "PBSr"                "PERMA"               "PPD"                
 [70] "PRFBM"               "PRFBMpost"           "PSC"                
 [73] "PSETPP"              "PSPPC"               "PSS"                
 [76] "PVC"                 "PWb"                 "REI40"              
 [79] "Report"              "RMET"                "RobToM"             
 [82] "RSS"                 "RTS"                 "SASS"               
 [85] "SBS"                 "SCGT"                "SCSORF"             
 [88] "SDG"                 "SILS"                "sProQOL"            
 [91] "SRA"                 "SRBQP"               "SRSav"              
 [94] "STAI"                "SWBQ"                "UCLA"               
 [97] "WaisMatrices"        "WaisMatricesES"      "WaisWorkingMemory"  
[100] "WaisWorkingMemoryES" "WEBEXEC"            

If you need help creating a NEW task, see the section help creating a new task.


Below, a table with an overview of the available tasks in the Google Sheet. If a task is here but not in the R package, you can open an issue to let us know.

3.2 Experiment configuration

In the config.js file you can find the main parameters to control how your experiment works.

You can edit the config file in one of the following two ways:

  1. Go to folder_output and edit config.js.

  2. Use the jsPsychMaker_config Shiny APP and copy the generated config.js file in the folder_output folder.

If you use the app, you will need to copy the generated config.js file to your protocol folder. The Shiny app can also help you create a parametrized consent form (see the Consent tab).

3.2.1 Main parameters

  • pid = 999;: Number of protocol

  • online = true;: true if the protocol runs in a server, false if it runs locally

  • max_participants = 3;: If you have between participants conditions (participants are assigned to only one of a number of conditions), this is the max number of participants per condition

  • random_id = false;: true if you want to assign a random id to participants, false if the participant needs to input an id

  • max_time = "24:00:00";: Maximum time to complete the protocol (HH:MM:SS; Hours:Minutes:Seconds)

  • accept_discarded = true;: If a participant is discarded (i.e. exceeded the max_time), should we allow them to continue, given there are available slots?

  • debug_mode = false;: When testing the protocol:

  • shows DEBUG messages

  • creates the DB tables if they don’t exist

  • Avoids randomization (e.g. order of items) so the jsPsychMonkeys can have a reproducible behavior

  • language = "English"; Language to use for the protocol. Either Spanish or English

3.2.2 Order of tasks

first_tasks = ['Consent'];//  The protocol will start with these tasks in sequential order
last_tasks = ['Goodbye'];//  Last block of tasks presented (in sequential order)

Create as many blocks as needed:

randomly_ordered_tasks_1 = ['TASK1', 'TASK2']; //  Block of tasks in random order
randomly_ordered_tasks_2 = ['TASK3']; //  Block of tasks in random order
secuentially_ordered_tasks_1 = ['TASK5', 'TASK4']; // Block of tasks in sequential order

The final array of tasks can be build combining the above blocks. The order of the tasks in the arrays starting with “random” will be randomized.

tasks = ['first_tasks', 
         'randomly_ordered_tasks_1', 
         'secuentially_ordered_tasks_1', 
         'randomly_ordered_tasks_2', 
         'last_tasks'];

3.2.3 Between-subject tasks

The variable all_conditions in config.js let’s you define the Independent Variables (IV) and levels for the between-subject tasks:

If there is no between-subject task:

all_conditions = {"protocol": {"type": ["survey"]}};

If there are between-subject tasks:

all_conditions = {"NAMETASK": {"name_IV": ["name_level1", "name_level2"]}};

jsPsychR will randomize participants to the different conditions keeping the unbalance between conditions to the minimum possible.

3.3 online-offline protocols

jsPsych uses standard web technologies (HTML, CSS y Javascript), so that protocols should run in any modern browser (updated, please). We recommend Google Chrome just because our test suite runs with Google Chrome, so we will catch its specific issues earlier.

3.3.1 Offline

If you want to run a protocol locally (on your computer, on a lab computer), you need to:

  • set online = false; in the config.js file

  • double click index.html

jsPsychR will use IndexedDB to store the participants’ progress and balance between conditions. The output csv files will be Downloaded to the Download folder of the computer where the protocol runs.

3.3.1.1 CORS ERRORS

If any of the tasks imports an html file, the Offline protocol will give a CORS error.

There are ways to disable web security in your browser, but it MUST only be done if your experiment computer runs offline, otherwise you will be exposed to very bad things.

See how to run chrome disabling web security to avoid CORS error:

  • google-chrome --disable-web-security --user-data-dir="~/"

3.3.2 Online

Tu run a protocol online, set online = true; in the config.js file. You will need a couple more things:

  • MySQL running in your server
  • A file .secrets_mysql.php with the content below
  • Define the route to .secrets_mysql.php in controllers/php/mysql.php
    • require_once '../../.secrets_mysql.php';
    • THIS FILE MUST NOT BE PUBLICLY VISIBLE FROM THE BROWSER
  • Upload the files to the server :)
<?php

/* DO NOT UPLOAD TO PUBLIC REPO */

  $servername = "127.0.0.1";
  $username = "USERNAME OF THE DATABASE";
  $password = "PASSWORD OF THE DATABASE";
  $dbname = "NAME OF THE DB";
  
?>

jsPsychR will use MySQL to store the participants’ progress and balance between conditions. The output csv files will be Downloaded in the .data/ folder inside the protocol folder in the server.

Before launching the final experiment, make sure you start with a clean slate! That can be summarized in 3 simple steps:

  1. Check the configuration for you experiment (config.js) and make sure all is well. Some of the critical bits are:
pid = 999; // SHOULD have your project ID!
online = true; // true is good
max_participants = 100; // Max participants per contition [number]
max_time = "24:00:00"; // Max time to complete the protocol [HH:MM:SS]
debug_mode = false; // SHOULD be false
  1. Check that the .data/ folder for your protocol is empty in the server. You will likely have remains of the piloting and Monkeys.

  2. Clean up the MySQL data associated to your protocol.

SET @PID = 999; // HERE YOUR PROTOCOL ID!

delete from experimental_condition where id_protocol=@PID;
delete from user where id_protocol=@PID;
delete from user_condition where id_protocol=@PID;
delete from user_task where id_protocol=@PID;
delete from task where id_protocol=@PID;
delete from protocol where id_protocol=@PID;

You will most likely need help from the server admin to perform these steps.

3.4 Language

We started implementing the basic blocks to be able to switch a protocol’s language from Spanish to English with the parameter language in the config.js file.

This will change the protocol’s hardwired messages (see config_messages.js), and will use the desired version of the task, if available. So far we only prepared a handful of tasks in multiple languages.

An example of a multilingual task would be Consent.js.

We have a Translations block:


// Translations --------------------------------------------------------------

switch (language) {

  case "Spanish":

    Consent_000 = ['<p><left><b><big>Consentimiento informado</big></b><br /></p>'];
    Consent_001_choices = ['acepto participar', 'rechazo participar'];
    Consent_001_end = 'Gracias por tu tiempo. Puedes cerrar esta página.';
    break;

  case "English":

    Consent_000 = ['<p><left><b><big>Informed consent</big></b><br /></p>'];
    Consent_001_choices = ['I agree to participate', 'I reject to participate'];
    Consent_001_end = 'Thanks for your time. You can close this page.';
    break;

}

And then a Task block, where the logic of the experiment is unique, and we use the variables created in the Translation block for the things the users will see in their screens:


// Task -----------------------------------------------------------------------

questions = ( typeof questions != 'undefined' && questions instanceof Array ) ? questions : [];
questions.push( check_fullscreen('Consent') );
Consent = [];    //temporal timeline


var instruction_screen_experiment = {
    type: 'instructions',
    pages: Consent_000,
    data: {trialid: 'Consent_000', procedure: 'Consent'},
    show_clickable_nav: true,
    on_trial_start: function(){
        bloquear_enter = 0;
    }
};


// Reads consent from media/consent/consent-placeholder.js
var question01 = {
  type: 'html-button-response',
  stimulus: intro_CONSENT,
  choices: Consent_001_choices,
  prompt: "<BR><BR>",
  // If 'rechazo participar' is pressed, end experiment
  on_finish: function(data){
    if(jsPsych.data.get().values().find(x => x.trialid === 'Consent_001').button_pressed == 1){
      jsPsych.endExperiment(Consent_001_end);
    }
  },
    data: {
    trialid: 'Consent_001',
    procedure: 'Consent'
   }
};
Consent.push(question01);


Consent.unshift(instruction_screen_experiment);
Consent.push.apply(questions, Consent);
call_function("Consent");

3.5 Need help implementing a task!

If you need help creating a NEW task, see the section help creating a new task.

3.6 Developing tasks

Remember to place an if (debug_mode === false) before the randomization of the item order so when running in debug_mode, the items are not randomized. This is important so the behaviour of the jsPsychMonkeys is reproducible:

if (debug_mode === false) NAMETASK = jsPsych.randomization.repeat(NAMETASK,1);

3.7 Technical aspects

We currently use jsPsych 6.3 as the default, and started to implement the last stable jsPsych (7.3) recently. To choose a version for the protocols you create, use the parameter jsPsych_version. For example, jsPsych_version = 7. Things are not fully tested in the v7 so, use with care.

There is a migration guide and a Github issue with migration questions.

3.7.1 Misc

When index.html is launched:

  • Checks if there are available slots

When an uid is assigned:

  • questions array is created

  • between-participants conditions are assigned and stored in the DB (MySQL if online, IndexedDB if offline)

Each question, timeline or conditional question needs to have a:

data: {trialid: 'NameTask_001', procedure: 'NameTask'}

The trialid identifies the trial, and the procedure makes possible to find that trial so participants can continue the tasks where they left, know when participants finished the tasks, etc. This is done in MySQL if online, IndexedDB if offline.

When running online tasks with between-participants variables, the system that balances conditions can change between-participants condition after the participants accept the consent form. If any of the items is not in a timeline (e.g. Instructions) and stores the condition_between, it may not be up to date. See Bayesian39 task for an example.

trialid’s need to have a standardized structure, which generally conforms with NameTask_3DigitNumber. When using conditional items the structure can be a bit more complex, but not much. We use the following rules to check for non-complying trialid’s:

^[a-zA-Z0-9]{1,100}_[0-9]{2,3}$ -> `NameTask_2or3DigitNumber`, for example `BNT_001`  
^[a-zA-Z0-9]{1,100}_[0-9]{2,3}_[0-9]{1,3}$ -> `NameTask_2or3DigitNumber_1to3DigitsSuffix`, for example `BNT_002_1`  
^[a-zA-Z0-9]{1,100}_[0-9]{2,3}_if$ -> `NameTask_2or3DigitNumber`, for example `BNT_002_if`  
^[a-zA-Z0-9]{1,100}_[0-9]{2,3}_[0-9]{1,3}_if$  -> `NameTask_2or3DigitNumber`, for example `BNT_002_1_if`  

3.7.2 jsPsychMaker main changes on a task

  1. Start of a task
questions = ( typeof questions != 'undefined' && questions instanceof Array ) ? questions : [];
questions.push( check_fullscreen('NameOfTask') );
NameOfTask = [];
  1. Each item
data: {trialid: 'NameOfTask_01', procedure: 'NameOfTask'}
  1. End of experiment
if (debug_mode == 'false') NameOfTask = jsPsych.randomization.repeat(NameOfTask, 1);
NameOfTask.unshift(instruction_screen_experiment);
questions.push.apply(questions, NameOfTask)

questions.push({
    type: 'call-function',
    data: {trialid: 'NameOfTask_000', procedure: 'NameOfTask'},
    func: function(){
      if (online) {
        var data = jsPsych.data.get().filter({procedure: 'NameOfTask'}).csv();
      } else {
        var data = jsPsych.data.get().filter({procedure: 'NameOfTask'}).json();
      }
      saveData(data, online, 'NameOfTask');
    }
});

3.7.3 Conditional questions

var question001 = {
  type: 'survey-multi-choice-vertical',
  questions: [{prompt: '<div class="justified">¿Usted se ha vacunado contra el coronavirus / covid-19?</div>', options: ['&nbsp;Si', '&nbsp;No'], required: true,  random_options: false, horizontal: false}],
  data: {trialid: 'PVC_001', procedure: 'PVC'}
};
PVC.push(question001);

var question001_1 = {
  type: 'survey-multi-choice-vertical',
  questions: [{prompt: '<div class="justified">¿Usted se va a vacunar contra el coronavirus covid-19?</div>', options: ['&nbsp;Si', '&nbsp;No', '&nbsp;No estoy seguro'], required: true,  random_options: false, horizontal: false}],
  data: {trialid: 'PVC_001_1', procedure: 'PVC'}
};

var if_question001_1 = {
  timeline: [question001_1],
  data: {trialid: 'PVC_001_1_if', procedure: 'PVC'},
  conditional_function: function(){
    let data = (JSON.parse((jsPsych.data.get().values().find(x => x.trialid === 'PVC_001'))['response'])['Q0']).trim();
    if((data) ==  'No'){
      return true;
    } else {
      return false;
    }
  }
};
PVC.push(if_question001_1);

3.8 Common ERRORS

If you get the following error in the console: Uncaught TypeError: Cannot read properties of undefined (reading 'procedure')

Run this in the console:

for (var i = 0; i < questions.length; i++) {
  console.log(i + questions[i].data["procedure"])
}

It will stop in one of the items. Go to the console, check the array questions and go to the number that failed.

When you know the task and item that fails, you probably need to add:

`data: {trialid: 'TASKNAME_ITEMNUMBER', procedure: 'TASKNAME'}