Tracking Offline Purchases With Enhanced eCommerce And Google Forms

November 19, 2015

If you’re an online retailer, you know the importance of measuring online marketing and advertising efforts and how valuable this data can be for decision making. With Google Analytics’ Enhanced eCommerce tracking, you can get insight into your online sales and how products and promotions are performing.

But what if your customers are making their final purchases offline? Wouldn’t it be great if you could get data for offline transactions into Google Analytics, ultimately allowing you to tie your users’ online behavior to their offline purchases? Even better, what if you could just do this with a simple form, without downloading or uploading any data?

Well, great news! You can do all of this with the Measurement Protocol.

Sayf has written extensively about this in the past, so make sure to read his post for more detailed information on why tying offline transaction information to online behavior is important for accurate attribution. In this post, I’m going to walk you through an Enhanced eCommerce example, using a Google Form and a Google App Script.

The Objective:

To use the Measurement Protocol to tie offline transactions to online users, in order to better understand their path toward purchase and what drove them to our site.

The Requirements:

If multiple service representatives are taking customers’ orders offline, we need a standard, easy way for all of them to send transactional data into Google Analytics.

In Google Analytics, we want to see the same types of Enhanced eCommerce data for offline transactions that we are already collecting for online purchases.

We want a tool that we can edit and troubleshoot quickly, without constantly involving a team of developers or designers.

With the objective and requirements in mind, we chose to build a custom Google Form that when submitted, sends a Measurement Protocol hit to Google Analytics. In addition to the form and App Script, we also had to do some preliminary setup on the site, as well as within Google Analytics and Google Tag Manager. You could try this solution if your orders typically don’t contain more than 2 or 3 products; any more and you will probably want something more automated.

Step 1: Create Custom Dimensions for Client ID and “Guest ID.”

We set up two user-scoped Custom Dimensions, called Client ID and Guest ID. The Client ID is really the important piece that we need.

When a user is browsing your website, their Client ID is what identifies them as a person. The Client ID is sent to GA with every hit (pageviews, events, etc.) If we can send this transaction to GA with the same Client ID, then GA will associate it back to the user that was browsing on the website. This means all of the great data about how that user found your website and what they browsed on your website is attached to this same transaction.

In our situation, it would be unreasonable to ask customers to provide the entire Client ID over the phone, so we created a shorter Guest ID based on the Client ID. The Guest ID is optional, but we chose to create it for this specific example.

If you are able to get the Client ID from another source (like if customers previously filled out a form on your site), there’s no need to create the Guest ID.

Step 2: Get the Client ID and Guest ID data using Google Tag Manager.

We pulled the Client ID from the tracker object with Google Tag Manager and sent it with a non-interaction event, similar to this method:

Within Google Tag Manager, we created the Guest ID from the Client ID by removing the “.” and then base-36-encoding it. At this point, we also enlisted the help of a developer, who placed an empty <div> tag on all pages with an ID of “guest-id.” We were then able to populate that <div> with the Guest ID, allowing customers to view their “code” on any page of the website.

We’ve included the code at the bottom of this post for doing the tough parts, you’ll need to decide where and how to display the Guest ID that it’s visible to the user calling in.

If you’re not sold on the idea of placing the Client ID on all of your site’s pages, no worries – you can display the Client ID or Guest ID wherever you’d like. For example, maybe you’d rather have it at the top of a user’s profile page or accessible from a specific URL, like www.example.com/guest-id.

guest-id-screenshot

Step 3: Build the Google form.

Since we’re using Google Forms and its built-in features, this is the easiest part. We were more concerned with functionality than design, so we created a basic form that looked like this:

Offline Transaction Google Form

Along with Guest ID, the customer service reps needed to enter total transaction revenue and product details (name, price, quantity). You can see the full list of form fields that we used below.

Step 4: Create the App Script.

The original App Script was borrowed from Daniel Waisberg (who also wrote the foreword for this awesome book on Google Tag Manager). Thanks to Dan Wilkerson who engineered the Guest ID solution and modified the App Script.

Our version of the script not only maps the form fields to Measurement Protocol parameters but base-36-decodes the Guest ID and returns it to the original Client ID format. It also adds additional required Measurement Protocol parameters like hit type and version, then sends the payload to http://www.google-analytics.com/collect via HTTP POST request.

You’ll need to add this to the project using the Script Editor.

You can get the full script at the bottom of this post.

The following parameters were sent automatically with the hit upon form submission.

  • tid – GA tracking id (required)
  • v – version (required)
  • cid – Client ID (required)
  • t – event (required – either event or pageview hit type can be used with Enhanced eCommerce parameters)
  • ec – event category
  • ea – event action
  • el – event label
  • ni – set to “1” for a non-interaction event
  • pa – product action (required for measuring purchases)
  • ti – transaction ID (set to a random value)

These additional parameters were mapped to the form fields and their values would be entered by the customer service rep. You could also add additional parameters and form fields, like tax, shipping, SKU, etc.

  • cd4 – Guest ID
  • tr – revenue, including tax and shipping
  • pr1nm – product 1 name
  • pr1qt – product 1 quantity
  • pr1pr – product 1 price
  • pr2nm – product 2 name
  • pr2qt – product 2 quantity
  • pr2pr – product 2 price
  • pr3nm – product 3 name
  • pr3qt – product 3 quantity
  • pr3pr – product 3 price

The final hit looked something like this:
http://www.google-analytics.com/collect?cd4=CQLMWMC43HJMA&tr=135&pr1nm=…

Step 5: Set up a trigger for the Google Form.

This is really simple. From your form, open your Script Editor (Tools > Script Editor). From the Script Editor menu, view the current project’s triggers (Resources > Current project’s triggers). Set up a trigger to run the script on form submission, which should actually be the default when you create a new trigger.

The Results:

If the customers could provide their Guest ID over the phone, the customer service reps were able to easily enter the other details into and submit the Google Form. In Google Analytics, we were able to view all of the offline transaction data in the standard Enhanced eCommerce reports, alongside data for online transactions. We were also able to see which sources and mediums drove offline transactions, providing insight into how our advertising and other marketing efforts were contributing to overall sales goals.

There are so many ways you could tweak this solution to work for any business or industry, so let us know how you’re using this or something similar!

Scripts We Used

GTM Custom JS Variable – {{Base36 Encoding and Decoding}}

function() { 
 return {
     encode: function(decoded) {

        decoded = Number(decoded);

        var encoded = '';
        var alphabet = {
          0: "A",
          1: "B",
          2: "C",
          3: "D",
          4: "E",
          5: "F",
          6: "G",
          7: "H",
          8: "I",
          9: "J",
          10: "K",
          11: "L",
          12: "M",
          13: "N",
          14: "O",
          15: "P",
          16: "Q",
          17: "R",
          18: "S",
          19: "T",
          20: "U",
          21: "V",
          22: "W",
          23: "X",
          24: "Y",
          25: "Z",
          26: "0",
          27: "1",
          28: "2",
          29: "3",
          30: "4",
          31: "5",
          32: "6",
          33: "7",
          34: "8",
          35: "9"
        };  

        while(decoded !== 0) {
          var n = alphabet[decoded % 36];
          decoded = Math.floor(decoded / 36);
          encoded = n + encoded;
        }

        return encoded;

      },

      decode: function(encoded) {
        var i;
        var decoded = '';
        var alphabet = {
          "0": 26,
          "1": 27,
          "2": 28,
          "3": 29,
          "4": 30,
          "5": 31,
          "6": 32,
          "7": 33,
          "8": 34,
          "9": 35,
          "A": 0,
          "B": 1,
          "C": 2,
          "D": 3,
          "E": 4,
          "F": 5,
          "G": 6,
          "H": 7,
          "I": 8,
          "J": 9,
          "K": 10,
          "L": 11,
          "M": 12,
          "N": 13,
          "O": 14,
          "P": 15,
          "Q": 16,
          "R": 17,
          "S": 18,
          "T": 19,
          "U": 20,
          "V": 21,
          "W": 22,
          "X": 23,
          "Y": 24,
          "Z": 25
        };

        for (i = 0; i &lt; encoded.length; i++) {
          var n = encoded.charAt(i);
          decoded = decoded * 36 + (alphabet[n] + 0);
        }

        return decoded;
      }
    };
}

GTM 1st-Party Cookie Variable - {{Value of _ga Cookie}}

This is just a 1st-Party Cookie variable that pulls in the _ga cookie value.

ga-cookie

GTM Custom JS Variable - {{Guest ID}}

function() {
  
  var gaCookie = {{Value of _ga Cookie}};

  if(gaCookie &amp;&amp; gaCookie.match(/\d+?\.\d+$/)) {
  
    var cookieVal = gaCookie.match(/\d+?\.\d+$/)[0];
    var base36 = {{Base36 Encoding and Decoding}};
    var parts = cookieVal.split('.');
    var randInt = Number(parts[0]);
    var ts = Number(parts[1]);
    
    var guestId = base36.encode(randInt) + '-' + base36.encode(ts);
 
    return guestId;

  }
  
}

Google App Script

var base36 = {
  
  encode: function(decoded) {
    if(isNaN(decoded)) {
      return false;
    }
    
    var encoded = '';
    var alphabet = {
      0: "A",
      1: "B",
      2: "C",
      3: "D",
      4: "E",
      5: "F",
      6: "G",
      7: "H",
      8: "I",
      9: "J",
      10: "K",
      11: "L",
      12: "M",
      13: "N",
      14: "O",
      15: "P",
      16: "Q",
      17: "R",
      18: "S",
      19: "T",
      20: "U",
      21: "V",
      22: "W",
      23: "X",
      24: "Y",
      25: "Z",
      26: "0",
      27: "1",
      28: "2",
      29: "3",
      30: "4",
      31: "5",
      32: "6",
      33: "7",
      34: "8",
      35: "9"
    };  
    while(decoded !== 0) {
      var n = alphabet[decoded % 36];
      decoded = Math.floor(decoded / 36);
      encoded = n + encoded;
    }
    return encoded;
  },
  decode: function(encoded) {
    var i;
    var decoded = '';
    var alphabet = {
      "0": 26,
      "1": 27,
      "2": 28,
      "3": 29,
      "4": 30,
      "5": 31,
      "6": 32,
      "7": 33,
      "8": 34,
      "9": 35,
      "A": 0,
      "B": 1,
      "C": 2,
      "D": 3,
      "E": 4,
      "F": 5,
      "G": 6,
      "H": 7,
      "I": 8,
      "J": 9,
      "K": 10,
      "L": 11,
      "M": 12,
      "N": 13,
      "O": 14,
      "P": 15,
      "Q": 16,
      "R": 17,
      "S": 18,
      "T": 19,
      "U": 20,
      "V": 21,
      "W": 22,
      "X": 23,
      "Y": 24,
      "Z": 25
    };
    for (i = 0; i &lt; encoded.length; i++) {
      var n = encoded.charAt(i);
      decoded = decoded * 36 + (alphabet[n] + 0);
    }
    return decoded;
  }
};

function myFunction(e) {
  var GA_TRACKING_ID = 'UA-XXXXXX-X'; // Add your own GA tracking ID
  
  // Maps form fields to fields in GA
  var data_mapping = {
    0: 'cd4',   // Guest ID
    1: 'tr',    // Revenue
    2: 'pr1nm', // Product 1 Name
    3: 'pr1pr', // Product 1 Price
    4: 'pr1qt', // Product 1 Quantity
    5: 'pr2nm', // Product 2 Name
    6: 'pr2pr', // Product 2 Price
    7: 'pr2qt', // Product 2 Quantity
    8: 'pr3nm', // Product 3 Name
    9: 'pr3pr', // Product 3 Price
    10: 'pr3qt', // Product 3 Quantity
  };
  
  trackForm(e);

  function trackForm(e) {
    var data = [],
        item,
        res = e.response.getItemResponses();
    var guestId;
    
    for (var i=0; i &lt; res.length; i++){
      item = res[i].getItem();
      if(item.getIndex() === 0) {
        guestId = res[i].getResponse();
      }
      
      if(data_mapping[item.getIndex()]) {
        data.push([
          data_mapping[item.getIndex()],
          res[i].getResponse()
        ]);
      }
    }  
    
    var clientIdParts = guestId.split('-');
    var clientId = base36.decode(clientIdParts[0]) + '.' + base36.decode(clientIdParts[1]);
    var transactionId = Math.floor(Math.random()*10E7); // Assigns a random value to Transaction ID.

    data.push(
      
      ['tid' , GA_TRACKING_ID],   // Uses the ID you provided in the beginning of the Script.
      ['v' , '1'],  
      ['cid' , clientId],   
      ['t' , 'event'],   // The Hit type - Enhanced Ecommerce uses either a pageview or an event hit type. 
      ['ec' , 'Ecommerce'], 
      ['ea' , 'Offline Purchase'], 
      ['el' , transactionId],
      ['ni' , '1'],
      ['pa' , 'purchase'], 
      ['ti' , transactionId],   
      ['z' , Math.floor(Math.random()*10E7)]   // Cache Buster.
    );
    
    var payload = data.map(function(el){el[1] = encodeURIComponent(el[1]); return el.join('=')}).join('&amp;');
    Logger.log(payload);
    
    var options =
        {
          'contentType': 'application/json',
          'method' : 'POST',
          'payload' : payload
        };
    
    UrlFetchApp.fetch('http://www.google-analytics.com/collect', options);
  }
}