Skip to content

Commit d9c9d85

Browse files
nevirbenvinegar
authored andcommitted
Capture and report react native fatals on next launch (#626)
1 parent ed71886 commit d9c9d85

File tree

5 files changed

+212
-17
lines changed

5 files changed

+212
-17
lines changed

Gruntfile.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,12 @@ module.exports = function(grunt) {
128128
},
129129
test: {
130130
src: 'test/**/*.test.js',
131-
dest: 'build/raven.test.js',
132-
options: {
131+
dest: 'build/raven.test.js',
132+
options: {
133133
browserifyOptions: {
134134
debug: true // source maps
135135
},
136+
ignore: ['react-native'],
136137
plugin: [proxyquire.plugin]
137138
}
138139
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"json-stringify-safe": "^5.0.1"
1818
},
1919
"devDependencies": {
20+
"bluebird": "^3.4.1",
2021
"browserify-versionify": "^1.0.6",
2122
"bundle-collapser": "^1.2.1",
2223
"chai": "2.3.0",

plugins/react-native.js

Lines changed: 103 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
* pathStrip: A RegExp that matches the portions of a file URI that should be
1313
* removed from stacks prior to submission.
1414
*
15+
* onInitialize: A callback that fires once the plugin has fully initialized
16+
* and checked for any previously thrown fatals. If there was a fatal, its
17+
* data payload will be passed as the first argument of the callback.
18+
*
1519
*/
1620
'use strict';
1721

@@ -20,6 +24,9 @@
2024

2125
var PATH_STRIP_RE = /^.*\/[^\.]+\.app/;
2226

27+
var FATAL_ERROR_KEY = '--rn-fatal--';
28+
var ASYNC_STORAGE_KEY = '--raven-js-global-error-payload--';
29+
2330
/**
2431
* Strip device-specific IDs from React Native file:// paths
2532
*/
@@ -57,15 +64,107 @@ function reactNativePlugin(Raven, options) {
5764
reactNativePlugin._normalizeData(data, options.pathStrip)
5865
});
5966

67+
// Check for a previously persisted payload, and report it.
68+
reactNativePlugin._restorePayload()
69+
.then(function(payload) {
70+
options.onInitialize && options.onInitialize(payload);
71+
if (!payload) return;
72+
Raven._sendProcessedPayload(payload, function(error) {
73+
if (error) return; // Try again next launch.
74+
reactNativePlugin._clearPayload();
75+
});
76+
})
77+
['catch'](function() {});
78+
79+
// Make sure that if multiple fatals occur, we only persist the first one.
80+
//
81+
// The first error is probably the most important/interesting error, and we
82+
// want to crash ASAP, rather than potentially queueing up multiple errors.
83+
var handlingFatal = false;
84+
6085
var defaultHandler = ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler() || ErrorUtils._globalHandler;
6186

62-
ErrorUtils.setGlobalHandler(function() {
87+
Raven.setShouldSendCallback(function(data, originalCallback) {
88+
if (!(FATAL_ERROR_KEY in data)) {
89+
return originalCallback.call(this, data);
90+
}
91+
92+
var origError = data[FATAL_ERROR_KEY];
93+
delete data[FATAL_ERROR_KEY];
94+
95+
reactNativePlugin._persistPayload(data)
96+
.then(function() {
97+
defaultHandler(origError, true);
98+
handlingFatal = false; // In case it isn't configured to crash.
99+
return null;
100+
})
101+
['catch'](function() {});
102+
103+
return false; // Do not continue.
104+
});
105+
106+
ErrorUtils.setGlobalHandler(function(error, isFatal) {
107+
var captureOptions = {
108+
timestamp: new Date() / 1000
109+
};
63110
var error = arguments[0];
64-
defaultHandler.apply(this, arguments)
65-
Raven.captureException(error);
111+
// We want to handle fatals, but only in production mode.
112+
var shouldHandleFatal = isFatal && !global.__DEV__;
113+
if (shouldHandleFatal) {
114+
if (handlingFatal) {
115+
console.log('Encountered multiple fatals in a row. The latest:', error);
116+
return;
117+
}
118+
handlingFatal = true;
119+
// We need to preserve the original error so that it can be rethrown
120+
// after it is persisted (see our shouldSendCallback above).
121+
captureOptions[FATAL_ERROR_KEY] = error;
122+
}
123+
Raven.captureException(error, captureOptions);
124+
// Handle non-fatals regularly.
125+
if (!shouldHandleFatal) {
126+
defaultHandler(error);
127+
}
66128
});
67129
}
68130

131+
/**
132+
* Saves the payload for a globally-thrown error, so that we can report it on
133+
* next launch.
134+
*
135+
* Returns a promise that guarantees never to reject.
136+
*/
137+
reactNativePlugin._persistPayload = function(payload) {
138+
var AsyncStorage = require('react-native').AsyncStorage;
139+
return AsyncStorage.setItem(ASYNC_STORAGE_KEY, JSON.stringify(payload))
140+
['catch'](function() { return null; });
141+
}
142+
143+
/**
144+
* Checks for any previously persisted errors (e.g. from last crash)
145+
*
146+
* Returns a promise that guarantees never to reject.
147+
*/
148+
reactNativePlugin._restorePayload = function() {
149+
var AsyncStorage = require('react-native').AsyncStorage;
150+
var promise = AsyncStorage.getItem(ASYNC_STORAGE_KEY)
151+
.then(function(payload) { return JSON.parse(payload); })
152+
['catch'](function() { return null; });
153+
// Make sure that we fetch ASAP.
154+
AsyncStorage.flushGetRequests();
155+
156+
return promise;
157+
};
158+
159+
/**
160+
* Clears any persisted payloads.
161+
*/
162+
reactNativePlugin._clearPayload = function() {
163+
var AsyncStorage = require('react-native').AsyncStorage;
164+
return AsyncStorage.removeItem(ASYNC_STORAGE_KEY)
165+
['catch'](function() { return null; });
166+
}
167+
69168
/**
70169
* Custom HTTP transport for use with React Native applications.
71170
*/
@@ -82,7 +181,7 @@ reactNativePlugin._transport = function (options) {
82181
}
83182
} else {
84183
if (options.onError) {
85-
options.onError();
184+
options.onError(new Error('Sentry error code: ' + request.status));
86185
}
87186
}
88187
};

src/raven.js

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1160,8 +1160,6 @@ Raven.prototype = {
11601160

11611161

11621162
_send: function(data) {
1163-
var self = this;
1164-
11651163
var globalOptions = this._globalOptions;
11661164

11671165
var baseData = {
@@ -1222,6 +1220,13 @@ Raven.prototype = {
12221220
return;
12231221
}
12241222

1223+
this._sendProcessedPayload(data);
1224+
},
1225+
1226+
_sendProcessedPayload: function(data, callback) {
1227+
var self = this;
1228+
var globalOptions = this._globalOptions;
1229+
12251230
// Send along an event_id if not explicitly passed.
12261231
// This event_id can be used to reference the error within Sentry itself.
12271232
// Set lastEventId after we know the error should actually be sent
@@ -1264,12 +1269,15 @@ Raven.prototype = {
12641269
data: data,
12651270
src: url
12661271
});
1272+
callback && callback();
12671273
},
1268-
onError: function failure() {
1274+
onError: function failure(error) {
12691275
self._triggerEvent('failure', {
12701276
data: data,
12711277
src: url
12721278
});
1279+
error = error || new Error('Raven send failed (no additional details provided)');
1280+
callback && callback(error);
12731281
}
12741282
});
12751283
},
@@ -1291,7 +1299,7 @@ Raven.prototype = {
12911299
opts.onSuccess();
12921300
}
12931301
} else if (opts.onError) {
1294-
opts.onError();
1302+
opts.onError(new Error('Sentry error code: ' + request.status));
12951303
}
12961304
}
12971305

test/plugins/react-native.test.js

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
var Promise = require('bluebird');
2+
13
var _Raven = require('../../src/raven');
24
var reactNativePlugin = require('../../plugins/react-native');
35

@@ -8,6 +10,10 @@ describe('React Native plugin', function () {
810
beforeEach(function () {
911
Raven = new _Raven();
1012
Raven.config('http://[email protected]:80/2');
13+
14+
reactNativePlugin._persistPayload = self.sinon.stub().returns(Promise.resolve());
15+
reactNativePlugin._restorePayload = self.sinon.stub().returns(Promise.resolve());
16+
reactNativePlugin._clearPayload = self.sinon.stub().returns(Promise.resolve());
1117
});
1218

1319
describe('_normalizeData()', function () {
@@ -135,16 +141,96 @@ describe('React Native plugin', function () {
135141
}
136142
});
137143

138-
it('should call the default React Native handler and Raven.captureException', function () {
144+
it('checks for persisted errors when starting', function () {
145+
var onInit = self.sinon.stub();
146+
reactNativePlugin(Raven, {onInitialize: onInit});
147+
assert.isTrue(reactNativePlugin._restorePayload.calledOnce);
148+
149+
return Promise.resolve().then(function () {
150+
assert.isTrue(onInit.calledOnce);
151+
});
152+
});
153+
154+
it('reports persisted errors', function () {
155+
var payload = {abc: 123};
156+
self.sinon.stub(Raven, '_sendProcessedPayload');
157+
reactNativePlugin._restorePayload = self.sinon.stub().returns(Promise.resolve(payload));
158+
var onInit = self.sinon.stub();
159+
reactNativePlugin(Raven, {onInitialize: onInit});
160+
161+
return Promise.resolve().then(function () {
162+
assert.isTrue(onInit.calledOnce);
163+
assert.equal(onInit.getCall(0).args[0], payload);
164+
assert.isTrue(Raven._sendProcessedPayload.calledOnce);
165+
assert.equal(Raven._sendProcessedPayload.getCall(0).args[0], payload);
166+
});
167+
});
168+
169+
it('clears persisted errors after they are reported', function () {
170+
var payload = {abc: 123};
171+
var callback;
172+
self.sinon.stub(Raven, '_sendProcessedPayload', function(p, cb) { callback = cb; });
173+
reactNativePlugin._restorePayload = self.sinon.stub().returns(Promise.resolve(payload));
174+
175+
reactNativePlugin(Raven);
176+
177+
return Promise.resolve().then(function () {
178+
assert.isFalse(reactNativePlugin._clearPayload.called);
179+
callback();
180+
assert.isTrue(reactNativePlugin._clearPayload.called);
181+
});
182+
});
183+
184+
it('does not clear persisted errors if there is an error reporting', function () {
185+
var payload = {abc: 123};
186+
var callback;
187+
self.sinon.stub(Raven, '_sendProcessedPayload', function(p, cb) { callback = cb; });
188+
reactNativePlugin._restorePayload = self.sinon.stub().returns(Promise.resolve(payload));
189+
139190
reactNativePlugin(Raven);
140-
var err = new Error();
141-
this.sinon.stub(Raven, 'captureException');
142191

143-
this.globalErrorHandler(err);
192+
return Promise.resolve().then(function () {
193+
assert.isFalse(reactNativePlugin._clearPayload.called);
194+
callback(new Error('nope'));
195+
assert.isFalse(reactNativePlugin._clearPayload.called);
196+
});
197+
});
198+
199+
describe('in development mode', function () {
200+
beforeEach(function () {
201+
global.__DEV__ = true;
202+
});
203+
204+
it('should call the default React Native handler and Raven.captureException', function () {
205+
reactNativePlugin(Raven);
206+
var err = new Error();
207+
this.sinon.stub(Raven, 'captureException');
144208

145-
assert.isTrue(this.defaultErrorHandler.calledOnce);
146-
assert.isTrue(Raven.captureException.calledOnce);
147-
assert.equal(Raven.captureException.getCall(0).args[0], err);
209+
this.globalErrorHandler(err, true);
210+
211+
assert.isTrue(this.defaultErrorHandler.calledOnce);
212+
assert.isTrue(Raven.captureException.calledOnce);
213+
assert.equal(Raven.captureException.getCall(0).args[0], err);
214+
});
215+
});
216+
217+
describe('in production mode', function () {
218+
beforeEach(function () {
219+
global.__DEV__ = false;
220+
});
221+
222+
it('should call the default React Native handler after persisting the error', function () {
223+
reactNativePlugin(Raven);
224+
var err = new Error();
225+
this.globalErrorHandler(err, true);
226+
227+
assert.isTrue(reactNativePlugin._persistPayload.calledOnce);
228+
229+
var defaultErrorHandler = this.defaultErrorHandler;
230+
return Promise.resolve().then(function () {
231+
assert.isTrue(defaultErrorHandler.calledOnce);
232+
});
233+
});
148234
});
149235
});
150236
});

0 commit comments

Comments
 (0)