This small tutorial will allow you to see how to store interactions with the game in an xAPI LRS. It uses the ADL xAPIWrapper to make sending statements to the LRS easier. Although this is a small tutorial it shows you how to:
- include the xAPI Wrapper in an HTML page,
- configure the xAPI Wrapper using xAPI Launch with the LRS and client credentials,
- send statements to the LRS,
- include extensions in the statement, and
- use registration and context activities to group statements, using xAPI Launch information.
Note: The code for each step is already included and commented out in the game.html. If you wish, you can just uncomment the code as you walk through the steps.
These examples use verbs defined specifically for the workshops. This is done to isolate the statements. ADL recommends looking for existing verbs and vocabularies to leverage before creating your own.
The first step is to include the xAPIWrapper in the game HTML. The xAPIWrapper is included in the lib
folder of the project for your convenience. For reference, the xAPIWrapper project is at https://github.com/adlnet/xAPIWrapper.
- Add a
<script>
tag in the<body>
of thegame.html
to include the xAPI Wrapper. (right below the game<script>
tag) And an opening and closing<script>
tag where we will add the xAPI code.
<script src="./lib/xapiwrapper.min.js"></script>
Next you have to configure the xAPI Wrapper. By default, the xAPI Wrapper is configured to communicate with an LRS at localhost. We want to have xAPI Launch tell the content what configuration to use, instead of hardcoding the LRS and authentication details. By calling ADL.launch, the xAPIWrapper will do the handshake with xAPI Launch and pass a configured object to the callback.
<script>
ADL.launch(function(err, launchdata, xAPIWrapper) {
if (!err) {
console.log("--- content launched via xAPI Launch ---\n", xAPIWrapper, "\n", launchdata);
} else {
alert("This was not initialized via xAPI Launch. Defaulting to hard-coded credentials");
console.log("--- content not launched ---\n", ADL.XAPIWrapper.lrs);
}
}, true);
</script>
The xAPIWrapper was specifically created to simplify connecting to and communicating with an LRS. This means the work of creating a statement - generating the JSON properly and setting the correct values - is up to the developer.
In this step you will
create a myXAPI
object that will contain a base statement and some helper functions to simplify sending xAPI statements.
var myXAPI = {};
The xAPI Wrapper has xAPI Launch functionality built in. By calling ADL.launch() with a callback function, we are able to get a configured xAPIWrapper and additional launch data from the launch server. xAPI Launch sends information (launch data) to the content, which the ADL.launch function sends to the callback. The launchdata.customData object contains content that can be configured in the xAPI Launch server, allowing us to enter a base URI we can use for all places that need a URI. And the xAPIWrapper parameter holds a new xAPIWrapper instance that is configured with settings from the launch server.
In the if (!err)
block of the ADL.launch
call back, set the original ADL.XAPIWrapper
to the configured one from the launch() method and save the launchdata.customData.content
value to a baseuri property on myXAPI
. (we will configure this value on the launch server)
ADL.XAPIWrapper = xAPIWrapper;
myXAPI.baseuri = launchdata.customData.content;
The else block is the case when an error occurred trying to talk to the launch server - typically this is because the content wasn't launched by the launch server. In this example we default back to hard coded values, however additional processing or error handling could occur here.
Call ADL.XAPIWrapper.changeConfig()
to change the configuration of the ADL.XAPIWrapper
to hard-coded values. Then, set the baseuri and launchdata to predetermined values.
ADL.XAPIWrapper.changeConfig({
"endpoint": "https://lrs.adlnet.gov/xapi/",
"user": "xapi-workshop",
"password": "password1234"
});
myXAPI.baseuri = "http://adlnet.gov/event/xapiworkshop/non-launch";
launchdata = {
actor: {
account:{
homePage:"http://anon.ymo.us/server",
name: "unknown-user"
},
name: "unknown"
}
};
We add functions and a base statement to the myXAPI object to report when actions in the game take place. The following step will go into the details of those functions.
At the end of the callback function, add two function calls. buildMyXAPI
takes the actor sent from the launch server and will create a base statement and the additional functions for the myAPI object. The second function will call the startGame process.
buildMyXAPI(launchdata.actor);
startGame();
We want to report 3 things to the LRS: When someone starts a game, when someone finishes a game, and when someone makes a guess. Since there are some things that need added to, or changed in the base statement, it would be nice to add methods to the myXAPI object to centralize those changes.
- Create a function called buildMyXAPI() after the end of the ADL.launch().
function buildMyXAPI(myactor) {
}
- In the
buildMyXAPI
function first create a base statement with parts of a statement that don't change much. Theactor
property is set to the value we got from the launch server. Theobject
is created with information about the game. We usemyXAPI.baseuri
that was initialized by the launch server to create the IRIs used within the content. And thecontext
property is populated withcontextActivities
that allow us to tag these statements as coming from this xAPI Workshop.
myXAPI.statement = {
actor: myactor,
object: {
id: myXAPI.baseuri + "/guess-the-number",
definition: {
name: {"en-US": "Guess the Number Game"},
description: {"en-US": "Simple guess the number game to demonstrate xAPI"},
type: "http://activitystrea.ms/schema/1.0/game"
}
},
context: {
contextActivities: {
"grouping": [
{
"id": myXAPI.baseuri + "/dev/web"
},
{
"id": myXAPI.baseuri
}
]
}
}
};
- Before we add the 3 functions, add one that will make a copy of the base statement, so when those 3 functions start changing values, it doesn't change the base statement.
myXAPI.getBase = function () {
return JSON.parse(JSON.stringify(this.statement));
};
- Next add
started
. It will accept thestarttime
so that the statement and the game stats are in sync. It will set the verb -myxAPI.baseuri + "/verb/started"
- to the statement, along with the start time. It also generates a GUID for the new attempt. We can then save that in the context registration value, allowing us to link all of statements for this attempt.
myXAPI.started = function (starttime) {
this.attemptGUID = ADL.ruuid();
var stmt = this.getBase();
stmt.verb = {
id: myXAPI.baseuri + "/verb/started",
display: {"en-US": "started"}
};
stmt.timestamp = starttime.toISOString();
stmt.context.registration = this.attemptGUID;
ADL.XAPIWrapper.sendStatement(stmt, function (resp) {
console.log(resp.status + " - statement id: " + resp.response);
});
};
- Now add
ended
. This will accept the stats object the game has maintained. Since the values of the stats object don't really fit in any property of a statement, we will use the resultextensions
property to store some of the stats.
myXAPI.ended = function (stats) {
var stmt = this.getBase();
stmt.verb = {
id: myXAPI.baseuri + "/verb/ended",
display: {"en-US": "ended"}
};
stmt.timestamp = stats.endedAt.toISOString();
stmt.context.registration = this.attemptGUID;
stmt.result = { extensions: {} };
stmt.result.extensions[myXAPI.baseuri + "/guess-the-number/ext/min"] = stats.min;
stmt.result.extensions[myXAPI.baseuri + "/guess-the-number/ext/max"] = stats.max;
stmt.result.extensions[myXAPI.baseuri + "/guess-the-number/ext/guesses"] = stats.guesses;
stmt.result.extensions[myXAPI.baseuri + "/guess-the-number/ext/number"] = stats.number;
stmt.result.extensions[myXAPI.baseuri + "/guess-the-number/ext/startedAt"] = stats.startedAt.toISOString();
stmt.result.extensions[myXAPI.baseuri + "/guess-the-number/ext/endedAt"] = stats.endedAt.toISOString();
ADL.XAPIWrapper.sendStatement(stmt, function (resp) {
console.log(resp.status + " - statement id: " + resp.response);
});
};
- Finally add a
guessed
function. This accepts the number guessed and sends a statement to the LRS. Since this statement would say something like "player guessed a number" and not "player guessed guess the number game", we need to change the the object of the statement, along with adding the number to the resultresponse
and setting the verb tomyxAPI.baseuri + "/verb/guessed"
.
myXAPI.guessed = function (num) {
var stmt = this.getBase();
stmt.verb = {
id: myXAPI.baseuri + "/verb/guessed",
display: {"en-US": "guessed"}
};
stmt.object = {
id: myXAPI.baseuri + "/number",
definition: {
name: {"en-US": "a number"},
description: {"en-US": "Represents a number guessed in the guess a number game"},
type: myXAPI.baseuri + "/activity/type/number"
}
};
stmt.timestamp = (new Date()).toISOString();
stmt.context.registration = this.attemptGUID;
stmt.result = { response: num.toString() };
ADL.XAPIWrapper.sendStatement(stmt, function (resp) {
console.log(resp.status + " - statement id: " + resp.response);
});
};
Now that everything is set up, it's time to call those helper functions during the game.
- Call
started
at the end of thestartGame
function ingame.html
.
myXAPI.started(thegame.stats.startedAt);
- Call
ended
in thehandleResult
function at the end beforealert('You won');
when the result is 0.
myXAPI.ended(thegame.stats);
- Call
guessed
in the try/catch in the form submit event aftervar res = thegame.evalGuess(num)
.
myXAPI.guessed(num);
The last line of the game script is startGame();
. This is no longer necessary becuase we call it in the launch script now. Remove it so we don't call startGame before we build the myXAPI object.
Launch doesn't require the game to be uploaded. This step is done as a convenience so we don't have to host our game on another server.
- Copy
cmi5.xml
fromwebcontent/final/packaged/
towebcontent/
. xAPI Launch has limited support of cmi5's package specification to allow us to package up our game and import on the server. The xml file is already set up, no edits are needed. - Zip cmi5.xml, game.html, and lib/. Make sure not to zip the containing folder (webcontent), just the files and lib/ folder.
- On the xAPI Launch server, login and under the Apps drop down select Upload App. Choose your zip and upload.
Before launching our game, we need to configure the launch settings to include our base URI so it can be passed to the game during the launch process.
- Select the '...' button beside 'Launch' and choose 'Edit'.
- Add
http://adlnet.gov/event/xapiworkshop/<<name>>
in the 'Custom Data' field. - Change 'Launch Type' to 'Popup'.
Launch the game! The game should report your attempts to the ADL LRS view here.
If you have extra time and would like to try out more ...
The xAPI Wrapper can also get statements. There is a session about reporting later in the day but if you have time you can try to get statements now.
- Look at the get statements section of the xAPI Wrapper, specifically getting statements based on search parameters.
- Try to filter the statements based on actor and activity id (see xAPI Get Statements for the filter parameters)
- Display the results on the game page