├── .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 |
--------------------------------------------------------------------------------