127.0.0.1:8000 budget / master src / js / utils / logger.js
master

Tree @master (Download .tar.gz)

logger.js @masterraw · history · blame

const storage = require('./storage');
const api = require('./api');

const MAX_DEPTH = 25;
const MAX_LINES = 1000000;
const INDENT = '    ';

const ROOT = "./src/js/";
const WRONG_FILE_ENDING = ".js?";
const GOOD_FILE_ENDING = ".js";

window.logger = {
    log: console.log,
    warn: console.warn,
    error: console.error
};
let gc_interval;
let logs = storage.get('logs');
let enabled = !!logs;
logs ? enable_logs() : disable_logs();

function enable_logs() {
    logs = logs || {
        lines: [],
        trimOffset: 0,
        strings: {}
    };

    Object.assign(window.logger, ["log", "warn", "error"].reduce(function(acc, level) {
        return Object.assign(acc, {
            [level]: function() {
                let log = log_and_format(level, [...arguments]);

                logs.lines.push({
                    timeStamp: getTime(),
                    level: level,
                    stack: getStackLocation()
                });

                if (logs.lines.length > MAX_LINES) {
                    logs.lines.shift();
                    logs.trimOffset++;
                }

                let stringLines = logs.strings[log] || [];
                stringLines.push(getCurrentLineNumber(logs));
                logs.strings[log] = stringLines;
            }
        });
    }, {}));

    logger.log(navigator.userAgent);

    window.addEventListener("beforeunload", store_logs);

    gc_interval = setInterval(function() {
        logger.log("GC Old Logs");
        // We should trim old strings for all pages if they are not getting deleted
        for (let string in logs.strings) {
            let stringLines = logs.strings[string];
            if (stringLines[stringLines.length - 1] < logs.trimOffset) {
                delete logs.strings[string];
            } else {
                // Remove references to line numbers that have been trimmed due to max log length.
                // This loop is safe because we know that at least the last index of stringLines
                // will have a line value > page.trimOffset since we just checked that condition above.
                while (stringLines[0] < logs.trimOffset) {
                    stringLines.shift();
                }
            }
        }
    }, 1000 * 60 * 60);
}

function disable_logs() {
    window.logger = {
        log: console.log,
        warn: console.warn,
        error: console.error
    };
    // Clear logs stored logs if they exist
    storage.set('logs', "");
    clearInterval(gc_interval);
    logs = undefined;
    window.removeEventListener("beforeunload", store_logs);
}

function toggle_logs() {
    enabled ? disable_logs() : enable_logs();
    enabled = !enabled;
    return enabled;
}

function store_logs() {
    logger.log(location.pathname, "unloading, save logs");
    storage.set('logs', logs);
}

module.exports = {
    enable_logs,
    disable_logs,
    toggle_logs,
    logs_enabled: function() {
        return enabled;
    },
    export_logs: function() {
        if (!logs) {
            return new Promise(r => r());
        }
        return api.post("/log/upload", {
            logs: logs.lines.reduce(function(logString, currLine, index) {
                let prefix = `${currLine.timeStamp} ${currLine.stack} `;
                if (!["log"].includes(currLine.level)) {
                    prefix += currLine.level + " ";
                }
                return logString + prefix + get_str_for_line(
                    logs.strings, index + logs.trimOffset
                ) + "\n";
            }, "")
        }).then(function() {
            storage.set('logs', "");
            logs = {
                lines: [],
                trimOffset: 0,
                strings: {}
            };
        })
    },
    logs_memory_usage: function() {
        let objectList = [];
        let stack = [logs];
        let bytes = 0;

        while (stack.length) {
            let value = stack.pop();

            if (typeof value === 'boolean') {
                bytes += 4;
            } else if (typeof value === 'string') {
                bytes += value.length * 2;
            } else if (typeof value === 'number') {
                bytes += 8;
            } else if (
                typeof value === 'object' &&
                objectList.indexOf(value) === -1
            ) {
                objectList.push(value);

                for (let i in value) {
                    stack.push(value[i]);
                }
            }
        }
        return bytes;
    }
}

function get_str_for_line(strings, line) {
    for (let string in strings) {
        let lines = strings[string];
        for (let i = 0; i < lines.length; i++) {
            if (lines[i] === line) {
                return string;
            }
        }
    }
    return "NO_STRING_FOUND_FOR_LINE_" + line;
}

function getCurrentLineNumber(page) {
    return page.lines.length + page.trimOffset - 1;
}

function log_and_format(level, args) {
    console[level](...args);

    let output = "";
    for (const data of args) {
        output += formatData(data, getType(data), 0) + " ";
    }
    return output.slice(0, -1);
}

function formatData(data, type, depth) {
    if (depth >= MAX_DEPTH) {
        return '... (max depth reached)';
    }

    const format = {
        object: function(obj, depth) {
            let lastIndex = Object.keys(obj).length;
            if (!lastIndex) {
                return '{}';
            }
            let formatted = '{\n';
            let i = 1;
            for (let key in obj) {
                formatted += indent(depth + 1) + key + ': ' + formatData(
                    obj[key], getType(obj[key]), depth + 1
                );
                if (i < lastIndex) {
                    formatted += ',';
                }
                formatted += '\n';
                i++;
            }
            return formatted + indent(depth) + '}';
        },
        array: function(arr, depth) {
            let formatted = '[';
            let lastIndex = arr.length - 1;
            for (let i = 0; i < arr.length; i++) {
                formatted += formatData(arr[i], getType(arr[i]), depth);
                if (i < lastIndex) {
                    formatted += ', ';
                }
            }
            return formatted + ']';
        },
        function: function(func, depth) {
            let lines = (func + '').split('\n');
            if (!lines.length) {
                return '';
            }
            let formatted = lines[0];
            let trimdentation = formatted.length - formatted.trim().length;
            if (lines.length > 1) {
                formatted += '\n';
                if (trimdentation === 0) {
                    let lastLine = lines[lines.length - 1];
                    trimdentation = lastLine.length - lastLine.trim().length;
                }
            }
            for (let i = 1; i < lines.length; i++) {
                formatted += indent(depth) + lines[i].substr(trimdentation);
                if (i < lines.length - 1) {
                    formatted += '\n';
                }
            }
            return formatted;
        },
        element: function(element) {
            let startInner = element.outerHTML.indexOf(element.innerHTML);
            return element.outerHTML.substr(0, startInner) + element.outerHTML.substr(
                startInner + element.innerHTML.length, element.outerHTML.length
            );
        },
        node: function(node) {
            let formatted = node.nodeName + ' (' + {
                '1': 'ELEMENT_NODE',
                '2': 'ATTRIBUTE_NODE',
                '3': 'TEXT_NODE',
                '4': 'CDATA_SECTION_NODE',
                '5': 'ENTITY_REFERENCE_NODE',
                '6': 'ENTITY_NODE',
                '7': 'PROCESSING_INSTRUCTION_NODE',
                '8': 'COMMENT_NODE',
                '9': 'DOCUMENT_NODE',
                '10': 'DOCUMENT_TYPE_NODE',
                '11': 'DOCUMENT_FRAGMENT_NODE',
                '12': 'NOTATION_NODE'
            } [node.nodeType] + ')';

            if (node.nodeValue) {
                formatted += ' ' + node.nodeValue.trim();
            }
            return formatted;
        },
        regex: function(regex) {
            return '/' + regex.source + '/';
        },
        string: function(str, depth) {
            if (depth > 0 || !str.length) {
                return '"' + str + '"';
            }
            return str;
        },
        boolean: function(bool) {
            return bool ? 'true' : 'false';
        }
    } [type];
    if (format) {
        return format(data, depth);
    } else {
        // null, undefined, window, and numbers end up here
        return data + '';
    }
}

function getType(data) {
    if (data === null) {
        return 'undefined';
    }

    let type = typeof data;
    if (data && type === 'object') {
        if (data instanceof Window) {
            return 'window';
        } else if (data instanceof HTMLElement) {
            return 'element';
        } else if (data instanceof Node) {
            return 'node';
        } else if (data.length == null) {
            if (typeof data.getTime === 'function') {
                // Dates get formatted as strings
                return 'string';
            } else if (typeof data.test === 'function') {
                return 'regexp';
            }
            return 'object';
        }
        return 'array';
    }
    return type;
}

function indent(depth) {
    let str = '';
    for (let i = 0; i < depth; i++) {
        str += INDENT;
    }
    return str;
}

function kerningIntegers(value, maxLength) {
    value = value + "";
    return new Array(maxLength - value.length + 1).join("0") + value;
}

function getTime() {
    let currTime = new Date();
    let hours = currTime.getHours();
    let evening = hours >= 12;
    if (hours > 12) {
        hours -= 12;
    }
    return kerningIntegers(hours, 2) + ":" +
        kerningIntegers(currTime.getMinutes(), 2) + ":" +
        kerningIntegers(currTime.getSeconds(), 2) + "." +
        kerningIntegers(currTime.getMilliseconds(), 4) +
        (evening ? "pm" : "am");
}

function getStackLocation() {
    for (let line of new Error().stack.split("\n").slice(1)) {
        // Find the first part of the stack that does not contain this file
        if (!line.includes("logger.js")) {
            // we get something like
            // "    at eval (webpack:///./src/js/path/to/file.js?:##:##)"
            let stackLocationStart = line.indexOf(ROOT) + ROOT.length;
            let filenameEndIndex = line.indexOf(WRONG_FILE_ENDING);
            if (filenameEndIndex !== -1) {
                // If file ends in .js? we trim the ?
                return line.substr(
                    stackLocationStart,
                    filenameEndIndex - stackLocationStart + GOOD_FILE_ENDING.length
                ) + line.substr(
                    filenameEndIndex + WRONG_FILE_ENDING.length
                ).slice(0, -1);
            } else {
                // we want to return "src/js/path/to/file.js:##:##"
                return line.substr(stackLocationStart).slice(0, -1);
            }
        }
    }
}