airbug/bugcore

View on GitHub
libraries/bugcore/js/src/trace/Tracer.js

Summary

Maintainability
A
1 hr
Test Coverage
/*
 * Copyright (c) 2016 airbug Inc. http://airbug.com
 *
 * bugcore may be freely distributed under the MIT license.
 */


//-------------------------------------------------------------------------------
// Annotations
//-------------------------------------------------------------------------------

//@Export('Tracer')

//@Require('ArgUtil')
//@Require('Class')
//@Require('Obj')
//@Require('Proxy')
//@Require('StackTraceUtil')
//@Require('StringUtil')
//@Require('Trace')
//@Require('TraceTree')
//@Require('TypeUtil')


//-------------------------------------------------------------------------------
// Context
//-------------------------------------------------------------------------------

require('bugpack').context("*", function(bugpack) {

    //-------------------------------------------------------------------------------
    // BugPack
    //-------------------------------------------------------------------------------

    var ArgUtil         = bugpack.require('ArgUtil');
    var Class           = bugpack.require('Class');
    var Obj             = bugpack.require('Obj');
    var Proxy           = bugpack.require('Proxy');
    var StackTraceUtil  = bugpack.require('StackTraceUtil');
    var StringUtil      = bugpack.require('StringUtil');
    var Trace           = bugpack.require('Trace');
    var TraceTree       = bugpack.require('TraceTree');
    var TypeUtil        = bugpack.require('TypeUtil');


    //-------------------------------------------------------------------------------
    // Declare Class
    //-------------------------------------------------------------------------------

    /**
     * @class
     * @extends {Obj}
     */
    var Tracer = Class.extend(Obj, {

        _name: "Tracer",


        //-------------------------------------------------------------------------------
        // Constructor
        //-------------------------------------------------------------------------------

        /**
         * @constructs
         */
        _constructor: function() {

            this._super();


            //-------------------------------------------------------------------------------
            // Private Properties
            //-------------------------------------------------------------------------------

            /**
             * @private
             * @type {boolean}
             */
            this.enabled        = false;

            /**
             * @private
             * @type {TraceTree}
             */
            this.traceTree      = new TraceTree();

            /**
             * @private
             * @type {Trace}
             */
            this.currentTrace   = null;
        },


        //-------------------------------------------------------------------------------
        // Init Methods
        //-------------------------------------------------------------------------------

        /**
         * @return {Tracer}
         */
        init: function() {
            var _this = this._super();
            if (_this) {
                _this.currentTrace = new Trace("", "ROOT_TRACE");
                _this.traceTree.setRootTrace(_this.currentTrace);
            }
            return _this;
        },


        //-------------------------------------------------------------------------------
        // Getters and Setters
        //-------------------------------------------------------------------------------

        /**
         * @return {Trace}
         */
        getCurrentTrace: function() {
            return this.currentTrace;
        },

        /**
         * @param {Trace} currentTrace
         */
        setCurrentTrace: function(currentTrace) {
            this.currentTrace = currentTrace;
        },


        //-------------------------------------------------------------------------------
        // Public Methods
        //-------------------------------------------------------------------------------

        /**
         * @param {Throwable | Error} error
         * @return {Throwable | Error}
         */
        $error: function(error) {
            if (this.enabled) {
                if (TypeUtil.isObject(error) && !error.bugTraced) {
                    error.bugTraced = true;
                    if (!error.stack) {
                        error.stack = StackTraceUtil.generateStackTrace();
                    }
                    var traceStack      = this.generateTraceStack(this.currentTrace);
                    error.stack         = error.stack + "\n" + traceStack;
                }
            }
            return error;
        },

        /**
         * @param {string} name
         */
        $name: function(name) {
            if (this.currentTrace) {
                this.currentTrace.setName(name);
            }
        },

        /**
         * @param {function(...):*} callback
         * @return {function}
         */
        $trace: function(callback) {
            if (this.enabled) {
                var _this = this;
                var stack = StackTraceUtil.generateStackTrace();
                var trace = this.addTraceStack(stack);

                if (callback.aCallback) {
                    throw new Error("This callback has already been wrapped in a trace");
                }
                var newCallback = function() {
                    newCallback.aCallback = true;
                    var args = ArgUtil.toArray(arguments);
                    _this.currentTrace = trace;
                    callback.apply(null, args);

                    //NOTE BRN: If one async thread ends and a new one starts that we have not wrapped in our own trace callback
                    //we do not want any new traces that the thread creates to attach to the previous current trace (since they
                    //are unrelated). So, we reset the current trace to the root trace after the completion of every callback.

                    _this.currentTrace = _this.traceTree.getRootTrace();
                    _this.checkTraceForRemoval(trace);
                };
                return newCallback;
            } else {
                return callback;
            }
        },

        /**
         * @param {function(Error, ...)} callback
         * @return {function}
         */
        $traceWithError: function(callback) {
            if (this.enabled) {
                var _this = this;
                var stack = StackTraceUtil.generateStackTrace();
                var trace = this.addTraceStack(stack);

                if (callback.aCallback) {
                    throw new Error("This callback has already been wrapped in a trace");
                }

                var newCallback = function() {
                    newCallback.aCallback = true;
                    var args = ArgUtil.toArray(arguments);
                    var error = args[0];

                    if (error) {
                        args[0] = _this.$error(error);
                    }
                    _this.currentTrace = trace;
                    callback.apply(null, args);
                    _this.currentTrace = _this.traceTree.getRootTrace();
                    _this.checkTraceForRemoval(trace);
                };
                return newCallback;
            } else {
                return callback;
            }
        },

        /**
         *
         */
        enable: function() {
            this.enabled = true;
        },

        /**
         * @param {string} name
         * @return {string}
         */
        generateNamedTraceStack: function(name) {
            var firstNamedTrace = this.traceTree.findFirstTrace(function(trace) {
                return (trace.getName() === name);
            });

            if (firstNamedTrace) {
                var currentTrace    = null;
                var nextTrace       = firstNamedTrace;
                while (nextTrace) {
                    currentTrace = nextTrace;
                    nextTrace    = null;
                    if (currentTrace.numberChildTraces() > 0) {
                        var childTraces = currentTrace.getChildTraces();
                        for (var i = childTraces.length - 1; i >= 0; i--) {
                            var childTrace = childTraces[i];
                            if (childTrace.getName() === name) {
                                nextTrace = childTrace;
                                break;
                            }
                        }
                    }
                }
                return this.generateTraceStack(currentTrace);
            } else {
                return "";
            }
        },

        /**
         * @param {Trace} trace
         * @return {string}
         */
        generateTraceStack: function(trace) {
            var traceStack   = [];
            var currentTrace = trace;
            while (!Obj.equals(currentTrace, this.traceTree.getRootTrace())) {
                var stack   = trace.getStack();
                var stackParts = stack.split("\n");
                traceStack.push("-------- Async Break ---------");
                traceStack = traceStack.concat(stackParts);
                currentTrace = currentTrace.getParentTrace();
            }
            return traceStack.join("\n");
        },


        //-------------------------------------------------------------------------------
        // Private Methods
        //-------------------------------------------------------------------------------

        /**
         * @private
         * @param {string} stack
         * @return {*}
         */
        addTraceStack: function(stack) {
            var trace   = new Trace(stack);
            trace.setName(this.currentTrace.getName());
            this.currentTrace.addChildTrace(trace);
            return trace;
        },

        /**
         * @private
         * @param {Trace} trace
         */
        checkTraceForRemoval: function(trace) {
            //console.log("check trace - numberChildren:" + node.numberChildTraces() + " Obj.equals(trace, this.traceTree.getRootTrace()):" + Obj.equals(trace, this.traceTree.getRootTrace()) + " trace:" + trace);
            if (trace.numberChildTraces() === 0 && !Obj.equals(trace, this.traceTree.getRootTrace())) {

                //TEST
                //console.log("removing trace - trace:" + trace);
                if (trace.removed) {
                    throw new Error("Trying to remove the same trace TWICE!");
                }
                var parentTrace = trace.getParentTrace();
                parentTrace.removeChildTrace(trace);
                trace.removed = true;

                this.checkTraceForRemoval(parentTrace);
            }
        }
    });


    //-------------------------------------------------------------------------------
    // Private Static Properties
    //-------------------------------------------------------------------------------

    /**
     * @static
     * @private
     * @type {Tracer}
     */
    Tracer.instance = null;


    //-------------------------------------------------------------------------------
    // Public Static Methods
    //-------------------------------------------------------------------------------

    /**
     * @static
     * @return {Tracer}
     */
    Tracer.getInstance = function() {
        if (Tracer.instance === null) {
            Tracer.instance = new Tracer();
        }
        return Tracer.instance;
    };

    Proxy.proxy(Tracer, Proxy.method(Tracer.getInstance), [
        "$error",
        "$name",
        "$trace",
        "$traceWithError",
        "enable",
        "generateNamedTraceStack"
    ]);


    //-------------------------------------------------------------------------------
    // Export
    //-------------------------------------------------------------------------------

    bugpack.export('Tracer', Tracer);
});