Extending AngularJS $log service or better way to monkey-patch angular services

AngularJS gives a really nice and clean way to extend functionality of existing services by the use of decorator. You can think of decorators as a very clean way to 'monkey-patch' angular services.

Decorator comes with $injector service, and can only be run in config phase of the application. It is done that way because under the hood decorator is intercepting the instantiation of the service, and alters it in a way that is useful for us, before that service gets injected anywhere.

I came up with this post as I needed to solve a problem that I faced on a production app. Basically we were using Honeybadger to log the exceptions, and when it came to frontend it was done in a standard way (described in their documentation). For newer components not written in angular it was not logging anything. All the angular modules were using $log.error() to output any unexpected behaviors, plus its a standard way to report any strictly angular related errors (e.g Error: $digest already in progress).

The first idea was to use monkey-patching applied on top of $log service. But after a bit of thinking, decorator came up as a lot better alternative. The result of the work can be found in this module now available on GitHub.

The beauty of this module is that you don't need much work other then including ng-honeybadger-logger as a dependency in the main angular app, and errors start coming to your Honeybadger dashboard right away.

So, what has been done to achieve this kind of functionality? The code underneath illustrates the core part of the decoration process, which is a function configureLogger run as a part of config phase.

First we inject $provide service into our function to make use of it later on. After that we instantiate a constant logDecorator which is used as a decorator for the $log service. The logDecorator function itself receives and instance of $delegate service, that is actually our decorated $log under the hood. The body of logDecorator function does monkey-patch like decoration of $log's error method, by calling Honeybadger with it parameters.

function configureLogger($provide) {  
  $provide.constant('logDecorator', logDecorator);

  $provide.decorator('$log', logDecorator);

  logDecorator.$inject = ['$delegate'];

  function logDecorator($delegate) {
    var errorFn = $delegate.error;
    $delegate.error = function(e) {
      /*global Honeybadger: true*/
      Honeybadger.notify(e);
      errorFn.apply(null, arguments);
    };

    return $delegate;
  }
}

The tricky part came when this piece of code had to be tested and not all things played smoothly there as testing $log service is kind of hard by itself because its a part of angular core services. I even asked a question on stackoverflow to get some help.

To solve a problem of testing $log decoration I had to use my own mocked service instead, as it was impossible to get mocked instance of that service in tests (it is hijacked by angular-mocks). In short we have to create and inject our own mocked $log instance and only then we are able to check weather or not calling error function also triggers call to the Honeybadger. The code below shows a full test for the decorator.

describe('ng-honeybadger-logger', function() {  
  var loggerModule,
    mockLog;

  beforeEach(function() {
    Honeybadger = jasmine.createSpyObj('honeybadgerMock', ['notify']);
    mockLog = jasmine.createSpyObj('logMock', ['error']);
  });

  beforeEach(function() {
    loggerModule = angular.module('ng-honeybadger-logger');
  });

  beforeEach(function() {
    angular.mock.module('ng-honeybadger-logger', function($injector, $provide) {
      $provide.value('$log', mockLog);
      $provide.decorator('$log', $injector.get('logDecorator'));
    });
  });

  it('should initialize the logger module', function() {
    expect(loggerModule).toBeDefined();
  });

  it('should monkey patch native logger with additional Honeybadger call', inject(function($log) {
    $log.error('test error');

    expect(Honeybadger.notify).toHaveBeenCalledWith('test error');
  }));
});

Summing up, decorator function gives a very powerful way to extend angular applications in a plugin-like way that does not pollute our main logic with unnecessary code. Hope this post made it a bit more clear how to use decorators and gave some hints on testing angular core services.

In case you want to experiment with the code yourself, all sources can be found here

Cover image by Harvey Barrison