In this tutorial, you'll learn best practices and key considerations for managing your Articulate Storyline xAPI implementation project. This will ensure that your xAPI and Storyline projects are scalable, organized, and easy to work with.
Before continuing, ensure that you’ve completed the 3-part Getting Started with xAPI Tutorial Series. We will build upon what you’ve learned in those tutorials, so it’s important that you’re familiar with the basics.
For this tutorial specifically, I also recommend that you complete all of the other intermediate xAPI tutorials. You will gain a deeper understanding of why we make the decisions we do in this tutorial if you've taken the time to go through the others.
If you don't have a grasp on the earlier concepts, then you will get confused rather quickly in the second half of this tutorial.
If you've followed along with all of the intermediate tutorials, then your code will look something like this:
{% c-block language="js" %}
var courseSeconds = 0;
var slideSeconds = 0;
var isCourseTimerActive = false;
var isSlideTimerActive = false;
window.setInterval( () => {
if (isCourseTimerActive === true) {
courseSeconds += 1
}
if (isSlideTimerActive === true) {
slideSeconds += 1
}
}, 1000);
const manageTimer = {
"course": {
"start": () => {isCourseTimerActive = true},
"stop": () => {isCourseTimerActive = false},
"reset": () => {courseSeconds = 0}
},
"slide": {
"start": () => {isSlideTimerActive = true},
"stop": () => {isSlideTimerActive = false},
"reset": () => {slideSeconds = false}
}
}
function sendStatement(verb, verbId, object, objectId, objectDescription, activityType, openTextVar, timer) {
const player = GetPlayer();
const uNamejs = player.GetVar("uName");
const uEmailjs = player.GetVar("uEmail");
const userResponse = player.GetVar(openTextVar);
const userScorejs = player.GetVar("userScore");
const maxScorejs = player.GetVar("maxScore");
const scaledScore = userScorejs / maxScorejs;
const userDidPass = scaledScore >= 0.8 ? true : false;
const conf = {
"endpoint": "https://trial-lrs.yetanalytics.io/xapi/",
"auth": "Basic " + toBase64("1212:3434")
};
ADL.XAPIWrapper.changeConfig(conf);
let finalDuration;
if (timer == "course") {
finalDuration = convertToIso(courseSeconds);
} else if (timer == "slide") {
finalDuration = convertToIso(slideSeconds);
} else {
finalDuration = null;
}
const statement = {
"actor": {
"name": uNamejs,
"mbox": "mailto:" + uEmailjs
},
"verb": {
"id": verbId,
"display": { "en-US": verb }
},
"object": {
"id": objectId,
"definition": {
"name": { "en-US": object },
"description": { "en-US": objectDescription },
"type": activityType
},
"objectType": "Activity"
},
"result": {
"response": userResponse,
"score": {
"min": 0,
"max": maxScorejs,
"raw": userScorejs,
"scaled": scaledScore
},
"success": userDidPass
}
};
const result = ADL.XAPIWrapper.sendStatement(statement);
function convertToIso(secondsVar) {
let seconds = secondsVar;
if (seconds > 60) {
if (seconds > 3600) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
seconds = (seconds % 3600) % 60;
return `PT${hours}H${minutes}M${seconds}S`;
} else {
const minutes = Math.floor(seconds / 60);
seconds %= 60;
return `PT${minutes}M${seconds}S`;
}
} else {
return `PT${seconds}S`;
}
}
};
{% c-block-end %}
As you can see, this is a bit unwieldy. Our sendStatement() function takes 8 arguments...keeping these in order is a daunting task even for seasoned developers.
That's why, in this tutorial, I'll walk you through exactly how to clean up this code and make the functions that you use to send your statement easier to work with. I'll also share xAPI implementation project best practices along the way.
Feel free to ask for help in the eLearning Development space in the ID community (free for mailing list subscribers).
We'll start things off light and stay away from the code for now. The first thing we need to keep track of during the xAPI implementation project is our verb list.
The verbs that we use have significant consequences on our ability to work with the data down the line. Not to get ahead of ourselves, but with the current xAPI specification, we can only query statements based on specific elements...and the verb ID is one of those elements.
For example, Storyline's current out-of-the-box xAPI reporting sends an "experienced" statement every time the user visits a slide. Let's say you have a slide titled "Intro Slide." The default xAPI statement that Storyline generates would look something like this:
Devlin experienced Intro Slide.
Likewise, let's say that you have several optional tooltips throughout your course. When the user views one of these tooltips, the slide gets lightboxed and it sends another "experienced" statement to the LRS. The xAPI statements would look like this:
Devlin experienced Resource 1.
Devlin experienced Resource 2.
Devlin experienced Resource 3.
Now let's imagine that we want to use our xAPI data to answer a question: How many times were the optional resources accessed while users completed the course?
One way to do this would be to query the LRS by Object ID...we would look at who accessed Resource 1, who accessed Resource 2, and who accessed Resource 3. But what if there are 10 optional resources? Or 20?
Querying by the Verb ID would not work here because the "experienced" verb is used for every slide. However, what if we used the "viewed" verb every time the user viewed an optional resource? Then our statement stream would look something like this:
Devlin experienced Intro Slide.
Devlin viewed Resource 1.
Devlin viewed Resource 2.
Devlin viewed Resource 3.
Now, if we want to query the statements where the users are viewing optional resources, we can query every statement that used the "viewed" verb. This returns the exact results that we are looking for.
Therefore, try to be conscious with your verb choice. Verbs hold a great deal of meaning for your xAPI statement, so aim for specificity and consistency. Don't use the same verb to mean different things, and don't use different verbs to mean the same thing.
The xAPI Vocab Server is a great place to start for selecting your verbs and verb IDs, but don't feel constrained by this list if it does not suit your organization's needs.
To ensure consistency, you need to make your list of verbs and their use cases available to other eLearning developers, programmers, and analysts who will work with xAPI at your organization.
I recommend doing this with a simple Google Sheet, Google Doc, or other internal document that you can easily share with your team and coworkers. You want to state the Verb, the Verb ID, and guidelines for when to use the said verb.
I also recommend collecting input from your team to ensure that you are not overlooking anything while making these important decisions.
As you likely know, xAPI is meant to be interoperable across different platforms, learning record stores, and activity providers. xAPI profiles help make this possible by standardizing the concepts, statement templates, and patterns used to create your xAPI statements.
For example, if your LMS sends xAPI statements with a verb that means one thing and your eLearning course uses that same verb to mean something different, then we will likely run into issues when it comes time to analyze the data.
That's why xAPI profiles are built into the specification; they serve as blueprints for how to craft your xAPI statements in order to ensure consistency within a specific organization or community of practice. You can view the current list of public xAPI profiles here.
While creating an xAPI profile is outside the scope of this tutorial, it's important that you know what they are. xAPI profiles are formal extensions of the verb list that we've been discussing above, and they are designed to make data as interoperable and analysis-friendly as possible.
Now it's time to dive into the code. We need to break our sendStatement() function into smaller functions so that when we execute our function in Storyline we don't need to include so many parameters.
For example, we don't need to tell the function which user response variable to include if we aren't including the user's response with our xAPI statement. We may just want to say that they viewed a learning resource.
I've found that the best way to address this is by creating a different function for each verb that we use with our xAPI statements.
For example, imagine that when the user responds to an open text question, we use the "responded" verb, and when they view a resource, we use the "viewed" verb.
We can then create functions based off of these verbs: sendResponded() and sendViewed(), respectively. This lets us remove the verb and verbId parameters since we will be able to bake them directly into the function. It also lets us tailor the parameters that we need to include for each function, like so:
{% c-block language="js" %}
function sendViewed(object, objectId, objectDescription, activityType) {
actor: {
/* Actor code goes here */
},
"verb": {
"id": "http://id.tincanapi.com/verb/viewed",
"display": { "en-US": "viewed" }
},
"object": {
/* Object code goes here */
}
}
function sendResponded(object, objectId, objectDescription, openTextVar) {
const player = GetPlayer();
const userResponse = player.GetVar(openTextVar);
actor: {
/* Actor code goes here */
},
"verb": {
"id": "http://adlnet.gov/expapi/verbs/responded",
"display": { "en-US": "responded" }
},
"object": {
"id": objectId,
"definition": {
"name": { "en-US": object },
"description": { "en-US": objectDescription },
"type": "http://activitystrea.ms/schema/1.0/question"
},
"objectType": "Activity"
},
"result": {
"response": userResponse,
"score": {
"min": 0,
"max": maxScorejs,
"raw": userScorejs,
"scaled": scaledScore
},
"success": userDidPass
}
}
}
{% c-block-end %}
As you can see, we reduced the number of parameters in each of these functions from eight to four. That's the benefit of using this approach. It's possible because, when we know which verb we are using, we can fill in pieces of the statement that we know will not change.
In the code above, we not only filled in the verb and verb ID for each statement, but we also knew not to include the openTextVar parameter for the sendViewed() function. Finally, we filled in the "type" property in the sendResponded() function since we know that the user will be responding to a question.
The exact functions that you use will depend on your verb list, and performing this operation will require that you have an understanding of how your code works (namely, how parameters work and whether or not the parameters will change based on how you're using your function).
Once this operation is complete, you can use the smaller functions to send xAPI statements from your Storyline course. For example, to send a statement that the user responded to a question, your code would look something like this:
{% c-block language="js" %}
sendResponded("xAPI Question 1", "https://devlinpeck.com/question/xapi-question-1", "First xAPI Question in Sample Course", "xapiQuestion1")
{% c-block-end %}
I also recommend that you add when to use each of the new functions to the verb spreadsheet that I suggested you create earlier. This will make it much easier for other eLearning developers to work with your project and send xAPI statements as needed.
Since we will break up our sendStatement function into multiple smaller functions, our code will become redundant if we are setting the same variables over and over within each function. One of these variables is the "conf" object, which we can set outside of any function, like so:
{% c-block language="js" %}
const conf = {
"endpoint": "https://trial-lrs.yetanalytics.io/xapi/",
"auth": "Basic " + toBase64("1212:3434")
};
ADL.XAPIWrapper.changeConfig(conf);
function sendViewed(object, objectId, objectDescription, activityType) {
/* Function code goes here */
}
function sendResponded(object, objectId, objectDescription, openTextVar) {
/* Function code goes here */
}
{% c-block-end %}
Since we will use the "const player = GetPlayer();" code in many of the functions as well, we can move that variable to the global scope, too.
If you remember from the Measuring Duration tutorial, you can set a global variable using the "var" keyword. This will let us use that variable in each of our functions.
{% c-block language="js" %}
const conf = {
"endpoint": "https://trial-lrs.yetanalytics.io/xapi/",
"auth": "Basic " + toBase64("1212:3434")
};
ADL.XAPIWrapper.changeConfig(conf);
var player = GetPlayer();
function sendViewed(object, objectId, objectDescription, activityType) {
/* Function code goes here */
}
function sendResponded(object, objectId, objectDescription, openTextVar) {
/* Function code goes here */
}
{% c-block-end %}
As you can see, this tutorial has been quite technical. Since your code at this stage will vary based on your specific project's verbs and use case, I will not include any final code samples here.
Feel free to contact me if you have any questions or if you'd like to engage an xAPI professional to help you manage your implementation project.
Finally, it's a great idea to document your xAPI code so that it's easier for other developers and programmers to work with it.
As you may have noticed from my previous tutorials, there are two ways to add a comment to JavaScript code:
Your comments should explain what the code does. This not only makes it easier for other developers to work with your code, but it also helps you remember what to do whenever you need to make changes.
To show you how commenting can be useful, I will comment up the original code that we had at the start of this tutorial (with minor modifications...namely, moving the global variables to the global scope). Note that I use the multi-line commenting syntax in these tutorials. This is to maintain correct code highlighting functionality (due to a limitation on my website).
{% c-block language="js" %}
/* Set conf object so that we can communicate with LRS */
const conf = {
"endpoint": "https://trial-lrs.yetanalytics.io/xapi/",
"auth": "Basic " + toBase64("1212:3434")
};
ADL.XAPIWrapper.changeConfig(conf);
/* Set both of the timer variables equal to 0 */
var courseSeconds = 0;
var slideSeconds = 0;
/* Initialize both of the timer controls */
var isCourseTimerActive = false;
var isSlideTimerActive = false;
/* Create an interval that increments the timers whenever they're set to true */
window.setInterval( () => {
if (isCourseTimerActive === true) {
courseSeconds += 1
}
if (isSlideTimerActive === true) {
slideSeconds += 1
}
}, 1000);
/* Object to allow easy timer management */
const manageTimer = {
"course": {
"start": () => {isCourseTimerActive = true},
"stop": () => {isCourseTimerActive = false},
"reset": () => {courseSeconds = 0}
},
"slide": {
"start": () => {isSlideTimerActive = true},
"stop": () => {isSlideTimerActive = false},
"reset": () => {slideSeconds = false}
}
}
/* Global variables that can be used in any function */
var player = GetPlayer();
var uNamejs = player.GetVar("uName");
var uEmailjs = player.GetVar("uEmail");
/* Function to send the statement to the LRS */
function sendStatement(verb, verbId, object, objectId, objectDescription, activityType, openTextVar, timer) {
/* Set the variables that can be used only in this function */
const userResponse = player.GetVar(openTextVar);
const userScorejs = player.GetVar("userScore");
const maxScorejs = player.GetVar("maxScore");
const scaledScore = userScorejs / maxScorejs;
const userDidPass = scaledScore >= 0.8 ? true : false;
/* Initialize finalDuration variable */
let finalDuration;
/* Report course time, slide time, or no time based on parameter */
if (timer == "course") {
finalDuration = convertToIso(courseSeconds);
} else if (timer == "slide") {
finalDuration = convertToIso(slideSeconds);
} else {
finalDuration = null;
}
/* Define the xAPI statement to send */
const statement = {
"actor": {
"name": uNamejs,
"mbox": "mailto:" + uEmailjs
},
"verb": {
"id": verbId,
"display": { "en-US": verb }
},
"object": {
"id": objectId,
"definition": {
"name": { "en-US": object },
"description": { "en-US": objectDescription },
"type": activityType
},
"objectType": "Activity"
},
"result": {
"response": userResponse,
"score": {
"min": 0,
"max": maxScorejs,
"raw": userScorejs,
"scaled": scaledScore
},
"success": userDidPass
}
};
/* Send the xAPI statement */
const result = ADL.XAPIWrapper.sendStatement(statement);
/* Function to convert the seconds into ISO 8601 format */
function convertToIso(secondsVar) {
let seconds = secondsVar;
if (seconds > 60) {
if (seconds > 3600) {
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
seconds = (seconds % 3600) % 60;
return `PT${hours}H${minutes}M${seconds}S`;
} else {
const minutes = Math.floor(seconds / 60);
seconds %= 60;
return `PT${minutes}M${seconds}S`;
}
} else {
return `PT${seconds}S`;
}
}
};
{% c-block-end %}
If you followed along with this tutorial, then congratulations! Implementing these changes requires a decent understanding of JavaScript and xAPI, and these principles will help you manage small to medium-sized xAPI implementation projects.
Likewise, if you had trouble following, then do not worry. This tutorial requires an understanding of many JavaScript and xAPI concepts, and I suggest that you return to the other intermediate tutorials before giving this tutorial another try.