├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── stack-beautifier.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # Compiled binary addons (http://nodejs.org/api/addons.html) 24 | build/Release 25 | 26 | # Dependency directory 27 | node_modules 28 | 29 | # Optional npm cache directory 30 | .npm 31 | 32 | # Optional REPL history 33 | .node_repl_history 34 | 35 | # vscode 36 | # not sure if you want my settings committed to your repo 37 | .vscode -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Software Mansion 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # stack-beautifier 2 | 3 | Tool for decrypting stack traces coming from the minified JS code. 4 | 5 | ## wat? 6 | 7 | This tool helps with translating minified/uglified stack traces from your production apps (NodeJS/react native/web) into a human readable form (with line number from your original source files) utilising the concept of [source maps](https://github.com/mozilla/source-map). This tool aims at providing a simple way of deminifying stack traces from a command line interface as opposed to other great existing alternatives, which provides similar functionality but require integration into a deployment process. 8 | 9 | ## Installation 10 | 11 | Install it using npm: `npm install -g stack-beautifier` 12 | 13 | ## Usage 14 | 15 | By default the tool accepts source map file as an argument and reads stack trace from the standard input and the prints out the translated stack trace to the standard output. The default behaviour can be altered by using the following command line options: 16 | 17 | ``` 18 | Usage: stack-beautifier [options] 19 | 20 | Options: 21 | 22 | -t, --trace [input_file] Read stack trace from the input file (stdin is used when this option is not set) 23 | -o, --output [output_file] Write result into the given output file (stdout is used when this option is not set) 24 | -l, --long Output complete javascript filenames in the stacktrace (tool will try to shorten file paths by default) 25 | ``` 26 | 27 | ## Stack trace input format 28 | 29 | The tool can understand javascript stack traces that starts with a single line of error message followed by many lines each representing a single stack trace level. The line format has been made compatible with what you can get from most popular JS engines (V8 and JSC used in React-Native). Here are a few examples of stack traces the tool can understand: 30 | 31 | ### V8 stack trace: 32 | 33 | ``` 34 | TypeError: Assignment to constant variable. 35 | at (app.bundle.js:34:612) 36 | at b (app.bundle.js:11:3018) 37 | at d (app.bundle.js:8:1074) 38 | at (app.bundle.js:1:5) 39 | ``` 40 | 41 | ### JSC (react-native) stack trace: 42 | 43 | ``` 44 | Fatal Exception: com.facebook.react.common.JavascriptException: Invalid attempt to destructure non-iterable instance, stack: 45 | @9:487 46 | o@781:121 47 | value@80:1312 48 | value@42:1514 49 | @18:912 50 | d@31:10 51 | value@11:1512 52 | ``` 53 | 54 | ## Using with React Native 55 | 56 | When using react-native, javascript bundle gets created as a part of the release build process (unless you use react-native-code-push or similar tooling). We recommend that you alter that build step to also generate a source map that you can archive somewhere and then use for decrypting stack traces when necessary. 57 | 58 | To do so on android you can add the following snippet to your `android/app/build.gradle`: 59 | 60 | ```groovy 61 | project.ext.react = [ 62 | extraPackagerArgs: ['--sourcemap-output', file("$buildDir/outputs/index.android.js.map")] 63 | ] 64 | ``` 65 | 66 | Note that it has to be added before the `apply from: "../../node_modules/react-native/react.gradle"` line. After a successful build the sourcemap will be located under `android/app/build/outputs/index.android.js.map`. 67 | 68 | ### Generate source map from the application sources 69 | 70 | If you don't have access to the source map file there is still hope. You can checkout the version at which the bundle has been generated and run the following command for android: 71 | 72 | ```bash 73 | react-native bundle --platform android --entry-file index.js --dev false --reset-cache --bundle-output /tmp/bundle.android.js --assets-dest /tmp/ --sourcemap-output index.android.js.map 74 | ``` 75 | 76 | or for iOS: 77 | 78 | ```bash 79 | react-native bundle --platform ios --entry-file index.js --dev false --reset-cache --bundle-output /tmp/bundle.ios.js --assets-dest /tmp/ --sourcemap-output index.ios.js.map 80 | ``` 81 | 82 | Note that it is crutial that all the dependencies from `node_module` are at the same version as when the JS bundle has been generated, otherwise the source map may not give you the correct mapping. We recommend using [yarn](https://yarnpkg.com/) that helps in ensuring reproducable JS bundle builds. 83 | 84 | 85 | ## Example usage: 86 | 87 | In order to use this tool you first need to have access to the source map file associated with the javascript bundle you're getting the stack trace from. Assume that the sourcemap is stored in `app.js.map` file and the stack trace is save in `mytrace.txt` and looks as follows: 88 | 89 | ``` 90 | Fatal Exception: com.facebook.react.common.JavascriptException: Invalid attempt to destructure non-iterable instance, stack: 91 | @9:487 92 | o@781:121 93 | value@80:1312 94 | value@42:1514 95 | @18:912 96 | d@31:10 97 | value@11:1512 98 | ``` 99 | 100 | Now you can call the following command in order to get the deminified stack trace printed: 101 | ```bash 102 | > stack-beautifier app.js.map -t mytrace.txt 103 | Fatal Exception: com.facebook.react.common.JavascriptException: Invalid attempt to destructure non-iterable instance, stack: 104 | at arr (./node_modules/react-native/packager/react-packager/src/Resolver/polyfills/babelHelpers.js:227:22) 105 | at _url$match (./js/launcher/index.js:9:30) 106 | at _currentSubscription (./node_modules/react-native/Libraries/EventEmitter/EventEmitter.js:185:11) 107 | at ./node_modules/react-native/Libraries/Utilities/MessageQueue.js:273:27 108 | at __callImmediates (./node_modules/react-native/Libraries/Utilities/MessageQueue.js:119:11) 109 | at fn (./node_modules/react-native/Libraries/Utilities/MessageQueue.js:46:4) 110 | at __callFunction (./node_modules/react-native/Libraries/Utilities/MessageQueue.js:118:20) 111 | ``` 112 | 113 | ## Troubleshooting 114 | 115 | #### Getting error "Stack trace parse error at line N" 116 | 117 | It means that the tool is not able to understand the format of the stacktrace. The tool only supports most common stack traces formats, please check if the stack trace you're trying to input is in one of the supported formats. There is also a chance that your stack trace is in a valid format but your file contain some 118 | 119 | #### Still having some issues 120 | 121 | Try searching over the issues on GitHub [here](https://github.com/SoftwareMansion/stack-beautifier/issues). If you don't find anything that would help feel free to open new issue! 122 | 123 | 124 | ## Contributing 125 | 126 | All PRs are welcome! 127 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-beautifier", 3 | "version": "1.0.2", 4 | "description": "Tool for decrypting stack traces coming from the minified JS code", 5 | "repository": { 6 | "type": "git", 7 | "url": "git+https://github.com/SoftwareMansion/stack-beautifier.git" 8 | }, 9 | "keywords": [ 10 | "react-native", 11 | "stack", 12 | "stacktrace", 13 | "source-map", 14 | "minify" 15 | ], 16 | "author": { 17 | "email": "krzys@swmansion.com", 18 | "name": "Krzysztof Magiera" 19 | }, 20 | "license": "MIT", 21 | "readmeFilename": "README.md", 22 | "bugs": { 23 | "url": "https://github.com/SoftwareMansion/stack-beautifier/issues" 24 | }, 25 | "homepage": "https://github.com/SoftwareMansion/stack-beautifier#readme", 26 | "dependencies": { 27 | "commander": "2.9.0", 28 | "source-map": "0.5.6" 29 | }, 30 | "bin": { 31 | "stack-beautifier": "./stack-beautifier.js" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stack-beautifier.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const fs = require('fs'); 6 | const sourceMap = require('source-map'); 7 | const readline = require('readline'); 8 | const commander = require('commander'); 9 | 10 | const program = new commander.Command('stack-beautifier'); 11 | program.arguments(''); 12 | program.option( 13 | '-t, --trace [input_file]', 14 | 'Read stack trace from the input file (stdin is used when this option is not set)' 15 | ); 16 | program.option( 17 | '-o, --output [output_file]', 18 | 'Write result into the given output file (stdout is used when this option is not set)' 19 | ); 20 | program.option( 21 | '-l, --long', 22 | 'Output complete javascript filenames in the stacktrace (tool will try to shorten file paths by default)' 23 | ); 24 | 25 | const pkg = require('./package.json'); 26 | program.version(pkg.version); 27 | program.usage('[options] '); 28 | program.description( 29 | 'stack-beautifier is a simple tool for decrypting stack traces coming from the minified JS code.' 30 | ); 31 | program.on('--help', () => { 32 | console.log(`\ 33 | Examples: 34 | 35 | $ stack-beautifier app.js.map 36 | $ stack-beautifier -t trace.txt app.js.map 37 | 38 | See more: 39 | 40 | https://github.com/swmansion/stack-beautifier 41 | `); 42 | }); 43 | program.action(mapFilename => { 44 | main(program); 45 | }); 46 | program.parse(process.argv); 47 | if (!program.args.length) { 48 | program.help(); 49 | } 50 | 51 | const STACK_LINE_MATCHERS = [ 52 | { regex: /^(.*)\@(\d+)\:(\d+)$/, idx: [1, 2, 3] }, // Format: someFun@13:12 53 | { regex: /^at (.*)\:(\d+)\:(\d+)$/, idx: [1, 2, 3] }, // Format: at filename:13:12 54 | { regex: /^at (.*) \((.*)\:(\d+)\:(\d+)\)$/, idx: [1, 3, 4] }, // Format: at someFun (filename:13:12) 55 | { regex: /^at (.*)\:(\d+)$/, idx: [1, 2, 3] }, // Format: at filename:13 56 | ]; 57 | 58 | function main(program) { 59 | const mapFilename = program.args[0]; 60 | const traceFilename = program.trace; 61 | const outputFilename = program.output; 62 | 63 | const rl = readline.createInterface({ 64 | input: traceFilename ? fs.createReadStream(traceFilename) : process.stdin, 65 | }); 66 | 67 | const sourceMapConsumer = new sourceMap.SourceMapConsumer( 68 | fs.readFileSync(mapFilename, 'utf8') 69 | ); 70 | 71 | const lines = []; 72 | rl.on('line', line => { 73 | lines.push(line.trim()); 74 | }); 75 | rl.on('close', () => { 76 | const stack = processStack(lines, sourceMapConsumer); 77 | const data = formatStack(stack, !program.long); 78 | if (outputFilename) { 79 | fs.writeFileSync(outputFilename, data); 80 | } else { 81 | process.stdout.write(data); 82 | process.stdout.write('\n'); 83 | } 84 | }); 85 | } 86 | 87 | function processMatchedLine(match, sourceMapConsumer) { 88 | return sourceMapConsumer.originalPositionFor({ 89 | line: Number(match.line), 90 | column: Number(match.column || 0), 91 | name: match.name, 92 | }); 93 | } 94 | 95 | function matchStackLine(line) { 96 | const found = STACK_LINE_MATCHERS.find(m => { 97 | return m.regex.test(line); 98 | }); 99 | if (found) { 100 | const match = line.match(found.regex); 101 | return { 102 | name: match[found.idx[0]], 103 | line: match[found.idx[1]], 104 | column: match[found.idx[2]], 105 | }; 106 | } 107 | return null; 108 | } 109 | 110 | function processStack(lines, sourceMapConsumer) { 111 | const result = []; 112 | for (let i = 0; i < lines.length; i++) { 113 | const line = lines[i]; 114 | const match = matchStackLine(line); 115 | if (!match) { 116 | if (i === 0) { 117 | // we allow first line to contain trace message, we just pass it through to the result table 118 | result.push({ text: line }); 119 | } else if (!line) { 120 | // we treat empty stack trace line as the end of an input 121 | break; 122 | } else { 123 | throw new Error(`Stack trace parse error at line ${i + 1}: ${line}`); 124 | } 125 | } else { 126 | result.push(processMatchedLine(match, sourceMapConsumer)); 127 | } 128 | } 129 | return result; 130 | } 131 | 132 | function formatStack(lines, shorten) { 133 | let replacePrefix = ''; 134 | if (shorten) { 135 | const sources = lines.filter(r => r.source).map(r => r.source); 136 | if (sources.length > 1) { 137 | let prefix = sources[0]; 138 | sources.forEach(s => { 139 | while ( 140 | prefix !== s.slice(0, prefix.length) || 141 | prefix.indexOf('node_modules') !== -1 142 | ) { 143 | prefix = prefix.slice(0, -1); 144 | } 145 | }); 146 | if (prefix !== sources[0]) { 147 | replacePrefix = prefix; 148 | } 149 | } 150 | } 151 | return lines 152 | .map(r => { 153 | if (r.text) { 154 | return r.text; 155 | } else if (!r.source) { 156 | return ' at '; 157 | } else { 158 | const source = 159 | replacePrefix && r.source.startsWith(replacePrefix) 160 | ? './' + r.source.slice(replacePrefix.length) 161 | : r.source; 162 | if (r.name) { 163 | return ` at ${r.name} (${source}:${r.line}:${r.column})`; 164 | } else { 165 | return ` at ${source}:${r.line}:${r.column}`; 166 | } 167 | } 168 | }) 169 | .join('\n'); 170 | } 171 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | commander@2.9.0: 6 | version "2.9.0" 7 | resolved "https://registry.yarnpkg.com/commander/-/commander-2.9.0.tgz#9c99094176e12240cb22d6c5146098400fe0f7d4" 8 | dependencies: 9 | graceful-readlink ">= 1.0.0" 10 | 11 | "graceful-readlink@>= 1.0.0": 12 | version "1.0.1" 13 | resolved "https://registry.yarnpkg.com/graceful-readlink/-/graceful-readlink-1.0.1.tgz#4cafad76bc62f02fa039b2f94e9a3dd3a391a725" 14 | 15 | source-map@0.5.6: 16 | version "0.5.6" 17 | resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412" 18 | --------------------------------------------------------------------------------