Tuesday, October 2, 2012

How to trick Safari and set 3rd party tracking cookie using HTML5 localStorage

Introduction to tracking cookies

“Tracking cookies” is a very important part of online advertising ecosystem. They tons of usage scenarios. Here's one example called retargeting.

It's known that a lot of internet shoppers doesn't make purchase right after they saw a good deal on e-commerce website. They first choose a good, leave website and return in couple of hours or days to make an actual order.

To stimulate those users, websites utilize so-called retargeting technology. Basically, they want to remember users who left their website without making an order and show them a relevant advertisement on other websites. Typically e-commerce websites delegates such work to online advertisement platforms, like AdExchanges, DSPs and so on.

From, technical point of view it works as follows:

  1. Website owner puts a small piece of HTML code. The piece of code is called "tracking pixel". Let's consider a simple case when the tracking pixel is a transparent GIF image:
  2. 
    <img src="http://pixel.sample-ad-exchange.com/pixel.gif">
    
  3. http://pixel.sample-ad-exchange.com/pixel.gif drops a cookie for domain '.sample-ad-exchange.com' with name user_id. In this cookie a generated unique user id is stored (If the cookie already exists, server just skips this part)
  4. sample-ad-exchange.com remembers internally that user with this id visited e-commerce site
  5. When sample-ad-exchange.com is requested to show an ad somewhere else (by calling tag.sample-ad-exchange.com/show_ad.js for example) it receives user_id cookie along with http request
  6. sample-ad-exchange.com checks internally if this user visited any e -commerce sites before. If he has, it could show a very relevant ad to him

The problem

As you can see, ability to drop cookie is the viable part of retargeting scheme. This kind of cookies is called "3rd party cookies" because pixel code is sitting on advertiser domain (e.g. my-cool-store.com), and pixel itself is located on 3rd-party ad-exchange domain (.sample-ad-exchange.com). By default, different browsers have different policy about 3rd party cookies.

  • Chrome, Firefox, IE before 8.0 - always accept 3rd party cookies
  • IE 8.0 and above - accept 3rd-party cookie only if website explicitly declared how it will use the cookies. The declaration is done via P3P protocol. As every spec from W3C, this one is also very cryptic. But the essence is the HTTP header called "P3P" that you need to send along with http response containing cookie. This header content works fine though I have no idea what's exactly it's declaring: 'P3P: CP="NOI DSP COR NID CURa ADMa DEVa PSAa PSDa OUR BUS COM INT OTC PUR STA"'
  • Safari - never accepts 3rd party cookies

Safari wasn't a huge problem for industry before iPad appeared and gained huge popularity. Studies shows that iPad users tend to shop online even more than usual PC guys.

Trick 1.0 (not working anymore)

In fact Safari sometimes doesn't reject 3rdparty cookies. It happens than user did some action related to 3rdparty domain. Google Analytics (and other platforms too) took advantage of this feature: they inserted an iframe and simulated form sumbit inside it. I won't stop on technical details here. First, this hack cost google $22.5 millions and second the trick isn't working anymore in last versions of Safari

Trick 2.0 (HTML5 localStorage)

The idea of this trick is use HTML5 localStorage API. This API is very similar to cookies - it allows managing user’s preferences from javascript and storing it locally on user's box. Why not store user id in localStorage? The first version of code I came up with:


<script type="text/javascript">
    if (typeof navigator != "undefined" && typeof navigator.vendor != "undefined" && navigator.vendor.indexOf("Apple") >= 0 && typeof localStorage != "undefined") {
        //Check if browser is made by Apple (means it's Safari) and local storage is available
        var userId = localStorage.getItem("user_id");
        if (userId == null) {
            //set user is if user is unknown
            userId = Math.random();
            localStorage.setItem("user_id", userId);
        }
        var img = document.createElement('img');
        img.src = "http://pixel.sample-ad-exchange.com/pixel.gif?user_id=" + user_id;
        var body = document.getElementsByTagName('body')[0];
        body.appendChild(img);
    }
</script>

The idea is pretty straightforward: look for user_id key in local storage (create one if it doesn't exist) and pass user_id to pixel server as GET parameter. Then server will record this id instead firing the cookie.

But this code isn't working well. Each domain has it's own local storage. And if you tracking pixel was fired at my-cool-store.com user_id will be stored in my-cool-store.com local storage. If the same user would visit other-domain.com with tracking code later on it will be treated as new one.

To fix that old good trick with iframe will work. Instead of img tag we will insert iframe tag with source somewhere inside pixel.sample-ad-exchange.com. And place user detection code inside iframe. As iframe is executed "inside" pixel.sample-ad-exchange.com local storage will be the same for all tracked sites. Here's a complete example:

Tracking code:


<script type="text/javascript">
    if (typeof navigator != "undefined" && typeof navigator.vendor != "undefined" && navigator.vendor.indexOf("Apple") >= 0 && typeof localStorage != "undefined") {
        var iframe = document.createElement('iframe');
        img.src = "http://pixel.sample-ad-exchange.com/iframe.html";
        var body = document.getElementsByTagName('body')[0];
        body.appendChild(img);
    }
</script>

Iframe code (http://pixel.sample-ad-exchange.com/iframe.html)


<html>
<head></head>
<body>
<script type="text/javascript">
    var userId = localStorage.getItem("user_id");
    if (userId == null) {
        //set user is if user is unknown
        userId = Math.random();
        localStorage.setItem("user_id", userId);
    }
    var img = document.createElement('img');
    img.src = "http://pixel.sample-ad-exchange.com/pixel.gif?user_id=" + user_id;
    var body = document.getElementsByTagName('body')[0];
    body.appendChild(img);
    </script>
</body>
</html>

Legal issue

The interesting question is if this method is legal. Znd if next company using it will get $22.5 million fine. I'm not a lawyer, but from my common sense perspective as Safari settings explicitly says "Block thirdparty cookies from third parties and advertisers" and localStorage isn't a "cookie" the approach above seems legit: