Receiving "Study run already finished" error when submitting results in multi-component study
Hi There,
First, I want to say thanks for developing such a terrific program for research. It is such a valuable and appreciated contribution to science.
The problem I am having:
When I am running my study online I am getting a consistent error when submitting results and moving from the first component of a study to another. This error only occurs when the study is moved online, it does not occur locally. Otherwise, the study is stable.
When this error occurs, the study breaks and does not continue to the subsequent 2 components. Data is not collected for the finished first component.
My study has three components. When I complete the first component and the study results are due to be uploaded to the JATOS webserver, I receive an error message when moving to the next component, with a "Study run already finished" error, citing the study run as complete.
I have attached an image of the console log.
A little bit of information:
I am running Firefox 140.0.2 (64-bit) Windows.
Jatos version v3.9.5
JsPsych v8
I am recruiting participants through Prolific, so am using the "General Multiple" study link.
What I have tried, and seemed to work:
In JATOS, when I enable "allow reload" for each separate component in the study this seems to resolve the problem (maybe in submitting the results the study component reloads, before moving on?).
Is this an appropriate way to deal with this error? Or is there a safer option?
A related question—is there a way to load/stress test the study with windows?
Thank you again for all of your support and building and maintaining such a terrific program, it is greatly appreciated.
Best,
Jesse
Comments
Hi Jesse,
strange, could you post the part of your jsPsych script where you submit data, finish one component, and ask jatos to go to the next?
I could imagine that the order of those functions is somehow messing things up
Hi Elisa,
Thanks for your speedy response. I appreciate your help!
The code references shared scripts/functions (I don't know the terminology, sorry!), so it is all not necessarily in the same place. I will paste the code for the first component where it submits data and should call the next component.
If it is helpful I can send a link to the other utility scripts to submit data to JATOS. (I think they would be far too long to attach to a forum post...).
Thanks again for your help—I'm a real beginner when it comes to code.
Data submission code for the first experiment:
////////////////////////// Finishing component and advancing next component ////////////////////////////////
/**
* Submit experiment data to JATOS
*/
submitDataToJATOS() {
try {
debugLog("EffortDiscounting", "Submitting data to JATOS");
// Check if data has already been submitted
if (this.dataAlreadySubmitted || this.checkLocalStorageFlag('data_submitted')) {
debugLog("EffortDiscounting", "Data already submitted, skipping duplicate submission");
return;
}
// Get all experiment data as an array of JSON objects, like honey-harvester
const rawData = this.jsPsych.data.get().ignore(['internal_node_id']);
// Convert to JSON string to match honey-harvester format
const jsonString = rawData.json();
debugLog("EffortDiscounting", "JSON data type:", typeof jsonString);
debugLog("EffortDiscounting", "JSON data sample (first 100 chars):", jsonString.substring(0, 100));
// Get experiment sequence using the shared module
const experimentSequence = getExperimentSequence();
debugLog("EffortDiscounting", `Using shared module: This is experiment #${experimentSequence} in sequence`);
// Get indifference points for each effort/time combination
const indifferencePoints = {};
if (this.searchAlgorithms) {
Object.entries(this.searchAlgorithms).forEach(([key, algo]) => {
if (algo && typeof algo.getIndifferencePoint === 'function') {
indifferencePoints[key] = algo.getIndifferencePoint();
}
});
}
// Extract Levenshtein distance and typing accuracy from the experiment data
let levenshteinDistance = null;
let typingAccuracy = null;
let expectedLength = null;
try {
// Look for typing task data in the jsPsych data
const allTrials = this.jsPsych.data.get().values();
for (let i = allTrials.length - 1; i >= 0; i--) {
const trial = allTrials[i];
if (typeof trial.levenshtein_distance !== 'undefined') {
levenshteinDistance = trial.levenshtein_distance;
typingAccuracy = trial.typing_accuracy;
expectedLength = trial.expected_length;
debugLog("EffortDiscounting", `Found typing metrics - Levenshtein: ${levenshteinDistance}, Accuracy: ${typingAccuracy}%, Expected length: ${expectedLength}`);
break;
}
}
// Also check global backup variables
if (levenshteinDistance === null && typeof window.last_levenshtein_distance !== 'undefined') {
levenshteinDistance = window.last_levenshtein_distance;
typingAccuracy = window.last_typing_accuracy;
debugLog("EffortDiscounting", `Found typing metrics from global backup - Levenshtein: ${levenshteinDistance}, Accuracy: ${typingAccuracy}%`);
}
} catch (e) {
debugLog("EffortDiscounting", "Error extracting typing metrics:", e);
}
// Log the extracted typing metrics for verification
if (levenshteinDistance !== null) {
debugLog("EffortDiscounting", `✅ Extracted typing metrics for top-level storage:`);
debugLog("EffortDiscounting", ` Levenshtein Distance: ${levenshteinDistance}`);
debugLog("EffortDiscounting", ` Typing Accuracy: ${typingAccuracy}%`);
debugLog("EffortDiscounting", ` Expected Text Length: ${expectedLength}`);
} else {
debugLog("EffortDiscounting", "⚠️ No typing metrics found - participant may not have completed typing task");
}
// Calculate completion time and duration
const completionTime = new Date();
const completionTimeIso = completionTime.toISOString();
const durationMs = completionTime - this.startTime;
// Calculate response time summary if available
const responseTimes = this.jsPsych.data.get().select('rt').values.filter(rt => rt !== null && rt !== undefined);
let responseTimeSummary = null;
if (responseTimes.length > 0) {
responseTimeSummary = {
min: Math.min(...responseTimes),
max: Math.max(...responseTimes),
mean: responseTimes.reduce((a, b) => a + b, 0) / responseTimes.length,
median: responseTimes.sort((a, b) => a - b)[Math.floor(responseTimes.length / 2)],
count: responseTimes.length
};
}
// Format the data package to match honey-harvester format
const dataPackage = {
type: "effort_discounting_data",
task_name: "effort_discounting",
data_format_version: "1.0.0",
experiment_data: jsonString, // Store JSON array as string like in honey-harvester
indifference_points: indifferencePoints,
// Add typing performance metrics as separate fields
levenshtein_distance: levenshteinDistance,
typing_accuracy: typingAccuracy,
expected_text_length: expectedLength,
metadata: {
// Component-specific metadata only
component_timestamp: Date.now(),
experiment_type: "effort_discounting_experiment",
component_position: jatos.componentPos || "unknown",
effort_levels: this.config.effortLevels,
time_points: this.config.timePoints,
effort_levels_randomized: this.config.randomizeEffortLevels,
effort_levels_order: this.randomizedEffortLevels || this.config.effortLevels,
time_points_randomized: this.config.randomizeTimePoints,
time_points_order_by_effort_level: this.randomizedTimePoints || {},
total_time_elapsed: this.jsPsych.getTotalTime(),
browser_info: {
user_agent: navigator.userAgent,
window_width: window.innerWidth,
window_height: window.innerHeight,
screen_width: window.screen.width,
screen_height: window.screen.height,
pixel_ratio: window.devicePixelRatio || 1
},
// Additional metadata fields
language_settings: navigator.language || navigator.userLanguage || "unknown",
time_zone: Intl.DateTimeFormat().resolvedOptions().timeZone || "unknown",
page_visibility_changes: this.visibilityChanges || 0,
fullscreen_exits: this.fullscreenExits || 0,
device_type: this.deviceType || "unknown",
input_method: this.inputMethod || "unknown",
operating_system: this.operatingSystem || { os: "unknown", version: "unknown" },
start_time_iso: this.startTimeIso,
completion_time_iso: completionTimeIso,
duration_ms: durationMs,
response_times_summary: responseTimeSummary,
// Include data format version for future compatibility
data_format_version: "1.0.0",
// Add essential identifiers
participant_id: jatos.studySessionData.participant_id || this.participantId,
study_id: jatos.studyId || (jatos.urlQueryParameters && jatos.urlQueryParameters.STUDY_ID) || null,
run_uuid: jatos.studySessionData.run_uuid,
prolific_pid: (jatos.urlQueryParameters && jatos.urlQueryParameters.PROLIFIC_PID) ||
jatos.studySessionData?.prolific_pid ||
(this.jsPsych?.data?.get ? this.jsPsych.data.get().select('prolific_pid').values[0] : null) ||
null,
session_id: (jatos.urlQueryParameters && jatos.urlQueryParameters.SESSION_ID) || jatos.studySessionData.id || null
}
};
// Mark data as submitted BEFORE attempting submission to prevent race conditions
this.dataAlreadySubmitted = true;
this.saveLocalStorageData({
data_submitted: true,
data_submission_time: Date.now(),
last_action: 'submitted_data'
});
//
if (typeof jatos !== "undefined" && !jatos.isMock) {
// Helper function to safely handle circular references in objects
function removeCircularReferences(obj, seen = new WeakSet()) {
// Handle non-objects or null
if (!obj || typeof obj !== 'object') return obj;
// If we've seen this object before, return null to break the circular reference
if (seen.has(obj)) return null;
// Add this object to our set of seen objects
seen.add(obj);
// Handle arrays
if (Array.isArray(obj)) {
return obj.map(item => removeCircularReferences(item, seen));
}
// Handle regular objects
const result = {};
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
// Skip functions and DOM elements
if (typeof obj[key] === 'function' ||
(typeof obj[key] === 'object' && obj[key] !== null && obj[key].nodeType)) {
continue;
}
result[key] = removeCircularReferences(obj[key], seen);
}
}
return result;
}
// Initialize studySessionData if it doesn't exist
if (!jatos.studySessionData) {
jatos.studySessionData = {};
}
// Initialize combined data structure if it doesn't exist
if (!jatos.studySessionData.combinedResultData) {
jatos.studySessionData.combinedResultData = {};
}
// Add effort discounting data to the combined object
jatos.studySessionData.combinedResultData.effort_discounting_data = dataPackage;
// Update metadata
if (!jatos.studySessionData.combinedResultData.metadata) {
jatos.studySessionData.combinedResultData.metadata = {};
}
jatos.studySessionData.combinedResultData.metadata.last_update = Date.now();
jatos.studySessionData.combinedResultData.metadata.last_component = "effort_discounting";
// Create a sanitized copy of studySessionData
const sanitizedStudySessionData = removeCircularReferences(jatos.studySessionData);
// Save the updated sanitized studySessionData
jatos.setStudySessionData(sanitizedStudySessionData);
debugLog("EffortDiscounting", "Updated studySessionData with effort discounting data");
// Create a deep copy without circular references for submission
// Start with a fresh object rather than copying jatos.studySessionData
let effort_discounting_submission = {
effort_discounting_data: dataPackage,
metadata: removeCircularReferences(jatos.studySessionData.combinedResultData.metadata)
};
// Add nasa_tlx if it exists in combinedResultData
if (jatos.studySessionData.combinedResultData.nasa_tlx) {
effort_discounting_submission.nasa_tlx =
removeCircularReferences(jatos.studySessionData.combinedResultData.nasa_tlx);
debugLog("EffortDiscounting", "Including NASA TLX data from combined data structure");
} else if (window.nasaTLXData && window.nasaTLXData.effort_discounting) {
// Include NASA TLX data from window object if available
const nasaTLXData = {
type: "nasa_tlx_data",
task_name: "effort_discounting",
data_format_version: "1.0.0",
scores: window.nasaTLXData.effort_discounting,
overall: window.nasaTLXData.effort_discounting.overall_score,
timestamp: window.nasaTLXData.effort_discounting.timestamp,
participant_id: this.participantId
};
effort_discounting_submission.nasa_tlx = nasaTLXData;
debugLog("EffortDiscounting", "Including NASA TLX data from window.nasaTLXData");
}
// Add self_report_scales if they exist
if (jatos.studySessionData.combinedResultData.self_report_scales) {
effort_discounting_submission.self_report_scales =
removeCircularReferences(jatos.studySessionData.combinedResultData.self_report_scales);
}
// Log what data we're keeping
debugLog("EffortDiscounting", "Data being submitted: " +
Object.keys(effort_discounting_submission).filter(key => key !== "metadata").join(", "));
// Create a clean copy of the submission package
const jsonData = JSON.stringify(effort_discounting_submission);
debugLog("EffortDiscounting", "Submitting data to JATOS:", (jsonData.length / 1024).toFixed(2) + "KB");
// Submit via JATOS API
const self = this; // Store reference to 'this' for use in callback
jatos.submitResultData(jsonData)
.then(function() {
debugLog("EffortDiscounting", "Data successfully submitted to JATOS");
// Mark data as submitted to prevent duplicate submissions
self.dataAlreadySubmitted = true;
})
.catch(function(err) {
debugLog("EffortDiscounting", "Error submitting data to JATOS:", err);
displayErrorMessage();
});
} else {
// If not in JATOS, log data to console
debugLog("EffortDiscounting", "Not in JATOS, logging data to console");
console.log("Experiment Data:", dataPackage);
}
} catch (e) {
debugLog("EffortDiscounting", "Error in submitDataToJATOS:", e);
// Display error function for critical failures
function displayErrorMessage() {
const errorDiv = document.createElement('div');
errorDiv.style = `
position: fixed;
top: 10px;
left: 50%;
transform: translateX(-50%);
background-color: #ffcccc;
border: 1px solid red;
padding: 10px;
border-radius: 5px;
z-index: 9999;
`;
errorDiv.innerHTML = `
<p><strong>Critical Error</strong></p>
<p>There was an error processing your data.</p>
<p>Error details: ${e.message}</p>
<p>Please contact the researcher for assistance.</p>
`;
document.body.appendChild(errorDiv);
}
displayErrorMessage();
}
}
Thanks again for your help!
Best,
Jesse
Hi Elisa,
Thanks again for responding to my question, and for pointing me in the right direction.
I figured out the problem—it was a lock problem. Because there were multiple data submission points in the component (the main data and then a short scale) the component was erroneously marked as complete, which broke the study.
I fixed it by deleting the duplication of data submission to JATOS within the task code, so it only submits data on the component finish page.
Cheers,
Jesse
Excellent. Glad you figured it out!