UNPKG

@bdelab/roar-firekit

Version:

A library to facilitate Firebase authentication and Cloud Firestore interaction for ROAR apps

326 lines (261 loc) 9.31 kB
[![npm version](https://badge.fury.io/js/@bdelab%2Froar-firekit.svg)](https://badge.fury.io/js/@bdelab%2Froar-firekit) ![NPM License](https://img.shields.io/npm/l/@bdelab/roar-firekit) # roar-firekit Welcome to roar-firekit! Roar-firekit helps you store the data from your [ROAR application](https://dyslexia.stanford.edu/roar/) in [Cloud Firestore](https://cloud.google.com/firestore). ## Installation You can install [roar-firekit from npm](https://www.npmjs.com/package/@bdelab/roar-firekit) with ```bash npm i @bdelab/roar-firekit ``` ## Usage Roar-firekit is agnostic about where your data comes from, but I anticipate most users will use roar-firekit with their experiments written in [jsPsych](https://www.jspsych.org/). The main entrypoint to roar-firekit's API is the [[`RoarAppkit`]] class. Its constructor expects an object with keys `userInfo`, `taskInfo`, and `config`, where `userInfo` is a [[`UserDataInAdminDb`]] object, `taskInfo` is a [[`TaskVariantInput`]] object, and `config` is a [[`AssessmentConfigData`]] object. ### Constructor inputs #### `userInfo` User information is encapsulated in a [[`UserDataInAdminDb`]] object. Its only required key is `id`, which should be the current user's ROAR UID, which is also sometimes called the ROAR PID: ```javascript const minimalUserInfo = { id: 'roar-user-id' }; ``` But you can supply other information about the user if you know it: ```javascript const fullUserInfo = { id: 'roar-user-id', birthMonth: 7, birthYear: 2014, classId: 'roar-class-id', schoolId: 'roar-school-id', districtId: 'roar-district-id', groupId: 'roar-group-id', userCategory: 'student', }; ``` #### `taskInfo` Information about the current task is encapsulated in a [[`TaskVariantInput`]] object. Here is the task information for a fictitious "Not Hotdog" task: ```javascript const taskInfo = { taskId: 'nhd', taskName: 'Not Hotdog', variantName: 'Not Hotdog, one block', taskDescription: 'A demonstration task using the hot dog / not hot dog problem', variantDescription: 'One block, random order', blocks: [ { blockNumber: 1, trialMethod: 'random-without-replacement', corpus: 'pointer-to-location-of-stimulus-corpus', }, ], }; ``` #### `config` The config object contains configuration information for your Firebase project. You may want to store your config object in separate javascript file (named "roarConfig.js" for this documentation). A template is provided below <details> <summary>Click to expand!</summary> ```javascript export const roarConfig = { firebaseConfig: { apiKey: 'insert your firebase API key here', authDomain: 'insert your firebase auth domain here', projectId: 'insert your firebase project ID here', storageBucket: 'insert your firebase storage bucket here', messagingSenderId: 'insert your firebase messaging sender ID here', appId: 'insert your firebase app ID here', measurementId: 'insert your firebase measurement ID here', }, rootDoc: ['some collection name', 'some document name'], }; ``` </details> ##### `firebaseConfig` To get the `firebaseConfig` fields, see [this article](https://support.google.com/firebase/answer/7015592#zippy=%2Cin-this-article) on how to retrieve your firebase config. TLDR: go directly to [your project's settings](https://console.firebase.google.com/project/_/settings/general/), scroll down to "SDK setup and configuration," click the "Config" radio button, and copy the snippet for your app's Firebase config object. ##### `rootDoc` The `rootDoc` is an array of strings representing the document under which all ROAR data will be stored. Note that `rootDoc` does not have to be in the actual root of your Cloud Firestore database. ### Constructing the firekit With the above defined input, you would construct a firekit using ```javascript import { RoarAppkit } from '@bdelab/roar-firekit'; import { roarConfig } from './roarConfig.js'; // Insert input definition code from above const firekit = new RoarAppkit({ userInfo: minimalUserInfo, taskInfo, config: roarConfig, }); ``` ## Starting a run Starting a run writes the user, task, and run information to Cloud Firestore: ```javascript await firekit.startRun(); ``` If you are using roar-firekit with jsPsych, you should call this method before experiment starts, either by awaiting it before the `jsPsych.run` method, ```javascript await firekit.startRun(); jsPsych.run(timeline); ``` or by calling it as part of the `on_timeline_start` callback, ```javascript const procedure = { timeline: [trial1, trial2], on_timeline_start: function() { await firekit.startRun(); } } ``` ## Writing a trial to Firestore After starting a run, you can write individual trial data to Cloud Firestore using the `writeTrial` method. This method can be added to individual jsPsych trials by calling it from the `on_finish` function, like so: ```javascript var trial = { type: 'image-keyboard-response', stimulus: 'imgA.png', on_finish: function (data) { firekit.writeTrial(data); }, }; ``` Or you can call it from all trials in a jsPsych timeline by calling it from the `on_data_update` callback. In this case, you can avoid saving extraneous trials by conditionally calling this method based on the data. For example: ```javascript initJsPsych({ on_data_update: function (data) { if (data.saveToFirestore) { firekit.addTrialData(data); } }, }); const timeline = [ // A fixation trial; don't save to Firestore { type: htmlKeyboardResponse, stimulus: '<div style="font-size:60px;">+</div>', choices: 'NO_KEYS', trial_duration: 500, }, // A stimulus and response trial; save to Firestore { type: imageKeyboardResponse, stimulus: 'imgA.png', data: { saveToFirestore: true }, }, ]; ``` ## Finishing a run After your experiment is over, you can mark it as completed in Firestore using the `finishRun` method. For example, you can call this method in the [`on_finish` (experiment) callback](https://www.jspsych.org/7.1/overview/events/#on_finish-experiment): ```javascript initJsPsych({ on_finish: function (data) { firekit.finishRun(); }, }); ``` ## Full example The following is an example jsPsych experiment that implements the NoHotdog assessment while writing data to Cloud Firestore using roar-firekit. <details> <summary>Click to expand!</summary> ```javascript import { initJsPsych } from 'jspsych'; import preload from '@jspsych/plugin-preload'; import htmlKeyboardResponse from '@jspsych/plugin-html-keyboard-response'; import imageButtonResponse from '@jspsych/plugin-image-button-response'; import { RoarAppkit } from '@bdelab/roar-firekit'; import { roarConfig } from './roarConfig.js'; const taskInfo = { taskId: 'nhd', taskName: 'Not Hotdog', variantName: 'nhd-1block-random', taskDescription: 'A ROAR demonstration using the hot dog / not hot dog task.', variantDescription: 'One block, random order', blocks: [ { blockNumber: 1, trialMethod: 'random-without-replacement', corpus: 'assets', }, ], }; const minimalUserInfo = { id: 'roar-user-id' }; const firekit = new RoarAppkit({ userInfo: minimalUserInfo, taskInfo, config: roarConfig, }); await firekit.startRun(); const jsPsych = initJsPsych({ on_data_update: function (data) { if (data.saveToFirestore) { firekit.writeTrial(data); } }, on_finish: function () { firekit.finishRun(); }, }); // This example assumes that the hot dog / not hot dog images are stored in the // assets folder. const numFiles = 30; const hotDogFiles = Array.from(Array(numFiles), (_, i) => i + 1).map( (idx) => new URL(`../assets/hotdog/${idx}.jpg`, import.meta.url), ); const notHotDogFiles = Array.from(Array(numFiles), (_, i) => i + 1).map( (idx) => new URL(`../assets/nothotdog/${idx}.jpg`, import.meta.url), ); const allFiles = hotDogFiles.concat(notHotDogFiles); const allTargets = allFiles.map((url) => { return { target: url, isHotDog: !url.pathname.includes('nothotdog') }; }); let timeline = []; /* preload images */ const preloadImages = { type: preload, auto_preload: true, }; timeline.push(preloadImages); /* define welcome message trial */ const welcome = { type: htmlKeyboardResponse, stimulus: 'Welcome to ROAR-HD, a rapid online assessment of hot dog differentiating ability. Press any key to begin.', }; timeline.push(welcome); const hotDogTrials = { timeline: [ { type: htmlKeyboardResponse, stimulus: '<div style="font-size:60px;">+</div>', choices: 'NO_KEYS', trial_duration: 500, }, { type: imageButtonResponse, stimulus: jsPsych.timelineVariable('target'), choices: ['Hot Dog', 'Not a Hot Dog'], prompt: 'Is this a hot dog?', data: { saveToFirestore: true }, on_finish: function (data) { data.correct = jsPsych.timelineVariable('isHotDog') == data.response; }, }, ], timeline_variables: allTargets, sample: { type: 'without-replacement', size: 20, }, }; timeline.push(hotDogTrials); const fixation = { type: htmlKeyboardResponse, stimulus: 'You are all done. Thanks!', choices: 'NO_KEYS', }; timeline.push(fixation); jsPsych.run(timeline); ``` </details>