Send usage data to Google Analytics via xAPI

Understanding how people use your content is important, and maybe even more important for interactive content. We have to understand how people interact with the content in order to know how we can improve it and what types of content we should focus on.

Together with NDLA we have looked into how H5P can report user data into Google Analytics. The problem NDLA faces is that they use Google Analytics as a tool to improve content, but via Google Analytics they don’t get much information about how their users interact with their H5P content.

H5P already reports what the user does within the H5Ps as xAPI statements. It was natural to think that these statements could be sent to Google Analytics, so we created a prototype that turns xAPI statements generated by H5P into Google Analytics events. We have the prototype installed on H5P.org, and in Google Analytics we can now for instance see that:

  • on October 12th, the multichoice questions were answered 665 times, 179 of the answers came from the UK and 170 from the US.
  • 561 of the multichoice answers on October 12th came from the multichoice questions within the October release note, and the average score from the release note was 39.93 % for these two questions combined.
  • in this Course Presentation example, people have changed slides 10 304 times and completed the presentation 1 293 timers the last month. See the image below:

Google analytics visualizes the data from H5P in a great way, and it's also able to combine the xAPI data with the data Google Analytics normally gathers so that you may gain really interesting insights.

More examples

An interesting insight we gained from the data from the October Release Note was that the Course Presentation we used to visualize how we've improved the selection of button vs poster embedding of elements in Interactive Videos had very low engagement compared to other interactivities on the page. See the Course Presentation below:

 

From this, we learned that it probably wasn't clear enough how the user could interact with this content, and we'll have to find a way to improve this the next time we do something similar.

Google Analytics can also be used to see how well users perform on different tests, and also to see if people in different regions perform differently. From the following Interactive Video, we can see that 35.85 % of Norwegians knew that Sushi actually doesn’t mean raw fish, and the average score was 28.23 %. Norway was only beaten by Japan:

We find our prototype very useful on H5P.org, and we'll probably create a module for integrating H5P with Google Analytics pretty soon.

The technical details

So how do we do this? For those of you not familiar with xAPI, xAPI in this context is pretty much statements about what a user is doing. For instance 

"User answered the test 'Can Pluto fly?'"

or

"User changed from slide 3 to slide 6 in the presentation 'Who is Pluto?'"

Statements consists of an Actor(the user), a verb ('answered' for instance) and an Object (the presentation "Who is Pluto" for instance). H5P generates statements like this all the time.

What we've done is made a separate peace of software that turn these statements into Google Analytics events. Google Analytics events have several properties, and this is how we've mapped them:

  • Category: Here we use the content type that sends the statement, e.g. H5P.MultiChoice
  • Action: Here we use the xAPI verb, e.g. "Answered"
  • Opt_label: The H5P's title
  • Opt_value: The result if a result exists or which slide a user jumped to if it was a progressed event.

For those of you who are eager to try this out on your own site here is the unpolished prototype code:

/**
 * Add scripts to h5ps
 *
 * @param array $scripts
 *  Array of objects with properties path and version. Version is on the form
 *  ?ver=1.0.2 and is used as a cache buster
 * @param array $libraries
 *  Array of libraries indexed by the library's machineName and with an array
 *  as value. The value has the properties majorVersion and minorVersion
 * @param string $mode
 *  What mode are we in? Possible values are "editor", "div", "iframe" and "external"
 */
function hook_h5p_scripts_alter(&$scripts, $libraries, $mode) {
  $scripts[] = (object) array(
    // Path relative to drupal root
    'path' => drupal_get_path('module', 'h5p_ga') . '/h5p-ga.js',
    // Cache buster
    'version' => '?ver=0.0.1',
  );
}

And the javascript:

(function () {
  // Improve performance by mapping IDs
  var subContentIdToLibraryMap = {};

  /**
   * Look through params to find library name.
   *
   * @private
   * @param {number} id
   * @param {object} params
   */
  function findSubContentLibrary(id, params) {
    for (var prop in params) {
      if (!params.hasOwnProperty(prop)) {
        continue;
      }

      if (prop === 'subContentId' && params[prop] === id) {
        return params.library; // Found it
      }
      else if (typeof params[prop] === 'object') {
        // Look in next level
        var result = findSubContentLibrary(id, params[prop]);
        if (result) {
          return result;
        }
      }
    }
  }

  if (window.H5P) {
    H5P.jQuery(window).on('ready', function () {
      H5P.externalDispatcher.on('xAPI', function (event) {
        try {
          if (!window.parent.ga) {
            return;
          }

          // First we need to find the category.
          var category;

          // Determine content IDs
          var contentId = event.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-local-content-id'];
          var subContentId = event.data.statement.object.definition.extensions['http://h5p.org/x-api/h5p-subContentId'];

          if (subContentId) {
            if (subContentIdToLibraryMap[subContentId]) {
              // Fetch from cache
              category = subContentIdToLibraryMap[subContentId];
            }
            else {
              // Find
              category = findSubContentLibrary(subContentId, JSON.parse(H5PIntegration.contents['cid-' + contentId].jsonContent));
              if (!category) {
                return; // No need to continue
                // TODO: Remember that it wasnt found?
              }

              // Remember for next time
              subContentIdToLibraryMap[subContentId] = category;
            }
          }
          else {
            // Use main content library
            category = H5PIntegration.contents['cid-' + contentId].library;
          }

          // Strip version number
          category = category.split(' ', 2)[0];

          // Next we need to determine the action.
          var action = event.getVerb();

          // Now we need to find an unique label
          var label = event.data.statement.object.definition.name['en-US']; // Title
          // Add contentID to make it eaiser to find
          label += ' (' + contentId;
          if (subContentId) {
            label += ' ' + subContentId;
          }
          label += ')';

          // Find value
          var value;

          // Use result if possible
          var result = event.data.statement.result;
          if (result) {
            // Calculate percentage
            value = result.score.raw / ((result.score.max - result.score.min) / 100);
          }

          // ... or slide number
          if (action === 'Progressed') {
            var progress = event.data.statement.object.definition.extensions['http://id.tincanapi.com/extension/ending-point'];
            if (progress) {
              value = progress;
            }
          }

          // Validate number
          value = Number(value);
          if (isNaN(value)) {
            value = undefined;
          }

          window.parent.ga('send', 'event', category, action, label, value);
        }
        catch (err) {
          // No error handling
        }
      });
    });
  }
})();

We appreciate you taking some time to share your feedback.