You are currently browsing the tag archive for the ‘statistics’ tag.

Where did the last month go!? As explained in an earlier post Moodle URLs don’t make for very good tracking using traditional server logs or Google Analytics (GA). We have implemented a fix using pseudo-URLs for GA and here I’ll show the basics of code we’ve used (minus some institution-specific tweaks).

We’re aiming to create a pseudo-URL similar to:
{school}.{role}.{course}.{activity-action}.{id}

All the code is in a section in the head part of our theme’s header.html (Moodle 1.9x) or layout/general.php etc (Moodle 2.x) plus any other layout pages that you want to track. Actually, the code is stored in an include and just included where needed.

To set the scene, Moodle authenticates against Active Directory and gets some data from this into the global $USER on login. So we need this:

  // get user department code
  if (isset($USER->department)) {
    $userDept =   trim($USER->department);
  } else { 
    $userDept = ''; 
  }

Next, we have a bit of code that sorts these department codes into school codes ($userSchool). I won’t show these but they result in values like CASS, SOI, SEMS etc. You could just as easily set up a variable here that gets the category code, like this:

  // get category ID
  $category = $COURSE->category;

Getting the user’s role in context is a bit trickier, we’re really only interested in the highest level role at course context level for our purposes but I suppose you could adapt to get a role for the activity context:

  // get role of user for the course context (level=50)   
  $gaUserID = $USER->id;
  $gaRole = 'unknownrole'; //default
  if ($gaUserID > 0) { 
    // get contextID for this course first
    $gaQry = get_record('context','instanceid',$COURSE->id,'contextlevel','50');
    $gaContextID = $gaQry->id; 
    // then count records to see if user has an entry in role_assignments table
    $gaQryCnt = count_records('role_assignments','contextid',$gaContextID,'userid',$USER->id);
    // note a user may have multiple roles in db so get highest one  
    if ($gaQryCnt > 0) { // ok, there are entries so get role
      $gaQry = get_records_sql("SELECT * FROM m_role_assignments WHERE contextid='" . $gaContextID . "' AND userid='" . $USER->id . "' ORDER BY roleid ASC",0,1);
      $gaQryObj = $gaQry[key($gaQry)]; // get key for 1st element in associative array
      $gaQry = get_record('role','id',$gaQryObj->roleid);  // get role name
      $gaRole = $gaQry->shortname;
    } else { // no specific course role, then what are they then?
      if ($gaUserID == 1) $gaRole = 'guest';  // assuming standard Moodle setup!
      if ($gaUserID == 2) $gaRole = 'superadmin';  // assuming standard Moodle setup!
      // is user a Moodle admin?
      $gaQryCnt2 = count_records('role_assignments','contextid',1,'userid',$USER->id);
      if ($gaQryCnt2 > 0) $gaRole = 'superadmin';
    }
  } else {
    $gaRole = 'notloggedin';
  }

Now, to get the module shortname … this bit’s easy:

  // get course shortname, could also use id, fullname, 
  // idnumber although latter not always given
  $gaCourse =  $COURSE->shortname;

Next we need the activity type and the action being undertaken, this could be split into several variables if required but we are happy with values like: mod-resource-view.

Getting useful info here required a lot of digging into how various activities constructed URLs, there is a lot of variation and custom request variables which are not all covered here (e.g. Turnitin):

  // work out what major task is being done by looking at raw URL
  $gaPagePath = $CFG->pagepath;
  $gaTask = $gaPagePath;
  $gaURLParts = explode('/',$gaPagePath); 
  $gaURLPartsCount = count($gaURLParts); 
  // we're interested in array elements 3 and above, the bits after Moodle root URL
  // so if URL is http://moodle.college.ac.uk/course/view.php?id=102
  // element 3 = 'course', 4 = 'view.php'   (except for some admin pages, see below)
  // following bit checks down the path and grabs relevent keywords to build task
  if ($gaURLPartsCount > 3) {
    $gaTask = $gaURLParts[3];
    if ($gaTask == '') $gaTask = 'home';
  }
  if ($gaURLPartsCount > 4) { 
    if ($gaURLParts[4] != '') {
      if (strpos($gaURLParts[4],'.') > 0) {
        $gaTask = $gaTask . '-' . substr($gaURLParts[4],0,strpos($gaURLParts[4],'.'));
      } else {
      $gaTask = $gaTask . '-' . $gaURLParts[4];
      }
    }
  }
  if ($gaURLPartsCount > 5) {
    if ($gaURLParts[5] != '') {
      if (strpos($gaURLParts[5],'.') > 0) {
        $gaTask = $gaTask . '-' . substr($gaURLParts[5],0,strpos($gaURLParts[5],'.'));
      } else {
        $gaTask = $gaTask . '-' . $gaURLParts[5];
      }
    }
  }
  
  // fix for moodle bug where admin urls are not getting reported correctly in $CFG->pagepath
  // http://tracker.moodle.org/browse/MDL-20342
  $gaTask = str_replace('/','-',$gaTask);

  // fix for help
  if ($gaTask == 'help.php') $gaTask = 'help'; 
                                    
  // if there is a current action specified then append that to task but only for non admin stuff
  if ($gaURLParts[0] != 'admin') {
    if (isset($_REQUEST['currentaction'])) $gaTask = $gaTask . '-' . $_REQUEST['currentaction'];
    // if there is a section specified then append that to task
    if (isset($_REQUEST['section'])) $gaTask = $gaTask . '-' . $_REQUEST['section'];
    // if there is an action specified then append that to task
    if (isset($_REQUEST['action'])) $gaTask = $gaTask . '-' . $_REQUEST['action'];
    if ( (isset($_REQUEST['frameset'])) && (isset($_REQUEST['page'])) ) $gaTask = $gaTask . "-ims-p" . $_REQUEST['page'];
  }

Now, the last part of the URL, the task or page ID:

  // now get task or page ID however best, this list not exhaustive
  $gaTaskID = '';
  // get id number if page has id=xx argument
  if (isset($_REQUEST['id'])) $gaTaskID = $_REQUEST['id'];
        
  // if $gaTaskID still 0 try looking for mod id in URL
  // e.g. mod/forum/post.php?forum=7 or admin/roles/assign.php?contextid=27
  // note that these URLS work best for course content related stuff not user or admin stuff
  // this list is not exhaustive, just the ones I managed to identify
  if ($gaTaskID == '') {
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'forum') && (isset($_REQUEST['f']))) $gaTaskID =  $_REQUEST['f'];if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'forum') && (isset($_REQUEST['f']))) $gaTaskID =  $_REQUEST['f'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'glossary') && (isset($_REQUEST['g']))) $gaTaskID =  $_REQUEST['g'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'forum') && (isset($_REQUEST['forum']))) $gaTaskID =  $_REQUEST['forum'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'resource') && (isset($_REQUEST['r']))) $gaTaskID =  $_REQUEST['r'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'quiz') && (isset($_REQUEST['cmid']))) $gaTaskID =  $_REQUEST['cmid'];
    if (($gaURLPartsCount > 4) && ($gaURLParts[4] == 'quiz') && (isset($_REQUEST['q']))) $gaTaskID =  $_REQUEST['q'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'calendar') && (isset($_REQUEST['course']))) $gaTaskID =  $_REQUEST['course'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'user') && (isset($_REQUEST['contextid']))) $gaTaskID =  $_REQUEST['contextid'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'blog') && (isset($_REQUEST['filterselect']))) $gaTaskID =  $_REQUEST['filterselect'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'blog') && (isset($_REQUEST['postid']))) $gaTaskID =  $_REQUEST['postid'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'blog') && (isset($_REQUEST['userid']))) $gaTaskID =  $_REQUEST['userid'];
    if (($gaURLPartsCount > 3) && ($gaURLParts[3] == 'help.php') && (isset($_REQUEST['file'])) && (isset($_REQUEST['module']))) $gaTaskID =  $_REQUEST['module'] . '/' . $_REQUEST['file'];
  }
  // still no taskID? try update=xx from editing resource
  if ( ($gaTaskID == '') && (isset($_REQUEST['update']))) $gaTaskID = $_REQUEST['update'];
        
  // if still no taskID then set to 0
  if ($gaTaskID == '') $gaTaskID = '0';

Finally, construct the pseudo-URL and set GA account #:

  // now construct pseudo URL using . separator    
  $pseudoUrl = $userSchool . '.' . $gaRole . '.' . $gaCourse . '.' . $gaTask . '.' . $gaTaskID;
  $pseudoUrl = strtolower($pseudoUrl); // all lower case for neatness
  $gaAccount = "UA-xxxxxxxx-1"; // enter your GA account code here

PHP stuff over, in the html part of the header

    <script>     
      // Google Analytics code as a function instead of inline
      function gaSSDSLoad (acct,pseudo_url) {  
        var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");
        var s,pageTracker;  
        s = document.createElement('script');  
        s.src = gaJsHost + 'google-analytics.com/ga.js';  
        s.type = 'text/javascript';  
        s.onloadDone = false;  
        function init () { 
          try { pageTracker = _gat._getTracker(acct); } catch (err) {}
          pageTracker._trackPageview(pseudo_url);
        }
        s.onload = function () {    
          s.onloadDone = true;    
          init();  
        };
        s.onreadystatechange = function() {    
          if (('loaded' === s.readyState || 'complete' === s.readyState) && !s.onloadDone) {      
            s.onloadDone = true;      
            init();    
          }  
        };   
      document.getElementsByTagName('head')[0].appendChild(s);
      }  
     
      // now run script using GA account # and pseudo_url
      // fix for IE8
      if(window.addEventListener) {
        /* W3C method. */
        window.addEventListener('load', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") }, false);
      } else if(window.attachEvent) {
        /* IE method. */
        window.attachEvent('onload', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") });
      } else {
        /* Old school method. */
        window.onload = function() {gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>"); };
      }
    
    </script> 

File attached with code, don’t forget to edit GA account #
code_snippet

Advertisements

When we started with Moodle at City back in 2009 we wanted to use Google Analytics (GA) to augment the stats that Moodle collects itself. Partly this is because user stats are a bit hard to get out of Moodle in a flexible and visually attractive way. True, you could deploy custom queries to the back-end database but we wanted to leverage the power of GA. Trouble is, GA uses as its base unit the URL, and Moodle’s URLs are not very informative.

We want to be able to look at stats for different type of activities and user role (e.g. student vs teacher), and to be able to differentiate between schools (and even departments). This just isn’t possible with the standard moodle/mod/{mod-name}/{action}.php?id={id} format of Moodle URLs – all this gives you is the ability to breakdown stats by activity type alone.

What we’re aiming for then is to send to GA some kind of pseudo-URL that contains the information we need to be able to analyse. A model for this pseudo-URL might look something like this:

{school}.{role}.{module}.{activity-action}.{id}

However an example URL from Moodle might look like this:

http://moodle.city.ac.uk/mod/turnitintool/view.php?id=123456

this tells GA nothing about the page other than it is a Turnitin assignment.

Using custom php in the Moodle theme header.html we can construct our pseudo-URL and send this to Google instead of the actual URL.

The URL we generate will look something like this:

law.student.gen_law_imm_2011-12.mod-turnitintool-view.123456

so now we can interrogate our analytics for school*, role, module, activity and action, down to an individual activity if we want but usually aggregated to some degree. There is also scope to extend this, for example, the Turnitin tool uses another request variable &do (values such as “intro” & “submissions”) which could tell GA a bit more about what is actually happening if it was included in the pseudo-URL. Other plugins and activities have other similarly useful request variables in their URLs.

[*Where do we get school from? Our user table grabs school and department codes from our user database on login via LDAP so it is available in the global $USER]

I won’t show the code we used to create the pseudo-URL (if anyone wants that see this post) but the adapted GA script from the header.html <head> section  is here **:

<?php
  $pseudoURL = "blah"; // insert code here to build pseudo-URL
  $gaAccount = "UA-xxxxxxxx-1"; // enter your GA account code here
?>
<script>
// Google Analytics code as a function instead of inline
  function gaSSDSLoad (acct,pseudo_url) {
    var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." :
"http://www.");
    var s,pageTracker;
    s = document.createElement('script');
    s.src = gaJsHost + 'google-analytics.com/ga.js';
    s.type = 'text/javascript';
    s.onloadDone = false;
    function init () {
      try { pageTracker = _gat._getTracker(acct); } catch (err) {}
    pageTracker._trackPageview(pseudo_url);
  }
  s.onload = function () {
    s.onloadDone = true;
    init();
  };
  s.onreadystatechange = function() {
    if (('loaded' === s.readyState || 'complete' === s.readyState) &&
!s.onloadDone) {
      s.onloadDone = true;
      init();
    }
  };
document.getElementsByTagName('head')[0].appendChild(s);
}

// now run script using GA account # and pseudo_url
// fix for IE8
if (window.addEventListener) {
  /* W3C method. */
  window.addEventListener('load', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") }, false);
} else if (window.attachEvent) {
  /* IE method. */
  window.attachEvent('onload', function(){gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>") });
} else {
  /* Old school method. */
  window.onload = function() {gaSSDSLoad ("<? echo $gaAccount; ?>","<? echo $pseudoUrl; ?>"); };
}

[**Not sure who to credit this to, it wasn’t me, someone in the webteam came up with this before I got here.]

In a future post I’ll talk about how we use Google Analytics and Moodle’s own stats here at City.

I am Mike Hughes, the opinions expressed on this blog are my own and not those of my employer ... do I really need to say this?

Enter your email address to follow this blog and receive notifications of new posts by email.

recent tweets