Blog

Durandal and Google Analytics


Something I've noticed in the development of a recent application of mine, (the new front-end for DracoTal, was that Google Analytics wasn't recording all of the hits in the real-time monitor when testing on my iPhone. Granted, I had a filter set up for my home IP, but browsing through my cell network should have been landing more hits than it should have.

As it turns out, Google Analytics was only seeing the initial page hit, but not the further navigation. At least, not until the user reloaded the page, and then it would only grab the hit for the page that was reloaded. This was a bit of a problem as I wanted to see all of the hits during navigation! Fortunately I managed to get through this problem.

For the purposes of describing how I resolved this, I'll make the assumption that the code is using ASP.NET MVC as the front-end template, like say if you're using the Durandal Starter Kit as a template.

In the Home/Index.cshtml view, I had to ensure that I had this code in the head section of the HTML:

@{
    Layout = null;
}
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <meta name="apple-mobile-web-app-capable" content="yes" />
    <meta name="apple-mobile-web-app-status-bar-style" content="black" />
    <meta name="format-detection" content="telephone=no" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta name="fragment" content="!">
    <meta name="p:domain_verify" content="390ee599d576a99e7419550da9282ae5" />
    <link rel="icon" href="http://dracotal.nodeomega.com/favicon.ico" />
<!-- snip irrelevant metas -->
    <title>DracoTal</title>
    @Styles.Render("~/Content/durandalcss")
    @Scripts.Render("~/bundles/modernizr")
    
    <!-- Google Analytics -->
    <script>
        (function (i, s, o, g, r, a, m) {
            i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
                (i[r].q = i[r].q || []).push(arguments);
            }, i[r].l = 1 * new Date(); a = s.createElement(o),
            m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m);
        })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');

        ga('create', 'UA-{my Google Analytics ID}', '{specific site}');
        ga('send', 'pageview');

    </script>
    <!-- End Google Analytics -->

    <script type="text/javascript">
        if (navigator.userAgent.match(/IEMobile\/10\.0/)) {
            var msViewportStyle = document.createElement("style");
            var mq = "@@-ms-viewport{width:auto!important}";
            msViewportStyle.appendChild(document.createTextNode(mq));
            document.getElementsByTagName("head")[0].appendChild(msViewportStyle);
        }
    </script>
</head>

That {specific site} part turned out to be very important. The default in Google's example was 'auto', which didn't work for me.

Anyways, the next step. One example by Peter Morlion I did see that referenced Durandal and Google Analtyics seemed to put the next code in the router activate method in the shell.js file like this:

router.on('router:navigation:complete', function (instance, instruction) {
    ga('create', 'UA-XXXXXXX-X', 'example.com');
    ga('send', 'pageview');
}

I have opted not to use the create line, and I wanted more control over the page title that would be sent, as the way it was, it going to be the default title not formatted for parameters in the Products page (which I wanted to record when they were hit). If not for this, I could have left it in the above call. For my purposes, instead of placing my solution in the router.on('router:navigation:complete' ...) part, I decided to place the code in my viewmodel's attached: property instead. Observe:

// Other Durandal viewmodel setup...
var section = ko.observable();

return {
    section: section,
    displayName: 'Products', // a default value for this page.
    GetProductImageByColorId: getProductImageByColorId, // later function
    attached: function () {
        var that = this;

        ga('set', { [age: window.location.pathname, title: that.displayName + ' | [Site Name]' }); // matches my Title convention for this site
        ga('send', 'pageview');
    },
    activate: function (mainsection, subsection, subsubsection, subsubsubsection) {
// more code...
}

The ugly part is, I would have to do this in every page's attached: call, and fortunately it was just a couple pages. If you have dozens of views or more, you would want to come up with a different way. For my purposes, though, this will do.

Once I implemented this and deployed, and then checked my Google Analytics realtime monitoring with my phone, I did see each individual hit recorded as I wanted them to be. And later on in the day, I checked the Behavior overview for that same day, and the site hits were in fact recorded! Granted, my analytics data for today and yesterday aren't going to be accurate, but it was in the name of testing and experimentation, and it's one of my sites, so in this case I feel it was worth it.

I suppose you could use the create line too. However, according to Google Analytics Unversal Analytics Guide section on single page applications:

Do not create new trackers in a single page app in an attempt to mimic what the analytics.js snippet code does for traditional websites. Doing so runs the risk of sending an incorrect referrer as well as incorrect campaign data as described above.

Going by that, that could skew your results depending on your goals. It seems to work fine for me without having the ga create part in the attached: portion of my viewmodel, so I'm going with that.

P.S. Yes, that activate has some pretty ugly names for parameters, but it made sense for the purposes of the site and how they were used. And yes, you can do multiple optional parameters in Durandal routing!

And to demonstrate, in the shell.js...

define(['plugins/router', 'durandal/app', 'knockout'], function (router, app, ko) {
    return {
        copyright: ko.observable('© 2009 - '
            + new Date().getFullYear()
            + ' [Site Name].  Store powered by <a href="http://www.cafepress.com">CafePress</a>.'),
        router: router,
        activate: function () {
            router.map([
                { route: '', title: 'Home', moduleId: 'viewmodels/home', nav: true },
                { route: 'Products(/:section)(/:subsection)(/:subsubsection)(/:subsubsubsection)', moduleId: 'viewmodels/Products/index', title: 'Products', hash: '#Products', nav: true },
                // Snip other routes...
                { route: 'NotFound', moduleId: 'viewmodels/not-found', title: '404 - Not Found', nav: false}
            ]).buildNavigationModel()
            .mapUnknownRoutes('viewmodels/not-found', 'not-found');
            
            return router.activate({ pushState: true });
        }
    };
});

Have fun with Durandal, and feel free to leave feedback! (Once I implement commenting on this blog, that is.)

Links

Archive