AngularJS - Logging Client Side Stack Trace

Nothing beats mythical errors that happens on the client side, but not on your development machine.

This post follows yesterday's post on configuring AngularJS to catch and log exceptions back to the server via an AJAX request.

Logging the Exception

Quick recap of the logging error handler.

function error(message, data, title) {
  var $http = angular.injector(['ng']).get('$http');

  data['url'] = document.location.href;

  var request = {
    method : 'POST',
    url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyService.svc/LogMessage',
    data: {
      'log': message,
      'meta': angular.toJson(data),
      'evt': 'error'
    },
    dataType: "json",
    headers: {
      "Content-Type": "application/json; charset=utf-8"
    }
  };
  var promise = $http(request);            

  promise['catch'](function(){
    // send to console log if can't log to webservice
    $log.error('Error: ' + message, data);
  });
}
{
  "exception":
  {
    "message":"[NG-Modular Error] Unable to get property 'Client' of undefined or null reference",
    "description":"Unable to get property 'Client' of undefined or null reference",
    "number":-2146823281
  },
  "url":"http://server/_layouts/MY/form.aspx?IsDlg=1&id=3#/client"
}

Yay it's logging.  But actually, it's pretty hard to figure out where the error is coming from.  "What we need", says the developer to the tester, "is the stack trace.  If we have the stack trace we'd be able to repo this and fix it."

stacktraceJS

Grab: https://github.com/stacktracejs/stacktrace.js/blob/stable/stacktrace.js

function error(message, data, title) {
  var $http = angular.injector(['ng']).get('$http');
  
  // work out the stacktrace
  var stack = printStackTrace({e: data, guess: true});
  data['stack'] = stack;
  // attach to the logged data

  data['url'] = document.location.href;

  var request = {
    //...
  };
  var promise = $http(request);            
}

The following JSON gets logged.

{
  "exception":
  {
    "message":"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference",
    "description":"Unable to get property 'exist' of undefined or null reference",
    "number":-2146823281
  },
  "url":"http://server/_layouts/MY/form.aspx?IsDlg=1&id=138#/",
  "stack":
  [
    "{anonymous}(#object,\"other\")",
    "printStackTrace(#object)",
    "error(\"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference\",#object)",
    "{anonymous}(?)",
    "{anonymous}(#function)",
    "{anonymous}(#object)",
    "{anonymous}(#object)",
    "{anonymous}(?)"
  ]
} 

Tweaks

"Those #function and #object, they look pretty stupid." commented the developer, who now has the stacktrace, but still thinks it doesn't help all that much.

A few hacks to stacktrace.js

if (arg.constructor === Object) {
  //result[i] = '#object';
  result[i] = arg.constructor.toString();
}
...
else {
   //result[i] = '?';
   result[i] = arg.constructor.toString();
}

This gives more readable stacktrace:

{
  "exception":
  {
    "message":"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference",
    "description":"Unable to get property 'exist' of undefined or null reference",
    "number":-2146823281
  },
  "stack":
  [
    "{anonymous}(\nfunction Object() {\n    [native code]\n}\n,\"other\")",
    "printStackTrace(\nfunction Object() {\n    [native code]\n}\n)",
    "error(\"[NG-Modular Error] Unable to get property 'exist' of undefined or null reference\",\nfunction Object() {\n    [native code]\n}\n)",
    "{anonymous}(TypeError)",
    "{anonymous}(#function)",
    "{anonymous}(\nfunction Object() {\n    [native code]\n}\n)",
    "{anonymous}(\nfunction Object() {\n    [native code]\n}\n)",
    "{anonymous}([object Event])"
  ],
  "url":"http://server/_layouts/MY/form.aspx?IsDlg=1&id=138#/"
}

 

Future of stacktraceJS

The latest version of StacktraceJS (as of August 2015) is marching towards a full promise/A pattern where you request the stacktrace, and it returns you a promise.  Then promise resolves to an array of stack frames, which you can then pretty-print to your own liking.

var callback = function(stackframes) {
    var stringifiedStack = stackframes.map(function(sf) { 
        return sf.toString(); 
    }).join('\n'); 
    console.log(stringifiedStack); 
};
var errback = function(err) { console.log(err.message); };
StackTrace.get().then(callback, errback)

Expect the syntax to change, soon.

I'm also assuming for modern browsers, it'll even look into requesting SourceMap files for minified versions of JavaScript/TypeScript and resolve those lines in the stacktrace produced.   Currently though, it doesn't do all those things.  What it does manage so far, cross browser, is already fairly impressive and a great starting point.

 

 

AngularJS $http and logger Circular Dependency

Error

[$injector:cdep] Circular dependency found: $rootScope <- $http <- logger <- $exceptionHandler <- $rootScope <- $location <- routehelper

TLDR:

If you are looking for RequireJS's easy way to late-bind a dependency, here's how you do it in AngularJS.

var $http = angular.injector(['ng']).get('$http');

Story

I imagine this could be a fairly common scenario.

You implemented AngularJS, with a logger service.

 angular
     .module('blocks.logger')
     .factory('logger', logger);

 logger.$inject = ['$log'];
 function logger($log) {
        var service = {
            error   : error,
            log     : $log.log
        };
        return service;

        /////////////////////

        function error(message, data, title) {
            $log.error('Error: ' + message, data);
        }
}

You want to try to catch errors and log it back to your webservice
So add a REST call.

logger.$inject = ['$log', '$http'];
function logger($log, $http) {
    // snip.

    function error(message, data) {
        $log.error('Error: ' + message, data);

        var request = {
            method : 'POST',
            url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyLogService.svc/LogMessage',
            data: {
                'log': message,
                'event': 'error',
                'meta': angular.toJson(data)
            },
            dataType: "json",
            headers: {
                "Content-Type": "application/json; charset=utf-8"
            }
        };
        $http(request);           
    }
}


Error!

[$injector:cdep] Circular dependency found: $rootScope <- $http <- logger <- $exceptionHandler <- $rootScope <- $location <- routehelper

I actually quite like this error.  It's telling me the dependency tree starting from routehelper and looping around to $rootScope

Bingle will tell you to re-architect your solution so you have proper separation of concerns.  But to be honest, I just want to log an error…

So here's how you do it.

var ng = angular.injector(['ng']);
var $http = ng.get('$http');

Final Code Summary

logger.$inject = ['$log'];
function logger($log) {
    // snip.

    function error(message, data) {
        $log.error('Error: ' + message, data);
        
        // inject $http here.
        var $http = angular.injector(['ng']).get('$http');
        if (!$http) return;
            var request = {
                method : 'POST',
                url: _spPageContextInfo.webServerRelativeUrl + '/_layouts/MyLogService.svc/LogMessage',
                data: {
                    'log': message,
                    'event': 'error',
                    'meta': angular.toJson(data)
                },
                dataType: "json",
                headers: {
                    "Content-Type": "application/json; charset=utf-8"
                }
            };
            $http(request);           
        }
}

AngularJS Extend and Computed Property

This is an Intermediate level discussion on AngularJS Extend, Computed Property in general, and a fairly complicated binding bug that needs some new learning.

 

I came from a KnockoutJS world.  In the KnockoutJS world which I'm familiar with, there is the concept of a Computed Property.

KnockoutJS Computed Property

Because native Javascript objects does not raise OnPropertyChanged events, KnockoutJS wraps properties with an observable wrapper.
A class would look like this:

function PersonViewModel(){
  var self = this;
  
  self.firstName = ko.observable("");
  self.lastName = ko.observable("");
  
  self.fullName = ko.computed(function(){
    return [
      ko.unwrap(self.firstName), 
      ko.unwrap(self.lastName)
    ].join(" ");
  });
}

The developer takes care of the properties firstName and lastName, and KnockoutJS understands when firstName or lastName changes, fullName needs to be re-evaluated.

AngularJS binds Native Javascript Object

In AngularJS, there is no direct concept of a Computed Property, instead, AngularJS watches various binding expressions and automatically re-evaluate expressions.  Since AngularJS binds to native Javascript directly, it employs a dirty checking mechanism.

A native person might look like this:

var person = {
  firstName: "",
  lastName: ""
};

And a View would look like this:

<div>
  First Name: {{ person.firstName }}
  Last Name: {{ person.lastName }}
  Full Name: {{ person.firstName + ' ' + person.lastName }}
</div>

Defining Class for AngularJS

But very quickly, one would find that this gets very unwieldy, and you end up duplicating expressions everywhere in your View.  Duplicated code is never good.  So we need to tidy up the definition of the model a bit more.

function Person(){
  var self = this;
  
  self.firstName = "";
  self.lastName = "";
  
  self.fullName = function(){
    return [self.firstName,self.lastName].join(" ");
  };
}

AngularJS Extend

Such definitions are nice and good, but you will need a way to populate these when you pull your data back from a REST service as JSON.  Luckily, AngularJS provides both angular.extend as well as angular.merge helper methods to get you going quickly. 

If you are familar with jQuery, this is nearly identical to jQuery.extend, it copies the properties from the later objects onto the first one.

var jsonJohn = { firstName: "John", lastName: "Liu" };
var personJohn = angular.extend( new Person(), jsonJohn );

// personJohn should have firstName, lastName 
// and fullName() == 'John Liu'

The updated View is then:

<div>
  First Name: {{ person.firstName }}
  Last Name: {{ person.lastName }}
  Full Name: {{ person.fullName() }}
</div>

Extending Arrays

When you have a collection of people, the code needs to look like this:

var people = [];
var jsonPeople = [
  { firstName: "John", lastName: "Liu" },
  { firstName: "Mark", lastName: "Liu" }
];
people = angular.extend( people, jsonPeople );
<div data-ng-repeat="person in people">
  First Name: {{ person.firstName }}
  Last Name: {{ person.lastName }}
  Full Name: {{ person.fullName() }}
</div>

This looks good initially, except there are a few problems:

  • angular.extend does not know anything about Person class, and fullName is not available
var people = [
  new Person(),
  new Person()
];
var jsonPeople = [
  { firstName: "John", lastName: "Liu" },
  { firstName: "Mark", lastName: "Liu" }
];
people = angular.extend( people, jsonPeople );
  • angular.extend will override people[0] with jsonPeople[0] (not null).  So the result still does not define fullName()

So you need something like this:

var people = [
  new Person() // always show one row by default even if blank
];
var jsonPeople = [
  { firstName: "John", lastName: "Liu" },
  { firstName: "Mark", lastName: "Liu" }
];
var l = jsonPeople.length > people.length ? jsonPeople.length : people.length;
for (var i=0; i < l; i++){
  people[i] = angular.extend( new Person(), people[i], jsonPeople[i] );
  // copy jsonPeople[i] (if not null) onto people[i]
  // copy people[i] onto new Person()
  // store it back on people[i]
}

A subtle bug in copying with extend

The Computed Property doesn't work because the way angular.extend copies references, people[i].fullName is copied to (new Person()).fullName, which means that the (new Person()).fullName is calculating from the previous people[i].fullName, because it uses the 'self' reference.

(let me know if I need to draw a picture)

There are two ways to fix this:

1. We can fix how we extend existing records.

//...
for (var i=0; i < l; i++){
  people[i] = angular.extend( people[i] || new Person(), jsonPeople[i] );
  // OR  people[i] and new Person()
  // so that if there is an existing Person object, it is kept
}

2. We can fix how we define the Person class.  I think this is the better method.

function Person(){
  var self = this;
  
  self.firstName = "";
  self.lastName = "";
  
  Person.prototype.fullName = function(){
    // re-connect 'self' to the current object
    var self = this;
    return [self.firstName,self.lastName].join(" ");
  };
}

This bumps the fullName method to the prototype/supertype, so it is no longer an immediate property defined on the Person object.

Summary

  • Notes on doing computed property in AngularJS
  • How to define a Class for AngularJS
  • How to handle extend and extend an array
  • How to move computed methods to the prototype
  • Plunker http://plnkr.co/edit/HcuxnT3EAVrtz1PyWwXb check out the two Count1 and Count2 computed properties and how they work differently after you clicked 'update'