GT2/GT2-Android/node_modules/raven-js/plugins/react-native.js

246 lines
7.2 KiB
JavaScript
Raw Normal View History

/*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;