Forum Discussion

cwang's avatar
cwang
Community Member
11 days ago

Sending xAPI statements through Storyline blocks in Rise 360

Cross-post from the Linkedin article I just wrote. 

I hope this discussion is helpful if you want to explore sending xAPI statements from Storyline blocks in Rise 360. 

It will involve some JavaScript work and customizing xAPI statements. 

Prerequisite: LRS endpoint, key, and secret

This workaround assumes that you have access to the LRS endpoint, key, and secret. You will use this information in the JavaScript code that runs in Storyline.

Step 1: Storing the xAPI JavaScript library on the web

This workaround uses the tincan.js library from the xapi.com > JavaScript library (TinCanJS). Store the tincan.js file somewhere accessible on the web so it can be linked from within the Storyline file.

After you have the link ready, add the following JavaScript to the Storyline slide master (or to individual slides from which you want to send xAPI statements). This method works similarly to editing the header of a locally published HTML file to reference the JavaScript library. However, because Storyline blocks are published in Review 360, there’s no local HTML file to edit.

In my own example, I place this JavaScript on the slide master and run it when the timeline starts.

function loadJS(FILE_URL, async = true) {
  let scriptEle = document.createElement("script");

 // change LINK TO tincan.js with the actual url
  scriptEle.setAttribute("src", "LINK TO tincan.js");
  scriptEle.setAttribute("type", "text/javascript");
  scriptEle.setAttribute("async", async);

  document.body.appendChild(scriptEle);

  // success event 
  scriptEle.addEventListener("load", () => {
    console.log("File loaded")
  });
   // error event
  scriptEle.addEventListener("error", (ev) => {
    console.log("Error on loading file", ev);
  });
}
loadJS("file1_path", true);

Step 2: Creating custom xAPI statements for the actions you want to track

Note: You won't be able to use the default xAPI functionality in Storyline 360 with this workaround.

To send xAPI statements, you can use the following sample code and associate the code with the action such as user chooses the Skip button. The code here would send "xAPI tester skipped 'Rise passing xapi statement' with response 'This is the sample response'" to your LRS.

var lrs;

try {
    lrs = new TinCan.LRS(
        {
            endpoint: "LRS endpoint",
            username: "LRS key",
            password: "LRS secret",
            allowFail: false
        }
    );
}
catch (ex) {
    console.log("Failed to setup LRS object: ", ex);
    // TODO: do something with error, can't communicate with LRS
}

var statement = new TinCan.Statement(
    {
  actor: {
    name: "xAPI tester",
    mbox: "mailto:123@example.com"
  },
  verb: {
    id: "https://w3id.org/xapi/adl/verbs/skipped",
    display: {
      "en-US": "Skipped"
    }
  },
  object: {
    objectType: "Activity",
    id: "https://example.com/rise-course",
    definition: {
      name: {
        "en-US": "Rise passing xapi statement"
      },
      //This makes the response in the Result element display
      type: "http://adlnet.gov/expapi/activities/cmi.interaction"
    }
  },
  result: {
    response: "This is the sample response"
  }
    }
);


lrs.saveStatement(
    statement,
    {
        callback: function (err, xhr) {
            if (err !== null) {
                if (xhr !== null) {
                    console.log("Failed to save statement: " + xhr.responseText + " (" + xhr.status + ")");
                    // TODO: do something with error, didn't save statement
                    return;
                }

                console.log("Failed to save statement: " + err);
                // TODO: do something with error, didn't save statement
                return;
            }

            console.log("Statement saved");
            // TOOO: do something with success (possibly ignore)
        }
    }
);

Step 3: Swapping out any values that can be templatized as variables

If you don't want to edit the code every time you change the verb or on a different slide, you can use variables to hold these values.

The following example has two triggers in the Storyline slide:

  • Set x_verb to value skipped when the timeline starts on this slide
  • Set x_slideTitle to variable Project.SlideTitle when the timeline starts on this slide

Then, you could feed these Storyline variables into variables in your code and reuse them (for example, jsVerb and jsSlide).

/*--Get endpoint, key, and secret value ---*/

var player = GetPlayer();
var jsEndpoint = player.GetVar("endpoint");
var jsKey = player.GetVar("key");
var jsSecret = player.GetVar("secret");
/*---connecting to LRS. ------*/
var lrs;

try {
    lrs = new TinCan.LRS(
        {
            endpoint: jsEndpoint,
            username: jsKey,
            password: jsSecret,
            allowFail: false
        }
    );
}
catch (ex) {
    console.log("Failed to setup LRS object: ", ex);
    // TODO: do something with error, can't communicate with LRS
}
//------CONNECTING END/


/*--------Capture SL Variables-----------*/
var player = GetPlayer();
var jsVerb = player.GetVar("x_verb");
var jsSlide = player.GetVar("x_slideTitle");
//---END OF SL VARIABLES/

//////////////////////////////////////

/*---------------Make update here----------*/

var statement = new TinCan.Statement(
    {
  actor: {
    name: "xAPI tester",
    mbox: "mailto:example@example.com"
  },
  verb: {
    id: "https://w3id.org/xapi/adl/verbs/"+jsVerb,
    display: {
       "en-US": jsVerb
    }
  },
  object: {
    objectType: "Activity",
    id: "https://example.com/rise-course",
    definition: {
      name: {
        "en-US": jsSlide
      }
    }
  },
  result: {
    response: "This is the sample response"
  }
 }
);
//-----END OF UPDATE/


/*----DO NOT TOUCH THIS------*/
lrs.saveStatement(
    statement,
    {
        callback: function (err, xhr) {
            if (err !== null) {
                if (xhr !== null) {
                    console.log("Failed to save statement: " + xhr.responseText + " (" + xhr.status + ")");
                    // TODO: do something with error, didn't save statement
                    return;
                }

                console.log("Failed to save statement: " + err);
                // TODO: do something with error, didn't save statement
                return;
            }

            console.log("Statement saved");
            // TOOO: do something with success (possibly ignore)
        }
    }
);

I hope this helps! 

No RepliesBe the first to reply