Ember Services: Logging Route Transitions

If you're hosting a web application it's more than likely that you are logging user visits and interactions. Sometimes it's enough to hook up a service like Google Analytics or Mixpanel, but in some situations you will also want to roll your own custom logs.

I ran into an interesting logger one day when I was looking at network requests sent by Netflix. Every couple of clicks or transitions Netflix "phones home" to send a comprehensive list of interactions. This is what some of the data looks like:

Netflix Logger Payload

As you can see, they're gathering lots of interesting stuff. I also liked their logging endpoint http://www.netflix.com/ichnaea/log - Ichnaea seems to be the Greek goddess of tracking.

This is what inspired today's post, in which I will introduce the concept of services in Ember and show you how to create a simple logger for gathering basic data, which would then be shipped of to our custom backend. This example will log all route transitions.

What are Services?

An Ember.Service is a singleton object that lives "outside" of the usual application. It can be injected into our routes or components, from where we can access it and call its methods.

This mechanism can be very useful for creating objects that should be accessible from wherever in the application and which should keep their state. For example, we could create a shopping cart service in a store application for keeping and manipulating items.

Setup

Okay, let's set up our Ember project. If you're not patient enough, just visit this Github repository to see the code.

We'll start by generating a new project via Ember CLI:

$ ember new ember-logging-service

After Ember CLI is done, enter the newly created directory with cd ember-logging-service.

Since the service we're about to create will log route transitions, we'll need to set up some sample routes. Open up app/router.js and add some:

// app/router.js
import Ember from 'ember';  
import config from './config/environment';

var Router = Ember.Router.extend({  
  location: config.locationType
});

Router.map(function() {  
  this.route('firstroute');
  this.route('secondroute');
  this.route('thirdroute');
});

export default Router;  

We don't need to generate any actual routes or templates, as we won't be displaying anything specific. Defining routes in router.js will be enough for Ember to create the routes implicitly, without us having to actually create the files.

Additionally, open up the application template and add the following:

Open Developer Tools, watch the console and click the below links a couple of times.

<p>Routes:</p>  
<ul>  
  <li>{{link-to 'Home' 'index'}}</li>
  <li>{{link-to 'First' 'firstroute'}}</li>
  <li>{{link-to 'Second' 'secondroute'}}</li>
  <li>{{link-to 'Third' 'thirdroute'}}</li>
</ul>

{{outlet}}

This will render a links, so that we're able to transition between routes.

The Service

Now let's generate the actual service and populate it with some methods. First, execute this command in your terminal:

$ ember g service logger

Ember CLI will create a services folder (if it doesn't exist yet) and add a logger.js file inside.

Here's the content of our service, an explanation follows below:

// app/services/logger.js
import Ember from 'ember';

export default Ember.Service.extend({  
  events: [],

  log(obj) {
    let eventObject = {
      time: new Date()
    }
    this.get('events').pushObject(Ember.merge(eventObject, obj));
    this.saveLogs();
  },

  saveLogs() {
    if(this.get('events.length') >= 5) {
      let log = {
        app: 'emberlogger', // an app name for your backend
        events: Ember.copy(this.get('events')),
        locale: navigator.language,
        time: new Date(),
        userAgent: navigator.userAgent
      }
      console.log(log); // push logs to server
      this.get('events').clear();
    }
  }
});

First we define an empty events array, which will hold the log events received by the service.

Next we define a log function, which accepts an object. The function will define a new object containing a timestamp. We want to combine this timestamp object with what was passed in as an argument: for that we'll use Ember.merge.
Afterwards the merged object is pushed into the events array of our service.
Finally, we call the second function of our service, which we have called saveLogs.

We only want to persist logs to the server once in a while, to reduce the request count. In our example we'll do this by simply checking whether we have at least 5 events waiting. Instead we could just as well implement a self-calling timer, which would send logs to the server every x seconds. Or we could open a websocket connection and push the logs in realtime.

Anyway, the saveLogs function declares a new event container object, we called it log. The properties of this object could be anything we might find useful on the server. Here we will send information such as:

  • A name of the application, so that the server knows how to index the message
  • A locale property, to categorize the user's language
  • A timestamp
  • A user agent property, for the server to be able to analyze user browsers/devices

Of course, we also attach all the events we've gathered so far. We're using Ember.copy, because we want to use a fresh array and not point to the internal object, which will be emptied using the clear() method.

In our example we simply console.log, but in a real-world scenario we'd add an AJAX request (or websocket message).

Initializing the Service

Right now our service won't do much. Services are instantiated once they're called for the first time. In order to do that, we will have to inject it somewhere and actually use it.

Let's generate an initializer:

$ ember g initializer logger

Now open the newly created app/initializers/logger.js and add the following:

// app/initializers/logger.js
export function initialize(application) {  
  application.inject('route', 'logger', 'service:logger');
  application.inject('component', 'logger', 'service:logger');
}

export default {  
  name: 'logger',
  initialize: initialize
};

Now we are able to access the service in all our routes and components by simple accessing this.get('logger'). We could also inject a service directly from the route/component:

export default Ember.Component.extend({  
  logger: Ember.inject.service()
});

But we won't use this method in our example.

Logging Route Transitions

So far we have defined some sample routes, a basic template and the logger service, which we've also injected into all routes and components. How do we go about actually logging route transitions?

It would be best if transitions were logged automatically, without us having to define any actions in all our routes. We could do this via a mixin and extend all routes with that, but we won't do that.

We will reopen the Ember.Route and override the afterModel method. By reopening the route, we will have added functionality that is shared between all instances of it. That means that all routes in our application will use the overridden method we've defined.

As far as I know, there is no built-in way in Ember CLI to handle such reopens (if I'm wrong, please feel free to correct me in the comments!). Let's manually create an override:

$ mkdir app/reopens
$ touch app/reopens/router.js

We have created a reopen folder for holding our custom definitions. Add the following to the newly created file:

import Ember from 'ember';

export default Ember.Route.reopen({  
  afterModel(resolvedModel, transition) {
    this._super(resolvedModel, transition);

    if(this.routeName === transition.targetName) {
      this.get('logger').log({
        type: 'transition',
        target: transition.targetName,
        queryParams: transition.queryParams
      });
    }
  }
});

First, we call the _super method, so that we won't lose anything important from the parent implementation. Next, we'll finally get to logging the transitions!

We only want to log a transition when the new route's name matches the transition's target. If we didn't have that, we might end up with unnecessary duplicate logs. For instance, when we first access the Ember application, the routeName would be application with a target of index. Immediately after that we'd have another transition with routeName of index and the same target. We simply want to avoid these duplicates.
You don't have to do that though, maybe you'd prefer to actually log all transitions.

Now we access the previously injected logger service and make use of its log method to push a new event. We add a 'type' property valued 'transition', so that the backend is able to categorize the event. We also add the target route's name and query parameters (might get useful for analytics).

Finally we need to import the above reopen into our application. Ember CLI will ignore the file we have just defined, so we will modify the app/app.js file:

// app/app.js
import Ember from 'ember';  
import Resolver from 'ember/resolver';  
import loadInitializers from 'ember/load-initializers';  
import config from './config/environment';  
import Route from './reopens/route';

var App;

Ember.MODEL_FACTORY_INJECTIONS = true;

App = Ember.Application.extend({  
  modulePrefix: config.modulePrefix,
  podModulePrefix: config.podModulePrefix,
  Resolver: Resolver
});

loadInitializers(App, config.modulePrefix);

export default App;  

We have only added one line here, and that's import Route from './reopens/route';. This will be enough for Ember to actually use our override.

Try it out!

Phew, that's it! Now we're able to play with what we've just created. Fire it up:

$ ember serve

Visit your browser under http://localhost:4200 and click the links a couple of times. Remember that we'll only offload logs once we have at least 5 of them. This is what it'll look like:

Ember Logging Service Console Sample

You can play around with it on your owner under this link or by cloning the code from this repository.

Summary

We have successfully created a simple logger service for logging route transitions in Ember applications. This was of course a very simple and crude example of what one could make with Ember.Service.

I hope this post will inspire you to introduce services in your apps.

If you have found any bugs, typos or wrongdoings in this post, or if you simply want to talk - please leave a comment below! I'd appreciate any input.

Thanks for reading and see you next time!

Kevin P. Kucharczyk

Read more posts by this author.

Kraków, Poland