Neurograd Day 1

Romain Ligneul

Installation

For this summer school, we will need to install a few important programs on our laptops:

  1. NodeJS: to create Javascript servers on our computers and in the cloud

  2. Miniforge: to create Python environments

  3. MongoDb Compass to access and visualize our databases

  4. Git and Github CLI: to facilitate collaborative work!

  5. VS Code: the perfect IDE for this summer school

NodeJS

To run a web experiment, we need a simple web server. One of the simplest options is to use Nodejs to develop this server with JavaScript. It is available on all platforms.

Download it here: https://nodejs.org/en

Take version 22 LTS

Miniforge

If you already have a virtual environment manager for Python, you may skip this step.

Otherwise, please follow the instructions on the Miniforge repository.

https://github.com/conda-forge/miniforge

MongoDB Compass

Just download and install from this site https://www.mongodb.com/products/tools/compass

Git / Github CLI

If you don’t already have it, download these tools from the following websites. You may want to open a terminal and type git --version to make sure you don’t have Git already.

VS Code

If you don’t already have VS Code (or a similar editor you are familiar with), download and install it from this website

https://code.visualstudio.com/

Note on terminal commands

In any terminal, the commands dir or ls lists the content of the current directory, and the command cd allows you to navigate in your filesystem.

  • cd .. to go back in the parent directory

  • cd example_subfolder to go into a subfolder of your current directory

  • cd example_subfolder/example_subsubfolder to go in a nested subsubfolder

  • cd C:/root_folder/ or cd /root_folder to use an “absolute” path

Note that the Tab key is quite useful to autocomplete when navigating

mkdir your_new_dir allows you to create a new directory

Exercise 1: get started with NodeJS and jsPsych

Get the repository, install and test

Go to the exercise repository: https://github.com/robustcircuit/neurograd-simple-experiment

Follow the instructions

Overview of the experiment code

<!DOCTYPE html>
<html>
  <head>
    <script src="js/jspsych/dist/jspsych.js"></script>
    <script src="js/jspsych/dist/plugin-preload.js"></script>
    <script src="js/jspsych/dist/plugin-image-button-response.js"></script>
    <script src="js/jspsych/dist/plugin-image-keyboard-response.js"></script>
    <script src="js/jspsych/dist/plugin-html-button-response.js"></script>
    <script src="js/jspsych/dist/plugin-html-keyboard-response.js"></script>
    <link rel="stylesheet" href="css/jspsych.css" />
    <link href="img/favicon.ico" rel="icon" />
  </head>

  <body></body>

  <script>
    // retrieve information

    // name the current session
    var basename = "expNOWdata";

    // get timestamp of initial loading of the script
    var currDate = new Date();

    //////////////////////////////////////////////////////
    // Define experiment parameters
    //////////////////////////////////////////////////////
    // We store experiment parameters in a dedicated object
    var expdef = {};

    // define timing parameters in milliseconds (jsPsych default unit)
    expdef["choiceMaxDuration"] = 3000;
    expdef["feedbackDuration"] = 500;
    expdef["fixationDuration"] = 500;

    // define default repetition of images within blocks
    expdef["defaultRepetition"] = 1;

    // define the message displayed at the beginning of the experiment
    expdef["experimentIntroMsg"] =
      "<p>In this short experiment, you will have to learn to associate " +
      "specific images with specific response buttons, by trial and error.<br>" +
      "The experiment is divived in blocks that correspond to different categories of images.</p>";
    expdef["experimentIntroButton"] = ["Click here to start the experiment"];

    // define the message displayed at the end of the experiment
    expdef["experimentEndMsg"] = "<p>Thanks for participating</p>";
    expdef["experimentEndButton"] = ["Click here to save the data"];

    // define the message displayed at the beginning of each block
    expdef["blockIntroMsg"] =
      "You are going to start a new block with images of ";
    expdef["blockIntroButton"] = ["Click here to start the block"];

    // define the message displayed below images
    expdef["choiceMsg"] =
      "<p><b>Click</b> on the correct response button for this image</p>";

    // define images and messages used for feedback
    expdef["feedbackMsg"] = [
      "<p>Missed! <br>Respond faster next time</p>",
      "<p>Incorrect</p>",
      "<p>Correct</p>",
    ];

    // define images paths for feedback
    expdef["feedbackImg"] = [
      "img/missedFb.png",
      "img/thumbsDown.png",
      "img/thumbsUp.png",
    ];

    // define the image names that will be used in the experiment
    var imageNames = {};
    imageNames["fruits"] = [
      "banana",
      "blueberry",
      "grapefruit",
      "kiwi",
      "pineapple",
      "raspberry",
    ];
    imageNames["vegetables"] = [
      "beet",
      "brussels_sprouts",
      "carrot",
      "eggplant",
      "lettuce",
      "pumpkin",
    ];

    //////////////////////////////////////////////////////
    // Init jsPsych
    //////////////////////////////////////////////////////

    var jsPsych = initJsPsych({
      on_finish: function (done) {
        document.body.style.cursor = "pointer";
        var interaction_data = jsPsych.data.getInteractionData();
        interaction_data.trial_tag = "interaction";
        jsPsych.data.get().push(interaction_data);
        jsPsych.data.get().push({
          completedTask: "RLWM_expNOW",
          basename: basename,
          starting_time: currDate,
        });
        jsPsych.data.get().localSave("csv", basename + ".csv");
        display_data();
        jsPsych.data.get()
        $.ajax({
          type: "POST",
          url: "/save-file",
          data: jsPsych.data.get().json(),
          contentType: "application/json",
        });
      },
    });

    //////////////////////////////////////////////////////
    // Generate trial list
    //////////////////////////////////////////////////////

    // block definition
    var includedCategories = ["vegetables", "fruits"];
    var setSizes = [1, 2];
    var mapSR = [[0], [0, 1]];
    var blockTrials = [expdef.defaultRepetition, expdef.defaultRepetition];

    // prepare block shuffling
    var blockIdx = [];
    for (var b = 0; b <= includedCategories.length - 1; b++) {
      blockIdx.push(b);
    }
    blockIdx = this.jsPsych.randomization.shuffle(blockIdx);

    // loop over blocks, images and repetitions
    trialStructure = [];
    for (var b = 0; b <= includedCategories.length - 1; b++) {
      // shuffle images (with respect to response mappings mapSR)
      blockImgs = this.jsPsych.randomization.shuffle(
        imageNames[includedCategories[blockIdx[b]]]
      );
      blockImgs = blockImgs.slice(0, setSizes[blockIdx[b]]);
      subStructure = [];
      // loop over images in the block
      for (var i = 0; i <= blockImgs.length - 1; i++) {
        // loop over repetition within blocks
        for (var r = 0; r <= blockTrials[blockIdx[b]] - 1; r++) {
          var trialspecs = {
            imgCategory: includedCategories[blockIdx[b]],
            imgId: blockImgs[i],
            repetitionCount: r,
            correctResponse: mapSR[blockIdx[b]][i],
            block: b,
            setSize: setSizes[blockIdx[b]],
          };
          subStructure.push(trialspecs);
        }
      }
      // randomize trials within a block
      subStructure = this.jsPsych.randomization.shuffle(subStructure);
      trialStructure.push(subStructure);
    }
    //console.log(trialStructure)

    //////////////////////////////////////////////////////
    // Generate trial objects
    //////////////////////////////////////////////////////

    var experimentIntro = {
      type: jsPsychHtmlButtonResponse,
      stimulus: function () {
        return expdef.experimentIntroMsg;
      },
      choices: expdef.experimentIntroButton,
      response_ends_trial: true,
    };

    var blockIntro = {
      type: jsPsychHtmlButtonResponse,
      stimulus: function () {
        return expdef.blockIntroMsg + includedCategories[blockIdx[blockNum]];
      },
      choices: expdef.blockIntroButton,
      response_ends_trial: true,
    };

    var fixationTrial = {
      type: jsPsychHtmlKeyboardResponse,
      stimulus: "<p style='font-size: 3rem;'>+</p>",
      trial_duration: expdef["fixationDuration"],
      response_ends_trial: false,
    };

    var choiceTrial = {
      type: jsPsychImageButtonResponse,
      stimulus: function () {
        var imgPath =
          "./img/" +
          trialStructure[blockNum][trialNum].imgCategory +
          "/" +
          trialStructure[blockNum][trialNum].imgId +
          ".jpg";
        return imgPath;
      },
      trial_duration: expdef["choiceMaxDuration"],
      choices: ["Left", "Middle", "Right"],
      prompt: expdef["choiceMsg"],
      stimulus_width: 400,
      maintain_aspect_ratio: true,
      response_ends_trial: true,
      on_start: function () {},
      on_finish: function (data) {
        if (data.response === null) {
          data.accuracy = -1;
        } else if (
          data.response == trialStructure[blockNum][trialNum].correctResponse
        ) {
          data.accuracy = 1;
        } else {
          data.accuracy = 0;
        }
      },
    };

    var feedbackTrial = {
      type: jsPsychImageKeyboardResponse,
      stimulus: function () {
        var lastCorrect = jsPsych.data.get().last(1).values()[0].accuracy;
        console.log(expdef["feedbackImg"]);
        return expdef["feedbackImg"][lastCorrect + 1];
      },
      prompt: function () {
        var lastCorrect = jsPsych.data.get().last(1).values()[0].accuracy;
        return expdef["feedbackMsg"][lastCorrect + 1];
      },
      stimulus_width: 150,
      maintain_aspect_ratio: true,
      trial_duration: expdef["feedbackDuration"],
      response_ends_trial: false,
    };

    var experimentEnd = {
      type: jsPsychHtmlButtonResponse,
      stimulus: function () {
        return expdef.experimentEndMsg;
      },
      choices: expdef.experimentEndButton,
      response_ends_trial: true,
    };

    // function to display data at the end of the experiment
    var display_data = function () {
      // set an HTML div
      const display_element = this.jsPsych.getDisplayElement();
      display_element.innerHTML = '<pre id="jspsych-data-display"></pre>';
      var data = jsPsych.data.get();
      var correctN = data.filter({ accuracy: 1 }).select("accuracy").count();
      var overallN = data.select("accuracy").count();
      var correctRT = data.filter({ accuracy: 1 }).select("rt").values;
      var incorrectRT = data.filter({ accuracy: 0 }).select("rt").values;
      var htmlStr =
        "<p>You gave " +
        ((100 * correctN) / overallN).toFixed(1) +
        "% of correct responses. </p><br><br>";
      htmlStr +=
        "<p>On average, it took you " +
        (correctRT / 1000).toFixed(3) +
        "s to give a correct response and ";
      htmlStr +=
        (incorrectRT / 1000).toFixed(3) +
        "s to give an incorrect response.</p>";
      document.getElementById("jspsych-data-display").innerHTML = htmlStr;
    };

    //////////////////////////////////////////////////////
    // Create nest timelines
    //////////////////////////////////////////////////////

    // counters
    var trialGlobNum = 0;
    var blockNum = 0;
    var trialNum = 0;

    // trial timeline, loops until end of block
    var trial_timeline = {
      timeline: [fixationTrial, choiceTrial, feedbackTrial],
      on_timeline_start: function () {
        tBlock = 0;
      },
      loop_function: function () {
        console.log("block" + blockNum + " and trial" + trialNum);
        if (trialNum < trialStructure[blockNum].length - 1) {
          trialNum++;
          return true;
        } else {
          trialNum = 0;
          blockNum++;
          return false;
        }
      },
    };

    // block timeline, loops until the end of the experiment
    var block_timeline = {
      timeline: [blockIntro, trial_timeline],
      loop_function: function () {
        if (blockNum < trialStructure.length) {
          return true;
        } else {
          return false;
        }
      },
    };

    // main timeline, will only run once
    var main_timeline = [experimentIntro, block_timeline, experimentEnd];

    jsPsych.run(main_timeline);
  </script>
</html>

Overview of the server code

// get packages
var fs = require("fs");
var path = require("path");
var express = require("express");
var cors = require("cors");
require("dotenv").config();

// --- INSTANTIATE THE APP
var app = express();

// manage cors policy
app.use(cors());

app.use(express.static(__dirname + "/public/"));

// set views
app.set("views", path.join(__dirname, "/public/"));

// set routes
app.get("/expNOW", function (request, response) {
  response.render("experiment.html");
});

// set view engigne
app.engine("html", require("ejs").renderFile);
app.set("view engine", "html");

// START THE SERVER
app.listen(3000, function () {
  console.log(
    "Server running. To see the experiment that it is serving, visit the following address:"
  );
  console.log("http://localhost:%d/expNOW", 3000);
});

Let’s launch the server

Open Visual Code or the IDE of your choice and type

cd PATH_TO_THE_GIT_REPO
cd server
node app

Then, open http://localhost:3000/expNOW in your browser.

You should see the experiment running (if you’ve followed the installation steps).

To stop and restart the server, just press Ctrl+C and type again node app.

The server should be restarted each time you make a change to the server code.

Key approaches to develop and debug web applications

Use the developer tools of your browser!

To see the console, you must toggle the developer tab of your browser (most often using key F12 or Fn+F12).

Developer tabs of Firefox

Select console and type expdef + Enter to see our experiment definitions object.

You can also add console.log(something) statements anywhere in your scripts to print dynamically the value (and structure) of Javacript variables, objects, etc.

Inspect your HTML and CSS

All developer tabs have an “inspector” tool that allows you to check and understand the static structure of any webpage, as well as the name of the elements you can manipulate. For example, the first screen of the experiment contains the following code:

<body style="margin: 0px; height: 100%; width: 100%;" tabindex="0" class=" jspsych-display-element">
  <div class="jspsych-content-wrapper">
    <div id="jspsych-content" class="jspsych-content">
      <div id="jspsych-html-button-response-stimulus">
        <p>In this short experiment, you will have to learn to associate specific images with specific response buttons, by trial and error.<br>The experiment is divived in blocks that correspond to different categories of images.</p>
      </div>
      <div id="jspsych-html-button-response-btngroup">
        <div class="jspsych-html-button-response-button" style="display: inline-block; margin:0px 8px" id="jspsych-html-button-response-button-0" data-choice="0">
          <button class="jspsych-btn">Click here to start the experiment</button>
        </div>
      </div>
    </div>
  </div>
</body>

The id and class attributes can be used to access each of these objets (divs, buttons, paragraph) and change their appeareance or behavior.

Reflect on what you see

In the previous slide, we show a piece of HTML code that appears in the Inspector. Can you find this code as is in the experiment.html file? Why?

Also, have a look at the Debugger tab. It looks a bit like our local directory “server”. But it is not complete. Do you know why? If not, have a look at the top of your experiment.html file :).

Start as short as possible

Experiments can fail at any stage, but they tend to fail much more often at the beginning or at the end. So, always keep them as short as possible when we still need to debug them.

So, the first thing we will do is to shorten ours even more. Try the following changes in `experiment.html

//expdef["feedbackDuration"] = 1000;
//expdef["fixationDuration"] = 1000;
expdef["feedbackDuration"] = 500;
expdef["fixationDuration"] = 250;
//var setSizes = [3, 6];
//var mapSR = [[0, 1, 2], [0, 1, 2, 0, 1, 2]];
var setSizes = [1, 2];
var mapSR = [[0], [0, 1]];

This should reduce quite drastically the duration without removing any key ingredients.

Modify further the example experiment

Create subject- and/or session-specific URL

URL parameters are passed in the URL using the symbol “?” and several parameters can be chained using the symbol “&”.

// just copy paste this somewhere at the top
  const queryString = window.location.search;
  const urlParams = new URLSearchParams(queryString);
  var SUBJECT = 'unknown'
  if (urlParams.has('SUBJECT')) {SUBJECT = urlParams.get('SUBJECT'); }
  var SUFFIX = 'unknown'
  if (urlParams.has('SUFFIX')) {SUFFIX = '_' + urlParams.get('SUFFIX');}

Note

This approach will allow to share specific URLs to our participants and therefore keep track of who did what.

Use the URL parameters to customize files and messages

Now, we should use a web address looking like: localhost:3000/expNOW?SUBJECT=someone&SUFFIX=v1

We also need to modify the following line if we want to use this information to identify the output data!

    // name the current session
    var basename = 'expNOW_' + SUBJECT + SUFFIX

Now, try to use the SUBJECT variable to give a personalized “Hello” to your participants!

Note

Our first message to participants is defined by expdef["experimentIntroMsg"]

Display a full screen experiment

To do so, we need to use a plugin that jsPsych does not import by default, so we need to import it manually in our experiment, in the <head> section.

<script src="js/jspsych/dist/plugin-fullscreen.js"></script>

Find the actual file and have a look at the list of plugins provided by jsPsych. They might be useful for other experiments of yours!

Then, we just need to add the following jsPsych objects to our script, after the initialization of jsPscych.

  var enter_fullscreen = {
    type: jsPsychFullscreen,
    fullscreen_mode: true,
    message: function () {return "<p>The experiment will now go full screen.</p>"},
    button_label: function () {return "Click here to continue"},
  }
  var exit_fullscreen = {type: jsPsychFullscreen, fullscreen_mode: false}

Improve the visual appearance of our experiment

By default, jsPsych comes with very sober Cascading Style Sheets (CSS), but CSS can be easily updated to make things prettier. So, if we add the custom_jspsych.css stylesheet to after the default jspsych.css one, the experiment is already looking different.

  <link rel="stylesheet" href="css/jspsych.css">
  <link rel="stylesheet" href="css/custom_jspsych.css">

This is because custom_jspsych.css contains new styling information. Try to make additional changes to the visual appearance of the experiment by modifying or extending custom_jspsych.css.

/*to change all font colors of a given class */
.whateverclass {
  color: blue;
}
/*to change font weight of all several headings of a given class at once */
.whateverclass h2,h3,h4 {
  font-weight: 700;
}
/*to change the background of elements with a given ID */
#whateverID {
  background-color: rgb(2500, 0, 0);
}

Tip

Right click on any part of a web pages and click inspect to see its class or id. Do not forget semi-colons after each attribute!

Use jsPsych documentation to add a small survey at the end of the experiment

Open this page and adapt the demo code provided to add a survey.

  • Use the CDN-hosted Javascript file or the Javascript file that is present in js/jspsych/dist/survey-likert

  • Create just 2 or 3 questions that make sense in the context of this experiment

Add a third block using another image category

Study the code to find out how you can do this. Don’t hesitate to make questions if you are stuck.

If you have access to Internet, you can be creative! Otherwise just use the third folder provided in img.

Just a hint to get you started, you might have to add code like this somewhere:

    imageNames['desserts'] = ["brownie","cake","dessert","donut","ice-cream_cone","milkshake"]

Of course, there will be a few other changes to implement to make it work.

Display their data to your participants

Add the following piece of code where it looks reasonable

var display_data = function () {
    const display_element = this.jsPsych.getDisplayElement(); // find main div of jsPsych
    display_element.innerHTML = '<pre id="jspsych-data-display"></pre>'; // set a new HTML div
    var data=jsPsych.data.get(); // get all jsPsych data
    // add the data to our new div
    document.getElementById("jspsych-data-display").textContent = JSON.stringify(data);
};

Then, you’ll need to update the on_finish routine of jsPsych to execut this function

  var jsPsych = initJsPsych({
    on_finish: function () {
      // ... add display_data to the end
      display_data();
    },
  });

Display their data to your participants (in a nicer way)

Remove line document.getElementById("jspsych-data-display").textContent = JSON.stringify(data); and add instead:

  var correctN = data.filter({accuracy: 1}).select('accuracy').count();
  var overallN = data.select('accuracy').count();
  var correctRT=data.filter({accuracy: 1}).select('rt').mean();
  var incorrectRT=data.filter({accuracy: 0}).select('rt').mean();
  var htmlStr = '<p>You gave ' + (100*correctN/overallN).toFixed(1) + '% of correct responses. <br><br>'
  htmlStr += '<p>On average, it took you ' + (correctRT/1000).toFixed(3) + 's to give a correct response and '
  htmlStr += (incorrectRT/1000).toFixed(3) + 's to give an incorrect response. '
  document.getElementById("jspsych-data-display").textContent = htmlStr;

Use Github and Render to serve your version of the experiment

  • Log in your Github account.

  • Go to “Repositories”

  • Click “New”

  • Give your repo a name, leave all other fields untouched.

  • Copy-paste the URL (e.g. https://github.com/username/yournewrepo.git)

Coming back to the terminal, make sure you are in the root of the repo (in neurograd-simple-experiment) and type: git remote set-url origin https://github.com/username/yournewrepo.git to associate your new Github repository with the local repo.

Use Github and Render to serve your version of the experiment

Now, let’s perform the most standard combo of git commands ever: git add . git commit -m "first commit message" git push

Refresh the URL of your new repo (e.g. https://github.com/username/yournewrepo.git) to see the results.

If you see the content of your local repo, we’re all set!

Now, we can work with Render.

Use Github and Render to serve your version of the experiment

Go to https://dashboard.render.com/register

Under “Create an account”, click on Github to create your account using your Github identity.

Then, once arrived on the Dashboard, click “+ Add New” (black button on the top right) and select “Web service”

Click on the tab “Public Git Repository” and copy paste the URL of your own repo.

On the screen screen, find the line “Build Command” and replace “yarn” by “npm –prefix server install”.

Just below, at the “Start Command” line, write “node server/app.js”

Use Github and Render to serve your version of the experiment

Once you see the lines

==> ///////////////////////////////////////////////////////////
==> Available at your primary URL https://xxxx-xxx.onrender.com
==> ///////////////////////////////////////////////////////////

You should be able to perform the experiment at https://xxxx-xxx.onrender.com/expNow

If you do the experiment, you’ll see that the data will be downloaded on the computer of the pilot, rather than yours.

This is not really convenient… Sure, you could ask participants to send you their logfile by email, but that not the idea of online testing.

How to send participant data to the server

Add this to the head section of your experiment.html file

<script src="js/jquery/jquery-3.7.1.js"></script>

Add this at the end of the on_finish function of initjsPsych

$.ajax({
  type: "POST",
  url: "/save-file",
  data: jsPsych.data.get().json(),
  contentType: "application/json",
}).done(function () {console.log('uploaded')})

How to send participant data to the server

Now add the following lines to your server app.js, before the app.listen() call.

var bodyparser = require("body-parser");
app.use(bodyparser.json({ limit: "50mb" }));
app.post("/save-file", function (request, response) {
  var datestr = new Date();
  datestr = String(datestr.toISOString()).replace(/:|\s+|_/g, '')
  var filename = String(request.body[request.body.length - 1].basename + "_" + datestr + ".json");
  // If you were serving the experiment from a "full" server, you could run the line below:
  //fs.writeFile(path.join(__dirname, "logdata/" + filename), JSON.stringify(request.body), (err) => {if (err) throw err; response.end(); });
  // However, if you are serving it from a web service (e.g. render.com) it won't work (no filesystem, only a running app)
  console.log(`A logfile has been received: ${filename}\nIf we are in the cloud, we should send this immediately to a database..`)
});

Send the collected data to a MongoDB database

To connect with a MongoDB database, we need to set a MONGODB_URI variable. But with this variable anyone will be able to read and write from our database, so we should make sure it is never published online! That why it was shared with you by email.

To store it, we’ll create a .env file in the “server” subfolder, we’ll open it and we’ll paste the connection string received by email after MONGODB_URI=

Send the collected data to a MongoDB database

Now, we need to install a NodeJS module to deal with MongoDB databases cd server npm install mongoose

Add this towards the top of you app.js file

var mongoose = require("mongoose");
const yourName="Romain"
const dbSchema = new mongoose.Schema({}, {
  strict: false,
  collection: yourName // bind schema to specific collection
});
const dbModel = mongoose.model(yourName, dbSchema);
mongoose.connect(process.env.MONGODB_URI);
var db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error"));
db.once("open", function callback() {
  console.log("database opened");
});

Send the collected data to a MongoDB database

Then, modify the lines dealing with save-file request

app.post("/save-file", function (request, response) {
  var datestr = new Date();
  datestr = String(datestr.toISOString()).replace(/:|\s+|_/g, '')
  var filename = String(request.body[request.body.length - 1].basename + "_" + datestr + ".json");
  dbModel.create(JSON.stringify(request.body));
  response.status(200).send({ message: 'success' });
});

See the result

Update your Gihub repo (add, commit, push).

Wait a little and check Render has successfully built the new version of the repo.

Do the experiment until the end.

Copy-paste the MONGODB_URI connection string in MongoDB Compass to explore the database.

That’s it, you have your own functional web experiment running!

Next: run a true experiment on Prolific?

Log into Prolific with the credentials provided by email. Explore the interface (Projects=>Study).

What is THE key change we should make to our experiment.html to make it work with Prolific?

Bonus - Adapt an existing experiment for your server

We will use an experiment found on the web, but you can try with your own pick.

  1. Open a terminal and cd inside now-workshop repository folder.

  2. Type git clone https://github.com/jspsych/experiment-demos

  3. Choose one of the 4 demo experiments and adapt it to serve it from experiment2.html

Overall steps to follows:

  1. Copy-paste the content of a demo folder into your public subfolder.

  2. Create a route towards the new .html file in our server script.

  3. Adapt the on_finish method contained in our jsPsych.init section to send the data to your server at the end of the experiment! You will need to: (i) make decision about whether you want to use URL parameters or not (and implement them or define the basename variable in another way), (ii) push the basename attribute in jsPsych.data in the same way as it is done in experiment.html

Bonus - Add a second experiment to your server

If you want to serve different experiments at different addresses, you will need to modify a bit the server (app.js).

ExpressJS makes it very simple to serve a new page. You just need to duplicate and modify the following lines to serve a new HTML file (e.g. visual_search.html) at a new address (e.g. at /visualsearch).

app.get('/expNOW', function (request, response) {
  response.render('experiment.html');
});

Each app.get() defines a “route” linking a URL subaddress (such as localhost:3000/expNOW) to a specific HTML file present in our views folder (such as experiment.html).

Bonus - Preload all images

Use the documentation of the preload plugin to find a way to (manually) preload all images of the experiment.

Tip

You can easily construct a list of the paths of all the images displayed in the experiment in the constructor loop that runs over blocks and images.

The JSON format

The JSON format is one of the most common and useful file formats for web programming, modern databases (NoSQL) and beyond. Right now, the data of this tutorial is saved in a very inefficient .csv format. Try to find the line that will allow you to save a .json file instead!

The resulting file may look like this. Can you see the key difference with a standard .csv format?

[{"trial_type":"call-function","trial_index":0,"time_elapsed":1,"internal_node_id":"0.0-0.0","task_id":"GONOGO","subject_id":"p001","study_id":"1","session_id":"2","date":"20230612_0915"},{"success":true,"timeout":false,"failed_images":[],"failed_audio":[],"failed_video":[],"trial_type":"preload","trial_index":1,"time_elapsed":34,"internal_node_id":"0.0-1.0","task_id":"GONOGO","subject_id":"p001","study_id":"1","session_id":"2","date":"20230612_0915"},{"success":true,"trial_type":"fullscreen","trial_index":2,"time_elapsed":2122,"internal_node_id":"0.0-2.0","task_id":"GONOGO","subject_id":"p001","study_id":"1","session_id":"2","date":"20230612_0915"}]

Perspectives

Going beyond tutorials

Go beyond tutorials to implement professional experiments can be tough.

  • Data protection requires a deeper understanding of server-side operations and encryption methods

  • Large-scale data curation requires databases that require specific code

  • Demanding experiments w.r.t timing accuracy require specific tools/code

  • Unusual or complex experiments require to program custom JS plugins from scratch

  • Scalability and usability of experiments across devices require responsive HTML or SVG

  • Reusability of the same code base in the lab for fMRI, EEG, TMS, etc., as well as mobile or desktop apps, is feasible but it requires advanced JS skills.

Conclusion

Questions?