├── .gitignore ├── README.md ├── index.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-source-maps 2 | Utilities to quickly add support of source-maps to your React Native project 3 | 4 | As you probably already now, React Native minifies release code. 5 | It's perfect for bundle size and execution speed. 6 | But it can become a real pain to find production bug in such code. 7 | 8 | For example error stack trace in your logging system can look like that: 9 | ``` 10 | 0:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:947:1763 11 | 1:u@file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:137 12 | 2:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:891 13 | ... 14 | ``` 15 | 16 | And.... 17 | It's useless just to know about an error without a possibility to find it in the exact place of code. So we need source maps. 18 | By using source maps, we can map the code being executed to the original code source files, making fixing bug much, much easier. 19 | 20 | To make it look like this: 21 | ``` 22 | 0:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:947:1763 23 | 1:u@file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:137 24 | 2:file:///var/containers/Bundle/Application/B2C0D7FF-6CA3-4298-BDBF-10285CD4D88D/Debitoor.app/main.jsbundle:199:891 25 | ... 26 | ``` 27 | 28 | Firstly I'd like to mention that React Native CLI has a flag to build source maps alongside with JS code bundle. 29 | 30 | How can we do this? 31 | 32 | ---------- 33 | #### Usage: 34 | ##### 1. We need to modify how we JS code is bundled 35 | 36 | For example, originally iOS builds use this packager script: 37 | ./node_modules/react-native/packager/react-native-xcode.sh 38 | Which call React Native CLI bundle command this way: 39 | ``` 40 | $NODE_BINARY "$REACT_NATIVE_DIR/local-cli/cli.js" bundle \ 41 | --entry-file "$ENTRY_FILE" \ 42 | --platform ios \ 43 | --dev $DEV \ 44 | --reset-cache \ 45 | --bundle-output "$BUNDLE_FILE" \ 46 | --assets-dest "$DEST" 47 | ``` 48 | We need to modify this script and add one line: 49 | ``` 50 | --sourcemap-output "$BUNDLE_FILE.map" 51 | ``` 52 | It can be done with simple patching of this file in npm postinstall script. Or creating custom bundle script. 53 | 54 | For Android builds, add this line to the build.gradle under app module to make js bundle name same with iOS build: 55 | ``` 56 | project.ext.react = [ 57 | bundleAssetName: "main.jsbundle", 58 | ] 59 | ``` 60 | Then edit Android packager script: ./node_modules/react-native/react.gradle 61 | 62 | and you need to add "--sourcemap-output" and bundle map file parameter to the build script, 63 | ``` 64 | def jsBundleMapFile = "${jsBundleFile}.map" 65 | "--sourcemap-output", jsBundleMapFile 66 | ``` 67 | 68 | for example: 69 | ``` 70 | def jsBundleMapFile = "${jsBundleFile}.map" 71 | 72 | def devEnabled = !targetName.toLowerCase().contains("release") 73 | if (Os.isFamily(Os.FAMILY_WINDOWS)) { 74 | commandLine("cmd", "/c", *nodeExecutableAndArgs, "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", 75 | "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, "--sourcemap-output", jsBundleMapFile, *extraPackagerArgs) 76 | } else { 77 | commandLine(*nodeExecutableAndArgs, "node_modules/react-native/local-cli/cli.js", "bundle", "--platform", "android", "--dev", "${devEnabled}", 78 | "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, "--sourcemap-output", jsBundleMapFile, *extraPackagerArgs) 79 | } 80 | ``` 81 | 82 | ##### 2. Install react-native-source-maps module 83 | This module was built to map minified sources to original JS files. 84 | And to solve our pain to find actual place where an error occurred. 85 | 86 | `npm i react-native-source-maps` 87 | 88 | ##### 3. Initialize module at the app start with options 89 | ``` 90 | import {initSourceMaps} from 'react-native-source-maps'; 91 | initSourceMaps({sourceMapBundle: "main.jsbundle.map"}); 92 | ``` 93 | 94 | ##### 4. Use it with your logging utilities to replace minified error stack with original 95 | Example: 96 | ``` 97 | const stack = !__DEV__ && await getStackTrace(error); 98 | if (stack) { 99 | error.stack = stack; 100 | } 101 | log.error({message: 'Error (' + error.message + ')', error}); 102 | ``` 103 | 104 | ##### 5. Caught all uncaught errors in app by adding global error handler 105 | ``` 106 | import log from "../log"; 107 | import {getStackTrace} from 'react-native-source-maps'; 108 | import ErrorUtils from "ErrorUtils"; 109 | 110 | (async function initUncaughtErrorHandler() { 111 | const defaultGlobalHandler = ErrorUtils.getGlobalHandler(); 112 | 113 | ErrorUtils.setGlobalHandler(async(error, isFatal) => { 114 | try { 115 | if (!__DEV__) { 116 | error.stack = await getStackTrace(error); 117 | } 118 | log.error({message: 'Uncaught error (' + error.message + ')', error, isFatal}); 119 | } 120 | catch (error) { 121 | log.error({message: 'Unable to setup global handler', error}); 122 | } 123 | 124 | if (__DEV__ && defaultGlobalHandler) { 125 | defaultGlobalHandler(error, isFatal); 126 | } 127 | }); 128 | })(); 129 | ``` 130 | 131 | ---------- 132 | #### API: 133 | Property | Type | Description | Default value 134 | ------------ | ---- | ----------- | ------------- 135 | `sourceMapBundle` | string | source map bundle, for example "main.jsbundle.map" | undefined (Required) 136 | `collapseInLine` | bool | Will collapse all stack trace in one line, otherwise return lines array | false 137 | `projectPath` | string | project path to remove from files path index | undefined (Optional) 138 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import RNFS from "react-native-fs"; 2 | import SourceMap from "source-map"; 3 | import StackTrace from "stacktrace-js"; 4 | import { Platform } from "react-native"; 5 | 6 | let sourceMapper = undefined; 7 | let options = undefined; 8 | 9 | /** 10 | * Init Source mapper with options 11 | * Required: 12 | * @param {String} [opts.sourceMapBundle] - source map bundle, for example "main.jsbundle.map" 13 | * Optional: 14 | * @param {String} [opts.projectPath] - project path to remove from files path 15 | * @param {Boolean} [opts.collapseInLine] — Will collapse all stack trace in one line, otherwise return lines array 16 | */ 17 | export const initSourceMaps = async opts => { 18 | if (!opts || !opts.sourceMapBundle) { 19 | throw new Error('Please specify sourceMapBundle option parameter'); 20 | } 21 | options = opts; 22 | }; 23 | 24 | export const getStackTrace = async error => { 25 | if (!options) { 26 | throw new Error('Please firstly call initSourceMaps with options'); 27 | } 28 | if (!sourceMapper) { 29 | sourceMapper = await createSourceMapper(); 30 | } 31 | try { 32 | let minStackTrace; 33 | 34 | if (Platform.OS === "ios") { 35 | minStackTrace = await StackTrace.fromError(error); 36 | } else { 37 | minStackTrace = await StackTrace.fromError(error, { offline: true }); 38 | } 39 | 40 | const stackTrace = minStackTrace.map(row => { 41 | const mapped = sourceMapper(row); 42 | const source = mapped.source || ""; 43 | const fileName = options.projectPath ? source.split(options.projectPath).pop() : source; 44 | const functionName = mapped.name || "unknown"; 45 | return { 46 | fileName, 47 | functionName, 48 | lineNumber: mapped.line, 49 | columnNumber: mapped.column, 50 | position: `${functionName}@${fileName}:${mapped.line}:${mapped.column}` 51 | }; 52 | }); 53 | return options.collapseInLine ? stackTrace.map(i => i.position).join('\n') : stackTrace; 54 | } 55 | catch (error) { 56 | throw error; 57 | } 58 | }; 59 | 60 | const createSourceMapper = async () => { 61 | const SoureMapBundlePath = Platform.OS === "ios" ? `${RNFS.MainBundlePath}/${options.sourceMapBundle}` : options.sourceMapBundle; 62 | try { 63 | const fileExists = (Platform.OS === "ios") ? (await RNFS.exists(SoureMapBundlePath)) : (await RNFS.existsAssets(SoureMapBundlePath)); 64 | if (!fileExists) { 65 | throw new Error(__DEV__ ? 66 | 'Unable to read source maps in DEV mode' : 67 | `Unable to read source maps, possibly invalid sourceMapBundle file, please check that it exists here: ${SoureMapBundlePath}` 68 | ); 69 | } 70 | 71 | const mapContents = (Platform.OS === "ios") ? (await RNFS.readFile(SoureMapBundlePath, 'utf8')) : (await RNFS.readFileAssets(SoureMapBundlePath, 'utf8')); 72 | const sourceMaps = JSON.parse(mapContents); 73 | const mapConsumer = new SourceMap.SourceMapConsumer(sourceMaps); 74 | 75 | return sourceMapper = row => { 76 | return mapConsumer.originalPositionFor({ 77 | line: row.lineNumber, 78 | column: row.columnNumber, 79 | }); 80 | }; 81 | } 82 | catch (error) { 83 | throw error; 84 | } 85 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-source-maps", 3 | "version": "1.0.2", 4 | "description": "Utilities to easily add support of Source Maps to your React Native project", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "React", 11 | "Native", 12 | "source", 13 | "maps", 14 | "crashlytics", 15 | "log", 16 | "crash", 17 | "analysis" 18 | ], 19 | "author": "Philip Shurpik", 20 | "license": "MIT", 21 | "dependencies": { 22 | "react-native-fs": "^2.3.3", 23 | "source-map": "0.5.6", 24 | "stacktrace-js": "1.3.1" 25 | }, 26 | "repository": { 27 | "type": "git", 28 | "url": "git+https://github.com/philipshurpik/react-native-source-maps.git" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/philipshurpik/react-native-source-maps/issues" 32 | }, 33 | "homepage": "https://github.com/philipshurpik/react-native-source-maps#readme" 34 | } 35 | --------------------------------------------------------------------------------