January 2, 2021

Tutorial: Tracking Browser Focus with xAPI

Overview

In this tutorial, you’ll learn how to track when a user switches away from a course to view another tab or window—changing “focus”—and also to track when their browser returns focus to the course.  

This tutorial assumes that you are already comfortable with creating and sending xAPI statements. If you are not, I strongly recommend that you first spend time with Devlin Peck’s Getting Started with xAPI tutorials. A basic familiarity with JavaScript will also be helpful.

Bonus: All of the code is handled in JavaScript, so you will not have to tinker within Storyline or another authoring tool to get this working.

The Why: Learner Experience

Why would you want to know when a learner switches away from your course?

Please note that tracking focus changes is not an effective or robust method for detecting “cheating.”

Tracking the browser focus may, however, reveal pain points in your course that you can then address. If many users across many attempts switch away from your course at the same point, it is probably an indication of something significant that warrants further investigation.

It may indicate that a topic or skill has not be appropriately developed to meet the challenge at hand without additional resources. Or it may indicate that a particular section of the course is ideal for an added, optional resource that the learner can consult. You might even be pleased if learners are self-sufficient enough to go find their own resources to tackle a problem.

Regardless, tracking browser focus will help you to understand better how users are experiencing and navigating through your course as well as where they may be needing extra help.

Code Overview

Our code will have three main components:

  1. Some setting-up code that will declare our global variables and keep the code running in the background of the course.
  2. An xAPI statement function that can report to the LRS when the course browser window loses or gains focus.
  3. A focus-checking function that will check if the user’s browser has changed focus and then call the xAPI statement with the correct parameters.

The Setting-Up Code

We are going to detect if the user’s browser has changed focus by comparing the user’s current focus with the user’s last previous known focus.

Whether your course has the user’s focus or not is revealed by the code: document.hasFocus()

We will keep track of the user’s last previous focus with the following variable: window.lastFocusStatus

When our JavaScript file loads, we will make sure that the widow.lastFocusStatus equals document.hasFocus(). In other words, both will begin as “true” since the course has to be focused to load the JavaScript in the first place.

{% c-block language="js" %}
window.lastFocusStatus = document.hasFocus();
{% c-block-end %}

Next, we will run our function to check the focus for the first time. We have not written this function yet, but we will shortly. We’ll just put in the call to the function now.

{% c-block language="js" %}
checkFocus();
{% c-block-end %}

Finally, we will give the instruction to run checkFocus() every half second in order to quickly detect if the learner changes their browser’s focus. We do this through the setInterval method. The “500” represents milliseconds.

{% c-block language="js" %}
setInterval(checkFocus, 500);
{% c-block-end %}

All together, our setting up code should look like this:

{% c-block language="js" %}
window.lastFocusStatus = document.hasFocus();
checkFocus();
setInterval(checkFocus, 500);
{% c-block-end %}

xAPI statement and the SendFocusStatus Function

You can make the xAPI statement as fancy or lean as you’d like. I am using a lean version (below) that will only require two parameters to be passed to it from the checkFocus() function. These parameters are the verb and the verbId.

Your own xAPI statement may include global variables as well as different variables from mine along with more comprehensive information in the actor, verb, and object objects.

This function also includes the code for the xAPI wrapper and your LRS endpoint and authorization info. Explanations and instructions for using the wrapper and the wrapper code can be found in Devlin Peck’s tutorials. Your actual code may have this information elsewhere, but I am including it within this function for simplicity and clarity.

The strings with all caps are areas where you will definitely need to fill in your own relevant information.

{% c-block language="js" %}
function sendFocusStatus(verb, verbId) {
  const conf = {
    "endpoint": "YOUR LRS URL STRING HERE",
    "auth": "Basic " + toBase64("YOUR LRS KEY USERNAME AND PASSWORD HERE")
  };

  ADL.XAPIWrapper.changeConfig(conf);

  const statement = {
    "actor": {
      "name": “USER NAME”,
      "mbox": “mailto:EMAIL ADDRESS”
    },

    "verb": {
      "id": verbId,
      "display": { "en-US": verb }
    },

    "object": {
      "id": "YOUR OBJECT URL",
      "definition": {
        "name": { "en-US": "YOUR OBJECT NAME" }
      }
    }
  };
  const result = ADL.XAPIWrapper.sendStatement(statement);
}
{% c-block-end %}

Checking the Browser Focus

Now it’s time to build our checkFocus() function. There are fancier ways of writing the code than what you see below (arrow functions, ternary operators, etc.), but I have decided to use slightly less efficient but clearer JavaScript to make it easier to follow if you are not used to reading JavaScript.

We will declare the function like so:

{% c-block language="js" %}
function checkFocus() {

}
{% c-block-end %}

Next, we will insert the code that checks if the current window focus is the same as the last known window focus. If the current focus matches the previous focus, the function will simply “return”—in other words, the function will just stop without doing anything further. There is nothing to report!

But if the current and previous focus do not match, the code inside the “else” brackets (which we will add in the next step) will be executed instead.

{% c-block language="js" %}
function checkFocus() {
  if(document.hasFocus() == lastFocusStatus){
    return;
  } 
  else {
  }
}{% c-block-end %}

Within our “else” code, we will use another round of if/else code to check whether the discrepancy between hasFocus() and lastFocusStatus was caused by the user either moving focus away from our course or by returning focus to the course.

If document.hasFocus() is true, it means that the learner currently has our course focused and a half second ago did not. Likewise, ifdocument.hasFocus() is not true, it means that the learner does not currently have our course focused but did a half second ago.

{% c-block language="js" %}
function checkFocus() {
  if(document.hasFocus() == lastFocusStatus){
    return;
  } 
  else {
    if(document.hasFocus() == true) {

    }
    else {

    }
  }
}
{% c-block-end %}

Now we just need to add in calls to our sendFocusStatus function with the appropriate parameters.

Finally, before the last bracket, we add one more line of code that flips the true/false value of lastFocusStatus. Since this line of code is only executed when document.hasFocus() and lastFocusStatus do not match each other, this is an efficient way to update lastFocusStatus as the final step of our function.

{% c-block language="js" %}
function checkFocus() {
  if(document.hasFocus() == lastFocusStatus){
    return;
  } 
  else {
    if(document.hasFocus() == true) {
      sendFocusStatus("focused","http://id.tincanapi.com/verb/focused")
    }
    else {
      sendFocusStatus("unfocused","http://id.tincanapi.com/verb/unfocused")
    }
  lastFocusStatus = !lastFocusStatus;
  }
}{% c-block-end %}

The Completed Code

We’re done! Here’s all of our code put together with the functions preceding the setup code.

{% c-block language="js" %}
function sendFocusStatus(verb, verbId) {
  const conf = {
    "endpoint": "YOUR LRS URL STRING HERE",
    "auth": "Basic " + toBase64("YOUR LRS KEY USERNAME AND PASSWORD HERE")
  };

  ADL.XAPIWrapper.changeConfig(conf);

  const statement = {
    "actor": {
      "name": “USER NAME”,
      "mbox": “mailto: EMAIL ADDRESS”
    },

    "verb": {
      "id": verbId,
      "display": { "en-US": verb }
    },

    "object": {
      "id": "YOUR OBJECT URL",
      "definition": {
        "name": { "en-US": "YOUR OBJECT NAME" }
      }
    }
  };
  const result = ADL.XAPIWrapper.sendStatement(statement);
}

function checkFocus() {
  if(document.hasFocus() == lastFocusStatus){
    return;
  } 
  else {
    if(document.hasFocus() == true) {
      sendFocusStatus("focused","http://id.tincanapi.com/verb/focused")
    }
    else {
      sendFocusStatus("unfocused","http://id.tincanapi.com/verb/unfocused")
    }
  lastFocusStatus = !lastFocusStatus;}
}

window.lastFocusStatus = document.hasFocus();
checkFocus();
setInterval(checkFocus, 500);

{% c-block-end %}

Place the code above into the JavaScript file with your other xAPI code and statements and you’re ready to go! Just make sure to replace the strings in CAPS with your own information.