├── .babelrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── License ├── Readme.md ├── __tests__ ├── get-test.js ├── long-stack-trace-test.js └── parse-test.js ├── index.js ├── package-lock.json └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ] 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.un~ 2 | /node_modules 3 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __test__/ 2 | .babelrc 3 | .travis.yml -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | dist: focal 3 | sudo: false 4 | node_js: 5 | - "iojs" 6 | - "15" 7 | 8 | matrix: 9 | allow_failures: 10 | - node_js: "iojs" 11 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Felix Geisendörfer (felix@debuggable.com) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # stack-trace 2 | 3 | [![Build Status](https://travis-ci.org/felixge/node-stack-trace.svg?branch=master)](https://travis-ci.org/felixge/node-stack-trace) 4 | 5 | Get v8 stack traces as an array of CallSite objects. 6 | 7 | ## Install 8 | 9 | ``` bash 10 | npm install stack-trace 11 | ``` 12 | 13 | ## Usage 14 | 15 | The stack-trace module makes it easy for you to capture the current stack: 16 | 17 | ```javascript 18 | import { get } from 'stack-trace'; 19 | const trace = get(); 20 | 21 | expect(trace[0].getFileName()).toBe(__filename); 22 | ``` 23 | 24 | However, sometimes you have already popped the stack you are interested in, 25 | and all you have left is an `Error` object. This module can help: 26 | 27 | ```javascript 28 | import { parse } from 'stack-trace'; 29 | const err = new Error('something went wrong'); 30 | const trace = parse(err); 31 | 32 | expect(trace[0].getFileName()).toBe(__filename); 33 | ``` 34 | 35 | Please note that parsing the `Error#stack` property is not perfect, only 36 | certain properties can be retrieved with it as noted in the API docs below. 37 | 38 | ## Long stack traces 39 | 40 | stack-trace works great with [long-stack-traces][], when parsing an `err.stack` 41 | that has crossed the event loop boundary, a `CallSite` object returning 42 | `'----------------------------------------'` for `getFileName()` is created. 43 | All other methods of the event loop boundary call site return `null`. 44 | 45 | [long-stack-traces]: https://github.com/tlrobinson/long-stack-traces 46 | 47 | ## API 48 | 49 | ### stackTrace.get([belowFn]) 50 | 51 | Returns an array of `CallSite` objects, where element `0` is the current call 52 | site. 53 | 54 | When passing a function on the current stack as the `belowFn` parameter, the 55 | returned array will only include `CallSite` objects below this function. 56 | 57 | ### stackTrace.parse(err) 58 | 59 | Parses the `err.stack` property of an `Error` object into an array compatible 60 | with those returned by `stackTrace.get()`. However, only the following methods 61 | are implemented on the returned `CallSite` objects. 62 | 63 | * getTypeName 64 | * getFunctionName 65 | * getMethodName 66 | * getFileName 67 | * getLineNumber 68 | * getColumnNumber 69 | * isNative 70 | 71 | Note: Except `getFunctionName()`, all of the above methods return exactly the 72 | same values as you would get from `stackTrace.get()`. `getFunctionName()` 73 | is sometimes a little different, but still useful. 74 | 75 | ### CallSite 76 | 77 | The official v8 CallSite object API can be found [here][https://github.com/v8/v8/wiki/Stack-Trace-API#customizing-stack-traces]. A quick 78 | excerpt: 79 | 80 | > A CallSite object defines the following methods: 81 | > 82 | > * **getThis**: returns the value of this 83 | > * **getTypeName**: returns the type of this as a string. This is the name of the function stored in the constructor field of this, if available, otherwise the object's [[Class]] internal property. 84 | > * **getFunction**: returns the current function 85 | > * **getFunctionName**: returns the name of the current function, typically its name property. If a name property is not available an attempt will be made to try to infer a name from the function's context. 86 | > * **getMethodName**: returns the name of the property of this or one of its prototypes that holds the current function 87 | > * **getFileName**: if this function was defined in a script returns the name of the script 88 | > * **getLineNumber**: if this function was defined in a script returns the current line number 89 | > * **getColumnNumber**: if this function was defined in a script returns the current column number 90 | > * **getEvalOrigin**: if this function was created using a call to eval returns a CallSite object representing the location where eval was called 91 | > * **isToplevel**: is this a toplevel invocation, that is, is this the global object? 92 | > * **isEval**: does this call take place in code defined by a call to eval? 93 | > * **isNative**: is this call in native V8 code? 94 | > * **isConstructor**: is this a constructor call? 95 | 96 | [v8stackapi]: https://v8.dev/docs/stack-trace-api 97 | 98 | ## License 99 | 100 | stack-trace is licensed under the MIT license. 101 | -------------------------------------------------------------------------------- /__tests__/get-test.js: -------------------------------------------------------------------------------- 1 | import { get } from "../index.js"; 2 | 3 | describe("get", () => { 4 | test("basic", () => { 5 | (function testBasic() { 6 | var trace = get(); 7 | 8 | //expect(trace[0].getFunction()).toBe(testBasic); 9 | expect(trace[0].getFunctionName()).toBe('testBasic'); 10 | expect(trace[0].getFileName()).toBe(__filename); 11 | })(); 12 | }); 13 | 14 | test("wrapper", () => { 15 | (function testWrapper() { 16 | (function testBelowFn() { 17 | var trace = get(testBelowFn); 18 | //expect(trace[0].getFunction()).toBe(testWrapper); 19 | expect(trace[0].getFunctionName()).toBe('testWrapper'); 20 | })(); 21 | })(); 22 | }); 23 | 24 | test("deep", () => { 25 | (function deep1() { 26 | (function deep2() { 27 | (function deep3() { 28 | (function deep4() { 29 | (function deep5() { 30 | (function deep6() { 31 | (function deep7() { 32 | (function deep8() { 33 | (function deep9() { 34 | (function deep10() { 35 | (function deep10() { 36 | const trace = get(); 37 | const hasFirstCallSite = trace.some(callSite => callSite.getFunctionName() === 'deep1'); 38 | expect(hasFirstCallSite).toBe(true); 39 | })(); 40 | })(); 41 | })(); 42 | })(); 43 | })(); 44 | })(); 45 | })(); 46 | })(); 47 | })(); 48 | })(); 49 | })(); 50 | }); 51 | }); -------------------------------------------------------------------------------- /__tests__/long-stack-trace-test.js: -------------------------------------------------------------------------------- 1 | import { parse } from "../index.js"; 2 | const _ = require('long-stack-traces'); 3 | 4 | describe("long stack trace", () => { 5 | test("basic", (done) => { 6 | function badFn() { 7 | var err = new Error('oh no'); 8 | var trace = parse(err); 9 | 10 | for (var i in trace) { 11 | var filename = trace[i].getFileName(); 12 | if (typeof filename === 'string' && filename.match(/-----/)) { 13 | done(); 14 | return; 15 | } 16 | } 17 | expect.fail(); 18 | } 19 | 20 | setTimeout(badFn, 10); 21 | }); 22 | }); -------------------------------------------------------------------------------- /__tests__/parse-test.js: -------------------------------------------------------------------------------- 1 | import { get, parse } from "../index.js"; 2 | 3 | describe("parse", () => { 4 | test("object in method name", () => { 5 | const err = {}; 6 | err.stack = 7 | 'Error: Foo\n' + 8 | ' at [object Object].global.every [as _onTimeout] (/Users/hoitz/develop/test.coffee:36:3)\n' + 9 | ' at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)\n'; 10 | 11 | const trace = parse(err); 12 | expect(trace[0].getFileName()).toBe("/Users/hoitz/develop/test.coffee"); 13 | expect(trace[1].getFileName()).toBe("timers.js"); 14 | }); 15 | 16 | test("basic", () => { 17 | (function testBasic() { 18 | const err = new Error('something went wrong'); 19 | const trace = parse(err); 20 | 21 | expect(trace[0].getFileName()).toBe(__filename); 22 | expect(trace[0].getFunctionName()).toBe('testBasic'); 23 | })(); 24 | }); 25 | 26 | test("wrapper", () => { 27 | (function testWrapper() { 28 | (function testBelowFn() { 29 | const err = new Error('something went wrong'); 30 | const trace = parse(err); 31 | expect(trace[0].getFunctionName()).toBe('testBelowFn'); 32 | expect(trace[1].getFunctionName()).toBe('testWrapper'); 33 | })(); 34 | })(); 35 | }); 36 | 37 | test("no stack", () => { 38 | const err = { stack: undefined }; 39 | const trace = parse(err); 40 | 41 | expect(trace).toStrictEqual([]); 42 | }); 43 | 44 | test("test corrupt stack", () => { 45 | const err = {}; 46 | err.stack = 47 | 'AssertionError: true == false\n' + 48 | ' fuck' + 49 | ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/test.js:45:10)\n' + 50 | 'oh no' + 51 | ' at TestCase.run (/Users/felix/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; 52 | 53 | const trace = parse(err); 54 | expect(trace.length).toBe(2); 55 | }); 56 | 57 | test("trace braces in path", () => { 58 | const err = {}; 59 | err.stack = 60 | 'AssertionError: true == false\n' + 61 | ' at Test.run (/Users/felix (something)/code/node-fast-or-slow/lib/test.js:45:10)\n' + 62 | ' at TestCase.run (/Users/felix (something)/code/node-fast-or-slow/lib/test_case.js:61:8)\n'; 63 | 64 | const trace = parse(err); 65 | expect(trace.length).toBe(2); 66 | expect(trace[0].getFileName()).toBe('/Users/felix (something)/code/node-fast-or-slow/lib/test.js'); 67 | }); 68 | 69 | test("trace without column numbers", () => { 70 | const err = {}; 71 | err.stack = 72 | 'AssertionError: true == false\n' + 73 | ' at Test.fn (/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js:6)\n' + 74 | ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/test.js:45)'; 75 | 76 | const trace = parse(err); 77 | expect(trace[0].getFileName()).toBe("/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js"); 78 | expect(trace[0].getLineNumber()).toBe(6); 79 | expect(trace[0].getColumnNumber()).toBeNull(); 80 | }); 81 | 82 | test("compare real with parsed stack trace", () => { 83 | var realTrace, err; 84 | function TestClass() { 85 | } 86 | TestClass.prototype.testFunc = function () { 87 | realTrace = get(); 88 | err = new Error('something went wrong'); 89 | } 90 | 91 | var testObj = new TestClass(); 92 | testObj.testFunc(); 93 | var parsedTrace = parse(err); 94 | 95 | realTrace.forEach(function(real, i) { 96 | var parsed = parsedTrace[i]; 97 | 98 | function compare(method, exceptions) { 99 | let realValue = real[method](); 100 | const parsedValue = parsed[method](); 101 | 102 | if (exceptions && typeof exceptions[i] != 'undefined') { 103 | realValue = exceptions[i]; 104 | } 105 | 106 | //const realJson = JSON.stringify(realValue); 107 | //const parsedJson = JSON.stringify(parsedValue); 108 | //console.log(method + ': ' + realJson + ' != ' + parsedJson + ' (#' + i + ')'); 109 | expect(realValue).toBe(parsedValue); 110 | } 111 | 112 | compare('getFileName'); 113 | compare('getFunctionName', { 114 | 2: 'Object.asyncJestTest', 115 | 4: 'new Promise' 116 | }); 117 | compare('getTypeName', { 118 | 7: null 119 | }); 120 | compare('getMethodName', { 121 | 2: 'asyncJestTest' 122 | }); 123 | compare('getLineNumber', { 124 | 0: 88, 125 | 1: 92 126 | }); 127 | compare('getColumnNumber', { 128 | 0: 13 129 | }); 130 | compare('isNative'); 131 | }); 132 | }); 133 | 134 | test("stack with native call", () => { 135 | const err = {}; 136 | err.stack = 137 | 'AssertionError: true == false\n' + 138 | ' at Test.fn (/Users/felix/code/node-fast-or-slow/test/fast/example/test-example.js:6:10)\n' + 139 | ' at Test.run (/Users/felix/code/node-fast-or-slow/lib/test.js:45:10)\n' + 140 | ' at TestCase.runNext (/Users/felix/code/node-fast-or-slow/lib/test_case.js:73:8)\n' + 141 | ' at TestCase.run (/Users/felix/code/node-fast-or-slow/lib/test_case.js:61:8)\n' + 142 | ' at Array.0 (native)\n' + 143 | ' at EventEmitter._tickCallback (node.js:126:26)'; 144 | 145 | const trace = parse(err); 146 | var nativeCallSite = trace[4]; 147 | 148 | expect(nativeCallSite.getFileName()).toBeNull(); 149 | expect(nativeCallSite.getFunctionName()).toBe('Array.0'); 150 | expect(nativeCallSite.getTypeName()).toBe('Array'); 151 | expect(nativeCallSite.getMethodName()).toBe('0'); 152 | expect(nativeCallSite.getLineNumber()).toBeNull(); 153 | expect(nativeCallSite.getColumnNumber()).toBeNull(); 154 | expect(nativeCallSite.isNative()).toBe(true); 155 | }); 156 | 157 | test("stack with file only", () => { 158 | const err = {}; 159 | err.stack = 160 | 'AssertionError: true == false\n' + 161 | ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; 162 | 163 | const trace = parse(err); 164 | var callSite = trace[0]; 165 | 166 | expect(callSite.getFileName()).toBe('/Users/felix/code/node-fast-or-slow/lib/test_case.js'); 167 | expect(callSite.getFunctionName()).toBeNull(); 168 | expect(callSite.getTypeName()).toBeNull(); 169 | expect(callSite.getMethodName()).toBeNull(); 170 | expect(callSite.getLineNumber()).toBe(80); 171 | expect(callSite.getColumnNumber()).toBe(10); 172 | expect(callSite.isNative()).toBe(false); 173 | }); 174 | 175 | test("stack with multiline message", () => { 176 | const err = {}; 177 | err.stack = 178 | 'AssertionError: true == false\nAnd some more shit\n' + 179 | ' at /Users/felix/code/node-fast-or-slow/lib/test_case.js:80:10'; 180 | 181 | const trace = parse(err); 182 | var callSite = trace[0]; 183 | 184 | expect(callSite.getFileName()).toBe('/Users/felix/code/node-fast-or-slow/lib/test_case.js'); 185 | }); 186 | 187 | test("stack with anonymous function call", () => { 188 | const err = {}; 189 | err.stack = 190 | 'AssertionError: expected [] to be arguments\n' + 191 | ' at Assertion.prop.(anonymous function) (/Users/den/Projects/should.js/lib/should.js:60:14)\n'; 192 | 193 | const trace = parse(err); 194 | var callSite0 = trace[0]; 195 | 196 | expect(callSite0.getFileName()).toBe('/Users/den/Projects/should.js/lib/should.js'); 197 | expect(callSite0.getFunctionName()).toBe('Assertion.prop.(anonymous function)'); 198 | expect(callSite0.getTypeName()).toBe("Assertion.prop"); 199 | expect(callSite0.getMethodName()).toBe("(anonymous function)"); 200 | expect(callSite0.getLineNumber()).toBe(60); 201 | expect(callSite0.getColumnNumber()).toBe(14); 202 | expect(callSite0.isNative()).toBe(false); 203 | }); 204 | }); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export function get(belowFn) { 2 | const oldLimit = Error.stackTraceLimit; 3 | Error.stackTraceLimit = Infinity; 4 | 5 | const dummyObject = {}; 6 | 7 | const v8Handler = Error.prepareStackTrace; 8 | Error.prepareStackTrace = function(dummyObject, v8StackTrace) { 9 | return v8StackTrace; 10 | }; 11 | Error.captureStackTrace(dummyObject, belowFn || get); 12 | 13 | const v8StackTrace = dummyObject.stack; 14 | Error.prepareStackTrace = v8Handler; 15 | Error.stackTraceLimit = oldLimit; 16 | 17 | return v8StackTrace; 18 | } 19 | 20 | export function parse(err) { 21 | if (!err.stack) { 22 | return []; 23 | } 24 | 25 | const lines = err.stack.split('\n').slice(1); 26 | return lines 27 | .map(function(line) { 28 | if (line.match(/^\s*[-]{4,}$/)) { 29 | return createParsedCallSite({ 30 | fileName: line, 31 | lineNumber: null, 32 | functionName: null, 33 | typeName: null, 34 | methodName: null, 35 | columnNumber: null, 36 | 'native': null, 37 | }); 38 | } 39 | 40 | const lineMatch = line.match(/at (?:(.+?)\s+\()?(?:(.+?):(\d+)(?::(\d+))?|([^)]+))\)?/); 41 | if (!lineMatch) { 42 | return; 43 | } 44 | 45 | let object = null; 46 | let method = null; 47 | let functionName = null; 48 | let typeName = null; 49 | let methodName = null; 50 | let isNative = (lineMatch[5] === 'native'); 51 | 52 | if (lineMatch[1]) { 53 | functionName = lineMatch[1]; 54 | let methodStart = functionName.lastIndexOf('.'); 55 | if (functionName[methodStart-1] == '.') 56 | methodStart--; 57 | if (methodStart > 0) { 58 | object = functionName.substr(0, methodStart); 59 | method = functionName.substr(methodStart + 1); 60 | const objectEnd = object.indexOf('.Module'); 61 | if (objectEnd > 0) { 62 | functionName = functionName.substr(objectEnd + 1); 63 | object = object.substr(0, objectEnd); 64 | } 65 | } 66 | } 67 | 68 | if (method) { 69 | typeName = object; 70 | methodName = method; 71 | } 72 | 73 | if (method === '') { 74 | methodName = null; 75 | functionName = null; 76 | } 77 | 78 | const properties = { 79 | fileName: lineMatch[2] || null, 80 | lineNumber: parseInt(lineMatch[3], 10) || null, 81 | functionName: functionName, 82 | typeName: typeName, 83 | methodName: methodName, 84 | columnNumber: parseInt(lineMatch[4], 10) || null, 85 | 'native': isNative, 86 | }; 87 | 88 | return createParsedCallSite(properties); 89 | }) 90 | .filter(function(callSite) { 91 | return !!callSite; 92 | }); 93 | } 94 | 95 | function CallSite(properties) { 96 | for (const property in properties) { 97 | this[property] = properties[property]; 98 | } 99 | } 100 | 101 | const strProperties = [ 102 | 'this', 103 | 'typeName', 104 | 'functionName', 105 | 'methodName', 106 | 'fileName', 107 | 'lineNumber', 108 | 'columnNumber', 109 | 'function', 110 | 'evalOrigin' 111 | ]; 112 | 113 | const boolProperties = [ 114 | 'topLevel', 115 | 'eval', 116 | 'native', 117 | 'constructor' 118 | ]; 119 | 120 | strProperties.forEach(function (property) { 121 | CallSite.prototype[property] = null; 122 | CallSite.prototype['get' + property[0].toUpperCase() + property.substr(1)] = function () { 123 | return this[property]; 124 | } 125 | }); 126 | 127 | boolProperties.forEach(function (property) { 128 | CallSite.prototype[property] = false; 129 | CallSite.prototype['is' + property[0].toUpperCase() + property.substr(1)] = function () { 130 | return this[property]; 131 | } 132 | }); 133 | 134 | function createParsedCallSite(properties) { 135 | return new CallSite(properties); 136 | } 137 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Felix Geisendörfer (http://debuggable.com/)", 3 | "name": "stack-trace", 4 | "description": "Get v8 stack traces as an array of CallSite objects.", 5 | "version": "1.0.0-pre2", 6 | "homepage": "https://github.com/felixge/node-stack-trace", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/felixge/node-stack-trace.git" 10 | }, 11 | "type": "module", 12 | "main": "index.js", 13 | "exports": { 14 | ".": "./index.js", 15 | "./package.json": "./package.json" 16 | }, 17 | "jest": { 18 | "testEnvironment": "node", 19 | "transform": { 20 | "^.+\\.js$": "babel-jest" 21 | } 22 | }, 23 | "scripts": { 24 | "test": "jest", 25 | "release": "git push && git push --tags && npm publish" 26 | }, 27 | "engines": { 28 | "node": ">=16" 29 | }, 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@babel/preset-env": "^7.14.1", 33 | "babel-jest": "^26.6.3", 34 | "jest": "^26.6.3", 35 | "long-stack-traces": "0.1.2" 36 | } 37 | } 38 | --------------------------------------------------------------------------------