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);
}
}
}
}