Online-To-Offline Attribution Made Simple

Note: If you’re looking for a more comprehensive understanding of attribution, check out our Digital Marketer’s Guide to Attribution for more info.

The biggest issue facing most B2B lead-generation organizations today is attributing their best leads—you know, the ones that end up becoming paying customers—back to the marketing activity (or activities) that drove them to the website: paid campaigns, specific content, all that good stuff.

Turns out, it’s not so hard to bridge the gap between your CRM, marketing automation (MA), and web analytics platforms if you know where to start. It’s a process called “closed-loop analytics.”

Researching an Integration

Here at Portent, we use HubSpot for marketing automation and as our CRM. So, naturally, we were chomping at the bit to integrate with Google Analytics for our online-to-offline attribution.

If you’ve ever looked into this kind of integration before, Google’s documentation for integrating with popular CRMs is non-existent, with the exception of Google Analytics 360’s integration with Salesforce Marketing Cloud, which is exclusive to folks paying for the expensive, premium version of GA. There really are no best practices for this for free GA users. It’s the wild west out there.

So where does one begin? The key is setting a non-PII (Personally Identifiable Information) User ID, which will serve as a way to join the online and offline data sets together in the reporting layer. (Note: Google Analytics doesn’t accept PII like email addresses or phone numbers.)

Setting a Unique Identifier

Portent developer extraordinaire, Andy Schaff, formulated a simple script that serves two purposes:

  1. To set a random unique identifier cookie for each website visitor (called RUID for short).
  2. To pass that ID as a hidden form field into any lead form the visitor fills out during their visit.

Here’s the script:

function setCookie(cname, cvalue, exdays) {
  var d = new Date();
  d.setTime(d.getTime() + (exdays*24*60*60*1000));
  var expires = "expires="+ d.toUTCString();
  document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/";
function getCookie(cname) {
  var name = cname + "=";
  var decodedCookie = decodeURIComponent(document.cookie);
  var ca = decodedCookie.split(';');
  for(var i = 0; i <ca.length; i++) {
    var c = ca[i];
    while (c.charAt(0) == ' ') {
      c = c.substring(1);
    if (c.indexOf(name) == 0) {
      return c.substring(name.length, c.length);
  return "";
// searches all input elements on the page with an id that starts with "_guid_" and writes the provided value
function populateFormFields(value) {
  var inputs = document.getElementsByTagName("input"), item;
  for (var i = 0, len = inputs.length; i < len; i++) { 
    item = inputs[i];
    // starts with _guid_
    if ( &&"_guid_") == 0) {
      document.getElementById( = value;
// if cookie "ruid" does not exist, generate and write 
if (!getCookie("ruid") || getCookie("ruid") == "") { 
  var ruidCookie = "";
  var chars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
  var length = 10;
  for (var i = length; i > 0; --i) 
    ruidCookie += chars[Math.floor(Math.random() * chars.length)];
  // set "ruid" cookie with our alpha-numeric value. expires in 182 days, or ~6 months
  setCookie("ruid", ruidCookie, 182);
  // set the "ruid" cookie value to the form field upon initial creation. this is an edge case for when a user first lands on the page
} else if (getCookie("ruid")) {
  // set the "ruid" cookie value to the form field.

You might say “this looks like gibberish to me” and wonder how to deploy it to your website. Fortunately, this can be rolled out using a custom HTML tag via Google Tag Manager without bothering your developer.

Passing the RUID into Google Analytics

While you’re there deploying the RUID script in Google Tag Manager, you can also, conveniently, send it to Google Analytics with every pageview that occurs, even before a visitor fills out a form.

You do this by setting a User-Defined Variable which scrapes the value of the RUID cookie.

Screenshot of setting a user-defined variable for a RUID cookie in Google Analytics

After the variable exists, you can send it to Google Analytics with every hit as a Custom Dimension using the Google Analytics Settings Variable.

Screenshot of setting the RUID cookie as a custom dimension in Google Analytics

Note: This also requires a User-Scoped Custom Dimension to be set up in Google Analytics (which generates the Index number “1” seen in the screenshot above).

Sending the RUID into Your CRM

The last important step before you get to your beautiful new closed-loop analytics report is setting up a field in your CRM system to receive the hidden RUID form field established earlier in Andy’s script.

Most CRM and MA platforms have the capability to establish “Custom Fields” that can be associated with any incoming contacts or leads to enrich the database with business-specific information. The creation of these fields and including them in the website lead forms is a slightly different procedure for each platform. Here’s an explanation of the process for several popular platforms.

We’ll be using HubSpot in our example, but we’ve included instructions to configure this field in other popular CRM and MA platforms.

Setting up Custom Properties in HubSpot

To generate a custom property in HubSpot, you go to Settings > Properties > Create a property.

Setting up Custom Fields in Pardot

To generate a custom field in Pardot, you go to Admin > Configure Fields > Prospects. Only Administrators can create this.

You can also set these fields to map to your Salesforce instance.

Setting up Customer Fields in Marketo

To generate a custom field in Marketo, you go to Admin > Field Management > New Custom Field.

Setting up Custom Fields in Eloqua

To generate a contact field in Eloqua, you go to Settings > Fields & Views > Add + > Add Contact Field.

Joining CRM Data to Analytics Data by RUID

Once you have the RUID appearing in both your CRM and Google Analytics, you can use data blending in Google Data Studio.

If you can export Lead Status information out of the aforementioned CRM platforms by User ID and store it in Google Sheets, you can join the data to any information we have in Google Analytics around that User ID (i.e., all of their web sessions, campaigns, content).

Here’s what our blend looks like:

Screenshot example of data blending in Google Data Studio

Once the blend is set up, you can get tables like these to introduce lead quality data to web analytics data:

Screenshot example of a table that introduces lead quality data to web analytics data

Tada! More Info on Your Best Leads

Now that you have a fairly straightforward way to tie online and offline information together, you can start thinking about things like Lead Scoring and bringing any PII associated with your IDs back into dashboards for your marketing and sales teams.

For an in-depth guide on how this works for PPC read our guide on how to track offline conversions in google ads.

Start call to action

See how Portent can help you own your piece of the web.

End call to action


  1. Hi Michael,

    Thanks to you and the Portent team for putting this together, this is excellent stuff.

    I’m trying to implement this for a client, and am running into some trouble with getting the RUID to pass to HubSpot. I’ve implemented the javascript code through GTM (and confirmed it was firing on the client’s site), and then set up hidden fields on the HubSpot forms to pass to a new conversion property named ‘ruid’. Unfortunately, the RUID doesn’t seem to pass on form submission.

    In troubleshooting, I noticed that in the code, there’s a section with this as the comment:

    // searches all input elements on the page with an id that starts with “_guid_” and writes the provided value

    I’m assuming this is how the code identifies which hidden field to pass the RUID to? I’m not sure where “_guid” came from, so I tried change that to “ruid”, which is what I named the property and field label in HubSpot. This didn’t seem to fix the problem.

    Could there be some issue with the code? Is there maybe something else that needs to be done on the HubSpot end to capture this?

    1. Hi Evan,

      Thanks for the note!

      We’ve learned a few things from trial and error since publishing this post:

      • “_guid_” can and should be changed to whatever your form field ID ends up being on the HubSpot side.
      • You need to delay firing the Set RUID script and Google Analytics to “Window Loaded” vs. “Page View” in GTM. The reason being, sometimes HubSpot doesn’t actually populate the form fields into the DOM until after our script has fired. So our script looks for the form field specified and doesn’t find anything because it doesn’t technically exist on the page yet. By delaying until Window Load, that gives the form field enough time to exist before we look for it.

      Be careful with that last step. If you move Google Analytics to fire on Window Loaded, you’ll also need to do that with any/all GA event tracking tags to ensure the pageview fires before the events do.

      1. Hi Michael,

        Thanks for the quick response!

        I changed the firing trigger in Google Analytics to “Window Loaded” then confirmed that the tag was firing in Window Load vs Page View using GTM preview. While I was there, I also confirmed that the cookie was working, and was successfully generating the RUID and storing it as a User-Defined Variable. Lastly, I made extra sure that the ID the code was scanning for matched what was in HubSpot. Still, the RUID doesn’t seem to be passing to HubSpot upon form submission.

        Any thoughts as to what I might be missing?

        Currently, our Google Analytics is published on the site through HubSpot’s integration. I figure GA has nothing to do with getting the information into HubSpot, so this shouldn’t be affecting the RUID passing or not, correct?

        Thanks again for your help, much appreciated!

        1. Hello guys,

          what was the outcome/solution here?

          I mean i set the tag to fire both on my form submissions events and all pages, am I expecting the ruid field to be filled inside hubspot? Or how should I verify this? It looks it is firing correctly.

        2. You should only need to set the tag on all pages. The form submission events will inherit the UID set at the pageview level.

          The UID should populate into HubSpot with your contact.

          One thing to look for is to ensure you select “Set as raw HTML form” in your form settings. Our script can’t see any HubSpot fields when the form is deployed through an iframe.

        3. Hubspot doesn’t provide an ID for hidden fields. So you will need to modify to search for a field based on it’s name. It worked for me this way.

          function populateFormFields(value) {
          var selection = document.querySelector(‘input[name=ruid]’);
          if (selection !== null) {
          selection.value = value;

  2. Hey Michael! Thanks for writing this up. When attempting to follow these steps, I got the following error message:

    Type: Invalid HTML
    Location: RUID
    Description: Invalid HTML, CSS, or JavaScript found

    Any idea what I should be troubleshooting here? I feel like I must have messed something up when attempting to set up the script.


    1. Hi Erica,

      You’ll need to paste the script we provided between open script () tags. I wish GTM did this automatically, but alas… *sigh*

      Thanks for reading!

  3. Hi Michael,

    Thanks for the write-up.

    Do I understand you correctly that you’re using the RUID as a UserID work around? Ie, creating and sending your own random (browser specific) ID into hidden form fields to connect marketing and sales data?

    If this is Portent’s approach to closed-loop analytics, I have a number of questions:

    1. How is this different from sending in the ga_client_id or any other browser unique ID created by your CMS analytics for example? Since GA doesn’t have an alias function like with Mixpanel, how would you stitch two browser records (if they’re the same user) without deploying robust User ID solution.

    Why do I bring this up…

    2. Since i’ve understood that you’re proposing a browser specific ID, a lead that signs up via your PPC > gated resource acquisition campaign on their work computer (RUID_1), then might end up opening your nurture email sequence on their mobile… (RUID_1) completing a demo call form, for example. What’s stopping the second ID simply overwriting the 1st in the CRM contact record? Wouldn’t this break any longer time-frame analysis with crm data exports… defeating the purpose of closed-loop? How do you combat this to ensure your weekly, monthly, quarterly attribution analysis are solid?

    Many thanks in advance!

    1. Hi Rhys,

      On the first question, the advantage of setting your own ID versus scraping another from Google or another system is you have full control over what domains that cookie is set on, its expiration, etc. Plus, a lot of cookies from other platforms you could scrape are third-party cookies and having your own ID allows you to set it as first-party and get around a lot of the restrictions browsers are levying on non-first-party cookies these days.

      As for the second question on potential RUID duplication, you’re right. This solution was never intended to work cross-device or cross-browser. That would require some more intensive device fingerprinting and some stuff we’d need to put more time into integrating. The intent here is that for B2B clients looking to integrate CRM data, potential customers are often researching and contacting from the same machine: their work machine. For other types of businesses like e-commerce retail outfits, they would need a more robust solution that relies on account creation.

      Thanks for reading and reaching out to us, Rhys!

  4. Hi Micheal,
    Very interesting article. One thing with HubSpot is that the moment it’s a hidden filed, the value dosen’t pass. It only seems to passe when the field is visible. Any insights on this?

    1. Field visibility should not factor into our script passing the value. If you can send me an example of how it’s passing for you to michael at portent dot com, I’ll take a look and advise.

      If you’re using HubSpot, ensure you select “Set as raw HTML form” in your form settings. Our script can’t see any HubSpot fields when the form is deployed through an iframe.

  5. Hi Michael,

    can you tell me how the new custom property in Hubspot should look like exactly so that this workaround is working? Maybe you can share screenshots from Hubspot?

    Many thanks in advance!


    1. Hi Witali,

      We need to update this post to incorporate some recent changes to HubSpot that weren’t in play when we first wrote this.

      Stay tuned!

Comments are closed.

Close search overlay