Search code examples
safaripage-refreshgoogle-dfp

How to find the cause of a constant page refresh in safari?


I'm trying to figure out an issue on a client website that has arisen in the past month or so, since the latest updates to both iOS and OSX. When users of Mac or iOS Safari are directed to any page in the site via a link in the EDM, they get caught in a constant loop of the page being refreshed. It also seems to leak memory between the refreshes, as Activity Monitor shows the Safari Web Content process for the site quickly grow in memory usage (got to 4 GB in two minutes or so before I killed it).

The issue can only be reproduced on the Safari browser. Chrome on Mac seems unaffected. Safari for Windows displays the refresh issue but does not seem to start hoarding memory, although the CPU usage of the WebKit2WebProcess.ee process sits at a rather high value continuously until the window is closed. I tried removing the utm_* parameters added to the EDM links one by one until I could confirm that the problem was caused by just utm_source on it's own, then found that I should have expected that as it's apparently the main tag GA looks for. More interesting is the fact that removing GA did not resolve the issue - I commented out the code creating the script tag (for what it's worth, the script in use was not the normal www.google-analytics.com/ga.js but rather the DFP one from stats.g.doubleclick.net/dc.js).

What does resolve the issue is not loading GPT - commenting the following code:

var googletag = googletag || {};
    googletag.cmd = googletag.cmd || [];
    (function() {
        var gads = document.createElement("script");
        gads.async = true;
        gads.type = "text/javascript";
        var useSSL = "https:" == document.location.protocol;
        gads.src = (useSSL ? "https:" : "http:") + "//www.googletagservices.com/tag/js/gpt.js";
        var node =document.getElementsByTagName("script")[0];
        node.parentNode.insertBefore(gads, node);
    })();

So I am fairly sure that GPT is detecting the utm_source parameter and trying to do ...something... which on Safari fails for ...Some Reason... Unfortunately the GPT code is obfuscated and I am not likely to find my problem just reading it, so I was hoping there is maybe some method to get the call stack at the moment before the page is refreshed in Safari's developer console that I am not aware of, or some other debugging technique I haven't thought of. Maybe someone else has even encountered this problem?

As far as I was aware the only script that cared about these parameters was Google Analytics, so I am also open to hearing I am heading down the garden path.

EDIT: I tried uncommenting the GPT script tag and then commenting the one place on the page that we push a command to it (with googletag.cmd.push()) in case our usage of it was causing the problems (we essentially just define then publish 8 slots in the pushed cmd, then enable ad services). Commenting this command did not resolve the issue, it appears to be related to having loaded GPT at all.


Solution

  • So this turned out to be an incredibly confusing issue on all counts, and continued to be so even after I fixed it.

    We were able to fix the issue by updating the History.js lib being used on the site which, on it's own, I can totally accept as a valid troubleshooting step (thus my trying it out). However I am totally confused as to how History.js and gpt.js would ever interface with each other. causing the issue to only occur under the situation:

    Using Safari Using older version of History.js using GPT (which should have no effect on History.js) Using utm_source tag (which should have no effect on History.js OR GPT)

    but I am happier to be confused by a fixed issue than a prevalent one, so I'm going to leave that in the 'too hard' basket.

    As an aside, my main question was to do, not with the issue persay, so here is how I troubleshot to the conclusion, in case it aids anyone else:

    • I added a script tag at the top of <head> which registered an event handler for beforeunload, calling the debugger statement. This would give me time to actually inspect the timeline and profiler before the page refreshed.
    • This showed me that immediately before the beforeunload event, a popstate event was called (with the benefit of hindsight I berate myself for not going straight to History.js when I saw popstate). This event installed a timer, which I noted was fired immediately, then every 1000 ms. It was the last thing to fire in the popstate event before the beforeunload event, so I figured there was a good chance it was causing the refresh.
    • Next, I spent some time pulling my hair out at the fact that safari does not expose the call stack of events in it's timeline. I know how to get that in Chrome, but I can't make the issue happen in Chrome. All I had to go on was the timer ID. Eventually I overrode the setTimeout like so:

      var origST = window.setTimeout;
      window.timers = [];
      window.setTimeout = function(f,t) {
        window.timers[origST(f,t)] = window.setTimeout.caller ? window.setTimeout.caller : 'Global Scope';
      }
      
    • And did the same for setInterval. Then I could reload the page, get the ID of the timer and then in the console check timers[the id] for the text of the function which had installed the timer.

    • Next I needed to search all the loaded script files for the function definition in question (it was an obfuscated and uglified script, not something I could easily guess) I'm not sure if this is actually possible in Safari, if it is I couldn't figure out how. Luckily this doesn't rely on reproducing the issue so I loaded up Chrome and used it's dev tools to search the script files.
    • Finding the function in History.js, we updated it and found the issue resolved.