Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Katie Maher
Ben Nadel at CFUNITED 2010 (Landsdown, VA) with: Katie Maher

Logging Error Streams To The Server In Angular 2 Beta 6

By Ben Nadel on

Earlier this week, I realized that the EventEmitter class in Angular 2 Beta 6 was a sub-class of the RxJS Subject class; which, in turn, implements both the RxJS Observer and Observable interfaces. I was excited to learn about this; but, at the time, I didn't have a solid use-case for it. As I started to think about logger errors in an Angular 2 application, however, I thought it would be really cool if I could consume errors as a stream. That way, I could leverage all of the RxJS operators and do things like throttle the logging, check for uniqueness, and pipe errors into an HTTP request (which is also implemented as an RxJS observable sequence).


 
 
 

 
 
 
 
 

Run this demo in my JavaScript Demos project on GitHub.

CAUTION: Unfortunately, the .post() request doesn't actually work on GitHub pages because it returns a "405 Method Not Allowed". The browser caches this 405 response and refuses to try again on subsequent errors.

This demo is really exciting for me because I got to experiment with three different aspects of Angular 2:

  • Providing custom exception handling.
  • Learning about RxJS streams.
  • Learning more about making HTTP requests.

To override the core error handling in the application, we need to provide a custom implementation of the ExceptionHandler class. When we do this, our implementation will be picked up by the Angular 2 framework itself and used internally when wiring the entire application together. As such, any error that gets thrown in the Angular 2 application will be piped into our ExceptionHandler's .call() method.


 
 
 

 
 Overriding the ExceptionHandler class in Angular 2 Beta 6. 
 
 
 

The .call() method is the only instance method on the ExceptionHandler class and, is therefore, the only method that we need to implement. The core ExceptionHandler class does have one static method - .exceptionToString(); however, as I've argued previously, static methods are not part of the "class contract" in a dependency-injection framework. That said, we can most definitely leverage the existing .exceptionToString() static method within our ExceptionHandler implementation! I mean, Angular 2 already did a lot of work in that regard - no need to throw it all out.

As our .call() method is invoked by the application, we need to log the errors both to the browser console and to the server. This is where the concept of RxJS streams come into play. Internally, our ExceptionHandler implementation is going to hold an instance of the EventEmitter class which will act as our "error stream." As errors come into the .call() method, we are going to pipe them into the EventEmitter instance using the .next() method. Each error will then be passed down through the RxJS operator chain - getting mapped and transformed - until it is eventually posted to the server using the HTTP class.

We could have called the HTTP service directly from within our .call() method implementation. But, by piping errors through an intermediary observable stream, we really get to take advantage of the power of RxJS. In this case, we are using the .distinctUntilChanged() operator to only log unique sequences of errors. This way, if a user sits there triggering the same error over and over again (which, believe you me happens), we don't have to flood the error logs with so much noise - we log it once and move on.

With that said, let's take a look at the code. This demo has two links so that different errors can be triggered on demand. This way, we can see how arbitrary combinations of errors are handled by the ExceptionHandler implementation:

  • <!doctype html>
  • <html>
  • <head>
  • <meta charset="utf-8" />
  •  
  • <title>
  • Logging Error Streams To The Server In Angular 2 Beta 6
  • </title>
  •  
  • <link rel="stylesheet" type="text/css" href="./demo.css"></link>
  • </head>
  • <body>
  •  
  • <h1>
  • Logging Error Streams To The Server In Angular 2 Beta 6
  • </h1>
  •  
  • <my-app>
  • Loading...
  • </my-app>
  •  
  • <!-- Load demo scripts. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/6/es6-shim.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/6/Rx.umd.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-polyfills.min.js"></script>
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/6/angular2-all.umd.js"></script>
  • <!-- AlmondJS - minimal implementation of RequireJS. -->
  • <script type="text/javascript" src="../../vendor/angularjs-2-beta/6/almond.js"></script>
  • <script type="text/javascript">
  •  
  • // Defer bootstrapping until all of the components have been declared.
  • // --
  • // NOTE: Not all components have to be required here since they will be
  • // implicitly required by other components.
  • requirejs(
  • [ /* Using require() for better readability. */ ],
  • function run() {
  •  
  • var App = require( "App" );
  • var LoggingExceptionHandler = require( "LoggingExceptionHandler" );
  •  
  • ng.platform.browser.bootstrap(
  • App,
  • [
  • ng.http.HTTP_PROVIDERS,
  •  
  • // Here, we are overriding the core implementation of the
  • // ExceptionHandler service with our own implementation. In this
  • // case, we have to override the core implementation rather than
  • // using some sort of class composition because we need AngularJS
  • // to pick up our version of the ExceptionHandler service and use
  • // it internally within its own Zone.js error handling.
  • ng.core.provide(
  • ng.core.ExceptionHandler,
  • {
  • useClass: LoggingExceptionHandler
  • }
  • )
  • ]
  • );
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  •  
  • // I provide the root App component.
  • define(
  • "App",
  • function registerApp() {
  •  
  • // Configure the App component definition.
  • ng.core
  • .Component({
  • selector: "my-app",
  • template:
  • `
  • <p>
  • <a (click)="triggerError( 1 )">Trigger Error One</a>
  • &mdash;
  • <a (click)="triggerError( 2 )">Trigger Error Two</a>
  • </p>
  • `
  • })
  • .Class({
  • constructor: AppController
  • })
  • ;
  •  
  • return( AppController );
  •  
  •  
  • // I control the App component.
  • function AppController() {
  •  
  • var vm = this;
  •  
  • // Expose the public methods.
  • vm.triggerError = triggerError;
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I trigger the given type of error.
  • function triggerError( which ) {
  •  
  • // In this demo, we are using two different types of errors so
  • // that we can see how subsequent errors of the same type are
  • // consumed by custom ExceptionHandler implementation (which
  • // depends on its own internal RxJS stream configuration).
  • if ( which === 1 ) {
  •  
  • var x = y; // Undefined y reference.
  •  
  • } else {
  •  
  • var foo = bar; // Undefined bar reference.
  •  
  • }
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  •  
  • // --------------------------------------------------------------------------- //
  • // --------------------------------------------------------------------------- //
  •  
  • // I provide a custom ExceptionHandler implementation that logs the errors to
  • // the server using HTTP.
  • define(
  • "LoggingExceptionHandler",
  • function registerLoggingExceptionHandler() {
  •  
  • LoggingExceptionHandler.parameters = [ new ng.core.Inject( ng.http.Http ) ];
  •  
  • return( LoggingExceptionHandler );
  •  
  •  
  • // I provide a custom implementation of the ExceptionHandler service
  • // that logs the errors to the server using the core HTTP service.
  • // --
  • // NOTE: While the core ExceptionHandler service has both a static and
  • // an instance method, the "service contract" only pertains to the
  • // instance methods. As such, we are only providing a .call() method in
  • // our custom version.
  • function LoggingExceptionHandler( http ) {
  •  
  • // I index the errors by their strinigified value
  • // --
  • // NOTE: This collection is only used when we are restricting the
  • // logging to UNIQUE errors - see the errorStream configuration below.
  • var uniqueErrorIndex = {};
  •  
  • // As errors come into our ExceptionHandler implementation, we going
  • // to pipe them into this error stream EventEmitter. We're using an
  • // error stream, instead of a making direct HTTP requests, so that we
  • // can take advantage of some of the RxJS operators.
  • var errorStream = new ng.core.EventEmitter();
  •  
  • // In user-land, it's quite easy for a user to trigger the same error
  • // over and over again. While there is some value to seeing EVERY
  • // error, for the sake of this demo we're going to limit the logging
  • // of errors to unique patterns (or unique instances -- see video).
  • // Meaning, if the same error gets triggered more than once in a
  • // row, we're going to log the first instance and then ignore the
  • // subsequent instances.
  • errorStream
  • .distinctUntilChanged()
  • // Uncomment this part to only let unique errors go to the sever.
  • // --
  • // NOTE: If you are using this, you don't need the "distinct"
  • // operator above - the filter will subsume that functionality.
  • // .filter(
  • // function filterUniqueErrors( value ) {
  • //
  • // if ( value in uniqueErrorIndex ) {
  • //
  • // return( false );
  • //
  • // }
  • //
  • // return( uniqueErrorIndex[ value ] = true );
  • //
  • // }
  • // )
  • .flatMap( sendToServer )
  • .subscribe(
  • function handleValue( value ) {
  •  
  • // Nothing to do here, but WE DO NEED TO SUBSCRIBE to
  • // the stream in order for the event emitter to pass its
  • // values downstream to the HTTP flat-mapper.
  • console.debug( "Error logging success.", value );
  •  
  • },
  • function handleError( error ) {
  •  
  • console.debug( "Error logging error.", error );
  •  
  • }
  • )
  • ;
  •  
  • // Return the public API.
  • return({
  • call: call
  • });
  •  
  •  
  • // ---
  • // PUBLIC METHODS.
  • // ---
  •  
  •  
  • // I handle the given exception data.
  • function call( exception, stackTrace, reason ) {
  •  
  • // While we are override the ExceptionHandler service in the
  • // context of the local provider chain, we can still leverage
  • // the STATIC METHODS of the core ExceptionHandler. And, in this
  • // case, the core ExceptionHandler provides a convenience method
  • // for converting error data into a normalized string. Since this
  • // can be a complicated process, we might as well hand it off to
  • // the method that already knows how to get it done right.
  • var stringified = ng.core.ExceptionHandler.exceptionToString( exception, stackTrace, reason );
  •  
  • // If the console is available, log the error to the console.
  • if ( window.console ) {
  •  
  • console.error( stringified );
  •  
  • }
  •  
  • // And, pipe the error into the next EventEmitter value so that
  • // we can [possibly] log it to the server.
  • errorStream.next( stringified );
  •  
  • }
  •  
  •  
  • // ---
  • // PRIVATE METHODS.
  • // ---
  •  
  •  
  • // I determine if the given hostname is running locally or live.
  • // When running locally (on my sweet-ass ColdFusion server), we can
  • // get a better (ie, more dynamic) API response object.
  • function isLocalHostname( hostname ) {
  •  
  • return( hostname.indexOf( "github.io" ) === -1 );
  •  
  • }
  •  
  •  
  • // I send the given stringified error to the server and return the
  • // normalized response sequence.
  • function sendToServer( stringifiedError ) {
  •  
  • // When we log the error, we want to log it along with the current
  • // browser location so that the developers can know where in the
  • // app "experience" the error occurred.
  • var body = {
  • location: window.location.href,
  • stackTrace: stringifiedError
  • };
  •  
  • // When running on GitHub pages, we can only use a static API
  • // response. However, locally, we can use a more dynamic API
  • // since we are running on ColdFusion.
  • var apiLocation = isLocalHostname( window.location.hostname )
  • ? "./api/log-error.cfm" // ColdFusion.
  • : "./api/log-error.json" // Static JSON.
  • ;
  •  
  • // Post the error payload as a JSON (JavaScript Object Notation)
  • // request.
  • // --
  • // NOTE: We are not returning the raw HTTP response. Instead, we
  • // are unwrapping both successful and failure responses so that
  • // the consuming context does not have to know about the HTTP
  • // transportation mechanism.
  • var response = http
  • .post(
  • apiLocation,
  • JSON.stringify( body ),
  • {
  • headers: {
  • "Content-Type": "application/json; charset=utf-8"
  • }
  • }
  • )
  • .map(
  • function unwrapSuccess( value ) {
  •  
  • return( value.json() );
  •  
  • }
  • )
  • .catch(
  • function unwrapError( error ) {
  •  
  • try {
  •  
  • return( Rx.Observable.throw( error.json() ) );
  •  
  • } catch ( jsonError ) {
  •  
  • // If the error couldn't be parsed as JSON data
  • // then it's possible the API is down or something
  • // went wrong with the parsing of the successful
  • // response. In any case, to keep things simple,
  • // we'll just create a minimum representation of
  • // a parsed error.
  • var minimumViableError = {
  • success: false
  • };
  •  
  • return( Rx.Observable.throw( minimumViableError ) );
  •  
  • }
  •  
  • }
  • )
  • ;
  •  
  • return( response );
  •  
  • }
  •  
  • }
  •  
  • }
  • );
  •  
  • </script>
  •  
  • </body>
  • </html>

As you can see, in our .call() method, we're always logging the error to the console before we pass it on to the EventEmitter error stream. This way, it always gets logged locally but only gets logged to the server based on the RxJS operators. And, when we run this page and try to trigger a few errors, we get the following output:


 
 
 

 
 Logging error streams to the server in Angular 2 Beta 6. 
 
 
 

As you can see, repetitive errors get logged to the browser console every time; but, only unique sequences of errors get logged to the server.

I could probably have put the console.log() call inside of a .do() operator within the RxJS chain. But to be honest, I am not yet sure where the line should be drawn - I'm not sure how much functionality I should try to shoehorn into a single RxJS stream. What I like about the current implementation is that the stream, itself, is focused entirely on HTTP logging. If I then mixed-in console logging as a concern, would that be to much? I don't know - still learning this stuff.

I still love the simplicity of Promises. But, the more I learn about RxJS streams, the more powerful they appear to be. I just hope that I don't accidentally take on the mentality of Maslow's Hammer, in which everything starts to look like a stream simply because I have the RxJS at my disposal.




Reader Comments

This is really awesome, thanks for bringing this up! Seeing the usefulness and how easily this could be incorporated into the angular core, you could probably raise an issue on angular repo to discuss this.

Reply to this Comment

Aaargh!

Don't use EventEmitter for this purpose. See my response to your prior post. EvE will not be an observable in future.

Use the RxJS `Subject` instead.

Reply to this Comment

@Ward,

Ha ha, sorry, I didn't know, I didn't know :P For anyone curious as to the previous comment to which Ward is referring:

> Do NOT count on EventEmitter continuing to be an Observable!
> Do NOT count on those Observable operators being there in the future!
> These will be deprecated soon and probably removed before release.
> Use EventEmitter only for event binding between a child and parent
> component. Do not subscribe to it. Do not call any of those methods.
> Only call `eve.emit()`

Reply to this Comment

@John,

I'm glad you like it, but I'm not sure it belongs in the "core". It's likely to be too specific a use-case for the exception handler. That said, it would be interesting if ExceptionHandler did expose something you could subscribe to. Imagine if you could do:

function ExceptionLogger( exceptionHandler ) {
. . . . exceptionHandler.subscribe( logErrorToServer );
}

Then, you wouldn't have to overwrite the core service, you could simply "tap" into it and logs errors in any way you wanted.

Reply to this Comment

@All,

I just realized that my understanding of streams is still quite incomplete. If the stream fails for some reason (say with an HTTP error), then it will stop working altogether because the subscriber will be unsubscribed. Really, we probably want to swallow the error and have the stream continue to work (unless of course we want to stop logging after an unexpected failure, which could make sense).

Anyway, just wanted to put that out there.

Reply to this Comment

@All,

Actually, that last statement is not entirely true - since the HTTP request is inside its own stream that catches errors, a network error won't break the chain. But, it is something that is giving me pause of thought.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.