From 3ac7b48adcafc0db62c3d12de89f88c0d274d113 Mon Sep 17 00:00:00 2001 From: Harrison Healey Date: Fri, 1 Sep 2017 13:50:17 -0400 Subject: [PATCH] RN-289 Sentry integration (#873) * Added JS code for Sentry * Removed leftover initializeSentry call * Added SentryOptions config setting * Added native components for react-native-exception-handler * Removed default props from ErrorText * Moved where Sentry is initialized * Added ios/sentry.properties to .gitignore * Added linking react-native-sentry to Fastlane * Fixed fastlane to include newlines in sentry.properties * Moved to manually link react-native-sentry * Captured redux errors with Sentry * Redid how Sentry is optionally compiled to be simpler * Added Sentry middleware to create redux breadcrumbs * Added Sentry tags for server version * Initialize Sentry when testing * Fixed string replacement for SentryEnabled in fastlane * Added react-native-sentry to NOTICE.txt --- .gitignore | 1 + NOTICE.txt | 39 ++++- android/app/build.gradle | 3 + .../mattermost/rnbeta/MainApplication.java | 6 +- android/settings.gradle | 4 + app/components/error_text.js | 7 +- app/mattermost.js | 64 ++++--- app/store/index.js | 5 +- app/utils/sentry/index.js | 162 ++++++++++++++++++ app/utils/sentry/middleware.js | 43 +++++ assets/base/config.json | 13 +- fastlane/Fastfile | 110 ++++++++---- index.android.js | 1 + ios/Mattermost.xcodeproj/project.pbxproj | 43 ++++- ios/Mattermost/AppDelegate.m | 5 + ios/bundleReactNative.sh | 15 ++ ios/makeSentryProperties.sh | 19 ++ ios/uploadDebugSymbols.sh | 14 ++ package.json | 5 +- test/setup.js | 4 + yarn.lock | 108 +++++++++++- 21 files changed, 597 insertions(+), 74 deletions(-) create mode 100644 app/utils/sentry/index.js create mode 100644 app/utils/sentry/middleware.js create mode 100755 ios/bundleReactNative.sh create mode 100755 ios/makeSentryProperties.sh create mode 100755 ios/uploadDebugSymbols.sh diff --git a/.gitignore b/.gitignore index 1b58ae951..b25298785 100644 --- a/.gitignore +++ b/.gitignore @@ -65,6 +65,7 @@ mattermost.keystore # Sentry android/sentry.properties +ios/sentry.properties # Testing .nyc_output diff --git a/NOTICE.txt b/NOTICE.txt index 00a0259ab..537b4cd85 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1295,8 +1295,6 @@ The MIT License (MIT) Copyright (c) 2015 Bart Gryszko - - Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights @@ -1304,13 +1302,9 @@ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - - The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE @@ -1351,3 +1345,36 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +--- + +## react-native-sentry + +This product contains 'react-native-sentry', the Sentry SDK for React Native + +* HOMEPAGE: + * https://github.com/getsentry/react-native-sentry + +* LICENSE: + +The MIT License (MIT) + +Copyright (c) 2017 Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/android/app/build.gradle b/android/app/build.gradle index a8f7d7e9a..06cda0b31 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -66,6 +66,7 @@ import com.android.build.OutputFile */ apply from: "../../node_modules/react-native/react.gradle" +apply from: "../../node_modules/react-native-sentry/sentry.gradle" apply from: "../../node_modules/react-native-vector-icons/fonts.gradle" /** @@ -144,6 +145,8 @@ android { } dependencies { + compile project(':react-native-sentry') + compile project(':react-native-exception-handler') compile project(':react-native-fetch-blob') compile fileTree(dir: "libs", include: ["*.jar"]) compile "com.android.support:appcompat-v7:25.0.1" diff --git a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java index c0ba9f3db..0a5993a8d 100644 --- a/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java +++ b/android/app/src/main/java/com/mattermost/rnbeta/MainApplication.java @@ -6,6 +6,8 @@ import android.content.Context; import android.os.Bundle; import com.facebook.react.ReactApplication; +import io.sentry.RNSentryPackage; +import com.masteratul.exceptionhandler.ReactNativeExceptionHandlerPackage; import com.RNFetchBlob.RNFetchBlobPackage; import com.gantix.JailMonkey.JailMonkeyPackage; import io.tradle.react.LocalAuthPackage; @@ -60,7 +62,9 @@ public class MainApplication extends NavigationApplication implements INotificat new LocalAuthPackage(), new JailMonkeyPackage(), new RNFetchBlobPackage(), - new MattermostManagedPackage() + new MattermostManagedPackage(), + new RNSentryPackage(this), + new ReactNativeExceptionHandlerPackage() ); } diff --git a/android/settings.gradle b/android/settings.gradle index 98bd4a3e2..17e329d8d 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,4 +1,8 @@ rootProject.name = 'Mattermost' +include ':react-native-sentry' +project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android') +include ':react-native-exception-handler' +project(':react-native-exception-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-exception-handler/android') include ':react-native-fetch-blob' project(':react-native-fetch-blob').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fetch-blob/android') include ':jail-monkey' diff --git a/app/components/error_text.js b/app/components/error_text.js index 1da192d70..ae3ddae6d 100644 --- a/app/components/error_text.js +++ b/app/components/error_text.js @@ -14,12 +14,7 @@ import {makeStyleSheetFromTheme} from 'app/utils/theme'; class ErrorText extends PureComponent { static propTypes = { error: PropTypes.oneOfType([PropTypes.string, PropTypes.object]), - theme: PropTypes.object - }; - - static defaultProps = { - error: {}, - theme: {} + theme: PropTypes.object.isRequired }; render() { diff --git a/app/mattermost.js b/app/mattermost.js index e2b4c14e8..e7dfc262d 100644 --- a/app/mattermost.js +++ b/app/mattermost.js @@ -15,7 +15,7 @@ import { Platform } from 'react-native'; import DeviceInfo from 'react-native-device-info'; -import {setJSExceptionHandler} from 'react-native-exception-handler'; +import {setJSExceptionHandler, setNativeExceptionHandler} from 'react-native-exception-handler'; import StatusBarSizeIOS from 'react-native-status-bar-size'; import semver from 'semver'; @@ -46,15 +46,18 @@ import PushNotifications from 'app/push_notifications'; import {registerScreens} from 'app/screens'; import configureStore from 'app/store'; import mattermostManaged from 'app/mattermost_managed'; +import { + captureException, + initializeSentry, + LOGGER_JAVASCRIPT, + LOGGER_NATIVE +} from 'app/utils/sentry'; import Config from 'assets/config'; const {StatusBarManager} = NativeModules; -const store = configureStore(initialState); const AUTHENTICATION_TIMEOUT = 5 * 60 * 1000; -registerScreens(store, Provider); - export default class Mattermost { constructor() { if (Platform.OS === 'android') { @@ -65,9 +68,13 @@ export default class Mattermost { } this.isConfigured = false; this.allowOtherServers = true; - setJSExceptionHandler(this.errorHandler, false); + Orientation.lockToPortrait(); - this.unsubscribeFromStore = store.subscribe(this.listenForHydration); + + initializeSentry(); + + this.store = configureStore(initialState); + this.unsubscribeFromStore = this.store.subscribe(this.listenForHydration); AppState.addEventListener('change', this.handleAppStateChange); EventEmitter.on(General.CONFIG_CHANGED, this.handleConfigChanged); EventEmitter.on(NavigationTypes.NAVIGATION_RESET, this.handleReset); @@ -87,12 +94,20 @@ export default class Mattermost { } ); } + + registerScreens(this.store, Provider); + + setJSExceptionHandler(this.errorHandler, false); + setNativeExceptionHandler(this.nativeErrorHandler); } errorHandler = (e, isFatal) => { + console.warn('Handling Javascript error ' + JSON.stringify(e)); // eslint-disable-line no-console + captureException(e, LOGGER_JAVASCRIPT, this.store); + const intl = this.getIntl(); - closeWebSocket()(store.dispatch, store.getState); - logError(e)(store.dispatch); + closeWebSocket()(this.store.dispatch, this.store.getState); + logError(e)(this.store.dispatch); if (isFatal) { Alert.alert( @@ -102,7 +117,7 @@ export default class Mattermost { text: intl.formatMessage({id: 'mobile.error_handler.button', defaultMessage: 'Relaunch'}), onPress: () => { // purge the store - store.dispatch(purgeOfflineStore()); + this.store.dispatch(purgeOfflineStore()); } }], {cancelable: false} @@ -110,8 +125,13 @@ export default class Mattermost { } }; + nativeErrorHandler = (e) => { + console.warn('Handling native error ' + JSON.stringify(e)); // eslint-disable-line no-console + captureException(e, LOGGER_NATIVE, this.store); + }; + getIntl = () => { - const state = store.getState(); + const state = this.store.getState(); let locale = DeviceInfo.getDeviceLocale().split('-')[0]; if (state.views.i18n.locale) { locale = state.views.i18n.locale; @@ -157,7 +177,7 @@ export default class Mattermost { }; handleAppStateChange = async (appState) => { - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; const isActive = appState === 'active'; setAppState(isActive)(dispatch, getState); @@ -211,7 +231,7 @@ export default class Mattermost { }; handleConfigChanged = async (serverVersion) => { - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; const version = serverVersion.match(/^[0-9]*.[0-9]*.[0-9]*(-[a-zA-Z0-9.-]*)?/g)[0]; const intl = this.getIntl(); @@ -235,7 +255,7 @@ export default class Mattermost { }; handleManagedConfig = async (serverConfig) => { - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; const state = getState(); let authNeeded = false; @@ -328,15 +348,15 @@ export default class Mattermost { }; handleResetDisplayName = (displayName) => { - store.dispatch(setChannelDisplayName(displayName)); + this.store.dispatch(setChannelDisplayName(displayName)); }; handleStatusBarHeightChange = (nextStatusBarHeight) => { - store.dispatch(setStatusBarHeight(nextStatusBarHeight)); + this.store.dispatch(setStatusBarHeight(nextStatusBarHeight)); }; handleVersionUpgrade = async () => { - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; Client4.serverVersion = ''; PushNotifications.setApplicationIconBadgeNumber(0); @@ -352,7 +372,7 @@ export default class Mattermost { // We need to wait for hydration to occur before load the router. listenForHydration = () => { - const state = store.getState(); + const state = this.store.getState(); if (state.views.root.hydrationComplete) { this.unsubscribeFromStore(); @@ -386,7 +406,7 @@ export default class Mattermost { }; onRegisterDevice = (data) => { - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; let prefix; if (Platform.OS === 'ios') { prefix = General.PUSH_NOTIFY_APPLE_REACT_NATIVE; @@ -403,7 +423,7 @@ export default class Mattermost { onPushNotification = (deviceNotification) => { const {data, foreground, message, userInfo, userInteraction} = deviceNotification; - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; const state = getState(); const notification = { data, @@ -439,7 +459,7 @@ export default class Mattermost { }; onPushNotificationReply = (data, text, badge, completed) => { - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; const state = getState(); const {currentUserId} = state.entities.users; @@ -479,7 +499,7 @@ export default class Mattermost { }; resetBadgeAndVersion = () => { - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; Client4.serverVersion = ''; Client.serverVersion = ''; Client.token = null; @@ -492,7 +512,7 @@ export default class Mattermost { restartApp = async () => { Navigation.dismissModal({animationType: 'none'}); - const {dispatch, getState} = store; + const {dispatch, getState} = this.store; await loadConfigAndLicense()(dispatch, getState); this.startApp('fade'); }; diff --git a/app/store/index.js b/app/store/index.js index eedb8b84d..b99af2981 100644 --- a/app/store/index.js +++ b/app/store/index.js @@ -13,6 +13,7 @@ import EventEmitter from 'mattermost-redux/utils/event_emitter'; import {NavigationTypes, ViewTypes} from 'app/constants'; import appReducer from 'app/reducers'; +import {createSentryMiddleware} from 'app/utils/sentry/middleware'; import {transformSet} from './utils'; @@ -169,5 +170,7 @@ export default function configureAppStore(initialState) { } }; - return configureStore(initialState, appReducer, offlineOptions, getAppReducer); + return configureStore(initialState, appReducer, offlineOptions, getAppReducer, { + additionalMiddleware: createSentryMiddleware() + }); } diff --git a/app/utils/sentry/index.js b/app/utils/sentry/index.js new file mode 100644 index 000000000..36be49b09 --- /dev/null +++ b/app/utils/sentry/index.js @@ -0,0 +1,162 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Platform} from 'react-native'; +import {Sentry} from 'react-native-sentry'; + +import Config from 'assets/config'; + +import {getConfig} from 'mattermost-redux/selectors/entities/general'; +import {getCurrentUser} from 'mattermost-redux/selectors/entities/users'; +import {getCurrentTeam, getCurrentTeamMembership} from 'mattermost-redux/selectors/entities/teams'; +import {getCurrentChannel, getMyCurrentChannelMembership} from 'mattermost-redux/selectors/entities/channels'; + +export const LOGGER_JAVASCRIPT = 'javascript'; +export const LOGGER_NATIVE = 'native'; +export const LOGGER_REDUX = 'redux'; + +export function initializeSentry() { + if (!Config.SentryEnabled) { + // Still allow Sentry to configure itself in case other code tries to call it + Sentry.config(''); + + return; + } + + const dsn = getDsn(); + + if (!dsn) { + console.warn('Sentry is enabled, but not configured on this platform'); // eslint-disable-line no-console + } + + Sentry.config(dsn, Config.SentryOptions).install(); +} + +function getDsn() { + if (Platform.OS === 'android') { + return Config.SentryDsnAndroid; + } else if (Platform.OS === 'ios') { + return Config.SentryDsnIos; + } + + return ''; +} + +export function captureException(error, logger, store) { + capture(() => { + Sentry.captureException(error, {logger}); + }, store); +} + +export function captureMessage(message, logger, store) { + capture(() => { + Sentry.captureMessage(message, {logger}); + }, store); +} + +// Wrapper function to any calls to Sentry so that we can gather any necessary extra data +// before sending. +function capture(captureFunc, store) { + if (!Config.SentryEnabled) { + return; + } + + try { + const state = store.getState(); + const config = getConfig(state); + + // Don't contact Sentry if we're connected to a server with diagnostics disabled. Note that this will + // still log if we're not connected to any server. + if (config.EnableDiagnostics != null && config.EnableDiagnostics !== 'true') { + return; + } + + Sentry.setUserContext(getUserContext(state)); + Sentry.setExtraContext(getExtraContext(state)); + Sentry.setTagsContext(getBuildTags(state)); + + console.warn('Capturing with Sentry at ' + getDsn() + '...'); // eslint-disable-line no-console + + captureFunc(); + } catch (e) { + // Don't want this to get into an infinite loop again... + console.warn('Exception occured while sending to Sentry'); // eslint-disable-line no-console + console.warn(e); // eslint-disable-line no-console + } +} + +function getUserContext(state) { + const currentUser = getCurrentUser(state); + + if (!currentUser) { + return {}; + } + + return { + userID: currentUser.id, // This can be changed to id after we upgrade to Sentry >= 0.14.10, + email: '', + username: '', + extra: { + locale: currentUser.locale, + roles: currentUser.roles + } + }; +} + +function getExtraContext(state) { + const context = {}; + + const currentTeam = getCurrentTeam(state); + if (currentTeam) { + context.currentTeam = { + id: currentTeam.id + }; + } + + const currentTeamMember = getCurrentTeamMembership(state); + if (currentTeamMember) { + context.currentTeamMember = { + roles: currentTeamMember.roles + }; + } + + const currentChannel = getCurrentChannel(state); + if (currentChannel) { + context.currentChannel = { + id: currentChannel.id, + type: currentChannel.type + }; + } + + const currentChannelMember = getMyCurrentChannelMembership(state); + if (currentChannelMember) { + context.currentChannelMember = { + roles: currentChannelMember.roles + }; + } + + const config = getConfig(state); + if (config) { + context.config = { + BuildDate: config.BuildDate, + BuildEnterpriseReady: config.BuildEnterpriseReady, + BuildHash: config.BuildHash, + BuildHashEnterprise: config.BuildHashEnterprise, + BuildNumber: config.BuildNumber + }; + } + + return context; +} + +function getBuildTags(state) { + const tags = {}; + + const config = getConfig(state); + if (config) { + tags.serverBuildHash = config.BuildHash; + tags.serverBuildNumber = config.BuildNumber; + } + + return tags; +} diff --git a/app/utils/sentry/middleware.js b/app/utils/sentry/middleware.js new file mode 100644 index 000000000..a47f0beae --- /dev/null +++ b/app/utils/sentry/middleware.js @@ -0,0 +1,43 @@ +// Copyright (c) 2017-present Mattermost, Inc. All Rights Reserved. +// See License.txt for license information. + +import {Sentry} from 'react-native-sentry'; +import {BATCH} from 'redux-batched-actions'; + +export const BREADCRUMB_REDUX_ACTION = 'redux-action'; + +export function createSentryMiddleware() { + return (store) => { // eslint-disable-line no-unused-vars + return (next) => { + return (action) => { + Sentry.captureBreadcrumb(makeBreadcrumbFromAction(action)); + + return next(action); + }; + }; + }; +} + +function makeBreadcrumbFromAction(action) { + const breadcrumb = { + category: BREADCRUMB_REDUX_ACTION, + message: action.type + }; + + if (action.type === BATCH) { + // Attach additional information so that batched actions display what they're doing + breadcrumb.data = action.payload.map((a) => a.type); + } + + if (action.type === BATCH) { + // Attach additional information so that batched actions display what they're doing, and make it + // into an object because that's what is expected + breadcrumb.data = {}; + + action.payload.forEach((a, index) => { + breadcrumb.data[index] = a.type; + }); + } + + return breadcrumb; +} diff --git a/assets/base/config.json b/assets/base/config.json index 50c9d6657..40803194e 100644 --- a/assets/base/config.json +++ b/assets/base/config.json @@ -6,5 +6,16 @@ "MinServerVersion": "4.0.0", "PlatformNoticeURL": "https://about.mattermost.com/platform-notice-txt/", "MobileNoticeURL": "https://about.mattermost.com/mobile-notice-txt/", - "SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX" + "SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX", + + "SentryEnabled": false, + "SentryDsnIos": "", + "SentryDsnAndroid": "", + "SentryOptions": { + "deactivateStacktraceMerging": true, + "autoBreadcrumbs": { + "xhr": false, + "console": true + } + } } diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8dc07e169..638ea747c 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -46,13 +46,7 @@ platform :ios do lane :beta do match(type: 'appstore', app_identifier: 'com.mattermost.rnbeta') - if ENV['SEGMENT_API_KEY'] - find_replace_string( - path_to_file: './dist/assets/config.json', - old_string: '"SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX"', - new_string: "\"SegmentApiKey\": \"#{ENV['SEGMENT_API_KEY']}\"" - ) - end + configure_from_env() build_ios({ release: true, @@ -98,13 +92,7 @@ platform :ios do lane :release do match(type: 'appstore', app_identifier: 'com.mattermost.rn') - if ENV['SEGMENT_API_KEY'] - find_replace_string( - path_to_file: './dist/assets/config.json', - old_string: '"SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX"', - new_string: "\"SegmentApiKey\": \"#{ENV['SEGMENT_API_KEY']}\"" - ) - end + configure_from_env() # snapshot @@ -259,13 +247,7 @@ platform :android do desc 'Submit a new Beta Build to Google Play' lane :alpha do - if ENV['SEGMENT_API_KEY'] - find_replace_string( - path_to_file: './dist/assets/config.json', - old_string: '"SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX"', - new_string: "\"SegmentApiKey\": \"#{ENV['SEGMENT_API_KEY']}\"" - ) - end + configure_from_env() build_android({ release: true, @@ -324,7 +306,6 @@ platform :android do new_string: 'return false;' ) - find_replace_string( path_to_file: './android/app/src/main/java/com/mattermost/rn/MainApplication.java', old_string: 'package com.mattermost.rnbeta;', @@ -385,13 +366,7 @@ platform :android do new_string: 'Mattermost;' ) - if ENV['SEGMENT_API_KEY'] - find_replace_string( - path_to_file: './dist/assets/config.json', - old_string: '"SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX"', - new_string: "\"SegmentApiKey\": \"#{ENV['SEGMENT_API_KEY']}\"" - ) - end + configure_from_env() build_android({ release: true, @@ -449,6 +424,8 @@ platform :android do ) end + link_sentry_android() + gradle( task: 'assemble', build_type: (options[:release] ? 'Release' : 'Debug'), @@ -552,3 +529,78 @@ platform :android do ) end end + +def configure_from_env() + if ENV['SEGMENT_API_KEY'] + find_replace_string( + path_to_file: './dist/assets/config.json', + old_string: '"SegmentApiKey": "3MT7rAoC0OP7yy3ThzqFSAtKzmzqtUPX"', + new_string: "\"SegmentApiKey\": \"#{ENV['SEGMENT_API_KEY']}\"" + ) + end + + if ENV['SENTRY_ENABLED'] + find_replace_string( + path_to_file: './dist/assets/config.json', + old_string: '"SentryEnabled": false', + new_string: "\"SentryEnabled\": #{ENV['SENTRY_ENABLED']}" + ) + end + + if ENV['SENTRY_ORG'] + find_replace_string( + path_to_file: './dist/assets/config.json', + old_string: '"SentryOrg": ""', + new_string: "\"SentryOrg\": \"#{ENV['SENTRY_ORG']}\"" + ) + end + + if ENV['SENTRY_PROJECT_IOS'] + find_replace_string( + path_to_file: './dist/assets/config.json', + old_string: '"SentryProjectIos": ""', + new_string: "\"SentryProjectIos\": \"#{ENV['SENTRY_PROJECT_IOS']}\"" + ) + end + + if ENV['SENTRY_PROJECT_ANDROID'] + find_replace_string( + path_to_file: './dist/assets/config.json', + old_string: '"SentryProjectAndroid": ""', + new_string: "\"SentryProjectAndroid\": \"#{ENV['SENTRY_PROJECT_ANDROID']}\"" + ) + end + + if ENV['SENTRY_DSN_IOS'] + find_replace_string( + path_to_file: './dist/assets/config.json', + old_string: '"SentryDsnIos": ""', + new_string: "\"SentryDsnIos\": \"#{ENV['SENTRY_DSN_IOS']}\"" + ) + end + + if ENV['SENTRY_DSN_ANDROID'] + find_replace_string( + path_to_file: './dist/assets/config.json', + old_string: '"SentryDsnAndroid": ""', + new_string: "\"SentryDsnAndroid\": \"#{ENV['SENTRY_DSN_ANDROID']}\"" + ) + end +end + +def link_sentry_android() + if ENV['SENTRY_ENABLED'] == 'true' + File.open('../android/sentry.properties', 'w+') do |f| + UI.message('Creating sentry.properties from environment') + f.write( + 'defaults.url=https://sentry.io/\n'\ + "defaults.org=#{ENV['SENTRY_ORG']}\n"\ + "defaults.project=#{ENV['SENTRY_PROJECT_ANDROID']}\n"\ + "auth.token=#{ENV['SENTRY_AUTH_TOKEN']}\n"\ + "cli.executable=${File.expand_path('node_modules/sentry-cli-binary/bin/sentry-cli')}\n" + ) + end + else + UI.message('Not creating sentry.properties because Sentry is disabled') + end +end diff --git a/index.android.js b/index.android.js index d6bad70fa..d04abcb79 100644 --- a/index.android.js +++ b/index.android.js @@ -2,6 +2,7 @@ // See License.txt for license information. /* eslint-disable no-unused-vars */ + // import {AppRegistry} from 'react-native'; import Mattermost from 'app/mattermost'; diff --git a/ios/Mattermost.xcodeproj/project.pbxproj b/ios/Mattermost.xcodeproj/project.pbxproj index b3ba705ca..00aa86779 100644 --- a/ios/Mattermost.xcodeproj/project.pbxproj +++ b/ios/Mattermost.xcodeproj/project.pbxproj @@ -65,6 +65,9 @@ F006936FC2884C24A1321FC0 /* MaterialIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 356C9186FA374641A00EB2EA /* MaterialIcons.ttf */; }; F083DB472349411A8E6E7AAD /* OpenSans-LightItalic.ttf in Resources */ = {isa = PBXBuildFile; fileRef = BE17F630DB5D41FD93F32D22 /* OpenSans-LightItalic.ttf */; }; FDEF24E9D9AB47728E11033B /* libRNPasscodeStatus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 17A75F5A3D0D4A4A995DCD76 /* libRNPasscodeStatus.a */; }; + 1B5204C4347D45A9A7AC3EA5 /* libReactNativeExceptionHandler.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4A22BC320BA04E18A7F2A4E6 /* libReactNativeExceptionHandler.a */; }; + CB1B312483534997BEE7E65E /* libRNSentry.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AE23B96BF7545B9A77C0C87 /* libRNSentry.a */; }; + DEBCF44F1F46413BB407D316 /* libz.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 8606EB1EB7E349EF8248933E /* libz.tbd */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -482,6 +485,11 @@ EE671DF7637347CD8C069819 /* libRNDeviceInfo.a */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = archive.ar; path = libRNDeviceInfo.a; sourceTree = ""; }; F1F071EE85494E269A50AE88 /* SimpleLineIcons.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = SimpleLineIcons.ttf; path = "../node_modules/react-native-vector-icons/Fonts/SimpleLineIcons.ttf"; sourceTree = ""; }; FBBEC29EE2D3418D9AC33BD5 /* OpenSans-ExtraBoldItalic.ttf */ = {isa = PBXFileReference; explicitFileType = undefined; fileEncoding = 9; includeInIndex = 0; lastKnownFileType = unknown; name = "OpenSans-ExtraBoldItalic.ttf"; path = "../assets/fonts/OpenSans-ExtraBoldItalic.ttf"; sourceTree = ""; }; + 3B1E210BF3B649BBBF427977 /* ReactNativeExceptionHandler.xcodeproj */ = {isa = PBXFileReference; name = "ReactNativeExceptionHandler.xcodeproj"; path = "../node_modules/react-native-exception-handler/ios/ReactNativeExceptionHandler.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + 4A22BC320BA04E18A7F2A4E6 /* libReactNativeExceptionHandler.a */ = {isa = PBXFileReference; name = "libReactNativeExceptionHandler.a"; path = "libReactNativeExceptionHandler.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; + 9552041247E749308CE7B412 /* RNSentry.xcodeproj */ = {isa = PBXFileReference; name = "RNSentry.xcodeproj"; path = "../node_modules/react-native-sentry/ios/RNSentry.xcodeproj"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; }; + 2AE23B96BF7545B9A77C0C87 /* libRNSentry.a */ = {isa = PBXFileReference; name = "libRNSentry.a"; path = "libRNSentry.a"; sourceTree = ""; fileEncoding = undefined; lastKnownFileType = archive.ar; explicitFileType = undefined; includeInIndex = 0; }; + 8606EB1EB7E349EF8248933E /* libz.tbd */ = {isa = PBXFileReference; name = "libz.tbd"; path = "usr/lib/libz.tbd"; sourceTree = SDKROOT; fileEncoding = undefined; lastKnownFileType = sourcecode.text-based-dylib-definition; explicitFileType = undefined; includeInIndex = 0; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -525,6 +533,9 @@ 38FB8B0A3AAD4F839EF6EF0B /* libJailMonkey.a in Frameworks */, 1C1F002AC5DE465FAE9E0D74 /* libFastImage.a in Frameworks */, 2B2679D44CFA48E6A60D68BC /* libRNFetchBlob.a in Frameworks */, + 1B5204C4347D45A9A7AC3EA5 /* libReactNativeExceptionHandler.a in Frameworks */, + CB1B312483534997BEE7E65E /* libRNSentry.a in Frameworks */, + DEBCF44F1F46413BB407D316 /* libz.tbd in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -867,6 +878,8 @@ 27A6EA89298440439DA9F98D /* JailMonkey.xcodeproj */, 50ECB1D221E44F51B5690DF2 /* FastImage.xcodeproj */, 546CB0A5BB8F453890CBA559 /* RNFetchBlob.xcodeproj */, + 3B1E210BF3B649BBBF427977 /* ReactNativeExceptionHandler.xcodeproj */, + 9552041247E749308CE7B412 /* RNSentry.xcodeproj */, ); name = Libraries; sourceTree = ""; @@ -903,6 +916,15 @@ name = Products; sourceTree = ""; }; + 08742915873A4636AD1EC89C /* Frameworks */ = { + isa = PBXGroup; + children = ( + 8606EB1EB7E349EF8248933E /* libz.tbd */, + ); + name = Frameworks; + path = Application; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -933,6 +955,7 @@ 13B07F8E1A680F5B00A75B9A /* Resources */, 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 37DA4BA41E6F55AD002B058E /* Embed Frameworks */, + AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */, ); buildRules = ( ); @@ -1476,7 +1499,21 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "export NODE_BINARY=node\n../node_modules/react-native/packager/react-native-xcode.sh\n\n"; + shellScript = "./bundleReactNative.sh"; + }; + AE4769B235D14E6C9C64EA78 /* Upload Debug Symbols to Sentry */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + name = "Upload Debug Symbols to Sentry"; + inputPaths = ( + ); + outputPaths = ( + ); + shellPath = /bin/sh; + shellScript = "./uploadDebugSymbols.sh"; }; /* End PBXShellScriptBuildPhase section */ @@ -1573,6 +1610,8 @@ "$(SRCROOT)/../node_modules/react-native/Libraries/**", "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**", "$(SRCROOT)/../node_modules/react-native-fetch-blob/ios/**", + "$(SRCROOT)/../node_modules/react-native-exception-handler/ios", + "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", ); INFOPLIST_FILE = Mattermost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; @@ -1613,6 +1652,8 @@ "$(SRCROOT)/../node_modules/react-native/Libraries/**", "$(SRCROOT)/../node_modules/react-native-fast-image/ios/FastImage/**", "$(SRCROOT)/../node_modules/react-native-fetch-blob/ios/**", + "$(SRCROOT)/../node_modules/react-native-exception-handler/ios", + "$(SRCROOT)/../node_modules/react-native-sentry/ios/**", ); INFOPLIST_FILE = Mattermost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; diff --git a/ios/Mattermost/AppDelegate.m b/ios/Mattermost/AppDelegate.m index b73499115..27b1813fc 100644 --- a/ios/Mattermost/AppDelegate.m +++ b/ios/Mattermost/AppDelegate.m @@ -10,6 +10,11 @@ #import "AppDelegate.h" #import #import +#if __has_include() +#import // This is used for versions of react >= 0.40 +#else +#import "RNSentry.h" // This is used for versions of react < 0.40 +#endif #import "Orientation.h" #import "RCCManager.h" #import "RNNotifications.h" diff --git a/ios/bundleReactNative.sh b/ios/bundleReactNative.sh new file mode 100755 index 000000000..247bc4130 --- /dev/null +++ b/ios/bundleReactNative.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +export NODE_BINARY=node + +if [[ "${SENTRY_ENABLED}" = "true" ]]; then + echo "Sentry native integration is enabled" + + ./makeSentryProperties.sh + + export SENTRY_PROPERTIES=sentry.properties + ../node_modules/sentry-cli-binary/bin/sentry-cli react-native xcode ../node_modules/react-native/packager/react-native-xcode.sh +else + echo "Sentry native integration is not enabled" + ../node_modules/react-native/packager/react-native-xcode.sh +fi diff --git a/ios/makeSentryProperties.sh b/ios/makeSentryProperties.sh new file mode 100755 index 000000000..33db7fdcb --- /dev/null +++ b/ios/makeSentryProperties.sh @@ -0,0 +1,19 @@ +#!/bin/sh + +sentry_properties="defaults.url=https://sentry.io +defaults.org=${SENTRY_ORG} +defaults.project=${SENTRY_PROJECT_IOS} +auth.token=${SENTRY_AUTH_TOKEN} +cli.executable=../node_modules/sentry-cli-binary/bin/sentry-cli" + +if [[ "${SENTRY_ENABLED}" = "true" ]]; then + if [[ ! -f "sentry.properties" ]]; then + echo "Creating sentry.properties from environment" + + echo "${sentry_properties}" > sentry.properties + else + echo "sentry.properties already exists" + fi +else + echo "Not creating sentry.properties because Sentry is disabled" +fi diff --git a/ios/uploadDebugSymbols.sh b/ios/uploadDebugSymbols.sh new file mode 100755 index 000000000..3379f9943 --- /dev/null +++ b/ios/uploadDebugSymbols.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +export SENTRY_PROPERTIES=sentry.properties + +if [[ "${SENTRY_ENABLED}" = "true" ]]; then + echo "Uploading debugging symbols to Sentry" + + ./makeSentryProperties.sh + + export SENTRY_PROPERTIES=sentry.properties + ../node_modules/sentry-cli-binary/bin/sentry-cli upload-dsym +else + echo "Not uploading debugging symbols to Sentry because Sentry is disabled" +fi diff --git a/package.json b/package.json index 255ef0060..a311f8001 100644 --- a/package.json +++ b/package.json @@ -26,9 +26,9 @@ "react-native-cookies": "3.1.0", "react-native-device-info": "0.11.0", "react-native-drawer": "2.3.0", - "react-native-exception-handler": "1.1.0", - "react-native-fetch-blob": "0.10.8", + "react-native-exception-handler": "2.0.1", "react-native-fast-image": "1.0.0", + "react-native-fetch-blob": "0.10.8", "react-native-image-picker": "jp928/react-native-image-picker#6ee35b69f3dbd6c7c66f580fd4d9eabf398703d4", "react-native-keyboard-aware-scroll-view": "0.2.8", "react-native-linear-gradient": "2.0.0", @@ -37,6 +37,7 @@ "react-native-notifications": "enahum/react-native-notifications.git", "react-native-orientation": "enahum/react-native-orientation.git", "react-native-passcode-status": "1.1.0", + "react-native-sentry": "0.14.5", "react-native-status-bar-size": "0.3.2", "react-native-svg": "5.1.8", "react-native-swiper": "1.5.4", diff --git a/test/setup.js b/test/setup.js index 3cbde6b2a..a1c5ae646 100644 --- a/test/setup.js +++ b/test/setup.js @@ -12,6 +12,7 @@ import register from 'babel-core/register'; import chai from 'chai'; import chaiEnzyme from 'chai-enzyme'; import ReactNative from 'react-native-mock'; +import {Sentry} from 'react-native-sentry'; const m = require('module'); const originalLoader = m._load; @@ -59,3 +60,6 @@ config.ignore = function(filename) { register(config); chai.use(chaiEnzyme()); + +// Initialize Sentry so that it doesn't complain when the middleware tries to create breadcrumbs +Sentry.config(''); diff --git a/yarn.lock b/yarn.lock index 17e239892..9e6ca3fb8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1439,6 +1439,12 @@ cli-cursor@^1.0.1: dependencies: restore-cursor "^1.0.1" +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + dependencies: + restore-cursor "^2.0.0" + cli-width@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.1.0.tgz#b234ca209b29ef66fc518d9b98d5847b00edf00a" @@ -2309,6 +2315,14 @@ external-editor@^1.0.1: spawn-sync "^1.0.15" tmp "^0.0.29" +external-editor@^2.0.1: + version "2.0.4" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.0.4.tgz#1ed9199da9cbfe2ef2f7a31b2fde8b0d12368972" + dependencies: + iconv-lite "^0.4.17" + jschardet "^1.4.2" + tmp "^0.0.31" + extglob@^0.3.1: version "0.3.2" resolved "https://registry.yarnpkg.com/extglob/-/extglob-0.3.2.tgz#2e18ff3d2f49ab2765cec9023f011daa8d8349a1" @@ -2382,6 +2396,12 @@ figures@^1.3.5: escape-string-regexp "^1.0.5" object-assign "^4.1.0" +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-2.0.0.tgz#c392990c3e684783d838b8c84a45d8a048458361" @@ -2860,6 +2880,10 @@ iconv-lite@0.4.15: version "0.4.15" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb" +iconv-lite@^0.4.17: + version "0.4.18" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.18.tgz#23d8656b16aae6742ac29732ea8f0336a4789cf2" + ignore@^3.2.0: version "3.2.7" resolved "https://registry.yarnpkg.com/ignore/-/ignore-3.2.7.tgz#4810ca5f1d8eca5595213a34b94f2eb4ed926bbd" @@ -2914,6 +2938,24 @@ inquirer@1.1.3: strip-ansi "^3.0.0" through "^2.3.6" +inquirer@3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.0.6.tgz#e04aaa9d05b7a3cb9b0f407d04375f0447190347" + dependencies: + ansi-escapes "^1.1.0" + chalk "^1.0.0" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^2.0.1" + figures "^2.0.0" + lodash "^4.3.0" + mute-stream "0.0.7" + run-async "^2.2.0" + rx "^4.1.0" + string-width "^2.0.0" + strip-ansi "^3.0.0" + through "^2.3.6" + inquirer@^0.12.0: version "0.12.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-0.12.0.tgz#1ef2bfd63504df0bc75785fff8c2c41df12f077e" @@ -3271,6 +3313,10 @@ jsbn@~0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-0.1.1.tgz#a5e654c2e5a2deb5f201d96cefbca80c0ef2f513" +jschardet@^1.4.2: + version "1.5.1" + resolved "https://registry.yarnpkg.com/jschardet/-/jschardet-1.5.1.tgz#c519f629f86b3a5bedba58a88d311309eec097f9" + jsdom-global@3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/jsdom-global/-/jsdom-global-3.0.2.tgz#6bd299c13b0c4626b2da2c0393cd4385d606acb9" @@ -3769,6 +3815,10 @@ mime@1.3.4, mime@^1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" +mimic-fn@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + min-document@^2.19.0: version "2.19.0" resolved "https://registry.yarnpkg.com/min-document/-/min-document-2.19.0.tgz#7bd282e3f5842ed295bb748cdd9f1ffa2c824685" @@ -3870,6 +3920,10 @@ mute-stream@0.0.6: version "0.0.6" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.6.tgz#48962b19e169fd1dfc240b3f1e7317627bbc47db" +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + nan@^2.3.0: version "2.6.2" resolved "https://registry.yarnpkg.com/nan/-/nan-2.6.2.tgz#e4ff34e6c95fdfb5aecc08de6596f43605a7db45" @@ -4080,6 +4134,12 @@ onetime@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + dependencies: + mimic-fn "^1.0.0" + opn@^3.0.2: version "3.0.3" resolved "https://registry.yarnpkg.com/opn/-/opn-3.0.3.tgz#b6d99e7399f78d65c3baaffef1fb288e9b85243a" @@ -4289,6 +4349,10 @@ process@~0.5.1: version "0.5.2" resolved "https://registry.yarnpkg.com/process/-/process-0.5.2.tgz#1638d8a8e34c2f440a91db95ab9aeb677fc185cf" +progress@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.0.tgz#8a1be366bf8fc23db2bd23f10c6fe920b4389d1f" + progress@^1.1.8: version "1.1.8" resolved "https://registry.yarnpkg.com/progress/-/progress-1.1.8.tgz#e260c78f6161cdd9b0e56cc3e0a85de17c7a57be" @@ -4370,6 +4434,10 @@ range-parser@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" +raven-js@^3.16.1: + version "3.17.0" + resolved "https://registry.yarnpkg.com/raven-js/-/raven-js-3.17.0.tgz#779457ac7910512c3c2cc9bb6d0a9eeb59a969ec" + raw-body@~2.1.2: version "2.1.7" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774" @@ -4508,9 +4576,9 @@ react-native-drawer@2.3.0: dependencies: tween-functions "^1.0.1" -react-native-exception-handler@1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/react-native-exception-handler/-/react-native-exception-handler-1.1.0.tgz#320b7fc9c104e11d66173a2b0daeac97831a1a37" +react-native-exception-handler@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/react-native-exception-handler/-/react-native-exception-handler-2.0.1.tgz#4ab9fc668927c2672adfffce3784316d81f130e8" react-native-fetch-blob@0.10.8: version "0.10.8" @@ -4598,6 +4666,17 @@ react-native-passcode-status@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/react-native-passcode-status/-/react-native-passcode-status-1.1.0.tgz#e5922d5f4d79bc09849438d620406e5ccd31168a" +react-native-sentry@0.14.5: + version "0.14.5" + resolved "https://registry.yarnpkg.com/react-native-sentry/-/react-native-sentry-0.14.5.tgz#477aeca99a4c36326a64ca575e2b3718a3fb05a6" + dependencies: + chalk "^1.1.1" + glob "7.1.1" + inquirer "3.0.6" + raven-js "^3.16.1" + sentry-cli-binary "^1.16.0" + xcode "0.9.3" + react-native-status-bar-size@0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/react-native-status-bar-size/-/react-native-status-bar-size-0.3.2.tgz#e3ffd906f021220c256857a8de6b945f8ea3fabc" @@ -5098,6 +5177,13 @@ restore-cursor@^1.0.1: exit-hook "^1.0.0" onetime "^1.0.0" +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -5285,6 +5371,12 @@ send@0.15.1: range-parser "~1.2.0" statuses "~1.3.1" +sentry-cli-binary@^1.16.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/sentry-cli-binary/-/sentry-cli-binary-1.19.1.tgz#0abc210ed7f35d7ec0680963e612b15bd2bc14c0" + dependencies: + progress "2.0.0" + serialize-error@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/serialize-error/-/serialize-error-2.1.0.tgz#50b679d5635cdf84667bdc8e59af4e5b81d5f60a" @@ -5368,7 +5460,7 @@ signal-exit@^2.0.0: version "2.1.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-2.1.2.tgz#375879b1f92ebc3b334480d038dc546a6d558564" -signal-exit@^3.0.0, signal-exit@^3.0.1: +signal-exit@^3.0.0, signal-exit@^3.0.1, signal-exit@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" @@ -5716,6 +5808,12 @@ tmp@^0.0.29: dependencies: os-tmpdir "~1.0.1" +tmp@^0.0.31: + version "0.0.31" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.31.tgz#8f38ab9438e17315e5dbd8b3657e8bfb277ae4a7" + dependencies: + os-tmpdir "~1.0.1" + tmpl@1.0.x: version "1.0.4" resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.4.tgz#23640dd7b42d00433911140820e5cf440e521dd1" @@ -6050,7 +6148,7 @@ ws@^2.0.3: safe-buffer "~5.0.1" ultron "~1.1.0" -xcode@^0.9.1: +xcode@0.9.3, xcode@^0.9.1: version "0.9.3" resolved "https://registry.yarnpkg.com/xcode/-/xcode-0.9.3.tgz#910a89c16aee6cc0b42ca805a6d0b4cf87211cf3" dependencies: