Google Universal Analytics

Implementing Google Universal Analytics Measurement Protocol in PHP and WordPress

Google Universal AnalyticsI’ve had some historic difficulty in being able to track ecommerce transactions in Google Analytics that happen ‘behind the scenes’ of the Smart Insights site – typically when PayPal sends an IPN message, server to server, for a subscription renewal in our case. Since those messages don’t touch the browser, you can’t use the Google Analytics javascript to track them. So I started looking to see how/if I could send data directly to Google Analytics from a backend script running on our WordPress install.

I found several custom PHP classes that seemed, initially, to do the trick;

Problem is, they’re both pretty old and seem to be no longer developed, so I figured there must be something better. My colleague Dave Chaffey then pointed me in the direction of the Google Measurement Protocol, which I hadn’t heard of before.

What is the Google Measurement Protocol and why should I care?

The Measurement Protocol is an addition to the latest version of , which is called ‘Universal Analytics’. From the explainer documentation;

The Google Analytics Measurement Protocol allows developers to make HTTP requests to send raw user interaction data directly to Google Analytics servers. This allows developers to measure how users interact with their business from almost any environment.

More at ‘‘

Google allows you to form and send any of the following types of Analytics request via this API;

  • Page Tracking
  • Event Tracking
  • Ecommerce Tracking
  • Social Interactions
  • Exception Tracking
  • User Timing Tracking
  • App tracking

Using the Google Measurement Protocol in PHP and WordPress

NOTE:  You can only use the Measurement Protocol if you have the Universal Analytics tracking code set up on your site. It will not work with the standard tracking code. If you need help with this, there’s a fantastic introduction at the KissMetrics site: Universal Analytics: Switching to the Next Version of Google Analytics

As is typical with Google and ‘beta’ products (as the Universal Analytics code is still in beta) they give pretty sparse examples and no specific code, so it’s up to you to work things out yourself.

In my case I wanted to be able to fire two types of requests to Google – an ecommerce transaction when a membership renewal occurs, and a custom pageview which is used as a ‘‘ on Analytics.

As I wanted the code to be reusable throughout the WordPress install, I came up with a set of functions (mine lives in a function file of its own, yours could live in your standard theme functions.php file if you wish).

There are two steps needed to make a request to the Measurement Protocol.

1. Obtain the current user’s unique identifier (CID) from the Google Analytics cookie (if it exists!)

Very vital, this, and something I ended up banging my head on the desk about. When a user lands on your site, the Google Analytics code assigns them a unique identifer. This lives in a cookie, and follows them across all requests they make on your site. If you fire a server side request and make up your own unique identifier, the Analytics service has no way of matching the two up.

Obviously this isn’t a problem in my original use case – a behind the scenes ecommerce renewal won’t have a browser cookie to get anything from, and it doesn’t matter that you assign it a unique identifier all of its own. But – if you decide to use the Measurement Protocol for other things, front of site (and I did within 10 minutes of finishing my first code), then you do need to check for the existence of that cookie first. If you don’t, a user that has logged page history will suddenly become anonymous when you fire your Measurement Protocol request. This means that things like conversion funnels won’t work, which is not good.

So, here’s how we parse the GA cookie;

// Handle the parsing of the _ga cookie or setting it to a unique identifier
function gaParseCookie() {
  if (isset($_COOKIE['_ga'])) {
    list($version,$domainDepth, $cid1, $cid2) = split('[\.]', $_COOKIE["_ga"],4);
    $contents = array('version' => $version, 'domainDepth' => $domainDepth, 'cid' => $cid1.'.'.$cid2);
    $cid = $contents['cid'];
  }
  else $cid = gaGenUUID();
  return $cid;
}

This code will pluck the CID value from the cookie if it exists. If not, it runs another function to generate one – gaGenUUID(). The Measurement Protocol documents say that the CID must “be a random UUID (version 4) as described in http://www.ietf.org/rfc/rfc4122.txt“. Ah, okay. So let’s create one of those, then;

// Generate UUID v4 function - needed to generate a CID when one isn't available
function gaGenUUID() {
  return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
    // 32 bits for "time_low"
    mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ),

    // 16 bits for "time_mid"
    mt_rand( 0, 0xffff ),

    // 16 bits for "time_hi_and_version",
    // four most significant bits holds version number 4
    mt_rand( 0, 0x0fff ) | 0x4000,

    // 16 bits, 8 bits for "clk_seq_hi_res",
    // 8 bits for "clk_seq_low",
    // two most significant bits holds zero and one for variant DCE1.1
    mt_rand( 0, 0x3fff ) | 0x8000,

    // 48 bits for "node"
    mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff ), mt_rand( 0, 0xffff )
  );
}

So, we now have a CID that we can use.

2. Build and send our Measurement Protocol request

There are two functions used here – one to build the request, the other to send it. In both cases you call gaBuildHit() with two parameters – the method (which in my example is either ‘pageview’ or ‘ecommerce’) and an array of data that is needed by the Measurement Protocol. This differs depending on the method you’re using, so let’s jump into the code and I’ll explain more afterward.

function gaBuildHit( $method = null, $info = null ) {
  if ( $method && $info) {

  // Standard params
  $v = 1;
  $tid = "UA-XXXXXXX"; // Put your own Analytics ID in here
  $cid = gaParseCookie();

  // Register a PAGEVIEW
  if ($method === 'pageview') {

    // Send PageView hit
    $data = array(
      'v' => $v,
      'tid' => $tid,
      'cid' => $cid,
      't' => 'pageview',
      'dt' => $info['title'],
      'dp' => $info['slug']
    );

    gaFireHit($data);

  } // end pageview method

  // Register an ECOMMERCE TRANSACTION (and an associated ITEM)
  else if ($method === 'ecommerce') {

    // Set up Transaction params
    $ti = uniqid(); // Transaction ID
    $ta = 'SI';
    $tr = $info['price']; // transaction value (native currency)
    $cu = $info['cc']; // currency code

    // Send Transaction hit
    $data = array(
      'v' => $v,
      'tid' => $tid,
      'cid' => $cid,
      't' => 'transaction',
      'ti' => $ti,
      'ta' => $ta,
      'tr' => $tr,
      'cu' => $cu
    );
    gaFireHit($data);

    // Set up Item params
    $in = urlencode($info['info']->product_name); // item name;
    $ip = $tr;
    $iq = 1;
    $ic = urlencode($info['info']->product_id); // item SKU
    $iv = urlencode('SI'); // Product Category - we use 'SI' in all cases, you may not want to

    // Send Item hit
    $data = array(
      'v' => $v,
      'tid' => $tid,
      'cid' => $cid,
      't' => 'item',
      'ti' => $ti,
      'in' => $in,
      'ip' => $ip,
      'iq' => $iq,
      'ic' => $ic,
      'iv' => $iv,
      'cu' => $cu
    );
    gaFireHit($data);

  } // end ecommerce method
 }
}

// See https://developers.google.com/analytics/devguides/collection/protocol/v1/devguide
function gaFireHit( $data = null ) {
  if ( $data ) {
    $getString = 'https://ssl.google-analytics.com/collect';
    $getString .= '?payload_data&';
    $getString .= http_build_query($data);
    $result = wp_remote_get( $getString );

    #$sendlog = error_log($getString, 1, ""); // comment this in and change your email to get an log sent to your email

    return $result;
  }
  return false;
}

Registering a pageview

Details on the parameters needed .

You need to send the code in the following way:

$data = array(
  'title' => 'Any page title you want to send',
  'slug' => '/please/change/me/to-whatever-you-want/'
);
gaBuildHit( 'pageview', $data);

Registering an ecommerce transaction

You have to send a minimum of two requests here – the first is the initial transaction, and the second is for the specific item, They’re bound together on the same ‘transaction ID’ so Analytics can match them up. Details on the parameters needed .

Please note – if you have multiple line items, my code doesn’t accommodate that as we only ever have one. You’ll need to amend the code to loop over the items and send them all.

You need to send the code in the following way:

$data = array(
  'info' => array ('product_id' => 102, 'product_name' => 'Smart Insights membership' ), // we already have these details in an array so send that, you may want to amend
  'cc' => 'GBP', // the currency code
  'price' => 209.00, // the price
  'country' => GB // the country ISO code
);
gaBuildHit( 'ecommerce', $data);

Some things to be aware of

  1. If you choose to take a peek at what is returned from the API after a request, you’ll be surprised to learn that you only ever get a 200 response, regardless whether your request has succeeded or failed. So it’s a bit pointless, really, you can’t debug it that way
  2. If you use the Real Time view on Analytics, you will be able to see your ‘pageview’ requests appear within seconds, so you know it works!
  3. If you use the ‘ecommerce’ method, you’ll probably have to wait for 2-3 hours (like I did) before data appears, or not. Not helpful, but the code should work – at least you don’t have to start from scratch like I did! 🙂
  4. The code above is written for our site – you will more than likely have to tweak and change it to make it work for yours.

And that’s that – though it could do with some tidying (which I’ll do when I extend it further), it should be easy to add any of the other methods available on the Measurement Protocol API to these functions. Running within your WordPress install it should give a good starting point to using the server side Google Analytics Measurement Protocol in PHP and WordPress.

I’d love to hear how other WordPress developers are using the new Google Universal Analytics Measurement Protocol within their WordPress sites – let me know in the comments!

Published by

Stu Miller

Web consultant and specialist, WordPress developer and PHP developer based in Leeds, UK. 15 years experience in architecting web sites and applications. Co-founder and Technical Director of SmartInsights.com, formerly the same of First 10 Digital

19 thoughts on “Implementing Google Universal Analytics Measurement Protocol in PHP and WordPress”

  1. This is a wonderful article, and thank you so much for the code examples! I’ve been looking for a solution to track PDF downloads from offsite links, and was going to start with the mod-rewrite + PHP-GA solution suggested here — http://www.lunametrics.com/blog/2013/06/04/tracking-pdfs-google-analytics-server-side/ — but had the same concern you did that PHP-GA hasn’t been updated in over a year. mod-rewrite will still be necessary for my need to intercept any requests for .PDF files, but a simplified version of your gaBuildHit code will certainly get me the rest of the way. I’ll let you know if/when it works.

  2. Well, that was easy. I wholesale took your functions and included them in my functions.php

    Then, in .htaccess,

    # Use PHP 5.3 by default (HostGator or other shared hosts might require this line)
    AddType application/x-httpd-php53 .php
    # If the user-agent string is NOT set by our PHP script…
    RewriteCond %{HTTP_USER_AGENT} !GA_PDF_Downloader [NC]
    # … rewrite all requests for PDFs to our PHP script
    RewriteRule ^.+\.pdf$ /downloader\.php [L,NC]

    Created ‘downloader.php’ in doc_root so those PDF requests have something to go to.

    In downloader.php, I immediately set the content type header and pulled in the basics of wordpress

    header(‘Content-type: application/pdf’);
    /** Include the bootstrap for setting up WordPress environment */
    include(‘wordpress/wp-load.php’);

    // and then
    // Get filename from the previous request
    $filename = parse_url(urldecode($_SERVER[‘REQUEST_URI’]), PHP_URL_PATH);
    $protocol = ((!empty($_SERVER[‘HTTPS’]) && $_SERVER[‘HTTPS’] != ‘off’) || $_SERVER[‘SERVER_PORT’] == 443) ? “https://” : “http://”;
    $url = $protocol.$_SERVER[‘HTTP_HOST’].$filename;

    //I’ll test to make sure its a PDF getting called eventually, but this is just a POC for now

    //set the UA so the rewrite rule doesn’t fire endlessly.
    $download_args = array( ‘user-agent’ => ‘GA_PDF_Downloader’,
    ‘httpversion’ => ‘1.1’ );

    //painful trial and error on how to get the file sent to the browser
    print wp_remote_retrieve_body(wp_remote_get($url, $download_args));

    // and now I fire off my GA request to track the PDF
    $ga_args = array(
    ‘title’ => ‘Tracked PDF’,
    ‘slug’ => $filename
    );
    gaBuildHit( ‘pageview’, $ga_args);
    exit;
    //since the wp_http does everything as non-blocking calls, explicitly wanted to exit the script

    And my early tests show tracked PDF downloads, even from direct, offsite hits! Lots of cleanup and further testing to do, but I’ve had people complaining about GA not showing deeplinked stuff forever.

    All of the sloppiness is mine, but the important guts are all you + Alex Moore for the core idea of running this through apache rewrite rules —
    http://www.lunametrics.com/blog/2013/06/04/tracking-pdfs-google-analytics-server-side/

    Thanks!

  3. Can it be that something is wrong with code because all e-commerce payments are shown that come from direct source. But that cant be 🙁

    1. I guess there is nothing wrong with the code above. It is just missing the referrer, campaign source, medium and keyword. Check for more info on that.

      I have another question about that. Do you need to send this information once with the transaction or do need to add it to the items as well? Maybe somebody already has some insights on that?!

      1. Hi Tilo,

        I’m on holiday at the moment with little access to internet, so your comments have been held in a queue – sorry about that. I’ll have a look at your comments when I return at the end of August – hope you get it working in the meantime!

      2. I need to submit the data with the referrer by myself? How could I know? I thought Google will trace back the referrer by given CID. This is meaningless because all my submitted data are shown as from direct source. Please assist. Thanks.

  4. Thank you for sharing you code example. It really helps me building up the e-commerce tracking with the help of universal analytics and the measurement protocol.

    I have one question though regarding multiple items per transaction. Can you combine all items and send them at once or do you have to send as many hits as you have items.

    As I’m writing these lines I’m leaning more and more towards the second option. Maybe you can confirm that?

    You say:
    “Please note – if you have multiple line items, my code doesn’t accommodate that as we only ever have one. You’ll need to amend the code to loop over the items and send them all.”

    Google says:
    “To send ecommerce data, send one transaction hit to represent an entire transaction, then send an item hit for each item in the transaction. The transaction ID ti links all the hits together to represent the entire purchase.”

    Again thanks a lot!

  5. I found that the cURL was slowing down responses too much, particularly on transactions with multiple items, when several calls are made. I post directly through a socket, so I can close it immediately without waiting for a response. Now it takes a few milliseconds instead of a few seconds:

    &$val)
    {
    $post_params[] = $key . '=' . urlencode($val);
    }
    $post_string = implode('&', $post_params);

    // get URL segments
    $parts = parse_url($url);

    // workout port and open socket
    $port = isset($parts['port']) ? $parts['port'] : 80;
    $success = $fp = fsockopen($parts['host'], $port, $errno, $errstr, 30);
    if($fp) {
    // create output string
    $output = "POST " . $parts['path'] . " HTTP/1.1\r\n";
    if(is_string($ua)) $output .= "User-Agent: " . $ua . "\r\n";
    $output .= "Host: " . $parts['host'] . "\r\n";
    $output .= "Content-Type: application/x-www-form-urlencoded\r\n";
    $output .= "Content-Length: " . strlen($post_string) . "\r\n";
    $output .= "Connection: Close\r\n\r\n";
    $output .= isset($post_string) ? $post_string : '';

    // send output to $url handle
    $success = fwrite($fp, $output);
    fclose($fp);
    }
    return $success ? true : false;
    }
    

  6. That’s odd – the previous post cut off the first lines of the code block – here they are:

    function socketPost($url, $params = array(), $ua = null)
    {
    // create POST string
    $post_params = array();
    foreach ($params as $key => &$val)
    ....

  7. Another thing that caused a aggravating bug: our e-commerce engine (Magento) returns item quantity as a decimal (eg. 2.000), which breaks the Measurement Protocol ‘item’ hit, which apparently requires an integer to parse correctly. Item posts just weren’t showing up, even though all the parameters seemed correct and present. Casting to an int fixed it.

Leave a Reply