246 lines
7.2 KiB
JavaScript
246 lines
7.2 KiB
JavaScript
|
/*global ErrorUtils:false*/
|
||
|
|
||
|
/**
|
||
|
* react-native plugin for Raven
|
||
|
*
|
||
|
* Usage:
|
||
|
* var Raven = require('raven-js');
|
||
|
* Raven.addPlugin(require('raven-js/plugins/react-native'));
|
||
|
*
|
||
|
* Options:
|
||
|
*
|
||
|
* pathStrip: A RegExp that matches the portions of a file URI that should be
|
||
|
* removed from stacks prior to submission.
|
||
|
*
|
||
|
* onInitialize: A callback that fires once the plugin has fully initialized
|
||
|
* and checked for any previously thrown fatals. If there was a fatal, its
|
||
|
* data payload will be passed as the first argument of the callback.
|
||
|
*
|
||
|
*/
|
||
|
var wrappedCallback = require('../src/utils').wrappedCallback;
|
||
|
|
||
|
// Example React Native path format (iOS):
|
||
|
// /var/containers/Bundle/Application/{DEVICE_ID}/HelloWorld.app/main.jsbundle
|
||
|
|
||
|
var PATH_STRIP_RE = /^.*\/[^\.]+(\.app|CodePush)/;
|
||
|
var stringify = require('../vendor/json-stringify-safe/stringify');
|
||
|
var FATAL_ERROR_KEY = '--rn-fatal--';
|
||
|
var ASYNC_STORAGE_KEY = '--raven-js-global-error-payload--';
|
||
|
|
||
|
/**
|
||
|
* Strip device-specific IDs from React Native file:// paths
|
||
|
*/
|
||
|
function normalizeUrl(url, pathStripRe) {
|
||
|
if (url.indexOf('/') !== -1) {
|
||
|
return url.replace(/^file\:\/\//, '').replace(pathStripRe, '');
|
||
|
} else {
|
||
|
return '/' + url;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Extract key/value pairs from an object and encode them for
|
||
|
* use in a query string
|
||
|
*/
|
||
|
function urlencode(obj) {
|
||
|
var pairs = [];
|
||
|
for (var key in obj) {
|
||
|
if ({}.hasOwnProperty.call(obj, key))
|
||
|
pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(obj[key]));
|
||
|
}
|
||
|
return pairs.join('&');
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Initializes React Native plugin
|
||
|
*/
|
||
|
function reactNativePlugin(Raven, options) {
|
||
|
options = options || {};
|
||
|
|
||
|
// react-native doesn't have a document, so can't use default Image
|
||
|
// transport - use XMLHttpRequest instead
|
||
|
Raven.setTransport(reactNativePlugin._transport);
|
||
|
|
||
|
// Use data callback to strip device-specific paths from stack traces
|
||
|
Raven.setDataCallback(
|
||
|
wrappedCallback(function(data) {
|
||
|
return reactNativePlugin._normalizeData(data, options.pathStrip);
|
||
|
})
|
||
|
);
|
||
|
|
||
|
// Check for a previously persisted payload, and report it.
|
||
|
reactNativePlugin
|
||
|
._restorePayload()
|
||
|
.then(function(payload) {
|
||
|
options.onInitialize && options.onInitialize(payload);
|
||
|
if (!payload) return;
|
||
|
Raven._sendProcessedPayload(payload, function(error) {
|
||
|
if (error) return; // Try again next launch.
|
||
|
reactNativePlugin._clearPayload();
|
||
|
});
|
||
|
})
|
||
|
['catch'](function() {});
|
||
|
|
||
|
// Make sure that if multiple fatals occur, we only persist the first one.
|
||
|
//
|
||
|
// The first error is probably the most important/interesting error, and we
|
||
|
// want to crash ASAP, rather than potentially queueing up multiple errors.
|
||
|
var handlingFatal = false;
|
||
|
|
||
|
var defaultHandler =
|
||
|
(ErrorUtils.getGlobalHandler && ErrorUtils.getGlobalHandler()) ||
|
||
|
ErrorUtils._globalHandler;
|
||
|
|
||
|
Raven.setShouldSendCallback(function(data, originalCallback) {
|
||
|
if (!(FATAL_ERROR_KEY in data)) {
|
||
|
// not a fatal (will not crash runtime), continue as planned
|
||
|
return originalCallback ? originalCallback.call(this, data) : true;
|
||
|
}
|
||
|
|
||
|
var origError = data[FATAL_ERROR_KEY];
|
||
|
delete data[FATAL_ERROR_KEY];
|
||
|
|
||
|
reactNativePlugin
|
||
|
._persistPayload(data)
|
||
|
.then(function() {
|
||
|
defaultHandler(origError, true);
|
||
|
handlingFatal = false; // In case it isn't configured to crash.
|
||
|
return null;
|
||
|
})
|
||
|
['catch'](function() {});
|
||
|
|
||
|
return false; // Do not continue.
|
||
|
});
|
||
|
|
||
|
ErrorUtils.setGlobalHandler(function(error, isFatal) {
|
||
|
var captureOptions = {
|
||
|
timestamp: new Date() / 1000
|
||
|
};
|
||
|
var error = arguments[0];
|
||
|
// We want to handle fatals, but only in production mode.
|
||
|
var shouldHandleFatal = isFatal && !global.__DEV__;
|
||
|
if (shouldHandleFatal) {
|
||
|
if (handlingFatal) {
|
||
|
console.log('Encountered multiple fatals in a row. The latest:', error);
|
||
|
return;
|
||
|
}
|
||
|
handlingFatal = true;
|
||
|
// We need to preserve the original error so that it can be rethrown
|
||
|
// after it is persisted (see our shouldSendCallback above).
|
||
|
captureOptions[FATAL_ERROR_KEY] = error;
|
||
|
}
|
||
|
Raven.captureException(error, captureOptions);
|
||
|
// Handle non-fatals regularly.
|
||
|
if (!shouldHandleFatal) {
|
||
|
defaultHandler(error);
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Saves the payload for a globally-thrown error, so that we can report it on
|
||
|
* next launch.
|
||
|
*
|
||
|
* Returns a promise that guarantees never to reject.
|
||
|
*/
|
||
|
reactNativePlugin._persistPayload = function(payload) {
|
||
|
var AsyncStorage = require('react-native').AsyncStorage;
|
||
|
return AsyncStorage.setItem(ASYNC_STORAGE_KEY, stringify(payload))['catch'](function() {
|
||
|
return null;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Checks for any previously persisted errors (e.g. from last crash)
|
||
|
*
|
||
|
* Returns a promise that guarantees never to reject.
|
||
|
*/
|
||
|
reactNativePlugin._restorePayload = function() {
|
||
|
var AsyncStorage = require('react-native').AsyncStorage;
|
||
|
var promise = AsyncStorage.getItem(ASYNC_STORAGE_KEY)
|
||
|
.then(function(payload) {
|
||
|
return JSON.parse(payload);
|
||
|
})
|
||
|
['catch'](function() {
|
||
|
return null;
|
||
|
});
|
||
|
// Make sure that we fetch ASAP.
|
||
|
AsyncStorage.flushGetRequests();
|
||
|
|
||
|
return promise;
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Clears any persisted payloads.
|
||
|
*/
|
||
|
reactNativePlugin._clearPayload = function() {
|
||
|
var AsyncStorage = require('react-native').AsyncStorage;
|
||
|
return AsyncStorage.removeItem(ASYNC_STORAGE_KEY)['catch'](function() {
|
||
|
return null;
|
||
|
});
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Custom HTTP transport for use with React Native applications.
|
||
|
*/
|
||
|
reactNativePlugin._transport = function(options) {
|
||
|
var request = new XMLHttpRequest();
|
||
|
request.onreadystatechange = function(e) {
|
||
|
if (request.readyState !== 4) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
if (request.status === 200) {
|
||
|
if (options.onSuccess) {
|
||
|
options.onSuccess();
|
||
|
}
|
||
|
} else {
|
||
|
if (options.onError) {
|
||
|
var err = new Error('Sentry error code: ' + request.status);
|
||
|
err.request = request;
|
||
|
options.onError(err);
|
||
|
}
|
||
|
}
|
||
|
};
|
||
|
|
||
|
request.open('POST', options.url + '?' + urlencode(options.auth));
|
||
|
|
||
|
// NOTE: React Native ignores CORS and will NOT send a preflight
|
||
|
// request for application/json.
|
||
|
// See: https://facebook.github.io/react-native/docs/network.html#xmlhttprequest
|
||
|
request.setRequestHeader('Content-type', 'application/json');
|
||
|
|
||
|
// Sentry expects an Origin header when using HTTP POST w/ public DSN.
|
||
|
// Just set a phony Origin value; only matters if Sentry Project is configured
|
||
|
// to whitelist specific origins.
|
||
|
request.setRequestHeader('Origin', 'react-native://');
|
||
|
request.send(stringify(options.data));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Strip device-specific IDs found in culprit and frame filenames
|
||
|
* when running React Native applications on a physical device.
|
||
|
*/
|
||
|
reactNativePlugin._normalizeData = function(data, pathStripRe) {
|
||
|
if (!pathStripRe) {
|
||
|
pathStripRe = PATH_STRIP_RE;
|
||
|
}
|
||
|
|
||
|
if (data.culprit) {
|
||
|
data.culprit = normalizeUrl(data.culprit, pathStripRe);
|
||
|
}
|
||
|
|
||
|
// NOTE: if data.exception exists, exception.values and exception.values[0] are
|
||
|
// guaranteed to exist
|
||
|
var stacktrace =
|
||
|
data.stacktrace || (data.exception && data.exception.values[0].stacktrace);
|
||
|
if (stacktrace) {
|
||
|
stacktrace.frames.forEach(function(frame) {
|
||
|
frame.filename = normalizeUrl(frame.filename, pathStripRe);
|
||
|
});
|
||
|
}
|
||
|
return data;
|
||
|
};
|
||
|
|
||
|
module.exports = reactNativePlugin;
|