Skip to content

Commit

Permalink
feat(core): First working version
Browse files Browse the repository at this point in the history
  • Loading branch information
grantila committed Jun 27, 2019
1 parent f77154a commit 1904a3f
Show file tree
Hide file tree
Showing 8 changed files with 299 additions and 0 deletions.
21 changes: 21 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
language: node_js
cache:
directories:
- ~/.npm
notifications:
email: false
node_js:
- '12'
- '11'
- '10'
- '9'
- '8'
- '7'
- '6'
after_success:
- npm run travis-deploy-once "npm run semantic-release"
branches:
except:
- /^v\d+\.\d+\.\d+$/
after_script:
- "test -e ./coverage/lcov.info && cat ./coverage/lcov.info | node_modules/.bin/coveralls"
3 changes: 3 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module "trace-unhandled" {
export function register( ): void;
}
2 changes: 2 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@

exports.register = ( ) => require( './register' );
21 changes: 21 additions & 0 deletions index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

const index = require( './' );

describe( "register", ( ) =>
{
it( "should export 'register'", ( ) =>
{
expect( index ).toMatchObject( {
register: expect.any( Function ),
} );
} );

it( "should load 'register'", ( ) =>
{
const spy = jest.fn( );
jest.doMock( './register.js', spy );
index.register( );
expect( spy.mock.calls.length ).toBe( 1 );
jest.dontMock( './register.js' );
} );
} );
4 changes: 4 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module.exports = {
testEnvironment: 'node',
coverageReporters: ['lcov', 'text', 'html'],
};
55 changes: 55 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"name": "trace-unhandled",
"version": "0.0.0-development",
"description": "Much better tracing of unhandled promise rejections in JavaScript",
"author": "Gustaf Räntilä",
"license": "MIT",
"bugs": {
"url": "https://github.com/grantila/trace-unhandled/issues"
},
"homepage": "https://github.com/grantila/trace-unhandled#readme",
"main": "./index.js",
"types": "./index.d.ts",
"engines": {
"node": ">=6"
},
"files": [
"index.js",
"index.d.ts",
"register.js"
],
"scripts": {
"test": "node --expose-gc node_modules/.bin/jest --coverage",
"travis-deploy-once": "travis-deploy-once",
"semantic-release": "semantic-release",
"cz": "git-cz"
},
"repository": {
"type": "git",
"url": "https://github.com/grantila/trace-unhandled"
},
"keywords": [
"trace",
"unhandled",
"rejection",
"promise",
"stack",
"stacktrace"
],
"devDependencies": {
"@types/jest": "^20",
"@types/node": "^12",
"already": "^1.8.0",
"commitizen": "^3",
"coveralls": "^3",
"cz-conventional-changelog": "^2",
"jest": "^20",
"semantic-release": "^15.13.18",
"travis-deploy-once": "^5"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
85 changes: 85 additions & 0 deletions register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@

Error.stackTraceLimit = 100;

const reStackEntry = /\s+at\s/;

function splitErrorStack( error )
{
const lines = error.stack.split( "\n" );
const index = lines.findIndex( line => reStackEntry.test( line ) );
const message = lines.slice( 0, index ).join( "\n" );
return { message, lines: lines.slice( index ) };
}

function mergeErrors( traceError, mainError )
{
const { lines: traceLines } = splitErrorStack( traceError );
const { lines: errorLines, message } = splitErrorStack( mainError )

if ( traceLines[ 0 ].includes( "at new TraceablePromise" ) )
{
traceLines.shift( );

const ignore = [
"at Function.reject (<anonymous>)",
"at Promise.__proto__.constructor.reject",
];
if ( ignore.some( test => traceLines[ 0 ].includes( test ) ) )
traceLines.shift( );
}

traceLines.reverse( );
errorLines.reverse( );

var i = 0;
for ( ;
i < errorLines.length &&
i < traceLines.length &&
errorLines[ i ] === traceLines[ i ];
++i
);

return message +
"\n ==== Promise at: ==================\n" +
traceLines.slice( i ).reverse( ).join( "\n" ) +
"\n\n ==== Error at: ====================\n" +
errorLines.slice( i ).reverse( ).join( "\n" ) +
"\n\n ==== Shared trace: ================\n" +
errorLines.slice( 0, i ).reverse( ).join( "\n" );
}

process.on( "unhandledRejection", ( reason, promise ) =>
{
const stack =
promise.__tracedError
? mergeErrors( promise.__tracedError, reason )
: reason.stack;

console.error(
`(node:${process.pid}) UnhandledPromiseRejectionWarning\n` +
(
!promise.__tracedError
? ""
: `[ Stacktrace altered by trace-unhandled-rejection ]\n`
) +
stack
);
} );

class TraceablePromise extends Promise
{
constructor( executor )
{
super( wrappedExecutor );

function wrappedExecutor( ...args )
{
return executor( ...args );
}

const err = new Error( "Non-failing tracing error" );
this.__tracedError = err;
}
}

global.Promise = TraceablePromise;
108 changes: 108 additions & 0 deletions register.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@

const { Finally, Try, delay } = require( 'already' );
require( './register' );

const withConsoleSpy = fn => async ( ) =>
{
const oldError = console.error;
console.error = jest.fn( );
return Try( fn )
.then( ...Finally( ( ) =>
{
console.error = oldError;
} ) );
}

async function triggerUnhandledWarnings( )
{
await delay( 0 );
global.gc && global.gc( );
await delay( 0 );
}

function splitLines( lines )
{
return [ ].concat( ...lines.map( line => line.split( "\n" ) ) );
}

function cutColumn( line )
{
const m = line.match( /(.*:\d+):\d+$/ );
if ( !m )
return line;
return m[ 1 ];
}

function cutLocation( line )
{
const m = line.match( /(.*):\d+:\d+$/ );
if ( !m )
return line;
return m[ 1 ];
}

describe( "register", ( ) =>
{
it( "Handle simplest case (same location)", withConsoleSpy( async ( ) =>
{
Promise.reject( new Error( "the error" ) );

await triggerUnhandledWarnings( );

const lines = splitLines( console.error.mock.calls[ 0 ] )
.filter( line => line.includes( "register.spec.js" ) )
.map( line => cutColumn( line ) );

expect( lines.length ).toBeGreaterThanOrEqual( 2 );
expect( lines[ 0 ] ).toBe( lines[ 1 ] );
} ) );

it( "Handle async case <reject()> (different locations)", withConsoleSpy( async ( ) =>
{
const err = new Error( "the error" );

Promise.reject( err );

await triggerUnhandledWarnings( );

const linesWoColumns = splitLines( console.error.mock.calls[ 0 ] )
.filter( line => line.includes( "register.spec.js" ) )
.map( line => cutColumn( line ) );

const linesWoLocation = splitLines( console.error.mock.calls[ 0 ] )
.filter( line => line.includes( "register.spec.js" ) )
.map( line => cutLocation( line ) );

expect( linesWoColumns.length ).toBeGreaterThanOrEqual( 2 );
expect( linesWoColumns[ 0 ] ).not.toBe( linesWoColumns[ 1 ] );

expect( linesWoLocation.length ).toBeGreaterThanOrEqual( 2 );
expect( linesWoLocation[ 0 ] ).toBe( linesWoLocation[ 1 ] );
} ) );

it( "Handle async case <new Promise> (different locations)", withConsoleSpy( async ( ) =>
{
const err = new Error( "the error" );

new Promise( ( resolve, reject ) =>
{
reject( err );
} );

await triggerUnhandledWarnings( );

const linesWoColumns = splitLines( console.error.mock.calls[ 0 ] )
.filter( line => line.includes( "register.spec.js" ) )
.map( line => cutColumn( line ) );

const linesWoLocation = splitLines( console.error.mock.calls[ 0 ] )
.filter( line => line.includes( "register.spec.js" ) )
.map( line => cutLocation( line ) );

expect( linesWoColumns.length ).toBeGreaterThanOrEqual( 2 );
expect( linesWoColumns[ 0 ] ).not.toBe( linesWoColumns[ 1 ] );

expect( linesWoLocation.length ).toBeGreaterThanOrEqual( 2 );
expect( linesWoLocation[ 0 ] ).toBe( linesWoLocation[ 1 ] );
} ) );
} );

0 comments on commit 1904a3f

Please sign in to comment.