diff --git a/lib/Interactor/Utility.js b/lib/Interactor/Utility.js index e8c8816d..951c33b0 100644 --- a/lib/Interactor/Utility.js +++ b/lib/Interactor/Utility.js @@ -92,43 +92,90 @@ function StackTraceParser (opts) { this._context_size = opts.context; } +StackTraceParser.prototype.attachContext = function (error) { + var self = this; + if (!error) return error; + + // if pmx attached callsites we can parse them to retrieve the context + if (typeof (error.stackframes) === 'object') { + var result = self.parse(error.stackframes); + // no need to send it since there is already the stacktrace + delete error.stackframes; + delete error.__error_callsites; + + if (result) { + error.callsite = result.callsite; + error.context = result.context; + } + } + // if the stack is here we can parse it directly from the stack string + // only if the context has been retrieved from elsewhere + if (typeof error.stack === 'string' && !error.callsite) { + var siteRegex = /(\/[^\\\n]*)/g; + var tmp; + var stack = []; + + // find matching groups + while ((tmp = siteRegex.exec(error.stack))) { + stack.push(tmp[1]); + } + + // parse each callsite to match the format used by the stackParser + stack = stack.map(function (callsite) { + // remove the trailing ) if present + if (callsite[callsite.length - 1] === ')') { + callsite = callsite.substr(0, callsite.length - 1); + } + var location = callsite.split(':'); + + return location.length < 3 ? callsite : { + file_name: location[0], + line_number: parseInt(location[1]) + }; + }); + + var finalCallsite = self.parse(stack); + if (finalCallsite) { + error.callsite = finalCallsite.callsite; + error.context = finalCallsite.context; + } + } + return error; +}; + /** * Parse the stacktrace and return callsite + context if available */ StackTraceParser.prototype.parse = function (stack) { var self = this; - if (!stack || stack.length == 0) - return false; + if (!stack || stack.length === 0) return false; for (var i = 0, len = stack.length; i < len; i++) { var callsite = stack[i]; // avoid null values - if (!callsite || - !callsite.file_name || - !callsite.line_number) - continue; + if (typeof callsite !== 'object') continue; + if (!callsite.file_name || !callsite.line_number) continue; - var type = (!path.isAbsolute(callsite.file_name) && callsite.file_name[0] !== '.') ? 'core' : 'user' + var type = path.isAbsolute(callsite.file_name) || callsite.file_name[0] === '.' ? 'user' : 'core'; // only use the callsite if its inside user space - if (!callsite || - type === 'core' || - callsite.file_name.indexOf('node_modules') > -1 || - callsite.file_name.indexOf('vxx') > -1) + if (!callsite || type === 'core' || callsite.file_name.indexOf('node_modules') > -1 || + callsite.file_name.indexOf('vxx') > -1) { continue; + } // get the whole context (all lines) and cache them if necessary - var context = self._cache.get(callsite.file_name) - var source = [] + var context = self._cache.get(callsite.file_name); + var source = []; if (context && context.length > 0) { // get line before the call var preLine = callsite.line_number - self._context_size - 1; var pre = context.slice(preLine > 0 ? preLine : 0, callsite.line_number - 1); if (pre && pre.length > 0) { pre.forEach(function (line) { - source.push(line.replace(/\t/g, ' ')) - }) + source.push(line.replace(/\t/g, ' ')); + }); } // get the line where the call has been made if (context[callsite.line_number - 1]) { @@ -136,23 +183,23 @@ StackTraceParser.prototype.parse = function (stack) { } // and get the line after the call var postLine = callsite.line_number + self._context_size; - var post = context.slice(callsite.line_number, postLine) + var post = context.slice(callsite.line_number, postLine); if (post && post.length > 0) { post.forEach(function (line) { - source.push(line.replace(/\t/g, ' ')) - }) - } - return { - context: source.join('\n'), - callsite: [ callsite.file_name, callsite.line_number ].join(':') + source.push(line.replace(/\t/g, ' ')); + }); } } + return { + context: source.length > 0 ? source.join('\n') : 'cannot retrieve source context', + callsite: [ callsite.file_name, callsite.line_number ].join(':') + }; } return false; -} +}; module.exports = { EWMA: EWMA, Cache: Cache, StackTraceParser: StackTraceParser -} +}; diff --git a/test/interface/stacktrace.mocha.js b/test/interface/stacktrace.mocha.js index 940d3e0d..630f844b 100644 --- a/test/interface/stacktrace.mocha.js +++ b/test/interface/stacktrace.mocha.js @@ -5,6 +5,7 @@ var Utility = require('../../lib/Interactor/Utility.js'); var TraceFactory = require('./misc/trace_factory.js'); var path = require('path'); var fs = require('fs'); +var assert = require('assert'); describe('StackTrace Utility', function() { var aggregator; @@ -29,7 +30,7 @@ describe('StackTrace Utility', function() { aggregator = new Aggregator({ stackParser: stackParser}); }); - describe('.parseStacktrace', function() { + describe('.parse', function() { it('should parse stacktrace and get context', function(done) { var obj = [{ labels: { @@ -125,4 +126,32 @@ describe('StackTrace Utility', function() { }); }); + + describe('.attachContext', function () { + it('should extract context from stackframes', function () { + var error = stackParser.attachContext({ + stackframes: [ + { + file_name: '/toto/tmp/lol', + line_number: 10 + } + ] + }); + assert(error !== undefined); + assert(error.stackframes === undefined); + assert(error.callsite !== undefined); + assert(error.callsite.indexOf('/toto/tmp/lol') >= 0); + }); + + it('should extract context from the stack string', function () { + var error = new Error(); + // stack is lazy so we need to load it + error.stack = error.stack; + error = stackParser.attachContext(error); + assert(error !== undefined); + assert(error.stackframes === undefined); + assert(error.callsite.indexOf(__filename) >= 0); + assert(error.context.indexOf('var error = new Error()') >= 0); + }); + }); });