Skip to content

Commit

Permalink
Support long stack traces
Browse files Browse the repository at this point in the history
  • Loading branch information
jamestalmage committed Jan 14, 2016
1 parent 7cfcedf commit ac75d47
Show file tree
Hide file tree
Showing 11 changed files with 338 additions and 21 deletions.
40 changes: 34 additions & 6 deletions index.js
Expand Up @@ -34,7 +34,12 @@ StackUtils.prototype.clean = function (stack) {
stack = stack.slice(1);
}

stack = stack.map(function (st) {
var outdent = false;
var lastNonAtLine = null;
var result = [];

stack.forEach(function (st1) {
var st = st1;
var isInternal = this._internals.some(function (internal) {
return internal.test(st);
});
Expand All @@ -43,13 +48,36 @@ StackUtils.prototype.clean = function (stack) {
return null;
}

return st.trim()
.replace(/^\s*at /, '')
var isAtLine = /^\s*at /.test(st);

if (outdent) {
st = st.replace(/\s+$/, '').replace(/^(\s+)at /, '$1');
} else {
st = st.trim();
if (isAtLine) {
st = st.substring(3);
}
}

st = st
.replace(/\\/g, '/')
.replace(this._cwd + '/', '');
}, this).filter(function (st) {
return st;
}).join('\n').trim();

if (st) {
if (isAtLine) {
if (lastNonAtLine) {
result.push(lastNonAtLine);
lastNonAtLine = null;
}
result.push(st);
} else {
outdent = true;
lastNonAtLine = st;
}
}
}, this);

stack = result.join('\n').trim();

if (stack) {
return stack + '\n';
Expand Down
11 changes: 4 additions & 7 deletions package.json
Expand Up @@ -22,19 +22,16 @@
"keywords": [
""
],
"config": {
"nyc": {
"exclude": [
"fixtures/*"
]
}
},
"dependencies": {},
"devDependencies": {
"ava": "^0.8.0",
"bluebird": "^3.1.1",
"coveralls": "^2.11.6",
"flatten": "0.0.1",
"nested-error-stacks": "^1.0.2",
"nyc": "^5.2.0",
"pify": "^2.3.0",
"q": "^1.4.1",
"xo": "^0.12.1"
}
}
10 changes: 10 additions & 0 deletions test/_utils.js
@@ -0,0 +1,10 @@
var flatten = require('flatten');
var path = require('path');

module.exports.join = join;
module.exports.fixtureDir = path.join(__dirname, 'fixtures');

function join() {
var args = Array.prototype.slice.call(arguments);
return flatten(args).join('\n') + '\n';
}
File renamed without changes.
18 changes: 18 additions & 0 deletions test/fixtures/internal-error.js
@@ -0,0 +1,18 @@
'use strict';
var NestedError = require('nested-error-stacks');
var util = require('util');

function InternalError(message, nested) {
NestedError.call(this, message, nested);
}

util.inherits(InternalError, NestedError);
InternalError.prototype.name = 'InternalError';

module.exports = function (cb, err) {
setTimeout(bound.bind(null, cb, err), 0);
};

function bound(cb, err) {
cb(new InternalError('internal' + (err ? ': ' + err.message : ''), err));
}
17 changes: 17 additions & 0 deletions test/fixtures/internal-then.js
@@ -0,0 +1,17 @@

//var p = global.InternalPromise.resolve().then(function () {});

module.exports = function internalLibraryOuterFn(then) {
return global.InternalPromise.resolve().then(function internalLibraryInnerFn() {
return global.InternalPromise.resolve().then(then);
});
};

module.exports.reject = function internalLibraryOuterReject() {
return global.InternalPromise.resolve().then(function internalLibraryInnerReject() {
return global.InternalPromise.reject(new Error('inner')).catch(function (e) {
return e.stack;
});
});
};

16 changes: 16 additions & 0 deletions test/fixtures/long-stack-traces.js
@@ -0,0 +1,16 @@
'use strict';

var Q = require('q');
Q.longStackSupport = true;
global.InternalPromise = Q;
module.exports.q = require('./produce-long-stack-traces');

var longStackTracePath = require.resolve('./produce-long-stack-traces');
var internalThen = require.resolve('./internal-then');
delete require.cache[longStackTracePath];
delete require.cache[internalThen];

var bluebird = require('bluebird');
bluebird.config({longStackTraces: true});
global.InternalPromise = bluebird;
module.exports.bluebird = require('./produce-long-stack-traces');
49 changes: 49 additions & 0 deletions test/fixtures/nested-errors.js
@@ -0,0 +1,49 @@
'use strict';

var NestedError = require('nested-error-stacks');
var util = require('util');
var internal = require('./internal-error');

function foo(cb) {
bar(function nested(err) {
cb(new FooError('foo' + err.message, err));
});
}

function bar(cb) {
internal(function moreNested(err) {
cb(new BarError('bar: ' + err.message, err));
});
}

function FooError(message, nested) {
NestedError.call(this, message, nested);
}

util.inherits(FooError, NestedError);
FooError.prototype.name = 'FooError';

function BarError(message, nested) {
NestedError.call(this, message, nested);
}

util.inherits(BarError, NestedError);
BarError.prototype.name = 'BarError';

module.exports.top = function(cb) {
internal(function (err) {
cb(err.stack);
}, new Error('baz'));
};

module.exports.middle = function (cb) {
internal(function (err) {
cb(new FooError('foo', err).stack);
}, new Error('bar'));
};

module.exports.bottom = function (cb) {
foo(function (err){
cb(err.stack);
});
};
50 changes: 50 additions & 0 deletions test/fixtures/produce-long-stack-traces.js
@@ -0,0 +1,50 @@
'use strict';

var Promise = global.InternalPromise;
var internalThen = require('./internal-then');

module.exports = Promise.resolve().then(function outer() {
return Promise.resolve().then(function inner() {
return Promise.resolve().then(function evenMoreInner() {
return Promise.resolve().then(function mostInner() {
a.b.c.d()
}).catch(function catcher(e) {
return e.stack;
});
});
});
});

module.exports.middle = Promise.resolve().then(function outer() {
return Promise.resolve().then(function inner() {
return internalThen(function evenMoreInner() {
return Promise.resolve().then(function mostInner() {
a.b.c.d()
}).catch(function catcher(e) {
return e.stack;
});
});
});
});

module.exports.top = Promise.resolve().then(function outer() {
return Promise.resolve().then(function inner() {
return Promise.resolve().then(function evenMoreInner() {
return Promise.resolve().then(internalThen.reject);
});
});
});

module.exports.bottom = new Promise(function (resolve) {
setTimeout(internalThen.bind(null, function outer() {
return Promise.resolve().then(function inner() {
return Promise.resolve().then(function evenMoreInner() {
return Promise.resolve().then(function mostInner() {
a.b.c.d()
}).catch(function catcher(e) {
resolve(e.stack);
});
});
});
}),0);
});
138 changes: 138 additions & 0 deletions test/long-stack-traces.js
@@ -0,0 +1,138 @@
import test from 'ava';
import StackUtils from '../';
import longStackTraces from './fixtures/long-stack-traces';
import pify from 'pify';
const nestedErrors = pify(require('./fixtures/nested-errors'), Promise);

import {join, fixtureDir} from './_utils';

function internals() {
return StackUtils.nodeInternals().concat([
/[\\\/]long-stack-traces\.js:[0-9]+:[0-9]+\)?$/,
/[\\\/]internal-error\.js:[0-9]+:[0-9]+\)?$/,
/[\\\/]internal-then\.js:[0-9]+:[0-9]+\)?$/,
/[\\\/]node_modules[\\\/]/,
// TODO: Should any of these be default internals?
/[\\\/]\.node-spawn-wrap-\w+-\w+[\\\/]node:[0-9]+:[0-9]+\)?$/,
/internal[\\\/]module\.js:[0-9]+:[0-9]+\)?$/,
/node\.js:[0-9]+:[0-9]+\)?$/
]);
}

const stackUtils = new StackUtils({internals: internals(), cwd: fixtureDir});

test('indents lines after first "From previous event:"', async t => {
const cleanedStack = stackUtils.clean(await longStackTraces.bluebird);
const expected = join([
'mostInner (produce-long-stack-traces.js:10:5)',
'From previous event:',
' evenMoreInner (produce-long-stack-traces.js:9:29)',
'From previous event:',
' inner (produce-long-stack-traces.js:8:28)',
'From previous event:',
' outer (produce-long-stack-traces.js:7:27)',
'From previous event:',
' Object.<anonymous> (produce-long-stack-traces.js:6:36)'
]);

t.is(cleanedStack, expected);
});

test('removes empty "From previous event:" sections from the bottom', async t => {
const stack = await longStackTraces.bluebird.bottom;
const cleanedStack = stackUtils.clean(stack);

const expected = join([
'mostInner (produce-long-stack-traces.js:43:6)',
'From previous event:',
' evenMoreInner (produce-long-stack-traces.js:42:30)',
'From previous event:',
' inner (produce-long-stack-traces.js:41:29)',
'From previous event:',
' outer (produce-long-stack-traces.js:40:28)'
]);

t.is(cleanedStack, expected);
});

test('removes empty "From previous event:" sections from the top', async t => {
const stack = await longStackTraces.bluebird.top;
const cleanedStack = stackUtils.clean(stack);

const expected = join([
'From previous event:',
' evenMoreInner (produce-long-stack-traces.js:33:29)',
'From previous event:',
' inner (produce-long-stack-traces.js:32:28)',
'From previous event:',
' outer (produce-long-stack-traces.js:31:27)',
'From previous event:',
' Object.<anonymous> (produce-long-stack-traces.js:30:40)'
]);

t.is(cleanedStack, expected);
});

test('removes empty "From previous event:" sections from the middle', async t => {
const stack = await longStackTraces.bluebird.middle;
const cleanedStack = stackUtils.clean(stack);

const expected = join([
'mostInner (produce-long-stack-traces.js:22:5)',
'From previous event:',
' evenMoreInner (produce-long-stack-traces.js:21:29)',
'From previous event:',
' inner (produce-long-stack-traces.js:20:10)',
'From previous event:',
' outer (produce-long-stack-traces.js:19:27)',
'From previous event:',
' Object.<anonymous> (produce-long-stack-traces.js:18:43)'
]);

t.is(cleanedStack, expected);
});

test.cb('removes empty "Caused by:" sections from the top', t => {
nestedErrors.top(stack => {
const cleanedStack = stackUtils.clean(stack);

const expected = join([
'Caused By: Error: baz',
' Object.module.exports.top (nested-errors.js:36:5)'
]);

t.is(cleanedStack, expected);
t.end();
});
});

test.cb('removes empty "Caused by:" sections from the bottom', t => {
nestedErrors.bottom(stack => {
const cleanedStack = stackUtils.clean(stack);

const expected = join([
'nested (nested-errors.js:9:6)',
'moreNested (nested-errors.js:15:3)',
'Caused By: BarError: bar: internal',
' moreNested (nested-errors.js:15:6)'
]);

t.is(cleanedStack, expected);
t.end();
});
});

test.cb('removes empty "Caused by:" sections from the middle', t => {
nestedErrors.middle(stack => {
const cleanedStack = stackUtils.clean(stack);

const expected = join([
'nested-errors.js:41:6',
'Caused By: Error: bar',
' Object.module.exports.middle (nested-errors.js:42:5)'
]);

t.is(cleanedStack, expected);
t.end();
});
});

0 comments on commit ac75d47

Please sign in to comment.