xAPI in Glasshouse using Redux

Zac Petterd
Sprout Labs: Engineering
3 min readMar 20, 2017

As you may know we have reworked our xAPI reporting to provide more relevant and clearer data in every statement. Part of this has come from our continued use of Redux for the management of our frontend state. We choose Redux over other systems as many of the interactions that a learner makes with Glasshouse can be easily modelled as an action which causes a change in state.

How is it implemented?

We use Redux sagas for a range of functionality and in this case we use them to watch all actions that occur. When an action is received it is picked up and the state of the activity is picked up and made available to the next step, through a variable stateData.

function getGenericStateData(state, action) {
const result = {};
const stateKey = getActionStateKey(action);
const activityIndex = findDataObject(state.api[stateKey], action.activityId);
result.activity = state.api[stateKey][activityIndex];
result.page = state.api.page;
result.action = action;
if (action.optionId && state.api[stateKey][activityIndex].choices) {
const choiceIndex = state.api[stateKey][activityIndex].choices.findIndex(choice => choice.id == action.optionId);
const choice = state.api.mcqs[activityIndex].choices[choiceIndex];
result.choice = choice;
result.choices = state.api[stateKey][activityIndex].choices;
}
return result;
}

The next step is to create the statement. We use a object like the below sample which maps a Redux action to a series of statement properties.

const actionToxAPIMap = {
mcqs: {
[MCQ_SELECT_CHOICE]: {
verb: xapi.verbs.answered,
objectId: '${activity.id}:${choice.id}',
objectDescription: '',
activityType: ACTIVITY_QUESTION,
activityName: '${activity.question}',
activityDescription: '',
interactionType: INTERACTION_CHOICE,
ghActivityType: 'http://getglasshouse.com/xapi/mcq',
},
},
tables: {
[TABLE_SELECT_CHOICE]: {
verb: xapi.verbs.answered,
objectId: '${activity.id}:${row.id}:${cell.id}:${choice.id}',
objectDescription: '',
activityType: ACTIVITY_QUESTION,
activityName: '${row.cells[0].content}',
activityDescription: '',
interactionType: INTERACTION_CHOICE,
ghActivityType: 'http://getglasshouse.com/xapi/table',
},
}
};

We then build the portions of the statement that are always included.

let statement = {
verb: stateData.verb || mappingData.verb, // the verb is dynamically added when useing custom xapi statement action
object: {
objectType: 'Activity',
id: `${window.location.origin}${window.location.pathname}:${template(mappingData.objectId, stateData)}`,
definition: {
name: {
'en-US': template(mappingData.activityName, stateData),
},
description: {
'en-US': (mappingData.activityDescription ? template(mappingData.activityDescription, stateData) : mappingData.activityType.description),
},
type: mappingData.activityType.type,
interactionType: mappingData.interactionType,
extensions: { [mappingData.ghActivityType]: true },
},
},
actor: {
mbox: `mailto:${window.glasshouseEmail}`,
name: window.glasshouseUserName,
objectType: 'Agent',
},
context: {
platform: 'Glasshouse',
contextActivities: { // This could be say a course refrence, ideally it would be page URL or unit and module type breakdown
grouping: [
{
id: `${window.location.origin}${window.location.pathname}`,
definition: {
name: {
'en-US': document.title,
},
},
objectType: 'Activity',
},
],
},
extensions: {
'http://getglasshouse.com/useragent': parser.getResult(),
'http://getglasshouse.com/page': `${window.location.origin}${window.location.pathname}`,
'http://getglasshouse.com/page-URI': window.location.href,
},
},
};

Things like the list of possible choices and the chosen choice are always included if they are available from the state.

if (stateData.activity) {
statement.context.extensions['http://getglasshouse.com/block'] = {
id: stateData.activity.id,
definition: {
name: {
'en-US': stateData.activity.heading,
},
},
};
statement.context.contextActivities.grouping.push({
id: `${window.location.origin}/block/${stateData.activity.id}`,
});
}
if (stateData.choice) {
statement.result = {
success: stateData.choice.correctAnswer, // true, false
response: stateData.choice.text,
completed: true, // /TODO: I'm not sure about this one, Indicates whether or not the Activity was completed.
};
}
if (stateData.choices) {
for (let i = 0; i < stateData.choices.length; i++) {
statement.object.definition.choices = statement.object.definition.choices || [];
statement.object.definition.choices.push({
id: stateData.choices[i].id,
description: {
'en-US': stateData.choices[i].text,
},
});
if (stateData.choices[i].hasOwnProperty('correctAnswer') && stateData.choices[i].correctAnswer == true) {
statement.object.definition.choices.correctResponsePattern = statement.object.definition.choices.correctResponsePattern || [];
statement.object.definition.choices.correctResponsePattern.push(stateData.choices[i].id);
}
}
}

Pulling it all together

What all this means is that we can tap on our xAPI reporting on to our state management system, which gives us flexibility around specific details being included in xAPI statements without having to duplicate common functionality. It also makes it very easy to expand our xAPI tracking to as many blocks within Glasshouse as possible.

--

--