AdWords Conversion Tag inside out!
Tech-News from Googles Codebusters @Webrepublic [tweet this!]
Here we go: the second part of our little write-up about the Google AdWords conversion script. After we took a close look at the script itself in the last part, we're going to discuss some of the challenges of implementing conversion tracking on an AJAX-enabled page in this installment.
To recap: Last time we tried to make sense of the obfuscated mess that the script presents at first glance, and discovered an interesting debug mode. We were so intrigued that we sat down and created a very simple Chrome extension to activate said debug mode on a permanent basis, and at the time of writing, there were actually a few dozen people using it. A resounding success!
Now, if we take it a bit further back and try to remember why we even wanted to analyze the conversion script in the first place, we will recall that there is a thing that Adwords conversion scripts do not particulary like: being dynamically loaded after the page's onload event has passed. To quote:
We recently stumbled about this subject ourselves when we wanted to insert remarketing tags on our own web site. On our site, there are different “main” categories, whose subpages are loaded dynamically using jQuery. When we just inserted the standard remarketing script into the loaded content…
So what is going on? There's multiple issues. For starters, we load the new content using (eventually) jQuery's html method, which allows the caller to replace the HTML contents of the given DOM node. But what the documentation doesn't tell you is that any script elements in the new content are specially handled. From a comment on the jQuery site:
All of jQuery's insertion methods use a domManip function internally to clean/process elements before and after they are inserted into the DOM. One of the things the domManip function does is pull out any script elements about to be inserted and run them through an "evalScript routine" rather than inject them with the rest of the DOM fragment. It inserts the scripts separately, evaluates them, and then removes them from the DOM.
So that's the reason why we can't see them and why they're hard to debug. But according to the above comment, they still are executed. So even though the script is invisible, it should still track a conversion---which it did not.
It turns out (and probably was expected by the more technically inclined reader) that the script inserts the tracking pixel which submits the actual conversion information by using the document's write method. Now, using document.write to insert stuff into the DOM is generally frowned upon, but as far as compatibility and simplicity is concerned, it is probably still unmatched (and that is why Google is using it).
However, the write method has a big drawback: it only works as long as the document has not been loaded (respectively, document.close has not been called). After that, it produces undesirable results---namely opening a new document, thus overwriting the previous DOM. So it clearly cannot be used for scripts that are injected into the DOM after the load event has fired.
But wait, that still doesn't explain our situation! If the above holds, then the dynamically loaded conversion script should overwrite our whole document and just leave the GIF request that's added via document.write. But that is not the case, the script in fact doesn't seem to do anything at all.
The solution lies again within jQuery. When it parses HTML data, script tags are handled peculiarly insofar as they are moved to the top level element. While the documentation warns that during parsing, "some browsers may not generate a DOM that exactly replicates the HTML source provided," this specific behavior was a bit surprising to us. As an example:
$parseTest = $('<div id="parse-test"><script>alert("Just a test");</script></div>');console.log($parseTest);
results in the following jQuery object (at least in WebKit)
[<div id="parse-test"></div>,<script>alert("Just a test");</script>]
After parsing, the script element is no longer a descendant of the div element, but a sibling. This does actually make some sense, as script elements should be either inside a head or a body element.
How does this relate to the original problem? Well, when we're AJAX-loading new content on our site, we download and parse a whole page but then only insert the main container's content into the current page. As a consequence of this, we put the conversion tracking snippet inside that main container, too, so that it would also be inserted. However, as the script element is moved to the top level (body) element by jQuery, it is never actually inserted into the DOM and thus not executed. No script execution, no conversion...
What we still didn't cover is how we actually solved the problem. That's left for the next part of the series, so stay tuned for more!