Get Skylight

Bringing Sanity to JavaScript UTC Dates with Moment.js and Ember Data

One "neat" (awful) feature in JavaScript is that it automatically changes dates to be in whatever timezone the user's computer is set to.

This is great if you are building the online version of The Podunk Gazette and all your servers and users are in one town. Otherwise, it can introduce small inconsistencies between clients and your backend servers.

This bit me this week when a customer reported a bug in the daily request chart on the Skylight billing page, a screenshot of which I've included here in case you aren't familiar with it.

Daily request chart

"We received a spike in traffic on the 16th, but your chart shows all of the traffic on the 15th. What gives?" the customer asked.

Good question! I went to investigate.

Our server reports usage to the Ember app by providing three values:

  1. The usage period start date
  2. The usage period end date
  3. An array of request counts

JSON payload

As you can see in the screenshot, our Rails server serializes dates into the ISO 8601 format for consumption by JavaScript.

One nice thing about ISO 8601 is it makes it easy to represent a datetime in UTC.

If you're not familiar with UTC, it stands for Coordinated Universal Time and defines a standard for coordinating datetimes across timezones.

Basically, just do everything in UTC everywhere, and you won't have to worry about timezones.

(If you're now pointing out that the abbreviation for Coordinated Universal Time is CUT and not UTC, you'd be right. The English-speakers wanted CUT and the French-speakers wanted TUC, for temps universel coordonné. In the spirit of compromise, it now doesn't make sense in either language. Progress!)

ISO 8601 defines a shorthand for denoting that a time is in UTC: just add a Z to the end. For example, if you wanted to represent 9:30am UTC in ISO 8601, it would be "09:30Z".

Additionally, JavaScript's Date object supports ISO 8601 out of the box.

Great! Back to Skylight. We can see in the screenshot that this particular billing period starts at "2014-09-13T00:00:00.000Z": September 13th, 2014.

Our entire billing system uses UTC, and we can see this date is provided in UTC because it ends in Z.

Let's see what happens when we put this date into JavaScript:

JavaScript Date WAT

WAT

JavaScript, like a well-meaning but not-very-bright puppy, has eagerly gone and done the wrong thing. We wanted to represent a specific date—September 13th—but, after adjusting to my local timezone, the date is now September 12th! All of the date-related UI will be off by one day.

In Skylight, we use Moment.js for all of our date parsing and formatting. Moment is a friggin' gem of a library and has made the process of writing Skylight way easier. I have no idea how people write JavaScript apps without Moment.

Moment, wisely, inherits JavaScript's behavior out of the box:

I was starting to despair. All of our UI code uses Moment to format dates and times for human consumption, and I was not looking forward to auditing everything to make it UTC aware.

That's when I discovered that Moment.js has a UTC mode. Once you put a Moment date into UTC mode, all of its display functions use UTC instead of the local timezone.

Salvation was at hand. My first inclination was to go into all of the UI code (mostly Handlebars helpers and Ember components that wrap D3) and make sure we normalized all dates into UTC before drawing.

This was going to be a lot of work, and if there's anything I hate, it's working hard. I took a step back and wondered if there was an option other than quitting my job.

Fortunately an easier solution sprang to mind, and ultimately I think it's a better one. Why not just normalize all dates into UTC at the boundary, as they're received from the server?

We are lucky enough to be using Ember Data, so this was insanely easy. Ember Data does all of the work of talking to the server, and supports the notion of attribute types.

Ember Data supports several different types out of the box: numbers, dates, Booleans, and strings. But this list is easily extended with transforms. A transform is just an object that determines how a JSON value is deserialized into a JavaScript object, and vice versa.

In this case, I wanted to define a transform called utc that created a new Moment date object in UTC mode.

This is RIDICULOUSLY EASY in Ember Data and requires zero configuration. If you're using Ember CLI (as we are), just create a new file called app/transforms/utc.js. Boom, done: we now have a transform called utc.

Inside utc.js, export a new subclass of DS.Transform:

import DS from "ember-data";

export default DS.Transform.extend({  
  serialize: function(value) {
    return value ? value.toJSON() : null;
  },

  deserialize: function(value) {
    return moment.utc(value);
  }
});

Transforms implement two methods:

  1. serialize, which takes a JavaScript object and returns the value that should be used in the JSON payload sent to the server.
  2. deserialize, which takes the JSON value and returns the value you want to use in your app.

Next, I just had to go into all of my models and change the attribute type from date to utc. For example:

// app/models/subscription.js

export default DS.Model.extend({  
  plan: belongsTo('plan'),

  features:       attr('string'),

  subscribedAt:        attr('utc'),
  unsubscribedAt:      attr('utc'),
  currentBillingCycle: attr('utc'),
  nextBillingCycle:    attr('utc'),
  nextInvoiceAt:       attr('utc')

  // ...
});

When I reloaded the app, boom, all of the dates displayed were based on UTC time, not the fact that I happened to be in Oregon at the moment.

In summary, tools like Moment.js and Ember Data don't just let you be a better developer. They also let you be very lazy, so you have more time to drink, which is definitely necessary if your job involves programming with dates and times.


NB: Kris Selden, Stefan Penner and Matthew Beale point out that Moment.js date parsing can be a slow operation. If you're loading many records, you may want to consider serializing dates as seconds since epoch, and doing the parsing at the UI layer. In our case, however, we only have a few billing-related models, so it did not become a performance bottleneck.

Does your Rails app feel slow but New Relic is telling you everything's fine? Sign up for a free 30-day trial of Skylight, the better Rails profiler.