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.)

Creating iCal files in ASP.NET MVC and C#


Recently, during a project I was working on for a client, I was tasked with adding events to a user's iCal in addition to being able to add events to Google Calendar. In researching this, I found that this would necessitate creating a .ics file for each event. Given that my client's site had hundreds if not thousands of events, that would be a lot of .ics files sitting around. There had to be a solution to this.

Fortunately, there is, and ASP.NET MVC and C# makes this surprisingly painless.

First, we need to know what goes into a iCal .ics file. This PasteBin page gives a very rough overview of a partial iCal format (the parts that I needed, as a bonus). It's at the bottom of the window, below the Google Calendar API and Yahoo Calendar API if you need those as well.

Now, while I had some of the spec, I needed a way to put it together into a .ics file. Now, there's some libraries out there, but I ran into this post at Balajiprasad's useful codes that had some sample code that I could adapt. He used a library for his solution. I opted for a simple Stringbuilder approach to start.


/// <summary>
/// Generates an iCalendar .ics link and returns it to the user.
/// </summary>
/// <param name="downloadFileName">Name of the download file to return.</param>
/// <param name="evt">The Event.</param>
/// <returns>The .ics file.</returns>
[System.Web.Mvc.HttpPost]
public ActionResult AddToICalendar(string downloadFileName, int eventId)
{
    // replace db with however you call your Entity Framework or however you get your data.
    // In this example, we have an Events collection in our model.
    using (
        var db =
            new ExampleEntities(
                ConfigurationManager.ConnectionStrings[YourConnectionString].ConnectionString))
    {
        // Alternatively, you may use db.Events.Find(eventId) if this fits better.
        var demoEvent = db.Events.Single(getEvent => getEvent.ID == eventId);

        var icalStringbuilder = new StringBuilder();

        icalStringbuilder.AppendLine("BEGIN:VCALENDAR");
        icalStringbuilder.AppendLine("PRODID:-//MyTestProject//EN");
        icalStringbuilder.AppendLine("VERSION:2.0");

        icalStringbuilder.AppendLine("BEGIN:VEVENT");
        icalStringbuilder.AppendLine("SUMMARY;LANGUAGE=en-us:" + demoEvent.EventName);
        icalStringbuilder.AppendLine("CLASS:PUBLIC");
        icalStringbuilder.AppendLine(string.Format("CREATED:{0:yyyyMMddTHHmmssZ}", DateTime.UtcNow));
        icalStringbuilder.AppendLine("DESCRIPTION:" + demoEvent.Description);
        icalStringbuilder.AppendLine(string.Format("DTSTART:{0:yyyyMMddTHHmmssZ}", demoEvent.StartDateTime));
        icalStringbuilder.AppendLine(string.Format("DTEND:{0:yyyyMMddTHHmmssZ}", demoEvent.EndDateTime));
        icalStringbuilder.AppendLine("SEQUENCE:0");
        icalStringbuilder.AppendLine("UID:" + Guid.NewGuid());
        icalStringbuilder.AppendLine(
            string.Format(
                "LOCATION:{0}\\, {1}\\, {2}\\, {3} {4}",
                evt.LocationName,
                evt.Address,
                evt.City,
                evt.State,
                evt.ZipCode).Trim());
        icalStringbuilder.AppendLine("END:VEVENT");
        icalStringbuilder.AppendLine("END:VCALENDAR");

        var bytes = Encoding.UTF8.GetBytes(icalStringbuilder.ToString());

        return this.File(bytes, "text/calendar", downloadFileName);
    }
}

And then, within the .cshtml file in question, we call it like this:


@using (Html.BeginForm("AddToICalendar", "Home", new { downloadFileName = "thisEvent.ics", eventId = Model.TheEvent.ID }))
{
    <input type="submit" value="Add to iCal" class="ical-button" id="button" /><br />
}

This will generate a form and will, when clicked, call the AddToICalendar function. It will then retrieve the event data and populate the fields in the generated .ics file with the relevant data. As a bonus, should you have the actual location data, someone on an iPhone or iPad can actually call up the map and get directions to go to wherever the event is located. (Now, how to call something other than Apple Maps as a default for this, I've yet to figure out and would prefer Google Maps or, better yet in my case, Waze).

The new Guid for the UID line will assign a new ID to this event. Now, if you had an event that would be updated, and wanted someone to just click on the same link and update their calendar that way, I would wager that you would have a different way of setting the UID earlier on and keeping it the same for the same event. I haven't had the opportunity to test this yet, but that's my understanding based on my research.

As an additional bonus, you can even download the .ics file on a Windows system and if you have Outlook, you can import that as an event into that calendar. I haven't tested this with other calendar applications or even Google Calendar. So in the end, this proved to not just be for iCal.

Also, if you wanted to keep the start and end date/times to a local time, you could remove the Z part in the string.Format strings for each, which would relegate it to be local to the user's time. Still, it's much better to use UTC-based time for everything in my opinion when feasible. As far as the formatting string, the {0:yyyyMMddTHHmmssZ} part would map to 20150601T123000Z if given a date/time of June 1st, 2015 at 12:30 UTC. Eliminate the Z part, and it becomes June 1st, 2015 at 12:30PM local time.

You'll also notice the double-backslash in front of the commas. For some reason, iCal needs those commas to be escaped, and a single backslash just won't do the job using this method. I've tested this without doing that in the generated iCal file, and it didn't work very well. On my iPhone, it would only put the LocationName before the first comma into the Location field on the Event entry in my iCal. With the proper escaping, it puts the entire address. I haven't tested what happens if you have a comma in one of those fields, so if you run into any problems you could easily put a .Replace(",", "\\,") onto the appropriate lines. The DESCRIPTION line, however, did not have this problem and did not seem to require the escaped commas, so it may just be for the LOCATION line. Have no worries about importing this into Outlook, as the backslashes won't show in the Location line on those events, at least not on my local Outlook with Office 365.

In closing, this should be a help to anyone who needs a basic iCal file to share events and reminders. I imagine the process would be similar if you wanted to make Reminders for iPhone/iPads the same way, and there are other fields that are available to put in these files too. This documentation at kanzaki.com should also be useful, however I don't really know if this is up to date. It's worth a shot though if you want to experiment. Happy coding.

Links

Archive