Tracing $scope events in AngularJS

Scope events in AngularJs is very powerful and useful in connecting disparate modules and directives. However, in a complex application things could become complicated and it would become hard to trace which event is triggered when and who act on those events.

I wrote a simple event tracer which displays a floating div with logs of all the events. The log is color coded, based on if the event was emitted, broadcasted or received.

The log looks like this:-

Event Trace

Event Trace

You can turn on or off the event data using the dataf link. The stackf toggles if the call stack or function code too will be shown which has emitted or consumed this event.

The code:-

var app = angular.module('tracer', []).run(Injector);

function Injector($rootScope) {
    'use strict';

    function log(msg, color) {
        var el = document.getElementById('tracerConsoleUl');
        el.innerHTML += itemHtml.format(color, msg)
    }

    function stringify(o) {
        try {
            return JSON.stringify(o, null, 3);
        } catch (err) {
            console.error('Scope Hack:', err, ' Actual object:', o);
            return '<i>Err: See console</i>';
        }
    }

    function getStackTrace() {
        var r;
        if (Error.captureStackTrace) {
            var obj = {};
            Error.captureStackTrace(obj, getStackTrace);
            r = obj.stack;
        } else if (Error.stack) {
            r = Error().stack;
        } else {
            r = '';
        }

        r = r.replace(/</gm, '&lt;').replace(/>/gm, '&gt;');
        r = r.replace(/^[^\s]+.+$/gm, ''); // removing first line
        r = r.replace(/^\s+at (.+) \([^(]+\)+$/gm, '$1');
        r = r.replace(/^\s+at [^()]+$/gm, ''); // removing rows with only text of format at ... and nothing in braces on right.
        r = r.replace(/\n\n/gm, '\n');
        r = r.replace(/\n/gm, ' &lt; ');

        return r;

    }

    function filter(e, clazz) {
        e.stopPropagation();
        e.preventDefault();

        var flag = angular.element('#tracerConsoleUl').hasClass(clazz);
        if (flag)
            angular.element('#tracerConsoleUl').removeClass(clazz);
        else
            angular.element('#tracerConsoleUl').addClass(clazz);
    }

    function fclear(e) {
        e.stopPropagation();
        e.preventDefault();

        angular.element('#tracerConsoleUl').empty();
    }

    function fmoveStart(e) {
        e.stopPropagation();
        e.preventDefault();

        angular.element(document).on('mousemove.scopehack', fmove).on('mouseup.scopehack', fmoveEnd);
        var el = angular.element('#tracerConsole');
        var left = e.pageX - parseInt(el.css('left'));
        var top = e.pageY - parseInt(el.css('top'));
        el.data('left', left);
        el.data('top', top);
    }

    function fmoveEnd(e) {
        e.stopPropagation();
        e.preventDefault();

        angular.element(document).off('.scopehack');
    }

    function fmove(e) {
        e.stopPropagation();
        e.preventDefault();

        var el = angular.element('#tracerConsole');

        var p = {
            left: (e.pageX - el.data('left')) + 'px',
            top: (e.pageY - el.data('top')) + 'px'
        };

        angular.element('#tracerConsole').css(p);

    }

    try {
        if (!String.prototype.format) {
            String.prototype.format = function() {

                var args = arguments;

                return this.replace(/{(\d+)}/g, function(match, number) {
                    return typeof args[number] != 'undefined' ? args[number] : '<i>NA</i>';
                });
            };

        }


        var html = "<div id='tracerConsole' style='position:absolute;top:50px;left:50px;background-color:white;border:2px solid black;z-index:1000;'>" +
            "<div><span class='ffilter' style='cursor:pointer;'>stackf</span> | <span class='dfilter' style='cursor:pointer;'>dataf</span> | <span class='fclear' style='cursor:pointer;'>clear</span> | <span class='fmove' style='cursor:move;'>move</span>" +
            "</div><ul id='tracerConsoleUl' style='display:block;list-style-type:none;max-height:500px;width:300px;overflow:scroll;'></ul></div>";



        var itemHtml = "<li style='border-bottom:1px solid gray;white-space:pre-wrap;background-color:{0};'>{1}</li>";

        var proto = Object.getPrototypeOf($rootScope);
        var oldOn = proto.$on;
        proto.$on = function mangaledOn(e, f) {
            var fWrapper = function fWrapper(e, d) {
                log('EVENT RECEIVED: {0} <span class="d">\nWITH DATA: {1}</span> <span class="f">\nBY f: {2}</span>'.format(e.name, stringify(d), f.toString()), '#7FD7B6');
                f.call(this, e, d);
            };

            oldOn.call(this, e, fWrapper);
        };

        var oldBroadcast = proto.$broadcast;
        proto.$broadcast = function mangaledBroadcast(e, d) {
            log('EVENT BROADCASTED: {0} <span class="d">\nWITH DATA: {1}</span> <span class="f">\nBY s: {2}</span>'.format(e, stringify(d), getStackTrace()), '#FF9C01');

            oldBroadcast.call(this, e, d);

        };

        var oldEmit = proto.$emit;
        proto.$emit = function mangaledEmit(e, d) {
            log('EVENT EMITTED: {0} <span class="d">\nWITH DATA: {1}</span> <span class="f">\nBY s: {2}</span>'.format(e, stringify(d), getStackTrace()), '#FFE78C');

            oldEmit.call(this, e, d);
        };

        console.log('Scope Hack Injected');
        angular.element('body')[0].innerHTML += html;
        angular.element('#tracerConsole .dfilter').on('click', function dfilter(e) {
            filter(e, 'dhide');
        });

        angular.element('#tracerConsole .ffilter').on('click', function ffilter(e) {
            filter(e, 'fhide');
        });
        angular.element('#tracerConsole .fclear').on('click', fclear);
        angular.element('#tracerConsole .fmove').on('mousedown', fmoveStart);
        angular.element(document).find('head').prepend('<style type="text/css">#tracerConsoleUl.dhide .d {display:none;}#tracerConsoleUl.fhide .f {display:none;} #tracerConsoleUl li{-moz-user-select:text;-webkit-user-select:text;-ms-user-select:text;}</style>');

    } catch (err) {
        console.error('Scope Hack:', err);
    }
}

To use this, paste this into some script block or some js file and make sure your app’s module depends on this module – tracer.

One Comment

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.