"use strict"; module.exports = function() { var async = require("./async.js"); var util = require("./util.js"); var bluebirdFramePattern = /[\\\/]bluebird[\\\/]js[\\\/](main|debug|zalgo|instrumented)/; var stackFramePattern = null; var formatStack = null; var indentStackFrames = false; function CapturedTrace(parent) { this._parent = parent; var length = this._length = 1 + (parent === undefined ? 0 : parent._length); captureStackTrace(this, CapturedTrace); if (length > 32) this.uncycle(); } util.inherits(CapturedTrace, Error); CapturedTrace.prototype.uncycle = function() { var length = this._length; if (length < 2) return; var nodes = []; var stackToIndex = {}; for (var i = 0, node = this; node !== undefined; ++i) { nodes.push(node); node = node._parent; } length = this._length = i; for (var i = length - 1; i >= 0; --i) { var stack = nodes[i].stack; if (stackToIndex[stack] === undefined) { stackToIndex[stack] = i; } } for (var i = 0; i < length; ++i) { var currentStack = nodes[i].stack; var index = stackToIndex[currentStack]; if (index !== undefined && index !== i) { if (index > 0) { nodes[index - 1]._parent = undefined; nodes[index - 1]._length = 1; } nodes[i]._parent = undefined; nodes[i]._length = 1; var cycleEdgeNode = i > 0 ? nodes[i - 1] : this; if (index < length - 1) { cycleEdgeNode._parent = nodes[index + 1]; cycleEdgeNode._parent.uncycle(); cycleEdgeNode._length = cycleEdgeNode._parent._length + 1; } else { cycleEdgeNode._parent = undefined; cycleEdgeNode._length = 1; } var currentChildLength = cycleEdgeNode._length + 1; for (var j = i - 2; j >= 0; --j) { nodes[j]._length = currentChildLength; currentChildLength++; } return; } } }; CapturedTrace.prototype.parent = function() { return this._parent; }; CapturedTrace.prototype.hasParent = function() { return this._parent !== undefined; }; CapturedTrace.prototype.attachExtraTrace = function(error) { if (error.__stackCleaned__) return; this.uncycle(); var parsed = CapturedTrace.parseStackAndMessage(error); var message = parsed.message; var stacks = [parsed.stack]; var trace = this; while (trace !== undefined) { stacks.push(cleanStack(trace.stack.split("\n"))); trace = trace._parent; } removeCommonRoots(stacks); removeDuplicateOrEmptyJumps(stacks); error.stack = reconstructStack(message, stacks); error.__stackCleaned__ = true; }; function reconstructStack(message, stacks) { for (var i = 0; i < stacks.length - 1; ++i) { stacks[i].push("From previous event:"); stacks[i] = stacks[i].join("\n"); } if (i < stacks.length) { stacks[i] = stacks[i].join("\n"); } return message + "\n" + stacks.join("\n"); } function removeDuplicateOrEmptyJumps(stacks) { for (var i = 0; i < stacks.length; ++i) { if (stacks[i].length === 0 || ((i + 1 < stacks.length) && stacks[i][0] === stacks[i+1][0])) { stacks.splice(i, 1); i--; } } } function removeCommonRoots(stacks) { var current = stacks[0]; for (var i = 1; i < stacks.length; ++i) { var prev = stacks[i]; var currentLastIndex = current.length - 1; var currentLastLine = current[currentLastIndex]; var commonRootMeetPoint = -1; for (var j = prev.length - 1; j >= 0; --j) { if (prev[j] === currentLastLine) { commonRootMeetPoint = j; break; } } for (var j = commonRootMeetPoint; j >= 0; --j) { var line = prev[j]; if (current[currentLastIndex] === line) { current.pop(); currentLastIndex--; } else { break; } } current = prev; } } function cleanStack(stack) { var ret = []; for (var i = 0; i < stack.length; ++i) { var line = stack[i]; var isTraceLine = stackFramePattern.test(line) || " (No stack trace)" === line; var isInternalFrame = isTraceLine && shouldIgnore(line); if (isTraceLine && !isInternalFrame) { if (indentStackFrames && line.charAt(0) !== " ") { line = " " + line; } ret.push(line); } } return ret; } function stackFramesAsArray(error) { var stack = error.stack.replace(/\s+$/g, "").split("\n"); for (var i = 0; i < stack.length; ++i) { var line = stack[i]; if (" (No stack trace)" === line || stackFramePattern.test(line)) { break; } } if (i > 0) { stack = stack.slice(i); } return stack; } CapturedTrace.parseStackAndMessage = function(error) { var stack = error.stack; var message = error.toString(); stack = typeof stack === "string" && stack.length > 0 ? stackFramesAsArray(error) : [" (No stack trace)"]; return { message: message, stack: cleanStack(stack) }; }; CapturedTrace.formatAndLogError = function(error, title) { if (typeof console === "object") { var message; if (typeof error === "object" || typeof error === "function") { var stack = error.stack; message = title + formatStack(stack, error); } else { message = title + String(error); } if (typeof console.warn === "function" || typeof console.warn === "object") { console.warn(message); } else if (typeof console.log === "function" || typeof console.log === "object") { console.log(message); } } }; CapturedTrace.unhandledRejection = function (reason) { CapturedTrace.formatAndLogError(reason, "^--- With additional stack trace: "); }; CapturedTrace.isSupported = function () { return typeof captureStackTrace === "function"; }; CapturedTrace.fireRejectionEvent = function(name, localHandler, reason, promise) { var localEventFired = false; try { if (typeof localHandler === "function") { localEventFired = true; if (name === "rejectionHandled") { localHandler(promise); } else { localHandler(reason, promise); } } } catch (e) { async.throwLater(e); } var globalEventFired = false; try { globalEventFired = fireGlobalEvent(name, reason, promise); } catch (e) { globalEventFired = true; async.throwLater(e); } var domEventFired = false; if (fireDomEvent) { try { domEventFired = fireDomEvent(name.toLowerCase(), { reason: reason, promise: promise }); } catch (e) { domEventFired = true; async.throwLater(e); } } if (!globalEventFired && !localEventFired && !domEventFired && name === "unhandledRejection") { CapturedTrace.formatAndLogError(reason, "Possibly unhandled "); } }; function formatNonError(obj) { var str; if (typeof obj === "function") { str = "[function " + (obj.name || "anonymous") + "]"; } else { str = obj.toString(); var ruselessToString = /\[object [a-zA-Z0-9$_]+\]/; if (ruselessToString.test(str)) { try { var newStr = JSON.stringify(obj); str = newStr; } catch(e) { } } if (str.length === 0) { str = "(empty array)"; } } return ("(<" + snip(str) + ">, no stack trace)"); } function snip(str) { var maxChars = 41; if (str.length < maxChars) { return str; } return str.substr(0, maxChars - 3) + "..."; } var shouldIgnore = function() { return false; }; var parseLineInfoRegex = /[\/<\(]([^:\/]+):(\d+):(?:\d+)\)?\s*$/; function parseLineInfo(line) { var matches = line.match(parseLineInfoRegex); if (matches) { return { fileName: matches[1], line: parseInt(matches[2], 10) }; } } CapturedTrace.setBounds = function(firstLineError, lastLineError) { if (!CapturedTrace.isSupported()) return; var firstStackLines = firstLineError.stack.split("\n"); var lastStackLines = lastLineError.stack.split("\n"); var firstIndex = -1; var lastIndex = -1; var firstFileName; var lastFileName; for (var i = 0; i < firstStackLines.length; ++i) { var result = parseLineInfo(firstStackLines[i]); if (result) { firstFileName = result.fileName; firstIndex = result.line; break; } } for (var i = 0; i < lastStackLines.length; ++i) { var result = parseLineInfo(lastStackLines[i]); if (result) { lastFileName = result.fileName; lastIndex = result.line; break; } } if (firstIndex < 0 || lastIndex < 0 || !firstFileName || !lastFileName || firstFileName !== lastFileName || firstIndex >= lastIndex) { return; } shouldIgnore = function(line) { if (bluebirdFramePattern.test(line)) return true; var info = parseLineInfo(line); if (info) { if (info.fileName === firstFileName && (firstIndex <= info.line && info.line <= lastIndex)) { return true; } } return false; }; }; var captureStackTrace = (function stackDetection() { var v8stackFramePattern = /^\s*at\s*/; var v8stackFormatter = function(stack, error) { if (typeof stack === "string") return stack; if (error.name !== undefined && error.message !== undefined) { return error.toString(); } return formatNonError(error); }; if (typeof Error.stackTraceLimit === "number" && typeof Error.captureStackTrace === "function") { Error.stackTraceLimit = Error.stackTraceLimit + 6; stackFramePattern = v8stackFramePattern; formatStack = v8stackFormatter; var captureStackTrace = Error.captureStackTrace; shouldIgnore = function(line) { return bluebirdFramePattern.test(line); }; return function(receiver, ignoreUntil) { Error.stackTraceLimit = Error.stackTraceLimit + 6; captureStackTrace(receiver, ignoreUntil); Error.stackTraceLimit = Error.stackTraceLimit - 6; }; } var err = new Error(); if (typeof err.stack === "string" && err.stack.split("\n")[0].indexOf("stackDetection@") >= 0) { stackFramePattern = /@/; formatStack = v8stackFormatter; indentStackFrames = true; return function captureStackTrace(o) { o.stack = new Error().stack; }; } var hasStackAfterThrow; try { throw new Error(); } catch(e) { hasStackAfterThrow = ("stack" in e); } if (!("stack" in err) && hasStackAfterThrow) { stackFramePattern = v8stackFramePattern; formatStack = v8stackFormatter; return function captureStackTrace(o) { Error.stackTraceLimit = Error.stackTraceLimit + 6; try { throw new Error(); } catch(e) { o.stack = e.stack; } Error.stackTraceLimit = Error.stackTraceLimit - 6; }; } formatStack = function(stack, error) { if (typeof stack === "string") return stack; if ((typeof error === "object" || typeof error === "function") && error.name !== undefined && error.message !== undefined) { return error.toString(); } return formatNonError(error); }; return null; })([]); var fireDomEvent; var fireGlobalEvent = (function() { if (util.isNode) { return function(name, reason, promise) { if (name === "rejectionHandled") { return process.emit(name, promise); } else { return process.emit(name, reason, promise); } }; } else { var customEventWorks = false; var anyEventWorks = true; try { var ev = new self.CustomEvent("test"); customEventWorks = ev instanceof CustomEvent; } catch (e) {} if (!customEventWorks) { try { var event = document.createEvent("CustomEvent"); event.initCustomEvent("testingtheevent", false, true, {}); self.dispatchEvent(event); } catch (e) { anyEventWorks = false; } } if (anyEventWorks) { fireDomEvent = function(type, detail) { var event; if (customEventWorks) { event = new self.CustomEvent(type, { detail: detail, bubbles: false, cancelable: true }); } else if (self.dispatchEvent) { event = document.createEvent("CustomEvent"); event.initCustomEvent(type, false, true, detail); } return event ? !self.dispatchEvent(event) : false; }; } var toWindowMethodNameMap = {}; toWindowMethodNameMap["unhandledRejection"] = ("on" + "unhandledRejection").toLowerCase(); toWindowMethodNameMap["rejectionHandled"] = ("on" + "rejectionHandled").toLowerCase(); return function(name, reason, promise) { var methodName = toWindowMethodNameMap[name]; var method = self[methodName]; if (!method) return false; if (name === "rejectionHandled") { method.call(self, promise); } else { method.call(self, reason, promise); } return true; }; } })(); return CapturedTrace; };