Overcoming the limitations in LinkedIn Website Retargeting

. Posted in: Inspiration
Tags: Google Tag Manager, Marketing, LinkedIn

Did you ever feel that the Website Retargeting in the LinkedIn Campaign Manager is just too simple? And have you wished for more advanced targeting options based on your first party data?

I, for one, have long wished for targeting options like those in Facebook’s Ads Manager. When it comes to website retargeting, Facebook lets you define audiences based on not only URLs, but also any custom events you might want to use.

On LinkedIn, however, we’re limited to build audiences based on a few simple URL matches:

URL retargeting options in LinkedIn Campaign Manager

Basically, LinkedIn allows you to define audiences based on website visitors who visited one or more specific URLs. Even though it’s possible to setup custom conversions (also URL based), these can’t be used to include or exclude visitors from an audience. Which is odd, to put it mildly.

What if…

What if we could use LinkedIn’s website retargeting to build audiences of visitors who visited our website and also:

  • Performed specific actions that are not URL based?
  • Belong to certain segments in a CDP?
  • Fit specific criteria in our CRM?

Well, we can. As long as all those data are visible in the URL tracked by the LinkedIn Insight tag. The problem is that they’re not (usually).

And while we could modify our website navigation to include those data in all URLs, this would be a nightmare to do web analytics with. We might not want to collect all kinds of data in Google Analytics that are only used on LinkedIn.

Override the URL tracked by LinkedIn

So what we’re looking for is a way to override the URL seen and tracked by LinkedIn. Some time ago, someone on the #measure Slack channel suggested to download the actual Insight javascript, modify it to send a custom URL and then host it locally. I’ve experimented with that myself, and while it can work, it’s prone to errors.

There must be another way! And there is. If you take a closer look at the javascript making up the LinkedIn Insight tag, it basically looks at the referring URL and use that as the URL to track (and to base audience definitions on). This just means that whatever page loads the Insight tag, the URL of that page is used and tracked by LinkedIn.

The semi-technical stuff

So once again, what we need to do is to somehow modify the URL of the page that loads the Insight tag. This can be done with vanilla javascript, but if doing a document.location.href to set a URL containing custom data, we force the browser to reload the page.

But let’s play with that thought. What if we used window.history.replaceState to update the URL? This doesn’t reload the page, and the Insight tag actual sees that new URL (depending on your GTM trigger). That’s good. On the other hand, it updates the URL in a way that might be caught by Google Analytics. And what if someone copied the URL to share it? That won’t be optimal either.

Then how about using an (gasp) iframe? I mean, with an iframe, we can control the URL. The problem with that is that we would want the URL to be identical to the page loading the iframe, and with some added parameters (with our custom data). In most server and website environments, that’s just not feasible. Or it could be, but would probably be difficult to implement.

So, we have a couple of good ideas here. And then I came up with the idea of combining a dynamic iframe element with window.history.replaceState:

Oh my god, what are you going to do?

I will tell you, dear reader. Enough with the cliffhangers. I wrote a javascript that can be implemented in Google Tag Manager and lets you append any text form data to all page URLs. These modified URLs will only be visible for LinkedIn, and this in turn allows you to setup more advanced website retargeting audiences.

Since it’s implemented with GTM, you can merge in any kind of data or value otherwise available to GTM. It does require some basic javascript know-how as to not eff it up. But here’s the script (and read on for a short explanation):

<script>
var gtm_linkedin_iframe = function() {
  var lipid = "INSERT_YOUR_LINKEDIN_PARTNER_ID";
  var params = {
    'foo': 'bar',
    'hello': 'world'
  };

  /**
   * DO NOT EDIT BELOW THIS LINE
   */

  var query = "";
  for( var key in params ) {
      if( params.hasOwnProperty(key) ) {
          query = query + '&' + key + '=' + params[key];
      }
  }

  query = ( document.location.search == "" ) ? query.replace('&','?') : query;
  var url = document.location.pathname + document.location.search + query;

  var iframe = document.createElement("iframe");
  iframe.style.display = "none";
  iframe.setAttribute('style','width:0; height:0;');
  document.body.appendChild(iframe);

  var scriptContainer = document.createElement("div");

  iframeDoc =
    iframe.contentWindow ||
    iframe.contentDocument.document ||
    iframe.contentDocument;

  var scripts = [
    'window.history.replaceState(null, null, "' + url + '");',
    '_linkedin_partner_id = "' + lipid + '";window._linkedin_data_partner_ids = window._linkedin_data_partner_ids || [];window._linkedin_data_partner_ids.push(_linkedin_partner_id);',
    '(function(){var s = document.getElementsByTagName("script")[0];var b = document.createElement("script");b.type = "text/javascript";b.async = true;b.src = "https://snap.licdn.com/li.lms-analytics/insight.min.js";s.parentNode.insertBefore(b, s);})();'
  ];

  for (i = 0; i < scripts.length; i++) {
    var script = document.createElement("script");
    script.type = "text/javascript";
    script.text = scripts[i];
    scriptContainer.appendChild(script);
  }

  iframeDoc.document.open();
  iframeDoc.document.appendChild(scriptContainer);
  iframeDoc.document.close();
};
gtm_linkedin_iframe();
</script>

First of all: Let’s say this script is loaded via GTM and is triggered on this page: https://analytical42.com/2019/datalayer-linkedin-insight

…then what LinkedIn will see, is actually this page: https://analytical42.com/2019/datalayer-linkedin-insight?foo=bar&hello=world

Awesome, right? The things you need to do are:

  1. Insert your own LinkedIn Partner ID
  2. Replace the key/value pairs in the params object with your own data (and note, the values can be references to GTM variables, which is key here!)

What’s going on?

Now, this is what’s going on:

Once you’ve inserted your own LinkedIn Partner ID and defined your own key/values that you want included in the URL, this happens:

  • The new LinkedIn specific URL is constructed using the current page URL and the provided key/value pairs
  • A blank iframe is constructed on the fly (this iframe inherits the URL of the page) and is nserted into the page
  • The iframe is inserted into an invisible HTML container (a div in this case)
  • We run a window.history.replaceState to update the iframe’s URL with using our newly constructed LinkedIn specific URL, without loading the actual page
  • Then we build the ‘normal’ scripts otherwise provided by LinkedIn
  • Finally, the scripts are inserted into the iframe, making LinkedIn track the updated URL of the iframe

And there we go. While I’m writing this, I’ve got it up and running on two sites.

Use cases

I’ll mostly leave it up to you to define the use cases. Don’t use this if you don’t need it. Personally, I set this up for some specific use cases.

The one example is for a client of mine who uses a large CDP (Customer Data Platform). This CDP uses a bunch of machine learning algorithms to dynamically create various segments of customers in real-time.

So on each page load, the CDP pushes a number of segment names into GTM’s dataLayer. As the names of these segments are received, I use GTM to trigger the modified LinkedIn script where I include the names of those segments.

This allows us to build retargeting audiences on LinkedIn not only based on visited pages, but also based on the segments each of our visitors belong to.

Caveats

I specifically designed this as a dynamic iframe because I didn’t want the custom parameters to appear in Google Analytics. But still, in one test with 22K sessions, I found that 0.5% of those sessions included pages where the parameters appeared in GA anyway.

I haven’t been able to pinpoint the reason other than it seems to occur only in Firefox 69. And that’s on different operating systems and even on different device types.

So, I simply configured the view in Analytics to exlude those custom query parameters. And since it’s such a small percentage, I think that the added benefits far outweigh that small nuisance.

Final remarks

I’ve written this ‘plugin’ as a Custom HTML script. This can’t be done in a Custom Tag Template in GTM. Custom Tag Templates are very strict in terms of security, permissions and how a page can be manipulated.

And page manipulation is exactly what I’m doing here; i.e. creating and modifying HTML elements on the fly.

Also, and as always, it’s your own responsibility to test and use this in your environment.

Comment if you like it or have other nice use cases!