Verified Commit 094d6aff authored by Sébastiaan Versteeg's avatar Sébastiaan Versteeg
Browse files

Add Sentry React Native SDK

parent fdbae6d8
{ {
"presets": ["react-native"], "presets": ["react-native", "react-native-dotenv"],
"plugins": ["@babel/plugin-proposal-object-rest-spread", "@babel/transform-regenerator"] "plugins": ["@babel/plugin-proposal-object-rest-spread", "@babel/transform-regenerator"]
} }
\ No newline at end of file
...@@ -60,3 +60,5 @@ buck-out/ ...@@ -60,3 +60,5 @@ buck-out/
*.jsbundle *.jsbundle
bundle.js bundle.js
sentry.properties
.env
\ No newline at end of file
...@@ -3,6 +3,7 @@ import * as matchers from 'redux-saga-test-plan/matchers'; ...@@ -3,6 +3,7 @@ import * as matchers from 'redux-saga-test-plan/matchers';
import { throwError } from 'redux-saga-test-plan/providers'; import { throwError } from 'redux-saga-test-plan/providers';
import Snackbar from 'react-native-snackbar'; import Snackbar from 'react-native-snackbar';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import { Sentry } from 'react-native-sentry';
import loginSaga, { DISPLAYNAMEKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY } from '../../app/sagas/login'; import loginSaga, { DISPLAYNAMEKEY, PHOTOKEY, TOKENKEY, USERNAMEKEY } from '../../app/sagas/login';
import { apiRequest } from '../../app/utils/url'; import { apiRequest } from '../../app/utils/url';
...@@ -27,6 +28,13 @@ jest.mock('../../app/utils/url', () => ({ ...@@ -27,6 +28,13 @@ jest.mock('../../app/utils/url', () => ({
tokenSelector: () => 'token', tokenSelector: () => 'token',
})); }));
jest.mock('react-native-sentry', () => ({
Sentry: {
setUserContext: () => {},
captureException: () => {},
},
}));
describe('login saga', () => { describe('login saga', () => {
const error = new Error('error'); const error = new Error('error');
...@@ -42,6 +50,7 @@ describe('login saga', () => { ...@@ -42,6 +50,7 @@ describe('login saga', () => {
it('should put the result data when the request succeeds', () => expectSaga(loginSaga) it('should put the result data when the request succeeds', () => expectSaga(loginSaga)
.provide([ .provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }], [matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
]) ])
.put(loginActions.success('username', 'abc123')) .put(loginActions.success('username', 'abc123'))
.put(loginActions.profile('abc123')) .put(loginActions.profile('abc123'))
...@@ -51,6 +60,7 @@ describe('login saga', () => { ...@@ -51,6 +60,7 @@ describe('login saga', () => {
it('should show a snackbar when the request succeeds', () => expectSaga(loginSaga) it('should show a snackbar when the request succeeds', () => expectSaga(loginSaga)
.provide([ .provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }], [matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
]) ])
.dispatch(loginActions.login('username', 'password')) .dispatch(loginActions.login('username', 'password'))
.silentRun() .silentRun()
...@@ -64,6 +74,7 @@ describe('login saga', () => { ...@@ -64,6 +74,7 @@ describe('login saga', () => {
expectSaga(loginSaga) expectSaga(loginSaga)
.provide([ .provide([
[matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }], [matchers.call.like({ fn: apiRequest, args: ['token-auth'] }), { token: 'abc123' }],
[matchers.call.like({ fn: Sentry.setUserContext }), {}],
]) ])
.dispatch(loginActions.login('username', 'password')) .dispatch(loginActions.login('username', 'password'))
.silentRun() .silentRun()
......
...@@ -78,6 +78,7 @@ project.ext.react = [ ...@@ -78,6 +78,7 @@ project.ext.react = [
] ]
apply from: "../../node_modules/react-native/react.gradle" apply from: "../../node_modules/react-native/react.gradle"
apply from: "../../node_modules/react-native-sentry/sentry.gradle"
/** /**
* Set this to true to create two separate APKs instead of one: * Set this to true to create two separate APKs instead of one:
...@@ -145,6 +146,7 @@ android { ...@@ -145,6 +146,7 @@ android {
} }
dependencies { dependencies {
implementation project(':react-native-sentry')
implementation project(':react-native-locale-detector') implementation project(':react-native-locale-detector')
implementation project(':react-native-vector-icons') implementation project(':react-native-vector-icons')
implementation project(':react-native-snackbar') implementation project(':react-native-snackbar')
......
...@@ -3,6 +3,7 @@ package com.thaliapp; ...@@ -3,6 +3,7 @@ package com.thaliapp;
import android.app.Application; import android.app.Application;
import com.facebook.react.ReactApplication; import com.facebook.react.ReactApplication;
import io.sentry.RNSentryPackage;
import com.azendoo.reactnativesnackbar.SnackbarPackage; import com.azendoo.reactnativesnackbar.SnackbarPackage;
import com.i18n.reactnativei18n.ReactNativeI18n; import com.i18n.reactnativei18n.ReactNativeI18n;
import com.evollu.react.fcm.FIRMessagingPackage; import com.evollu.react.fcm.FIRMessagingPackage;
...@@ -28,6 +29,7 @@ public class MainApplication extends Application implements ReactApplication { ...@@ -28,6 +29,7 @@ public class MainApplication extends Application implements ReactApplication {
protected List<ReactPackage> getPackages() { protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList( return Arrays.<ReactPackage>asList(
new MainReactPackage(), new MainReactPackage(),
new RNSentryPackage(),
new SnackbarPackage(), new SnackbarPackage(),
new ReactNativeI18n(), new ReactNativeI18n(),
new FIRMessagingPackage(), new FIRMessagingPackage(),
......
rootProject.name = 'ThaliApp' rootProject.name = 'ThaliApp'
include ':react-native-sentry'
project(':react-native-sentry').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sentry/android')
include ':react-native-snackbar' include ':react-native-snackbar'
project(':react-native-snackbar').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-snackbar/android') project(':react-native-snackbar').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-snackbar/android')
include ':react-native-locale-detector' include ':react-native-locale-detector'
......
import { import { call, put, select, takeEvery } from 'redux-saga/effects';
call, put, select, takeEvery, import { Sentry } from 'react-native-sentry';
} from 'redux-saga/effects';
import { apiRequest, tokenSelector } from '../utils/url'; import { apiRequest, tokenSelector } from '../utils/url';
import * as navigationActions from '../actions/navigation'; import * as navigationActions from '../actions/navigation';
...@@ -30,10 +29,11 @@ const calendar = function* calendar() { ...@@ -30,10 +29,11 @@ const calendar = function* calendar() {
partner: true, partner: true,
})); }));
} catch (error) { } catch (error) {
// Swallow the error Sentry.captureException(error);
} }
yield put(calendarActions.success(events.concat(partnerEvents))); yield put(calendarActions.success(events.concat(partnerEvents)));
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(calendarActions.failure()); yield put(calendarActions.failure());
} }
}; };
......
import { import { call, put, takeEvery, select } from 'redux-saga/effects';
call, put, takeEvery, select, import { Sentry } from 'react-native-sentry';
} from 'redux-saga/effects';
import { apiRequest, tokenSelector } from '../utils/url'; import { apiRequest, tokenSelector } from '../utils/url';
import * as eventActions from '../actions/event'; import * as eventActions from '../actions/event';
...@@ -36,6 +35,7 @@ const event = function* event(action) { ...@@ -36,6 +35,7 @@ const event = function* event(action) {
eventRegistrations, eventRegistrations,
)); ));
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(eventActions.failure()); yield put(eventActions.failure());
} }
}; };
......
import { call, takeEvery, put } from 'redux-saga/effects'; import { call, takeEvery, put } from 'redux-saga/effects';
import { AsyncStorage } from 'react-native'; import { AsyncStorage } from 'react-native';
import Snackbar from 'react-native-snackbar'; import Snackbar from 'react-native-snackbar';
import { Sentry } from 'react-native-sentry';
import { apiRequest } from '../utils/url'; import { apiRequest } from '../utils/url';
import * as loginActions from '../actions/login'; import * as loginActions from '../actions/login';
...@@ -78,15 +79,22 @@ const profile = function* profile(action) { ...@@ -78,15 +79,22 @@ const profile = function* profile(action) {
]); ]);
yield put(loginActions.profileSuccess(userProfile.display_name, userProfile.avatar.medium)); yield put(loginActions.profileSuccess(userProfile.display_name, userProfile.avatar.medium));
} catch (error) { } catch (error) {
Sentry.captureException(error);
// Swallow error // Swallow error
} }
}; };
function* success({ payload }) {
const { username } = payload;
yield call(Sentry.setUserContext, { username });
}
const loginSaga = function* loginSaga() { const loginSaga = function* loginSaga() {
yield takeEvery(loginActions.LOGIN, login); yield takeEvery(loginActions.LOGIN, login);
yield takeEvery(loginActions.LOGOUT, logout); yield takeEvery(loginActions.LOGOUT, logout);
yield takeEvery(loginActions.PROFILE, profile); yield takeEvery(loginActions.PROFILE, profile);
yield takeEvery(loginActions.TOKEN_INVALID, tokenInvalid); yield takeEvery(loginActions.TOKEN_INVALID, tokenInvalid);
yield takeEvery(loginActions.SUCCESS, success);
}; };
export default loginSaga; export default loginSaga;
import { Dimensions } from 'react-native'; import { Dimensions } from 'react-native';
import { import { call, put, select, takeEvery } from 'redux-saga/effects';
call, put, select, takeEvery, import { Sentry } from 'react-native-sentry';
} from 'redux-saga/effects';
import { TOTAL_BAR_HEIGHT } from '../ui/components/standardHeader/style/StandardHeader'; import { TOTAL_BAR_HEIGHT } from '../ui/components/standardHeader/style/StandardHeader';
import { memberSize } from '../ui/screens/memberList/style/MemberList'; import { memberSize } from '../ui/screens/memberList/style/MemberList';
...@@ -38,6 +37,7 @@ const members = function* members(action) { ...@@ -38,6 +37,7 @@ const members = function* members(action) {
const response = yield call(apiRequest, 'members', data, params); const response = yield call(apiRequest, 'members', data, params);
yield put(memberActions.success(response.results, response.next, keywords)); yield put(memberActions.success(response.results, response.next, keywords));
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(memberActions.failure()); yield put(memberActions.failure());
} }
}; };
...@@ -61,6 +61,7 @@ const more = function* more(action) { ...@@ -61,6 +61,7 @@ const more = function* more(action) {
const responseJson = yield fetch(url, data).then(response => response.json()); const responseJson = yield fetch(url, data).then(response => response.json());
yield put(memberActions.moreSuccess(responseJson.results, responseJson.next)); yield put(memberActions.moreSuccess(responseJson.results, responseJson.next));
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(memberActions.moreSuccess([], null)); yield put(memberActions.moreSuccess([], null));
} }
}; };
......
import { import { call, put, takeEvery, select } from 'redux-saga/effects';
call, put, takeEvery, select, import { Sentry } from 'react-native-sentry';
} from 'redux-saga/effects';
import { apiRequest, tokenSelector } from '../utils/url'; import { apiRequest, tokenSelector } from '../utils/url';
import * as pizzaActions from '../actions/pizza'; import * as pizzaActions from '../actions/pizza';
...@@ -33,6 +32,7 @@ const retrievePizzaInfo = function* retrievePizzaInfo() { ...@@ -33,6 +32,7 @@ const retrievePizzaInfo = function* retrievePizzaInfo() {
if (error.response !== null && error.response.status === NOT_FOUND) { if (error.response !== null && error.response.status === NOT_FOUND) {
yield put(pizzaActions.success(event, null, pizzaList)); yield put(pizzaActions.success(event, null, pizzaList));
} else { } else {
Sentry.captureException(error);
yield put(pizzaActions.failure()); yield put(pizzaActions.failure());
} }
} }
...@@ -40,6 +40,7 @@ const retrievePizzaInfo = function* retrievePizzaInfo() { ...@@ -40,6 +40,7 @@ const retrievePizzaInfo = function* retrievePizzaInfo() {
if (error.response !== null && error.response.status === NOT_FOUND) { if (error.response !== null && error.response.status === NOT_FOUND) {
yield put(pizzaActions.success(null, null, [])); yield put(pizzaActions.success(null, null, []));
} else { } else {
Sentry.captureException(error);
yield put(pizzaActions.failure()); yield put(pizzaActions.failure());
} }
} }
...@@ -60,6 +61,7 @@ const cancel = function* cancel() { ...@@ -60,6 +61,7 @@ const cancel = function* cancel() {
yield call(apiRequest, 'pizzas/orders/me', data); yield call(apiRequest, 'pizzas/orders/me', data);
yield put(pizzaActions.cancelSuccess()); yield put(pizzaActions.cancelSuccess());
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(pizzaActions.failure()); yield put(pizzaActions.failure());
} }
}; };
...@@ -84,6 +86,7 @@ const order = function* order(action) { ...@@ -84,6 +86,7 @@ const order = function* order(action) {
const orderData = yield call(apiRequest, route, data); const orderData = yield call(apiRequest, route, data);
yield put(pizzaActions.orderSuccess(orderData)); yield put(pizzaActions.orderSuccess(orderData));
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(pizzaActions.failure()); yield put(pizzaActions.failure());
} }
}; };
......
import { call, put, takeEvery } from 'redux-saga/effects'; import { call, put, takeEvery } from 'redux-saga/effects';
import { Sentry } from 'react-native-sentry';
import { apiRequest } from '../utils/url'; import { apiRequest } from '../utils/url';
import * as profileActions from '../actions/profile'; import * as profileActions from '../actions/profile';
...@@ -23,6 +24,7 @@ const profile = function* profile(action) { ...@@ -23,6 +24,7 @@ const profile = function* profile(action) {
const profileData = yield call(apiRequest, `members/${member}`, data); const profileData = yield call(apiRequest, `members/${member}`, data);
yield put(profileActions.success(profileData)); yield put(profileActions.success(profileData));
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(profileActions.failure()); yield put(profileActions.failure());
} }
}; };
......
import { call, takeEvery, select } from 'redux-saga/effects'; import { call, takeEvery, select } from 'redux-saga/effects';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
import FCM from 'react-native-fcm'; import FCM from 'react-native-fcm';
import { Sentry } from 'react-native-sentry';
import { apiRequest, tokenSelector } from '../utils/url'; import { apiRequest, tokenSelector } from '../utils/url';
import * as pushNotificationsActions from '../actions/pushNotifications'; import * as pushNotificationsActions from '../actions/pushNotifications';
...@@ -43,6 +44,7 @@ const register = function* register() { ...@@ -43,6 +44,7 @@ const register = function* register() {
try { try {
yield call(apiRequest, 'devices', data); yield call(apiRequest, 'devices', data);
} catch (err) { } catch (err) {
Sentry.captureException(err);
// eat error, om nom nom // eat error, om nom nom
} }
}; };
......
...@@ -3,6 +3,7 @@ import { ...@@ -3,6 +3,7 @@ import {
call, put, select, takeEvery, call, put, select, takeEvery,
} from 'redux-saga/effects'; } from 'redux-saga/effects';
import Snackbar from 'react-native-snackbar'; import Snackbar from 'react-native-snackbar';
import { Sentry } from 'react-native-sentry';
import { apiRequest, tokenSelector } from '../utils/url'; import { apiRequest, tokenSelector } from '../utils/url';
...@@ -38,6 +39,7 @@ const register = function* register(action) { ...@@ -38,6 +39,7 @@ const register = function* register(action) {
} }
Snackbar.show({ title: 'Registration successful!' }); Snackbar.show({ title: 'Registration successful!' });
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(eventActions.failure()); yield put(eventActions.failure());
} }
}; };
...@@ -71,6 +73,7 @@ const update = function* update(action) { ...@@ -71,6 +73,7 @@ const update = function* update(action) {
yield delay(50); yield delay(50);
Snackbar.show({ title: 'Successfully updated registration' }); Snackbar.show({ title: 'Successfully updated registration' });
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(registrationActions.failure()); yield put(registrationActions.failure());
} }
}; };
...@@ -95,6 +98,7 @@ const cancel = function* cancel(action) { ...@@ -95,6 +98,7 @@ const cancel = function* cancel(action) {
yield call(apiRequest, `registrations/${registration}`, data); yield call(apiRequest, `registrations/${registration}`, data);
Snackbar.show({ title: 'Successfully cancelled registration' }); Snackbar.show({ title: 'Successfully cancelled registration' });
} catch (error) { } catch (error) {
Sentry.captureException(error);
// Swallow error for now // Swallow error for now
} }
...@@ -122,6 +126,7 @@ const fields = function* fields(action) { ...@@ -122,6 +126,7 @@ const fields = function* fields(action) {
yield put(registrationActions.showFields(registration, response.fields)); yield put(registrationActions.showFields(registration, response.fields));
yield put(eventActions.done()); yield put(eventActions.done());
} catch (error) { } catch (error) {
Sentry.captureException(error);
yield put(eventActions.failure()); yield put(eventActions.failure());
} }
}; };
......
import { import { call, put, select, takeEvery } from 'redux-saga/effects';
call, put, select, takeEvery, import { Sentry } from 'react-native-sentry';
} from 'redux-saga/effects';
import { apiRequest, tokenSelector } from '../utils/url'; import { apiRequest, tokenSelector } from '../utils/url';
import * as welcomeActions from '../actions/welcome'; import * as welcomeActions from '../actions/welcome';
...@@ -29,6 +28,7 @@ const welcome = function* welcome() { ...@@ -29,6 +28,7 @@ const welcome = function* welcome() {
if (error.name === 'TokenInvalidError') { if (error.name === 'TokenInvalidError') {
yield put(loginActions.tokenInvalid()); yield put(loginActions.tokenInvalid());
} }
Sentry.captureException(error);
yield put(welcomeActions.failure()); yield put(welcomeActions.failure());
} }
}; };
......
import { AppRegistry } from 'react-native'; import { AppRegistry } from 'react-native';
import { Sentry } from 'react-native-sentry';
import { SENTRY_DSN } from 'react-native-dotenv';
import App from './app/app'; import App from './app/app';
Sentry.config(SENTRY_DSN).install();
AppRegistry.registerComponent('ThaliApp', () => App); AppRegistry.registerComponent('ThaliApp', () => App);
// Copyright (c) 2015 Doe Pics Hit, Inc. All rights reserved.
#import <Foundation/Foundation.h>
#import <UIKit/UIApplication.h>
typedef NSString*(^BBReturnNSStringCallback)(void);
typedef BOOL (^BBReturnBooleanCallback)(void);
typedef void (^BBCallback)(void);
@interface BuddyBuildSDK : NSObject
// Deprecated
+ (void)setup:(id<UIApplicationDelegate>)bbAppDelegate;
/**
* Initialize the SDK
*
* This should be called at (or near) the start of the appdelegate
*/
+ (void)setup;
/*
* Associate arbitrary key/value pairs with your crash reports and user feedback
* which will be visible from the buddybuild dashboard
*/
+ (void)setMetadataObject:(id)object forKey:(NSString*)key;
/*
* Programatically trigger the screenshot feedback UI without pressing the screenshot buttons
* If you have screenshot feedback disabled through the buddybuild setting,
* you can still trigger it by calling this method
*/
+ (void)takeScreenshotAndShowFeedbackScreen;
/*
* If you distribute a build to someone with their email address, buddybuild can
* figure out who they are and attach their info to feedback and crash reports.
*
* However, if you send out a build to a mailing list, or through TestFlight or
* the App Store we are unable to infer who they are. If you see 'Unknown User'
* this is likely the cause.
* Often you'll know the identity of your user, for example, after they've
* logged in. You can provide buddybuild a callback to identify the current user.
*/
+ (void)setUserDisplayNameCallback:(BBReturnNSStringCallback)bbCallback;
/*
* You might have API keys and other secrets that your app needs to consume.
* However, you may not want to check these secrets into the source code.
*
* You can provide your secrets to buddybuild. Buddybuild can then expose them
* to you at build time through environment variables. These secrets can also be
* configured to be included into built app. We obfuscate the device keys to
* prevent unauthorized access.
*/
+ (NSString*)valueForDeviceKey:(NSString*)bbKey;
/*
* To temporarily disable screenshot interception you can provide a callback
* here.
*
* When screenshotting is turned on through a buddybuild setting, and no
* callback is provided then screenshotting is by default on.
*
* If screenshotting is disabled through the buddybuild setting, then this
* callback has no effect
*
*/
+ (void)setScreenshotAllowedCallback:(BBReturnBooleanCallback)bbCallback;
/*
* Once a piece of feedback is sent this callback will be called
* so you can take additional actions if necessary
*/
+ (void)setScreenshotFeedbackSentCallback:(BBCallback)bbCallback;
/*
* Once a crash report is sent this callback will be called
* so you can take additional actions if necessary
*/
+ (void)setCrashReportSentCallback:(BBCallback)bbCallback;
/*
* Buddybuild Build Number
*/
+ (NSString*)buildNumber;
/*
* Scheme
*/
+ (NSString*)scheme;
/*
* App ID
*/
+ (NSString*)appID;
/*
* Build ID
*/