emberjs

A collection of 3 posts

How to reset the EmberJS router namespace with this.route()

With this.route() being fully nestable as of Ember 1.7.0, the best practice has been to drop use of this.resource() altogether. I highly recommend always using this.route() for one reason in particular: preventing conflicting route names.

Example

In our sample app, we might have a route called workspaces, with an administrative area at admin.workspaces. If we were to follow the older convention of declaring any route with a noun as name as a resource, the route file should look like this:

// router.js using this.resource()

Router.map(function() {
  this.resource('workspace');
  this.route('admin', function () {
    this.resource('workspace');
  });
});

This router will generate route names of workspace, admin, and workspace.

Wait. Hold up. Two routes with workspace as the name?

You bet. Using this.resource() resets the namespace of the route. You can override the nested workspace route name by doing this.resource('admin.workspace', { path: /workspace }), but why bother when this.route() can take care of this for us?

// router.js using this.route()

Router.map(function() {
  this.route('workspace');
  this.route('admin', function () {
    this.route('workspace');
  });
});

Now we get generated route names of workspace, admin, and admin.workspace. Neat!

If in the event that you absolutely must reset the namespace of a nested route, you can use the { resetNamespace: true } option on the route.

// router.js using this.route() with reset namespace

Router.map(function() {
  this.route('workspace');
  this.route('admin', function () {
    this.route('workspace');
    this.route('project', { resetNamespace: true });
  });
});

We get the same three route names generated as the previous example, as well as a new route at project. That's right, no admin. is prepended. Resetting the namespace can be used prudently for lengthy route names. Be careful only to reset when you can guarantee that you'll likely never have a naming collision with another route.

Effective error reporting with EmberJS and Bugsnag

An important part of a professional application is catching and reporting bugs. In Candidio, we report bugs from our EmberJS application to Bugsnag. Bugsnag has great documentation and a good amount of language support with their first-party notifier libraries. We're quite happy with their service and look forward to using them for the forseeable future.

Raygun recently detailed how to send error reports to their service, so I thought I'd pass along how we accomplished a similar feat with Bugsnag.

Bugsnag.js

Get Bugsnag's Javascript notifier with Bower using the command bower install --save bugsnag. Import the bugsnag.js source into your app in Brocfile.js with app.import('bower_components/bugsnag/src/bugsnag.js');.

In .jshintrc add "Bugsnag": true to the predef hash, so you don't get squawked at every time you use the Bugsnag global variable.

Initializer

Create a new initializer at app/initializers/bugsnag.js on the command line with the ember g initializer bugsnag command. In the initialize function, we're going to declare the Bugsnag API key, the releaseStage (current environment), and the stages we want to notify Bugsnag from. In Candidio, we only send Bugsnag errors from our staging environment and the production site.

Bugsnag.apiKey = config.APP.BUGSNAG.API_KEY;
Bugsnag.releaseStage = config.environment;
Bugsnag.notifyReleaseStages = ["staging", "production"];

Routing Errors

The first type of error we want to report is routing and edge cases, which we'll catch with Ember.onerror. In this error report and the following, we're sending along the current path, rather than letting Bugsnag grabbing the URL from location.href. I've found that in the context of EmberJS development, this is more useful.

Ember.onerror = function(error) {
  Bugsnag.context = appController.get('currentPath');
  Bugsnag.notifyException(error);
  console.error(error.stack); // Still send the error to the console
};

Promise/AJAX Errors

We should also catch errors from rejected Promises, which we can do similarly with the RSVP.on('error') method.

Ember.RSVP.on('error', function (error) {
  Bugsnag.context = appController.get('currentPath');
  Bugsnag.notifyException(error);
});

Ember Logger Errors

Let's also send any errors logged by Ember's internal logger service and make sure they still get sent to the console.

Ember.Logger.error = function (message, cause, stack) {
  var currentRoute = appController.get('currentPath');
  Bugsnag.context = currentRoute;
  Bugsnag.notifyException(new Error(message), null, { cause: cause, stack: stack });
  console.error(stack);
};

Bonus: Javascript Sourcemaps

Sourcemaps are enabled by default on the ember-cli development build, but to get a sourcemap on every build, including production, add the following sourcemaps hash in Brocfile.js. Bugsnag will get the sourcemap declaration at the bottom of our concatenated source and give you a much improved stack track over the minified source.

// Brocfile.js
var app = new EmberApp({
  ...
  sourcemaps: {
    "enabled": true,
    "extensions": ["js"]
  }
});

Gist of the Bugsnag integration

Other useful details you can send to Bugsnag


Updated March 18, 2015: We should still log out appropriate errors to the console. Thanks to Ben Holmes for the tip!

Intercom + EmberJS: A Match Made in Heaven

With the release of Candidio 3.0 in early January, we wanted to make a change away from Zendesk as our helpdesk software. I've had my eye on Intercom for a quite a few months now. Not only is Intercom a robust customer service application, but they seem to be great people who know what they're taking about, not only on the customer service side of the business, but with product management and developing great software. Lastly, the Intercom web app is built with EmberJS, so that can't hurt.

Setting up Intercom in EmberJS

We took a fairly simple course of action to intergrate the Intercom tools into our ember-cli application.

Include the JS snippet

In app/index.html add the Intercom JS library snippet (don't forget to update the APP_ID in this snippet):

<script>(function(){var w=window;var ic=w.Intercom;if(typeof ic==="function"){ic('reattach_activator');ic('update',intercomSettings);}else{var d=document;var i=function(){i.c(arguments)};i.q=[];i.c=function(args){i.q.push(args)};w.Intercom=i;function l(){var s=d.createElement('script');s.type='text/javascript';s.async=true;
s.src='https://widget.intercom.io/widget/<INSERT APP_ID HERE>';
var x=d.getElementsByTagName('script')[0];x.parentNode.insertBefore(s,x);}if(w.attachEvent){w.attachEvent('onload',l);}else{w.addEventListener('load',l,false);}}})()</script>

We've also added Intercom to our global variables list in .jshintrc so we can call that variable anywhere in the app.

Initialize Intercom

Intercom provides a method to boot up the service and give them context, which you could add a number of places. We had it in an initializer previously, but currently we include it in an observer on the application controller, which watches the current user object. In our circumstance, this isn't an issue, because we never change the authenticated user. The boot method tells Intercom details about the current user, which is ultimately what you're using Intercom for in the first place.

Intercom('boot', {
  app_id: app_id,
  user_id: user_id,
  user_hash: hashed_user_id, // This is for secure Intercom connections
  name: user_full_name,
  email: user_email,
  created_at: create_at_in_utc
});

At minimum, you must send the user_id and email for Intercom to function. The documentation on setting up Intercom in a single-page app has more details.

Notify Intercom of route changes

Every time a user navigates to a new page, we want to notify Intercom. We can easily do this by reopening the router class in router.js.

Router.reopen({
  notifyIntercom: function () {
    Intercom('update');
  }.on('didTransition')
});

Other Useful Intercom Methods

  • Tracking events and providing context
  • Javascript API
    We use the JS API to implement our own button to open the chat panel. The chat bubble overlay they provide by default interferes with our user interface. In route/application.js, we've added the following action, which can be trigger from anywhere in the app.
// route/application.js
actions: {
    openIntercomWidget: function () {
      Intercom('show');
    },
    ...
}

// Usage in a template
<button {{action 'openIntercomWidget'}}>...</button>

Feel free to response in the comments below, or hit me up on Twitter at @ToddSmithSalter with any.