Strange bug with client-side action looping through a hierarchy of Things

I have a very confusing bug…

I have a data model with a three level hierarchy: Survey has one or more Respondents has one or more QuestionResponses (where each QuestionResponse connects back to a Question under the Survey). Each Thing has a collection of child Things.

I have created a client-side Action plugin with some fairly simple Javascript that tries to export that data as a CSV, with one row per Respondent - there are a set of columns coming from the Respondent thing, and then some more columns for each QuestionResponse (q1_field, q2_field etc). I have to flatten the data this way for various legacy integration reasons.

My client-side action can export a dummy CSV with no problem. The challenge comes when I try and populate the data…

The function looks like this (I’ve cut a load of redundant stuff out, this is the guts):

function action(properties, context) {
    var respondents = properties.survey.get('respondents_list_custom_respondent');
    respondents = respondents.get(0, respondents.length());
    const rows = [];
    console.log(respondents.length+' respondents');
    for (var i=0; i<respondents.length; i++) {
            console.log('Row '+i);
    	const row = {
            response_date: respondents[i].get('response_date_date'),
            // etc
    	};

        // then add each question's response
        var questionResponses = respondents[i].get('responses_list_custom_dlrespondentresponse');
        questionResponses = questionResponses.get(0, questionResponses.length());
        for (var j=0; j<questionResponses.length; j++) {
            console.log('Question '+i+'.'+j);
            const question = questionResponses[j].get('question_custom_dlsurveyquestions');
            const prefix = 'q' + question.get('order_number') + '_';
            const type = question.get('type_option_questiontype');
            if (type.get('data_value')) {
                row[prefix+'value'] = question.get('non_video_response_text');
            }
            // etc
        }
        rows.push(row);
    };

    // CSV stuff - not the problem
}

I have a sample survey with 3 questions and 2 respondents, each of whom completed all 3 questions. I’d therefore expect to get back 2 rows with the core columns plus 3 sets of question-specific columns.

In actual fact, I get less data - and when I log the loops, the entire function appears to have been run multiple times - this is consistent whether I write the loops like this or with forEach() functions. The logging comes out like this:

2 respondents
2 respondents
Row 0
Question 0.0
2 respondents
Row 0
Question 0.0
Question 0.1
2 respondents
Row 0
Question 0.0
Question 0.1
2 respondents
Row 0
Question 0.0
Question 0.1
Row 1
Question 1.0
2 respondents
Row 0
Question 0.0
Question 0.1
Row 1
Question 1.0
Question 1.1
2 respondents
Row 0
Question 0.0
Question 0.1
Row 1
Question 1.0
Question 1.1
Question 1.2
2 respondents
Row 0
Question 0.0
Question 0.1
Row 1
Question 1.0
Question 1.1
Question 1.2

The rows array comes back with the first row containing only two questions, and the second containing them all. If I comment out the inner loop, I get this:

2 respondents
2 respondents
Row 0 
2 respondents
Row 0
Row 1

It’s possible I’m just being very stupid but this seems like fairly simple logic, and I don’t think I’ve messed up the loops… or am I doing something stupid?
My guess is that Bubble is lazy loading the data, and simulating a synchronous method where in reality that lazy loading is done asynchronously, and the way it does that is broken?

This is how custom plugin actions are supposed to execute in bubble. You can find an explanation in the documentation, but the gist is that when your code tries to access data that is not loaded bubble stops the execution of your action, loads the data and execute the action again.

A first suggestion would be to follow what bubble recommends: load all the data first.
Execute your loop first without adding data to rows so you know that after the loop all the data will be ready. Then execute it again but this time save your data to rows.
See how it goes

Hmm OK thanks, yes that does make sense given how they simulate a synchronous function. Having messed with it further, I suspect it does actually work, and the problem is that there was a question response missing from the underlying database - the logging then just sent me off down a rabbit hole :frowning: Cheers!