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
This commit is contained in:
Harrison Healey 2017-09-01 13:50:17 -04:00 committed by enahum
parent 220fb588b9
commit 3ac7b48adc
21 changed files with 597 additions and 74 deletions

1
.gitignore vendored
View file

@ -65,6 +65,7 @@ mattermost.keystore
# Sentry
android/sentry.properties
ios/sentry.properties
# Testing
.nyc_output

View file

@ -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.

View file

@ -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"

View file

@ -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()
);
}

View file

@ -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'

View file

@ -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() {

View file

@ -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');
};

View file

@ -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()
});
}

162
app/utils/sentry/index.js Normal file
View file

@ -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;
}

View file

@ -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;
}

View file

@ -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
}
}
}

View file

@ -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

View file

@ -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';

View file

@ -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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
3B1E210BF3B649BBBF427977 /* ReactNativeExceptionHandler.xcodeproj */ = {isa = PBXFileReference; name = "ReactNativeExceptionHandler.xcodeproj"; path = "../node_modules/react-native-exception-handler/ios/ReactNativeExceptionHandler.xcodeproj"; sourceTree = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
4A22BC320BA04E18A7F2A4E6 /* libReactNativeExceptionHandler.a */ = {isa = PBXFileReference; name = "libReactNativeExceptionHandler.a"; path = "libReactNativeExceptionHandler.a"; sourceTree = "<group>"; 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 = "<group>"; fileEncoding = undefined; lastKnownFileType = wrapper.pb-project; explicitFileType = undefined; includeInIndex = 0; };
2AE23B96BF7545B9A77C0C87 /* libRNSentry.a */ = {isa = PBXFileReference; name = "libRNSentry.a"; path = "libRNSentry.a"; sourceTree = "<group>"; 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 = "<group>";
@ -903,6 +916,15 @@
name = Products;
sourceTree = "<group>";
};
08742915873A4636AD1EC89C /* Frameworks */ = {
isa = PBXGroup;
children = (
8606EB1EB7E349EF8248933E /* libz.tbd */,
);
name = Frameworks;
path = Application;
sourceTree = "<group>";
};
/* 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";

View file

@ -10,6 +10,11 @@
#import "AppDelegate.h"
#import <React/RCTBundleURLProvider.h>
#import <React/RCTRootView.h>
#if __has_include(<React/RNSentry.h>)
#import <React/RNSentry.h> // 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"

15
ios/bundleReactNative.sh Executable file
View file

@ -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

19
ios/makeSentryProperties.sh Executable file
View file

@ -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

14
ios/uploadDebugSymbols.sh Executable file
View file

@ -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

View file

@ -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",

View file

@ -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('');

108
yarn.lock
View file

@ -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: