├── .gitignore ├── README.md ├── adapters └── in-memory.js ├── colors.js ├── data-writers ├── realm-data-writer.js ├── storage-server-hoc-writer.js └── stringify-data-writer.js ├── debug-list-view.js ├── debug-service.js ├── debug-view.js ├── guid.js ├── index.js ├── package.json └── timers.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-device-log 2 | 3 | ### Description 4 | A debug-view that prints your debug-messages in a neat listview. 5 | Supports different levels of log-messages, complex data (With pretty printing), timers for measuring perf and much more. 6 | Adheres to a simple, async, protocol for saving messages where you can plug in your own adapter, or 7 | use AsyncStorage from React Native to persist log-messages between session. (Or just use simple session in-memory storage). 8 | 9 | Also tracks Connectivity of Device and App-State-changes (Background, Idle, Active). 10 | 11 | Will also, if you choose to (flag), track exceptions in your app and in React Native and log linenumbers and methods 12 | so you can track crashes in production. 13 | 14 | Configure how many messages that should be rendered in the ListView and how many messages should be persisted. 15 | All built to be efficent and fast. 16 | 17 | 18 | 19 | 20 | # Install: 21 | ``` 22 | npm install react-native-device-log --save 23 | ``` 24 | # Example: 25 | ```js 26 | /** 27 | * Sample React Native App width react-native-device-log 28 | * https://github.com/facebook/react-native 29 | */ 30 | import React, { Component } from 'react'; 31 | import { 32 | AppRegistry, 33 | StyleSheet, 34 | Text, 35 | View, 36 | AsyncStorage 37 | } from 'react-native'; 38 | 39 | //The device-log contains the public api that you will use in your app. 40 | //The LogView is the GUI/Log-list that you can render at desired location //in your app: 41 | 42 | import deviceLog, {LogView, InMemoryAdapter} from 'react-native-device-log'; 43 | 44 | //Call init and set a custom adapter that implements the interface of 45 | //AsyncStorage: getItem, removeItem, setItem. 46 | //By default the log uses a in-memory object, in this example we 47 | //explicitly set the log to use the persistent AsyncStorage instead: 48 | 49 | deviceLog.init(AsyncStorage /* You can send new InMemoryAdapter() if you do not want to persist here*/ 50 | ,{ 51 | //Options (all optional): 52 | logToConsole : false, //Send logs to console as well as device-log 53 | logRNErrors : true, // Will pick up RN-errors and send them to the device log 54 | maxNumberToRender : 2000, // 0 or undefined == unlimited 55 | maxNumberToPersist : 2000 // 0 or undefined == unlimited 56 | }).then(() => { 57 | 58 | //When the deviceLog has been initialized we can clear it if we want to: 59 | //deviceLog.clear(); 60 | 61 | }); 62 | 63 | //The device-log contains a timer for measuring performance: 64 | deviceLog.startTimer('start-up'); 65 | 66 | class AwesomeProject extends Component { 67 | 68 | componentDidMount() { 69 | //Print the current time of the above timer: 70 | deviceLog.logTime('start-up'); 71 | 72 | //Available log messages: 73 | deviceLog.log("Hello", "world!"); 74 | deviceLog.info("A info message"); 75 | deviceLog.debug("A debug message", {test: "test"}); 76 | deviceLog.success("A success message"); 77 | 78 | //Print the current time of the above timer again: 79 | deviceLog.logTime('start-up'); 80 | 81 | //Later stop and remove the timer: 82 | //Will not print anything. 83 | deviceLog.stopTimer('start-up'); 84 | 85 | setTimeout(() => { 86 | deviceLog.error("I'm late!!"); 87 | }, 3000); 88 | } 89 | 90 | render() { 91 | /* 92 | inverted: will write the log inverted. 93 | multiExpanded: means that multiple logmessages 94 | that are longer then one row can be expanded simultaneously 95 | timeStampFormat: moment format for timeStamp 96 | */ 97 | return ( 98 | 99 | ); 100 | } 101 | } 102 | 103 | AppRegistry.registerComponent('AwesomeProject', () => AwesomeProject); 104 | ``` 105 | -------------------------------------------------------------------------------- /adapters/in-memory.js: -------------------------------------------------------------------------------- 1 | export default class InMemory { 2 | constructor() { 3 | this.data = {}; 4 | } 5 | 6 | async setItem(key, value, callback) { 7 | return await this.promisify(() => { 8 | this.data[key] = value; 9 | callback && callback(); 10 | }); 11 | } 12 | 13 | async getItem(key, callback) { 14 | return await this.promisify(() => { 15 | let data = this.data[key]; 16 | callback && callback(data); 17 | return data; 18 | }); 19 | } 20 | 21 | async removeItem(key, callback) { 22 | return await this.promisify(() => { 23 | delete this.data[key]; 24 | callback && callback(); 25 | }); 26 | } 27 | 28 | promisify(cb) { 29 | return new Promise((resolve, reject) => { 30 | resolve(cb()); 31 | }); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /colors.js: -------------------------------------------------------------------------------- 1 | export default { 2 | "start-timer": "#54d7df", 3 | "end-timer": "#54d7df", 4 | log: "#afafaf", 5 | debug: "#5787cf", 6 | info: "#7fa9db", 7 | error: "#df5454", 8 | fatal: "#ff0000", 9 | success: "#54df72", 10 | RNFatal: "#ff0000", 11 | RNError: "#df5454", 12 | seperator: "#ffffff", 13 | }; 14 | -------------------------------------------------------------------------------- /data-writers/realm-data-writer.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export const RealmBaseScheme = { 4 | name: "LogRow", 5 | properties: { 6 | id: "string", 7 | lengthAtInsertion: "int", 8 | level: "string", 9 | message: "string", 10 | _timeStamp: "date", 11 | color: "string", 12 | }, 13 | }; 14 | export default class RealmDataWriter { 15 | constructor(realm) { 16 | this._readOnly = true; 17 | this.realm = realm; 18 | } 19 | 20 | setReadOnly(readOnly) { 21 | this._readOnly = readOnly; 22 | } 23 | 24 | async insertRows(rows, allRows) { 25 | if (this._readOnly) { 26 | return rows; 27 | } 28 | return await new Promise((resolve, reject) => { 29 | this.realm.write(() => { 30 | rows.map(row => { 31 | this.realm.create("LogRow", { 32 | ...row, 33 | _timeStamp: row.timeStamp.toDate(), 34 | }); 35 | }); 36 | return resolve(rows); 37 | }); 38 | }); 39 | } 40 | 41 | async getRows() { 42 | const realmRows = this.realm.objects("LogRow"); 43 | return realmRows.slice(0, realmRows.length).map(row => { 44 | row.timeStamp = moment(row._timeStamp); 45 | return row; 46 | }); 47 | } 48 | 49 | //Required 50 | async clear() { 51 | return this.realm.write(() => { 52 | let allRows = this.realm.objects("LogRow"); 53 | return this.realm.delete(allRows); 54 | }); 55 | } 56 | 57 | //Optional 58 | logRowCreated(logRow) {} 59 | 60 | //Optional 61 | appendToLogRow(logRow) { 62 | return logRow; 63 | } 64 | 65 | //Optional 66 | async initalDataRead(logRows) {} 67 | } 68 | -------------------------------------------------------------------------------- /data-writers/storage-server-hoc-writer.js: -------------------------------------------------------------------------------- 1 | import { AsyncStorage } from "react-native"; 2 | import StringifyDataWriter from "./stringify-data-writer"; 3 | import moment from "moment"; 4 | 5 | export default class StorageServerHocWriter { 6 | static SERVER_SEND_STORAGE_KEY = "debug-rows-SERVER_SEND_STORAGE_KEY"; 7 | constructor(writer, options) { 8 | this.writer = writer || new StringifyDataWriter(AsyncStorage); 9 | this.options = { 10 | serverUrl: null, 11 | getExtraData: () => null, 12 | modifyRowBeforeSend: null, 13 | appendToLogRow: logRow => logRow, 14 | skipSendingMessagesOlderThen: null, 15 | sendInterval: 10000, 16 | batchSize: 30, 17 | isDebug: true, 18 | sendLevels: ["all"], 19 | }; 20 | this.options = { ...this.options, ...options }; 21 | this.sentToServerRows = null; 22 | this.initPromise = this.getSentToServerRows().then(rows => { 23 | this.sentToServerRows = rows; 24 | return rows; 25 | }); 26 | } 27 | 28 | setReadOnly(readOnly) { 29 | this.writer.setReadOnly(readOnly); 30 | } 31 | //Required 32 | async getRows() { 33 | if (!this.initalRowFetchPromise) { 34 | await this.initPromise; 35 | this.initalRowFetchPromise = this.writer.getRows(); 36 | const logRows = await this.initalRowFetchPromise; 37 | await this.filterRemoved(logRows); 38 | const logRowsToSend = logRows.filter(logRow => 39 | this.shouldAppendRowToQueue(logRow, this.sentToServerRows) 40 | ); 41 | this.addToQueue(logRowsToSend); 42 | this.sendToServerLoop(); 43 | return logRows; 44 | } 45 | return await this.writer.getRows(); 46 | } 47 | 48 | async insertRows(logRows = [], allRows) { 49 | if (this.readOnly) { 50 | return logRows; 51 | } 52 | const rows = await this.writer.insertRows(logRows, allRows); 53 | await this.initPromise; 54 | this.addToQueue( 55 | logRows.filter(logRow => 56 | this.shouldAppendRowToQueue(logRow, this.sentToServerRows) 57 | ) 58 | ); 59 | return rows; 60 | } 61 | 62 | //Required 63 | async clear() { 64 | await AsyncStorage.removeItem( 65 | StorageServerHocWriter.SERVER_SEND_STORAGE_KEY 66 | ); 67 | this.sendQueue = []; 68 | this.sentToServerRows = []; 69 | await this.writer.clear(); 70 | } 71 | 72 | async filterRemoved(logRows) { 73 | await this.initPromise; 74 | if (this.sentToServerRows) { 75 | this.sentToServerRows = this.sentToServerRows.filter(sentRow => 76 | logRows.some(row => row.id === sentRow.id) 77 | ); 78 | await this.setSentToServerRows(this.sentToServerRows); 79 | } 80 | return logRows; 81 | } 82 | 83 | async sendToServerLoop() { 84 | if (!this.initalSendToServerPromise && this.options.sendInterval) { 85 | try { 86 | this.initalSendToServerPromise = await this.sendToServer( 87 | this.options.batchSize 88 | ); 89 | } catch (err) { 90 | this.log( 91 | "[StorageServerHocWriter] error while sending clientLog to server", 92 | err 93 | ); 94 | } finally { 95 | setTimeout(() => { 96 | this.sendToServerLoop(); 97 | }, this.options.sendInterval); 98 | } 99 | } 100 | } 101 | 102 | async sendAllMessagesToServer() { 103 | await this.initPromise; 104 | await this.initalRowFetchPromise; 105 | await this.sendToServer(this.sendQueue.length); 106 | } 107 | 108 | async sendToServer(batchSize) { 109 | const { rowsToSend, sentToServerRows } = await this.getPostData( 110 | batchSize 111 | ); 112 | if (!this.options.serverUrl) { 113 | throw new Error( 114 | "You need to supply an serverUrl option to StorageServerHocWriter" 115 | ); 116 | } 117 | if (rowsToSend && rowsToSend.length) { 118 | try { 119 | const data = { 120 | rows: this.options.modifyRowBeforeSend 121 | ? rowsToSend.map(row => 122 | this.options.modifyRowBeforeSend(row) 123 | ) 124 | : rowsToSend, 125 | extraData: this.options.getExtraData(), 126 | }; 127 | this.log(`Sending ${rowsToSend.length} log-rows to server`); 128 | const response = await fetch(this.options.serverUrl, { 129 | method: "POST", 130 | body: this.options.serializeData 131 | ? this.options.serializeData(data) 132 | : JSON.stringify(data), 133 | headers: { 134 | Accept: "application/json", 135 | "Content-Type": "application/json", 136 | }, 137 | }); 138 | if (!response.ok) { 139 | this.log(response); 140 | throw new Error("Failed sending logRows to server"); 141 | } 142 | sentToServerRows.forEach(row => (row.success = true)); 143 | rowsToSend.forEach(row => this.removeFromSendQueue(row)); 144 | return rowsToSend; 145 | } finally { 146 | await this.setSentToServerRows(this.sentToServerRows); 147 | } 148 | } 149 | return []; 150 | } 151 | 152 | removeFromSendQueue(row) { 153 | const rowIndex = this.sendQueue.indexOf(row); 154 | if (rowIndex !== -1) { 155 | this.sendQueue.splice(rowIndex, 1); 156 | } 157 | } 158 | 159 | getPostData(batchSize) { 160 | if (this.sendQueue && this.sendQueue.length) { 161 | const rowsToSend = this.sendQueue.slice(0, batchSize); 162 | if (rowsToSend && rowsToSend.length) { 163 | let sentToServerRows = rowsToSend.map(rowToSend => { 164 | let sentToServerRow = this.sentToServerRows.find( 165 | row => row.id === rowToSend.id 166 | ); 167 | if (!sentToServerRow) { 168 | sentToServerRow = { 169 | id: rowToSend.id, 170 | success: false, 171 | }; 172 | this.sentToServerRows.push(sentToServerRow); 173 | } 174 | sentToServerRow.sendTimeStamp = moment(); 175 | return sentToServerRow; 176 | }); 177 | return { rowsToSend, sentToServerRows }; 178 | } 179 | } 180 | return []; 181 | } 182 | 183 | shouldAppendRowToQueue(logRow, sentToServerRows) { 184 | if ( 185 | this.options.skipSendingMessagesOlderThen && 186 | logRow.timeStamp < this.options.skipSendingMessagesOlderThen 187 | ) { 188 | return false; 189 | } 190 | if (!this.includedLevel(logRow)) { 191 | return false; 192 | } 193 | const foundRow = sentToServerRows.find( 194 | serverRow => serverRow.id === logRow.id 195 | ); 196 | const shouldAddToQueue = !foundRow || !foundRow.success; 197 | return shouldAddToQueue; 198 | } 199 | 200 | includedLevel(logRow) { 201 | return ( 202 | this.options.sendLevels.indexOf("all") !== -1 || 203 | this.options.sendLevels.indexOf(logRow.level) !== -1 204 | ); 205 | } 206 | 207 | addToQueue(logRows) { 208 | if (logRows && logRows.length) { 209 | this.sendQueue = this.sendQueue || []; 210 | this.sendQueue = this.sendQueue.concat(logRows); 211 | } 212 | } 213 | 214 | log(...args) { 215 | if (this.options.isDebug) { 216 | console.log.apply(console, args); 217 | } 218 | } 219 | 220 | async getSentToServerRows() { 221 | const rowsString = await AsyncStorage.getItem( 222 | StorageServerHocWriter.SERVER_SEND_STORAGE_KEY 223 | ); 224 | if (rowsString) { 225 | return JSON.parse(rowsString); 226 | } 227 | return await this.setSentToServerRows([]); 228 | } 229 | 230 | async setSentToServerRows(sentToServerRows) { 231 | await AsyncStorage.setItem( 232 | StorageServerHocWriter.SERVER_SEND_STORAGE_KEY, 233 | JSON.stringify(sentToServerRows || []) 234 | ); 235 | return sentToServerRows; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /data-writers/stringify-data-writer.js: -------------------------------------------------------------------------------- 1 | import moment from "moment"; 2 | 3 | export default class StringifyDataWriter { 4 | static STORAGEKEY = "debug-rows"; 5 | 6 | constructor(storage) { 7 | this._readOnly = true; 8 | this.storage = storage; 9 | } 10 | 11 | setReadOnly(readOnly) { 12 | this._readOnly = readOnly; 13 | } 14 | 15 | //Required 16 | async getRows() { 17 | let dataAsString = await this.storage.getItem( 18 | StringifyDataWriter.STORAGEKEY 19 | ); 20 | if (!dataAsString) { 21 | return []; 22 | } 23 | let rows = JSON.parse(dataAsString); 24 | rows.forEach(row => { 25 | row.timeStamp = moment(row.timeStamp); 26 | }); 27 | return rows; 28 | } 29 | 30 | async insertRows(logRows = [], allRows) { 31 | if (this._readOnly) { 32 | return logRows; 33 | } 34 | await this.storage.setItem( 35 | StringifyDataWriter.STORAGEKEY, 36 | JSON.stringify(allRows) 37 | ); 38 | return logRows; 39 | } 40 | 41 | //Required 42 | async clear() { 43 | await this.storage.removeItem(StringifyDataWriter.STORAGEKEY); 44 | } 45 | 46 | //Optional 47 | logRowCreated() {} 48 | 49 | //Optional 50 | appendToLogRow(logRow) { 51 | return logRow; 52 | } 53 | 54 | //Optional 55 | async initalDataRead(logRows) {} 56 | } 57 | -------------------------------------------------------------------------------- /debug-list-view.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { 3 | View, 4 | Text, 5 | StyleSheet, 6 | ListView, 7 | Animated, 8 | TouchableOpacity, 9 | PixelRatio, 10 | NativeModules, 11 | LayoutAnimation, 12 | } from "react-native"; 13 | import moment from "moment"; 14 | import debugService from "./debug-service"; 15 | import InvertibleScrollView from "react-native-invertible-scroll-view"; 16 | const NativeAnimatedModule = NativeModules.NativeAnimatedModule; 17 | const AnimatedTouchableOpacity = Animated.createAnimatedComponent( 18 | TouchableOpacity 19 | ); 20 | const PRIMARY_COLOR = "#3b3b3b"; 21 | const SELECT_COLOR = "#292929"; 22 | const SEPERATOR_COLOR = "rgb(252, 217, 28)"; 23 | const SECONDARY_COLOR = "#181818"; 24 | const TEXT_COLOR = "#D6D6D6"; 25 | const LISTVIEW_REF = "listview"; 26 | export default class Debug extends React.Component { 27 | constructor() { 28 | super(); 29 | let ds = new ListView.DataSource({ 30 | rowHasChanged: (r1, r2) => { 31 | let rowHasChanged = r1.id !== r2.id; 32 | if (r1.expanded !== r2.expanded) { 33 | return true; 34 | } 35 | return rowHasChanged; 36 | }, 37 | }); 38 | this.preparedRows = { blob: {} }; 39 | this.state = { 40 | dataSource: ds.cloneWithRows([]), 41 | paused: false, 42 | rows: [], 43 | }; 44 | } 45 | 46 | prepareRows(rows) { 47 | return rows.reduce((o, m, i) => { 48 | const previousRender = 49 | this.preparedRows !== undefined 50 | ? this.preparedRows[m.id] 51 | : null; 52 | const previousRenderExists = !!previousRender; 53 | o[m.id] = { 54 | ...m, 55 | anim: previousRenderExists 56 | ? previousRender.anim 57 | : new Animated.Value(0), 58 | }; 59 | return o; 60 | }, {}); 61 | } 62 | 63 | renderList(props) { 64 | if (!this.state.paused) { 65 | this.preparedRows = this.prepareRows(props.rows); 66 | this.setState({ 67 | rows: props.rows, 68 | dataSource: this.state.dataSource.cloneWithRows( 69 | this.preparedRows 70 | ), 71 | }); 72 | } 73 | } 74 | 75 | componentWillReceiveProps(nextProps) { 76 | this.renderList(nextProps); 77 | } 78 | 79 | onPauseButtonPressed() { 80 | this.setState({ 81 | paused: !this.state.paused, 82 | }); 83 | this.renderList(this.props); 84 | } 85 | 86 | onClearButtonPressed() { 87 | debugService.clear(); 88 | } 89 | 90 | _formatTimeStamp(timeStamp, rowData) { 91 | if (rowData.format) { 92 | return rowData.format(timeStamp); 93 | } 94 | return timeStamp.format(this.props.timeStampFormat || "HH:mm:ss"); 95 | } 96 | 97 | onRowPress(sectionID, rowID) { 98 | const rowBefore = this.preparedRows[rowID]; 99 | if (this.props.multiExpanded) { 100 | const row = this.state.rows.find(row => row.id === rowID); 101 | row.expanded = !row.expanded; 102 | } else { 103 | this.state.rows.forEach(row => { 104 | row.expanded = row.id === rowID && !row.expanded; 105 | }); 106 | } 107 | this.preparedRows = this.prepareRows(this.state.rows); 108 | LayoutAnimation.configureNext({ 109 | update: { 110 | springDamping: 0.7, 111 | type: "spring", 112 | }, 113 | duration: 650, 114 | }); 115 | this.setState({ 116 | dataSource: this.state.dataSource.cloneWithRows(this.preparedRows), 117 | }); 118 | } 119 | 120 | onRowLayout(rowData) { 121 | Animated.timing(rowData.anim, { 122 | useNativeDriver: !!NativeAnimatedModule, 123 | toValue: 1, 124 | duration: 700, 125 | }).start(); 126 | } 127 | 128 | _renderSeperator(rowData, sectionID, rowID, highlightRow, animationStyle) { 129 | const seperatorStyles = [ 130 | styles.logRowMessage, 131 | styles.logRowMessageBold, 132 | styles.seperator, 133 | ]; 134 | return ( 135 | 139 | ***** 140 | 147 | {rowData.message} 148 | - {rowData.timeStamp.format("YYYY-MM-DD HH:mm:ss")} 149 | 150 | ***** 151 | 152 | ); 153 | } 154 | 155 | _renderLogRow(rowData, sectionID, rowID, highlightRow, animationStyle) { 156 | return ( 157 | 169 | 178 | 181 | {`[${rowData.level.toUpperCase()}]`} 182 | 183 | 192 | {rowData.message} 193 | 194 | 195 | {this._formatTimeStamp(rowData.timeStamp, rowData)} 196 | 197 | 198 | 199 | ); 200 | } 201 | 202 | _renderRow(rowData, sectionID, rowID, highlightRow) { 203 | let animationStyle = {}; 204 | if (rowData.anim) { 205 | animationStyle = { 206 | opacity: rowData.anim, 207 | transform: [ 208 | { 209 | scale: rowData.anim.interpolate({ 210 | inputRange: [0, 0.3, 1], 211 | outputRange: [1, 1.05, 1], 212 | }), 213 | }, 214 | ], 215 | }; 216 | } 217 | 218 | switch (rowData.level) { 219 | case "seperator": 220 | return this._renderSeperator( 221 | rowData, 222 | sectionID, 223 | rowID, 224 | highlightRow, 225 | animationStyle 226 | ); 227 | default: 228 | return this._renderLogRow( 229 | rowData, 230 | sectionID, 231 | rowID, 232 | highlightRow, 233 | animationStyle 234 | ); 235 | } 236 | } 237 | 238 | onCenterColumnPressed() { 239 | if (this.refs[LISTVIEW_REF]) { 240 | this.refs[LISTVIEW_REF].scrollTo({ x: 0, y: 0, animated: true }); 241 | } 242 | } 243 | 244 | _renderSeparator(sectionID, rowID, adjacentRowHighlighted) { 245 | return ( 246 | 255 | ); 256 | } 257 | 258 | render() { 259 | const { rows, ...props } = this.props; 260 | return ( 261 | 262 | 263 | 267 | 268 | {this.state.paused ? "Resume log" : "Pause log"} 269 | 270 | 271 | 275 | {`${this.state.rows 276 | .length} rows`} 277 | 278 | 282 | Clear log 283 | 284 | 285 | 286 | ( 293 | 297 | )} 298 | enableEmptySections={true} 299 | ref={LISTVIEW_REF} 300 | dataSource={this.state.dataSource} 301 | renderRow={this._renderRow.bind(this)} 302 | {...props} 303 | /> 304 | 305 | 306 | ); 307 | } 308 | } 309 | 310 | const styles = StyleSheet.create({ 311 | container: { 312 | flex: 1, 313 | backgroundColor: SECONDARY_COLOR, 314 | paddingTop: 5, 315 | }, 316 | toolBar: { 317 | backgroundColor: SECONDARY_COLOR, 318 | flexDirection: "row", 319 | padding: 10, 320 | borderBottomWidth: 2, 321 | borderColor: PRIMARY_COLOR, 322 | }, 323 | toolbarButton: { 324 | padding: 7, 325 | borderWidth: 2, 326 | borderRadius: 7, 327 | borderColor: PRIMARY_COLOR, 328 | }, 329 | centerColumn: { 330 | flex: 1, 331 | alignItems: "center", 332 | justifyContent: "center", 333 | }, 334 | titleText: { 335 | color: "#FFF", 336 | fontWeight: "bold", 337 | fontFamily: "System", 338 | fontSize: 16, 339 | alignSelf: "center", 340 | textAlign: "center", 341 | }, 342 | toolbarButtonText: { 343 | color: TEXT_COLOR, 344 | fontFamily: "System", 345 | fontSize: 12, 346 | }, 347 | listContainer: { 348 | flex: 1, 349 | }, 350 | debugRowContainer: { 351 | padding: 5, 352 | flex: 1, 353 | flexDirection: "row", 354 | backgroundColor: SECONDARY_COLOR, 355 | borderStyle: "solid", 356 | borderBottomWidth: 1 / PixelRatio.get(), 357 | borderBottomColor: PRIMARY_COLOR, 358 | }, 359 | debugRowContainerButton: { 360 | flexDirection: "row", 361 | flex: 1, 362 | overflow: "hidden", 363 | }, 364 | logRowMessage: { 365 | color: TEXT_COLOR, 366 | fontFamily: "System", 367 | fontSize: 11, 368 | paddingHorizontal: 5, 369 | lineHeight: 20, 370 | }, 371 | logRowMessageBold: { 372 | fontWeight: "bold", 373 | }, 374 | logRowLevelLabel: { 375 | minWidth: 80, 376 | fontWeight: "bold", 377 | }, 378 | logRowMessageSeperator: { 379 | fontSize: 11, 380 | fontWeight: "bold", 381 | textAlign: "center", 382 | color: SEPERATOR_COLOR, 383 | }, 384 | seperator: { 385 | fontSize: 18, 386 | color: SEPERATOR_COLOR, 387 | }, 388 | logRowMessageMain: { 389 | flex: 1, 390 | }, 391 | welcome: { 392 | fontSize: 20, 393 | textAlign: "center", 394 | margin: 10, 395 | }, 396 | instructions: { 397 | textAlign: "center", 398 | color: "#333333", 399 | marginBottom: 5, 400 | }, 401 | }); 402 | -------------------------------------------------------------------------------- /debug-service.js: -------------------------------------------------------------------------------- 1 | import { AsyncStorage, AppState, NetInfo } from "react-native"; 2 | import moment from "moment"; 3 | import InMemory from "./adapters/in-memory"; 4 | import timers from "./timers"; 5 | import debounce from "debounce"; 6 | import stacktraceParser from "stacktrace-parser"; 7 | import stringify from "json-stringify-safe"; 8 | import StringifyDataWriter from "./data-writers/stringify-data-writer"; 9 | import guid from "./guid"; 10 | import colors from "./colors"; 11 | 12 | const APP_START_LOG_MESSAGE = { 13 | id: guid(), 14 | lengthAtInsertion: 0, 15 | level: "seperator", 16 | message: "APP START", 17 | timeStamp: moment(), 18 | color: "#FFF", 19 | }; 20 | 21 | class DebugService { 22 | constructor(options) { 23 | options = options || {}; 24 | options.colors = options.colors || {}; 25 | options.colors = { ...colors, ...options.colors }; 26 | 27 | this.logRows = []; 28 | this.store = new StringifyDataWriter(new InMemory()); 29 | this.listners = []; 30 | this.options = { 31 | logToConsole: false, 32 | logToConsoleMethod : 'match', 33 | logToConsoleFunc : undefined, 34 | logRNErrors: false, 35 | maxNumberToRender: 0, 36 | maxNumberToPersist: 0, 37 | rowInsertDebounceMs: 200, 38 | logAppState: true, 39 | logConnection: true, 40 | ...options, 41 | }; 42 | } 43 | 44 | _handleConnectivityTypeChange(connectionInfo) { 45 | let { type, effectiveType } = connectionInfo; 46 | if (type === "none") { 47 | this.hasBeenDisconnected = true; 48 | this.seperator(`DISCONNECTED FROM INTERNET`); 49 | } else { 50 | const buildConnectionString = () => { 51 | return `${type.toUpperCase()}${effectiveType === "unknown" 52 | ? "" 53 | : ` - ${effectiveType}`}`; 54 | }; 55 | if (this.hasBeenDisconnected) { 56 | this.seperator( 57 | `[NETINFO] RECONNECTED TO ${buildConnectionString()}` 58 | ); 59 | } else { 60 | if (this.connectionHasBeenEstablished) { 61 | this.seperator( 62 | `[NETINFO] CHANGED TO ${buildConnectionString()}` 63 | ); 64 | } else { 65 | this.seperator( 66 | `[NETINFO] CONNECTION TO ${buildConnectionString()}` 67 | ); 68 | } 69 | } 70 | } 71 | this.connectionHasBeenEstablished = true; 72 | } 73 | 74 | _handleAppStateChange(currentAppState) { 75 | this.seperator(`APP STATE: ${currentAppState.toUpperCase()}`); 76 | } 77 | 78 | setupRNErrorLogging() { 79 | if (ErrorUtils) { 80 | const defaultHandler = 81 | ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler(); 82 | if (defaultHandler) { 83 | const parseErrorStack = error => { 84 | if (!error || !error.stack) { 85 | return []; 86 | } 87 | return Array.isArray(error.stack) 88 | ? error.stack 89 | : stacktraceParser.parse(error.stack); 90 | }; 91 | 92 | ErrorUtils.setGlobalHandler((error, isFatal) => { 93 | const stack = parseErrorStack(error); 94 | this.rnerror(isFatal, error.message, stack); 95 | defaultHandler && defaultHandler(error, isFatal); 96 | }); 97 | } 98 | } 99 | } 100 | 101 | async init(storageAdapter, options = {}) { 102 | options.colors = options.colors || {}; 103 | options.colors = { ...colors, ...options.colors }; 104 | 105 | this.options = { 106 | ...this.options, 107 | ...options, 108 | }; 109 | 110 | if (this.options.customDataWriter) { 111 | this.store = this.options.customDataWriter; 112 | } else { 113 | this.store = new StringifyDataWriter( 114 | storageAdapter || new InMemory() 115 | ); 116 | } 117 | 118 | if (this.options.logAppState) { 119 | AppState.addEventListener( 120 | "change", 121 | this._handleAppStateChange.bind(this) 122 | ); 123 | } 124 | if (this.options.logConnection) { 125 | NetInfo.addEventListener( 126 | "connectionChange", 127 | this._handleConnectivityTypeChange.bind(this) 128 | ); 129 | } 130 | if (this.options.logRNErrors) { 131 | this.setupRNErrorLogging(); 132 | } 133 | 134 | this._initalGet(); 135 | return this.insertAppStartMessage(); 136 | } 137 | 138 | async insertAppStartMessage() { 139 | if (!this.appStartRendered) { 140 | await this._appendToLog([APP_START_LOG_MESSAGE]); 141 | this.appStartRendered = true; 142 | } 143 | } 144 | 145 | async insertStoreRows(rows) { 146 | if (this.store.readOnly) { 147 | return; 148 | } 149 | this.rowsToInsert = this.rowsToInsert || []; 150 | this.rowsToInsert = this.rowsToInsert.concat(rows); 151 | if (!this.debouncedInsert) { 152 | this.debouncedInsert = debounce(() => { 153 | if (this.store && this.store.insertRows) { 154 | const insertArray = this.rowsToInsert; 155 | this.rowsToInsert = []; 156 | return this.store.insertRows( 157 | insertArray, 158 | this.getEmittableData(this.logRows) 159 | ); 160 | } 161 | }, this.options.rowInsertDebounceMs); 162 | } 163 | this.debouncedInsert(); 164 | } 165 | 166 | async _initalGet() { 167 | this.initPromise = this.store.getRows(); 168 | const rows = await this.initPromise; 169 | this.store.setReadOnly(false); 170 | const newRows = this.logRows; 171 | this.logRows = this.logRows.concat(rows); 172 | this.sortLogRows(); 173 | await this.insertStoreRows(newRows); 174 | if (this.store.initalDataRead) { 175 | await this.store.initalDataRead(this.logRows); 176 | } 177 | return this.emitDebugRowsChanged(this.logRows); 178 | } 179 | 180 | sortLogRows() { 181 | this.logRows.sort((left, right) => { 182 | let dateDiff = moment 183 | .utc(right.timeStamp) 184 | .diff(moment.utc(left.timeStamp)); 185 | if (dateDiff === 0) { 186 | return right.lengthAtInsertion - left.lengthAtInsertion; 187 | } 188 | return dateDiff; 189 | }); 190 | } 191 | 192 | async _getAndEmit() { 193 | const rows = await this.store.getRows(); 194 | return this.emitDebugRowsChanged(rows); 195 | } 196 | 197 | stopTimer(name) { 198 | timers.stop(name); 199 | timers.remove(name); 200 | } 201 | 202 | async startTimer(name) { 203 | let timer = timers.start(name); 204 | return await this._log("start-timer", undefined, name); 205 | } 206 | 207 | async logTime(name) { 208 | let timer = timers.stop(name); 209 | return await this._log( 210 | "end-timer", 211 | undefined, 212 | `${name}-timer delta: ${timer.delta}ms` 213 | ); 214 | } 215 | 216 | async clear() { 217 | this.logRows = []; 218 | await this.store.clear(); 219 | return await this._getAndEmit(); 220 | } 221 | 222 | log(...logRows) { 223 | return this._log("log", undefined, ...logRows); 224 | } 225 | 226 | debug(...logRows) { 227 | return this._log("debug", undefined, ...logRows); 228 | } 229 | 230 | info(...logRows) { 231 | return this._log("info", undefined, ...logRows); 232 | } 233 | 234 | error(...logRows) { 235 | return this._log("error", undefined, ...logRows); 236 | } 237 | 238 | fatal(...logRows) { 239 | return this._log("fatal", undefined, ...logRows); 240 | } 241 | 242 | success(...logRows) { 243 | return this._log("success", undefined, ...logRows); 244 | } 245 | 246 | rnerror(fatal, message, stackTrace) { 247 | let errorString = `ERROR: ${message} \nSTACKSTRACE:\n`; 248 | if (stackTrace && Array.isArray(stackTrace)) { 249 | const stackStrings = stackTrace.map(stackTraceItem => { 250 | let methodName = "-"; 251 | let lineNumber = "-"; 252 | let column = "-"; 253 | if (stackTraceItem.methodName) { 254 | methodName = 255 | stackTraceItem.methodName === "" 256 | ? "-" 257 | : stackTraceItem.methodName; 258 | } 259 | if (stackTraceItem.lineNumber !== undefined) { 260 | lineNumber = stackTraceItem.lineNumber.toString(); 261 | } 262 | if (stackTraceItem.column !== undefined) { 263 | column = stackTraceItem.column.toString(); 264 | } 265 | return `Method: ${methodName}, LineNumber: ${lineNumber}, Column: ${column}\n`; 266 | }); 267 | errorString += stackStrings.join("\n"); 268 | } 269 | if (fatal) { 270 | return this._log("RNFatal", undefined, errorString); 271 | } else { 272 | return this._log("RNError", undefined, errorString); 273 | } 274 | } 275 | 276 | seperator(name) { 277 | return this._log("seperator", undefined, name); 278 | } 279 | 280 | getColorForLogLevel(level) { 281 | return this.options.colors[level] || "#fff"; 282 | } 283 | 284 | async _log(level, options, ...logRows) { 285 | let color = this.getColorForLogLevel(level); 286 | if (options) { 287 | if (options.color) { 288 | color = options.color; 289 | } 290 | } 291 | this.logToConsole(level, color, ...logRows); 292 | let formattedRows = logRows.map((logRow, idx) => ({ 293 | id: guid(), 294 | lengthAtInsertion: this.logRows.length + idx, 295 | level, 296 | message: this._parseDataToString(logRow), 297 | timeStamp: moment(), 298 | color, 299 | })); 300 | if (this.options.appendToLogRow) { 301 | formattedRows = formattedRows.map(logRow => 302 | this.options.appendToLogRow(logRow) 303 | ); 304 | } 305 | 306 | if (!this.logRows.length) { 307 | await this.insertAppStartMessage(); 308 | } 309 | if (this.store.logRowCreated) { 310 | formattedRows.forEach(logRow => this.store.logRowCreated(logRow)); 311 | } 312 | await this._appendToLog(formattedRows); 313 | if (!this.initPromise) { 314 | await this._initalGet(); 315 | } 316 | } 317 | 318 | logToConsole(level, color, ...logRows) { 319 | if ( 320 | this.options.logToConsole && 321 | (!this.options.disableLevelToConsole || 322 | !this.options.disableLevelToConsole.some( 323 | disabledLevel => disabledLevel === level 324 | )) 325 | ) { 326 | if(this.options.logToConsoleFunc) { 327 | this.options.logToConsoleFunc(level, color, logRows) 328 | }else { 329 | if(this.options.logToConsoleMethod === 'match') { 330 | let avaliableConsoleLogs = ["log", "info", "debug", "error"]; 331 | let consoleLogFunc = 332 | avaliableConsoleLogs.find(avCL => avCL === level) || "log"; 333 | console[consoleLogFunc](...logRows); 334 | }else { 335 | console[this.options.logToConsoleMethod](...logRows); 336 | } 337 | } 338 | 339 | } 340 | } 341 | 342 | _parseDataToString(data) { 343 | if (typeof data === "string" || data instanceof String) { 344 | return data; 345 | } else { 346 | let dataAsString = stringify(data, null, " "); //FYI: spaces > tabs 347 | if (dataAsString && dataAsString.length > 12000) { 348 | dataAsString = 349 | dataAsString.substring(0, 12000) + 350 | "...(Cannot display more RN-device-log)"; 351 | } 352 | return dataAsString; 353 | } 354 | } 355 | 356 | _appendToLog(logRows) { 357 | this.logRows = logRows.concat(this.logRows); 358 | this.insertStoreRows(logRows); 359 | this.emitDebugRowsChanged(this.logRows); 360 | } 361 | 362 | onDebugRowsChanged(cb) { 363 | this.listners.push(cb); 364 | cb(this.getEmittableData(this.logRows)); 365 | return () => { 366 | var i = this.listners.indexOf(cb); 367 | if (i !== -1) { 368 | this.listners.splice(i, 1); 369 | } 370 | }; 371 | } 372 | 373 | emitDebugRowsChanged(data) { 374 | this.listners.forEach(cb => cb(this.getEmittableData(data))); 375 | } 376 | 377 | getEmittableData(rows) { 378 | if (this.options.maxNumberToRender !== 0) { 379 | return rows.slice(0, this.options.maxNumberToRender); 380 | } 381 | return rows; 382 | } 383 | } 384 | 385 | export default new DebugService(); 386 | -------------------------------------------------------------------------------- /debug-view.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { View, StyleSheet } from "react-native"; 3 | import DebugListView from "./debug-list-view.js"; 4 | import debugService from "./debug-service"; 5 | import debounce from "debounce"; 6 | 7 | export default class DebugView extends React.Component { 8 | constructor() { 9 | super(); 10 | this.state = { 11 | rows: [], 12 | }; 13 | this.unmounted = false; 14 | this.updateDebounced = debounce(this.update.bind(this), 150); 15 | } 16 | 17 | componentWillUnmount() { 18 | this.unmounted = true; 19 | if (this.listner) { 20 | this.listner(); 21 | } 22 | } 23 | 24 | update(data) { 25 | if (data) { 26 | if (!this.unmounted) { 27 | this.setState({ rows: data }); 28 | } 29 | } 30 | } 31 | 32 | componentWillMount() { 33 | this.listner = debugService.onDebugRowsChanged(this.updateDebounced); 34 | } 35 | 36 | render() { 37 | return ( 38 | 39 | 40 | 41 | ); 42 | } 43 | } 44 | 45 | const styles = StyleSheet.create({ 46 | container: { 47 | flex: 1, 48 | }, 49 | }); 50 | -------------------------------------------------------------------------------- /guid.js: -------------------------------------------------------------------------------- 1 | export default function guid() { 2 | function s4() { 3 | return Math.floor((1 + Math.random()) * 0x10000) 4 | .toString(16) 5 | .substring(1); 6 | } 7 | return ( 8 | s4() + 9 | s4() + 10 | "-" + 11 | s4() + 12 | "-" + 13 | s4() + 14 | "-" + 15 | s4() + 16 | "-" + 17 | s4() + 18 | s4() + 19 | s4() 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export { default as LogView } from "./debug-view"; 2 | import deviceLog from "./debug-service"; 3 | export { default as InMemoryAdapter } from "./adapters/in-memory"; 4 | export { 5 | default as StorageServerHocWriter, 6 | } from "./data-writers/storage-server-hoc-writer"; 7 | export default deviceLog; 8 | export { 9 | default as StringifyDataWriter, 10 | } from "./data-writers/stringify-data-writer"; 11 | export { 12 | default as RealmDataWriter, 13 | RealmBaseScheme, 14 | } from "./data-writers/realm-data-writer"; 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-device-log", 3 | "keywords": [ 4 | "react-component", 5 | "react-native", 6 | "ios", 7 | "android" 8 | ], 9 | "version": "1.0.2", 10 | "description": "A GUI and service for handling development log messages on a device", 11 | "main": "index.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/olofd/react-native-device-log.git" 15 | }, 16 | "scripts": { 17 | "test": "echo \"Error: no test specified\" && exit 1" 18 | }, 19 | "author": "Olof Dahlbom", 20 | "license": "MIT", 21 | "dependencies": { 22 | "json-stringify-safe": "^5.0.1", 23 | "react-native-invertible-scroll-view": "^1.1.0", 24 | "moment": "^2.18.1", 25 | "debounce": "^1.0.2", 26 | "stacktrace-parser": "^0.1.4" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /timers.js: -------------------------------------------------------------------------------- 1 | const timestamp = { 2 | now: () => { 3 | return new Date().getTime(); 4 | }, 5 | }; 6 | 7 | export class Timers { 8 | constructor() { 9 | this.timers = {}; 10 | } 11 | 12 | start(timer_name) { 13 | this.timers[timer_name] = { 14 | start: timestamp.now(), 15 | stop: undefined, 16 | delta: undefined, 17 | }; 18 | return this.timers[timer_name]; 19 | } 20 | 21 | stop(timer_name) { 22 | const timer = this.timers[timer_name]; 23 | timer.stop = timestamp.now(); 24 | timer.delta = timer.stop - timer.start; 25 | return timer; 26 | } 27 | 28 | get(timer_name) { 29 | return this.timers[timer_name]; 30 | } 31 | 32 | remove(timer_name) { 33 | if (this.timers[timer_name]) { 34 | delete this.timers[timer_name]; 35 | } 36 | } 37 | } 38 | 39 | export default new Timers(); 40 | --------------------------------------------------------------------------------