Replace Fabric Crashlytics with Sentry (#1376)

* Remove Fabric and Crashlytics

The Logger now logs to `console.log` exclusively.

* Add Sentry reporting

Errors are reported using `captureException`, and non-errors are added
as breadcrumbs. This means the non-error events won't be directly sent
to Sentry, but they will be included as context for any errors that do
occur.

The sentry configuration is mainly in the `sentry.debug.properties`
file. The only config outside of that file is the DSN used for
reporting errors, which is hard-coded in the `setupSentry.js` module.

There are three separate sets of config: default, debug, and release.
The default config is missing the auth token, so it can't be used to
publish source maps. However, builds that use the default config will
still correctly report errors to the `test-metamask-mobile` Sentry
project.

The debug config is identical to the default config except that it
includes the auth token. The debug config is for publishing any
non-production builds (e.g. testing, pre-releases).

The release configuration is to be used for production builds only.
This is the only config that sends errors to the `metamask-mobile`
Sentry project.

The debug and release config files have not been added to the repo.
They are automatically instantiated from the example files if the
`MM_SENTRY_AUTH_TOKEN` environment variable is set. Setting this
environment variable in CI should allow publishing from CI.

Closes #1321

* Use absolute path for Sentry file

The SENTRY_PROPERTIES environment variable is used at different
directory levels in the Android and iOS builds respectively; it's
used one level down with iOS, but two levels down for Android. This
means the properties path used is incorrect on iOS at the moment.

Instead an absolute path is now used, to prevent any such errors in the
future.

The error message for the missing auth token has been improved as well.

* Improve handling of missing auth token

The properties file is now checked to ensure the auth token is present,
so the build can fail early rather than partway through.

The logic for handling the auth token has been moved to a separate
function as well, so it can be shared between the release and
prerelease builds.

* Force setting METAMASK_ENVIRONMENT explicitly for release builds

The Sentry environment is set to `local` by default (e.g. for dev
builds), but the build script now requires it to be explicitly set for
release builds. This is to ensure it isn't missed in the production
builds, which are done manually.

* Ensure only Errors are thrown/rejected

Whenever using the `throw` keyword or calling `reject` in a Promise,
the value passed should be an Error. Various tools expect thrown
"errors" to be errors; this is the convention. Using Error types is
also more useful for debugging, as they have stack traces.

* Update `Logger.error` function signature

`Logger.error` now expects an actual error object as the first
argument. This allows us to submit proper Errors with stack traces to
Sentry. The second parameter is an optional set of extra data, which
gets attached to the Error and is viewable on Sentry in the "Additional
Data" section of the error report.

Unfortunately any messages added to contextualize errors had to be
submitted as extra data instead of as a wrapper Error. Wrapper error
types provide for a richer debugging experience in general, because
you get additional context for the error plus an additional stack trace
to help trace the flow of the error. Sentry even has an Integration
meant to facilitate this pattern, the `LinkedErrors` integration.
But unfortunately that integration doesn't seem to work with React
Native, so that option was off the table.

* Update `@sentry` packages to latest

The most recent Sentry packages have better support for capturing
native crashes on Android. I had held off on this update at first
because I thought it required React Native >= 0.60, but apparently
not. It still works on 0.59, just not as well (no sourcemaps)
pull/1450/head
Mark Stacey 3 years ago committed by GitHub
parent 201cd7e836
commit 2375f8017a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      .android.env.example
  2. 4
      .circleci/config.yml
  3. 6
      .gitignore
  4. 1
      .ios.env.example
  5. 6
      RELEASE.MD
  6. 54
      android/app/build.gradle
  7. 3
      android/app/fabric.properties
  8. 58
      android/app/src/main/java/io/metamask/MainApplication.java
  9. 6
      android/app/src/main/java/io/metamask/nativeModules/RCTAnalytics.java
  10. 14
      android/build.gradle
  11. 4
      android/settings.gradle
  12. 4
      app/components/UI/AccountList/index.js
  13. 2
      app/components/UI/TransactionElement/TransactionDetails/index.js
  14. 7
      app/components/UI/UrlAutocomplete/index.js
  15. 2
      app/components/Views/Browser/index.js
  16. 10
      app/components/Views/BrowserTab/index.js
  17. 2
      app/components/Views/Entry/index.js
  18. 2
      app/components/Views/ImportPrivateKeySuccess/index.js
  19. 10
      app/components/Views/ImportWallet/index.js
  20. 2
      app/components/Views/LockScreen/index.js
  21. 3
      app/components/Views/PaymentChannel/index.js
  22. 4
      app/components/Views/Settings/AdvancedSettings/index.js
  23. 2
      app/components/Views/Settings/SecuritySettings/index.js
  24. 10
      app/components/Views/SyncWithExtension/index.js
  25. 2
      app/components/Views/WalletConnectSessions/index.js
  26. 18
      app/core/PaymentChannelsClient.js
  27. 2
      app/core/WalletConnect.js
  28. 31
      app/util/Logger.js
  29. 2
      app/util/blockies.js
  30. 3
      app/util/middlewares.js
  31. 18
      app/util/setupSentry.js
  32. 17
      app/util/syncWithExtension.js
  33. 1
      app/util/testSetup.js
  34. 4
      index.js
  35. BIN
      ios/Crashlytics.framework/Crashlytics
  36. 31
      ios/Crashlytics.framework/Headers/ANSCompatibility.h
  37. 210
      ios/Crashlytics.framework/Headers/Answers.h
  38. 33
      ios/Crashlytics.framework/Headers/CLSAttributes.h
  39. 64
      ios/Crashlytics.framework/Headers/CLSLogging.h
  40. 103
      ios/Crashlytics.framework/Headers/CLSReport.h
  41. 38
      ios/Crashlytics.framework/Headers/CLSStackFrame.h
  42. 288
      ios/Crashlytics.framework/Headers/Crashlytics.h
  43. BIN
      ios/Crashlytics.framework/Info.plist
  44. 14
      ios/Crashlytics.framework/Modules/module.modulemap
  45. 73
      ios/Crashlytics.framework/run
  46. BIN
      ios/Crashlytics.framework/submit
  47. BIN
      ios/Crashlytics.framework/upload-symbols
  48. BIN
      ios/Fabric.framework/Fabric
  49. 51
      ios/Fabric.framework/Headers/FABAttributes.h
  50. 82
      ios/Fabric.framework/Headers/Fabric.h
  51. BIN
      ios/Fabric.framework/Info.plist
  52. 6
      ios/Fabric.framework/Modules/module.modulemap
  53. 73
      ios/Fabric.framework/run
  54. BIN
      ios/Fabric.framework/upload-symbols
  55. 107
      ios/MetaMask.xcodeproj/project.pbxproj
  56. 14
      ios/MetaMask/Info.plist
  57. 3
      ios/MetaMask/NativeModules/RCTAnalytics/RCTAnalytics.m
  58. 5
      package.json
  59. 15
      patches/react-native-fabric+0.5.2.patch
  60. 38
      scripts/build.sh
  61. 5
      sentry.debug.properties.example
  62. 5
      sentry.properties
  63. 5
      sentry.release.properties.example
  64. 267
      yarn.lock

@ -1,5 +1,4 @@
export MM_FOX_CODE=
export MM_FABRIC_API_KEY=
export MM_BRANCH_KEY_TEST=
export MM_BRANCH_KEY_LIVE=
export MM_MIXPANEL_TOKEN=

@ -121,7 +121,7 @@ jobs:
name: build:pre-release
command:
|
yarn build:android:pre-release:bundle
METAMASK_ENVIRONMENT='prerelease' yarn build:android:pre-release:bundle
- store_artifacts:
path: android/app/build/outputs/bundle/release
destination: bundle
@ -147,7 +147,7 @@ jobs:
at: .
- run:
name: pre-release
command: yarn build:ios:pre-release
command: METAMASK_ENVIRONMENT='prerelease' yarn build:ios:pre-release
- store_artifacts:
path: sourcemaps/ios
destination: sourcemaps-ios

6
.gitignore vendored

@ -29,8 +29,6 @@ app/bin
.gradle
local.properties
*.iml
android/app/src/main/assets/crashlytics-build.properties
android/app/src/main/res/values/com_crashlytics_export_strings.xml
android/.project
android/app/.project
android/app/bin/
@ -65,5 +63,9 @@ coverage
.ios.env
.android.env
# Sentry
/sentry.debug.properties
/sentry.release.properties
# editor
.vscode

@ -1,4 +1,3 @@
MM_FOX_CODE =
MM_FABRIC_API_KEY =
MM_BRANCH_KEY_TEST =
MM_BRANCH_KEY_LIVE =

@ -4,7 +4,7 @@
2 - Bump the version number in `info.plist` and commit the change
3 - Run `npm run release:ios`
3 - Run `METAMASK_ENVIRONMENT='production' npm run release:ios`
4 - Wait for the appstore email that the build has completed processing (10 min - 1 hour)
@ -26,7 +26,7 @@
2 - Save and commit
3 - Run `npm run release:android`
3 - Run `METAMASK_ENVIRONMENT='production' npm run release:android`
4 - Go to the playstore: https://play.google.com/apps/publish/?account=9089866037123936197
@ -46,7 +46,7 @@
### Once you're done with both stores:
- Submit a PR with the changes
- Submit a PR with the changes
- Once it's merged create a tag on master for that version
- Go to the release pages and create a new release for that tag, including the changelog

@ -24,31 +24,8 @@ def getPassword(String currentUser, String keyChain) {
stdout.toString().trim()
}
buildscript {
repositories {
maven { url 'https://maven.fabric.io/public' }
}
dependencies {
// These docs use an open ended version so that our plugin
// can be updated quickly in response to Android tooling updates
// We recommend changing it to the latest version from our changelog:
// https://docs.fabric.io/android/changelog.html#fabric-gradle-plugin
classpath 'io.fabric.tools:gradle:1.+'
}
}
apply plugin: "com.android.application"
apply plugin: 'io.fabric'
repositories {
maven { url 'https://maven.fabric.io/public' }
jcenter()
}
import com.android.build.OutputFile
/**
@ -127,6 +104,13 @@ project.ext.react = [
apply from: "../../node_modules/react-native/react.gradle"
project.ext.sentryCli = [
logLevel: "debug",
sentryProperties: System.getenv('SENTRY_PROPERTIES') ? System.getenv('SENTRY_PROPERTIES') : '../../sentry.properties'
]
apply from: "../../node_modules/@sentry/react-native/sentry.gradle"
/**
* Set this to true to create two separate APKs instead of one:
* - An APK that only works on ARM devices
@ -142,27 +126,8 @@ def enableSeparateBuildPerCPUArchitecture = false
*/
def enableProguardInReleaseBuilds = false
/**
*
* override fabric properties file if MM_FABRIC_API_KEY is set
*/
def buildFabricPropertiesIfNeeded() {
def FABRIC_API_KEY = System.getenv('MM_FABRIC_API_KEY')
if (FABRIC_API_KEY) {
def commentMessage = "AUTOGEN FABRIC PROPERTIES"
ant.propertyfile(file: "fabric.properties", comment: commentMessage) {
entry(key: "apiKey", value: FABRIC_API_KEY)
}
}
}
android {
afterEvaluate {
buildFabricPropertiesIfNeeded()
}
compileSdkVersion rootProject.ext.compileSdkVersion
compileOptions {
@ -250,10 +215,10 @@ android {
}
dependencies {
implementation project(':@sentry_react-native')
implementation project(':react-native-sensors')
implementation project(':react-native-reanimated')
implementation project(':react-native-webview')
implementation project(':react-native-fabric')
implementation project(':@react-native-community_netinfo')
implementation project(':react-native-view-shot')
implementation project(':lottie-react-native')
@ -285,9 +250,6 @@ dependencies {
implementation project(':react-native-vector-icons')
implementation 'com.mixpanel.android:mixpanel-android:5.+'
implementation('com.crashlytics.sdk.android:crashlytics:2.9.4@aar') {
transitive = true;
}
androidTestImplementation('com.wix:detox:+') { transitive = true }
androidTestImplementation 'junit:junit:4.12'
}

@ -1,3 +0,0 @@
#Contains API Secret used to validate your application. Commit to internal source control; avoid making secret public.
#Thu Aug 23 18:04:07 EDT 2018
apiKey=0

@ -1,10 +1,10 @@
package io.metamask;
import com.facebook.react.ReactApplication;
import io.sentry.RNSentryPackage;
import com.sensors.RNSensorsPackage;
import com.swmansion.reanimated.ReanimatedPackage;
import com.reactnativecommunity.webview.RNCWebViewPackage;
import com.smixx.fabric.FabricPackage;
import com.reactnativecommunity.netinfo.NetInfoPackage;
import fr.greweb.reactnativeviewshot.RNViewShotPackage;
import com.airbnb.android.react.lottie.LottiePackage;
@ -48,34 +48,34 @@ public class MainApplication extends MultiDexApplication implements ShareApplica
@Override
protected List<ReactPackage> getPackages() {
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNSensorsPackage(),
new ReanimatedPackage(),
new RNCWebViewPackage(),
new FabricPackage(),
new NetInfoPackage(),
new RNViewShotPackage(),
new LottiePackage(),
new AsyncStoragePackage(),
new ReactNativePushNotificationPackage(),
new BackgroundTimerPackage(),
new RNDeviceInfo(),
new SvgPackage(),
new RNGestureHandlerPackage(),
new RNScreensPackage(),
new RNBranchPackage(),
new KeychainPackage(),
new RandomBytesPackage(),
new RCTAesPackage(),
new RNCameraPackage(),
new RNFSPackage(),
new RNI18nPackage(),
new RNOSModule(),
new RNSharePackage(),
new VectorIconsPackage(),
new RCTAnalyticsPackage()
);
return Arrays.<ReactPackage>asList(
new MainReactPackage(),
new RNSentryPackage(),
new RNSensorsPackage(),
new ReanimatedPackage(),
new RNCWebViewPackage(),
new NetInfoPackage(),
new RNViewShotPackage(),
new LottiePackage(),
new AsyncStoragePackage(),
new ReactNativePushNotificationPackage(),
new BackgroundTimerPackage(),
new RNDeviceInfo(),
new SvgPackage(),
new RNGestureHandlerPackage(),
new RNScreensPackage(),
new RNBranchPackage(),
new KeychainPackage(),
new RandomBytesPackage(),
new RCTAesPackage(),
new RNCameraPackage(),
new RNFSPackage(),
new RNI18nPackage(),
new RNOSModule(),
new RNSharePackage(),
new VectorIconsPackage(),
new RCTAnalyticsPackage()
);
}
@Override

@ -4,8 +4,6 @@ import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import com.crashlytics.android.Crashlytics;
import com.crashlytics.android.core.CrashlyticsCore;
import com.facebook.react.bridge.ReactApplicationContext;
import com.facebook.react.bridge.ReactContextBaseJavaModule;
import com.facebook.react.bridge.ReactMethod;
@ -23,8 +21,6 @@ import org.json.JSONObject;
import java.util.ArrayList;
import java.util.HashMap;
import io.fabric.sdk.android.Fabric;
public class RCTAnalytics extends ReactContextBaseJavaModule {
MixpanelAPI mixpanel;
@ -63,8 +59,6 @@ public class RCTAnalytics extends ReactContextBaseJavaModule {
if(val){
this.mixpanel.optInTracking();
Fabric.with(this.getReactApplicationContext(), new Crashlytics());
}else{
this.mixpanel.optOutTracking();
}

@ -57,17 +57,3 @@ allprojects {
}
}
}
subprojects {project ->
if (project.name.contains('react-native-fabric')) {
buildscript {
repositories {
google()
jcenter()
maven {
url = 'https://dl.bintray.com/android/android-tools/'
}
}
}
}
}

@ -1,12 +1,12 @@
rootProject.name = 'MetaMask'
include ':@sentry_react-native'
project(':@sentry_react-native').projectDir = new File(rootProject.projectDir, '../node_modules/@sentry/react-native/android')
include ':react-native-sensors'
project(':react-native-sensors').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-sensors/android')
include ':react-native-reanimated'
project(':react-native-reanimated').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-reanimated/android')
include ':react-native-webview'
project(':react-native-webview').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-webview/android')
include ':react-native-fabric'
project(':react-native-fabric').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-fabric/android')
include ':@react-native-community_netinfo'
project(':@react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android')
include ':react-native-view-shot'

@ -182,7 +182,7 @@ class AccountList extends PureComponent {
} catch (e) {
// Restore to the previous index in case anything goes wrong
this.mounted && this.setState({ selectedAccountIndex: previousIndex });
Logger.error('error while trying change the selected account', e); // eslint-disable-line
Logger.error(e, 'error while trying change the selected account'); // eslint-disable-line
}
InteractionManager.runAfterInteractions(() => {
setTimeout(() => {
@ -217,7 +217,7 @@ class AccountList extends PureComponent {
this.mounted && this.setState({ orderedAccounts });
} catch (e) {
// Restore to the previous index in case anything goes wrong
Logger.error('error while trying to add a new account', e); // eslint-disable-line
Logger.error(e, 'error while trying to add a new account'); // eslint-disable-line
this.mounted && this.setState({ loading: false });
}
});

@ -232,7 +232,7 @@ class TransactionDetails extends PureComponent {
}
} catch (e) {
// eslint-disable-next-line no-console
Logger.error(`can't get a block explorer link for network `, networkID, e);
Logger.error(e, { message: `can't get a block explorer link for network `, networkID });
}
};

@ -7,7 +7,6 @@ import { connect } from 'react-redux';
import WebsiteIcon from '../WebsiteIcon';
import { colors, fontStyles } from '../../../styles/common';
import { getHost } from '../../../util/browser';
import Logger from '../../../util/Logger';
const styles = StyleSheet.create({
wrapper: {
@ -130,11 +129,7 @@ class UrlAutocomplete extends PureComponent {
}
updateResults(results) {
try {
this.mounted && this.setState({ results });
} catch (e) {
Logger.error('Autocomplete crash', results);
}
this.mounted && this.setState({ results });
}
onSubmitInput = () => this.props.onSubmit(this.props.input);

@ -270,7 +270,7 @@ class Browser extends PureComponent {
resolve(true);
},
error => {
Logger.error(`Error saving tab ${url}`, error);
Logger.error(error, `Error saving tab ${url}`);
reject(error);
}
);

@ -792,7 +792,7 @@ export class BrowserTab extends PureComponent {
handleDeeplinks = async ({ error, params }) => {
if (!this.isTabActive()) return false;
if (error) {
Logger.error('Error from Branch: ', error);
Logger.error(error, 'Error from Branch');
return;
}
if (params['+non_branch_link']) {
@ -1043,7 +1043,7 @@ export class BrowserTab extends PureComponent {
this.go(fullUrl);
return { url: null };
}
Logger.error('Failed to resolve ENS name', err);
Logger.error(err, 'Failed to resolve ENS name');
Alert.alert(strings('browser.error'), strings('browser.failed_to_resolve_ens_name'));
this.goBack();
}
@ -1178,7 +1178,7 @@ export class BrowserTab extends PureComponent {
try {
SearchApi.indexSpotlightItem(item);
} catch (e) {
Logger.error('Error adding to spotlight', e);
Logger.error(e, 'Error adding to spotlight');
}
}
const analyticsEnabled = Analytics.getEnabled();
@ -1308,7 +1308,7 @@ export class BrowserTab extends PureComponent {
break;
}
} catch (e) {
Logger.error(`Browser::onMessage on ${this.state.inputValue}`, e.toString());
Logger.error(e, `Browser::onMessage on ${this.state.inputValue}`);
}
};
@ -1690,7 +1690,7 @@ export class BrowserTab extends PureComponent {
this.setState({ showApprovalDialog: false, showApprovalDialogHostname: undefined });
this.approvalRequest &&
this.approvalRequest.reject &&
this.approvalRequest.reject('User rejected account access');
this.approvalRequest.reject(new Error('User rejected account access'));
};
renderApprovalModal = () => {

@ -103,7 +103,7 @@ class Entry extends PureComponent {
handleDeeplinks = async ({ error, params }) => {
if (error) {
Logger.error('Error from Branch: ', error);
Logger.error(error, 'Error from Branch');
return;
}
if (params['+non_branch_link']) {

@ -87,7 +87,7 @@ class ImportPrivateKeySuccess extends PureComponent {
const accountsOrdered = allKeyrings.reduce((list, keyring) => list.concat(keyring.accounts), []);
PreferencesController.setSelectedAddress(accountsOrdered[accountsOrdered.length - 1]);
} catch (e) {
Logger.error('Error while refreshing imported pkey', e);
Logger.error(e, 'Error while refreshing imported pkey');
}
InteractionManager.runAfterInteractions(() => {
BackHandler.addEventListener('hardwareBackPress', this.handleBackPress);

@ -172,7 +172,7 @@ class ImportWallet extends PureComponent {
} catch (e) {
if (!firstAttempt) {
this.props.navigation.goBack();
if (e.toString() === 'sync-timeout') {
if (e.message === 'Sync::timeout') {
Alert.alert(
strings('sync_with_extension.outdated_qr_code'),
strings('sync_with_extension.outdated_qr_code_desc')
@ -184,9 +184,7 @@ class ImportWallet extends PureComponent {
);
}
}
Logger.log('Sync::startSync', firstAttempt);
Logger.log('Sync::startSync', e.toString());
Logger.error('Sync::startSync', e);
Logger.error(e, { message: 'Sync::startSync', firstAttempt });
return false;
}
};
@ -315,7 +313,7 @@ class ImportWallet extends PureComponent {
await AsyncStorage.setItem('@MetaMask:biometryChoice', opts.biometryType);
}
} catch (e) {
Logger.error('User cancelled biometrics permission', e);
Logger.error(e, 'User cancelled biometrics permission');
await AsyncStorage.removeItem('@MetaMask:biometryChoice');
await AsyncStorage.setItem('@MetaMask:biometryChoiceDisabled', 'true');
await AsyncStorage.setItem('@MetaMask:passcodeDisabled', 'true');
@ -338,7 +336,7 @@ class ImportWallet extends PureComponent {
this.dataToSync = null;
this.props.navigation.push('SyncWithExtensionSuccess');
} catch (e) {
Logger.error('Sync::disconnect', e);
Logger.error(e, 'Sync::disconnect');
Alert.alert(strings('sync_with_extension.error_title'), strings('sync_with_extension.error_message'));
this.setState({ loading: false });
this.props.navigation.goBack();

@ -151,7 +151,7 @@ class LockScreen extends PureComponent {
if (this.unlockAttempts <= 3) {
this.attemptUnlock();
} else {
Logger.error('Lockscreen:maxAttemptsReached', { attemptNumber: this.unlockAttempts, error });
Logger.error(error, { message: 'Lockscreen:maxAttemptsReached', attemptNumber: this.unlockAttempts });
this.props.navigation.navigate('Login');
}
}

@ -323,8 +323,7 @@ class PaymentChannel extends PureComponent {
!this.state.connextStateDisabled &&
Alert.alert(strings('payment_channel.error_title'), strings('payment_channel.error_desc'));
this.setState({ connextStateDisabled: true });
Logger.log('InstaPay:ChainSawError', channelState);
Logger.error('InstaPay:ChainSawError');
Logger.error(new Error('InstaPay:ChainSawError'), { channelState });
}
};

@ -225,7 +225,7 @@ class AdvancedSettings extends PureComponent {
url
});
} catch (err) {
Logger.error('State log error', err);
Logger.error(err, 'State log error');
}
};
@ -253,7 +253,7 @@ class AdvancedSettings extends PureComponent {
url
});
} catch (err) {
Logger.error('Instapay log error', err);
Logger.error(err, 'Instapay log error');
}
};

@ -368,7 +368,7 @@ class Settings extends PureComponent {
if (e.message === 'Invalid password') {
Alert.alert(strings('app_settings.invalid_password'), strings('app_settings.invalid_password_message'));
}
Logger.error('SecuritySettings:biometrics', e);
Logger.error(e, 'SecuritySettings:biometrics');
// Return the switch to the previous value
if (type === 'biometrics') {
this.setState({ biometryChoice: !enabled });

@ -151,7 +151,7 @@ class SyncWithExtension extends PureComponent {
} catch (e) {
if (!firstAttempt) {
this.props.navigation.goBack();
if (e.toString() === 'sync-timeout') {
if (e.message === 'Sync::timeout') {
Alert.alert(
strings('sync_with_extension.outdated_qr_code'),
strings('sync_with_extension.outdated_qr_code_desc')
@ -163,9 +163,7 @@ class SyncWithExtension extends PureComponent {
);
}
}
Logger.log('Sync::startSync', firstAttempt);
Logger.log('Sync::startSync', e.toString());
Logger.error('Sync::startSync', e);
Logger.error(e, 'Sync::startSync', { firstAttempt });
return false;
}
};
@ -265,7 +263,7 @@ class SyncWithExtension extends PureComponent {
}
await AsyncStorage.setItem('@MetaMask:biometryChoice', this.state.biometryType);
} catch (e) {
Logger.error('User cancelled biometrics permission', e);
Logger.error(e, 'User cancelled biometrics permission');
await AsyncStorage.removeItem('@MetaMask:biometryChoice');
}
}
@ -283,7 +281,7 @@ class SyncWithExtension extends PureComponent {
this.dataToSync = null;
this.props.navigation.push('SyncWithExtensionSuccess');
} catch (e) {
Logger.error('Sync::disconnect', e);
Logger.error(e, 'Sync::disconnect');
Alert.alert(strings('sync_with_extension.error_title'), strings('sync_with_extension.error_message'));
this.setState({ loading: false });
this.props.navigation.goBack();

@ -115,7 +115,7 @@ export default class WalletConnectSessions extends PureComponent {
);
this.loadSessions();
} catch (e) {
Logger.error('WC: Failed to kill session', e);
Logger.error(e, 'WC: Failed to kill session');
}
};

@ -156,7 +156,7 @@ class PaymentChannelsClient {
this.setState({ blocked: false });
}, 60 * BLOCKED_DEPOSIT_DURATION_MINUTES * 1000);
}
Logger.error('ExternalWallet::sign', e);
Logger.error(e, 'ExternalWallet::sign');
throw e;
}
}
@ -179,7 +179,7 @@ class PaymentChannelsClient {
});
} catch (e) {
this.logCurrentState('PC::createClient');
Logger.error('PC::createClient', e);
Logger.error(e, 'PC::createClient');
throw e;
}
}
@ -203,7 +203,7 @@ class PaymentChannelsClient {
Logger.log('PC::pollConnextState connext.start succesful');
} catch (e) {
this.logCurrentState('PC::start');
Logger.error('PC::start', e);
Logger.error(e, 'PC::start');
}
// register connext listeners
connext.on('onStateChange', async state => {
@ -229,7 +229,7 @@ class PaymentChannelsClient {
}
} catch (e) {
this.logCurrentState('PC::onStateChange');
Logger.error('PC::onStateChange', e);
Logger.error(e, 'PC::onStateChange');
}
});
}
@ -265,7 +265,7 @@ class PaymentChannelsClient {
await this.autoSwap();
} catch (e) {
this.logCurrentState('PC::autoswap');
Logger.error('PC::autoswap', e);
Logger.error(e, 'PC::autoswap');
this.setState({ swapPending: false });
}
this.autoswapHandler = setTimeout(() => {
@ -380,7 +380,7 @@ class PaymentChannelsClient {
this.setState({ depositPending: true });
} catch (e) {
this.logCurrentState('PC::deposit');
Logger.error('PC::deposit', e);
Logger.error(e, 'PC::deposit');
throw e;
}
};
@ -419,7 +419,7 @@ class PaymentChannelsClient {
await connext.buy(data);
} catch (e) {
this.logCurrentState('PC::buy');
Logger.error('PC::buy', e);
Logger.error(e, 'PC::buy');
}
};
@ -443,7 +443,7 @@ class PaymentChannelsClient {
this.setState({ withdrawalPending: true, withdrawalPendingValue: toWei(renderFromWei(balanceTokenUser)) });
} catch (e) {
this.logCurrentState('PC::withdraw');
Logger.error('PC::withdraw', e);
Logger.error(e, 'PC::withdraw');
}
};
@ -483,7 +483,7 @@ const instance = {
await client.pollAndSwap();
} catch (e) {
client.logCurrentState('PC::init');
Logger.error('PC::init', e);
Logger.error(e, 'PC::init');
}
}
},

@ -283,7 +283,7 @@ class WalletConnect {
});
hub.on('walletconnectSessionRequest::rejected', peerId => {
if (peerInfo.peerId === peerId) {
reject(false);
reject(new Error('walletconnectSessionRequest::rejected'));
}
});
});

@ -1,8 +1,7 @@
'use strict';
// eslint-disable-next-line import/default
import Fabric from 'react-native-fabric';
import { addBreadcrumb, captureException, withScope } from '@sentry/react-native';
import AsyncStorage from '@react-native-community/async-storage';
import Device from '../util/Device';
/**
* Wrapper class that allows us to override
@ -18,35 +17,41 @@ export default class Logger {
* @returns - void
*/
static async log(...args) {
// TODO use crashlytics opt-in
// Check if user passed accepted opt-in to metrics
const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn');
if (__DEV__) {
args.unshift('[MetaMask DEBUG]:');
console.log.apply(null, args); // eslint-disable-line no-console
} else if (metricsOptIn === 'agreed') {
Fabric.Crashlytics.log(JSON.stringify(args));
addBreadcrumb({
message: JSON.stringify(args)
});
}
}
/**
* console.error wrapper
*
* @param {object} args - data to be logged
* @param {Error} error - error to be logged
* @param {string|object} extra - Extra error info
* @returns - void
*/
static async error(...args) {
// TODO use crashlytics opt-in
static async error(error, extra) {
// Check if user passed accepted opt-in to metrics
const metricsOptIn = await AsyncStorage.getItem('@MetaMask:metricsOptIn');
if (__DEV__) {
args.unshift('[MetaMask DEBUG]:');
console.warn(args); // eslint-disable-line no-console
console.warn('[MetaMask DEBUG]:', error); // eslint-disable-line no-console
} else if (metricsOptIn === 'agreed') {
if (Device.isAndroid()) {
Fabric.Crashlytics.logException(JSON.stringify(args));
if (extra) {
if (typeof extra === 'string') {
extra = { message: extra };
}
withScope(scope => {
scope.setExtras(extra);
captureException(error);
});
} else {
Fabric.Crashlytics.recordError(JSON.stringify(args));
captureException(error);
}
}
}

@ -325,7 +325,7 @@
function buildOpts(opts) {
if (!opts.seed) {
throw 'No seed provided';
throw new Error('No seed provided');
}
seedrand(opts.seed);

@ -28,7 +28,8 @@ export function createLoggerMiddleware(opts) {
return function loggerMiddleware(/** @type {any} */ req, /** @type {any} */ res, /** @type {Function} */ next) {
next((/** @type {Function} */ cb) => {
if (res.error) {
Logger.error('Error in RPC response:\n', res);
const { error, ...resWithoutError } = res;
Logger.error(error, { message: 'Error in RPC response', res: resWithoutError });
}
if (req.isMetamaskInternal) {
return;

@ -0,0 +1,18 @@
import { init } from '@sentry/react-native';
import { Dedupe, ExtraErrorData } from '@sentry/integrations';
const METAMASK_ENVIRONMENT = process.env['METAMASK_ENVIRONMENT'] || 'local'; // eslint-disable-line dot-notation
const SENTRY_DSN_PROD = 'https://ae39e4b08d464bba9fbf121c85ccfca0@sentry.io/2299799'; // metamask-mobile
const SENTRY_DSN_DEV = 'https://332890de43e44fe2bc070bb18d0934ea@sentry.io/2651591'; // test-metamask-mobile
// Setup sentry remote error reporting
export default function setupSentry() {
const environment = __DEV__ || !METAMASK_ENVIRONMENT ? 'development' : METAMASK_ENVIRONMENT;
const dsn = environment === 'production' ? SENTRY_DSN_PROD : SENTRY_DSN_DEV;
init({
dsn,
debug: __DEV__,
environment,
integrations: [new Dedupe(), new ExtraErrorData()]
});
}

@ -59,8 +59,9 @@ export default class PubNubWrapper {
() => {
setTimeout(() => {
if (this.timeout) {
Logger.error('Sync::timeout');
reject('sync-timeout');
const error = new Error('Sync::timeout');
Logger.error(error);
reject(error);
} else {
resolve();
}
@ -141,15 +142,18 @@ export default class PubNubWrapper {
this.pubnubListener = {
message: ({ channel, message }) => {
if (channel !== this.channelName || !message) {
Logger.log('Sync::message', channel !== this.channelName, !message);
Logger.error('Sync::message', channel !== this.channelName, !message);
Logger.error(new Error('Unrecognized message'), {
thisChannelName: this.channelName,
channel,
message
});
this.timeout = false;
return false;
}
if (message.event === 'error-sync') {
this.timeout = false;
this.disconnectWebsockets();
Logger.error('Sync::error-sync');
Logger.error(new Error('Sync::error-sync'), { channel, message });
onErrorSync();
}
if (message.event === 'syncing-data' && message.currentPkg > this.lastReceivedPkg) {
@ -161,8 +165,7 @@ export default class PubNubWrapper {
const data = JSON.parse(this.incomingDataStr);
onSyncingData(data);
} catch (e) {
Logger.log('Sync::parsing', e.toString());
Logger.error('Sync::parsing', e);
Logger.error(e, 'Sync::parsing');
}
}
}

@ -82,7 +82,6 @@ jest.mock('../core/Engine', () => ({
jest.mock('react-native-keychain', () => ({ getSupportedBiometryType: () => Promise.resolve('FaceId') }));
jest.mock('react-native-share', () => 'RNShare');
jest.mock('react-native-fabric', () => 'Fabric');
jest.mock('react-native-branch', () => 'RNBranch');
jest.mock('react-native-sensors', () => 'RNSensors');
jest.mock('react-native-device-info', () => 'DeviceInfo');

@ -2,6 +2,10 @@ import './shim.js';
import crypto from 'crypto'; // eslint-disable-line import/no-nodejs-modules, no-unused-vars
require('react-native-browser-polyfill'); // eslint-disable-line import/no-commonjs
import setupSentry from './app/util/setupSentry';
setupSentry();
import { AppRegistry, YellowBox } from 'react-native';
import Root from './app/components/Views/Root';
import { name } from './app.json';

Binary file not shown.

@ -1,31 +0,0 @@
//
// ANSCompatibility.h
// AnswersKit
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#pragma once
#if !__has_feature(nullability)
#define nonnull
#define nullable
#define _Nullable
#define _Nonnull
#endif
#ifndef NS_ASSUME_NONNULL_BEGIN
#define NS_ASSUME_NONNULL_BEGIN
#endif
#ifndef NS_ASSUME_NONNULL_END
#define NS_ASSUME_NONNULL_END
#endif
#if __has_feature(objc_generics)
#define ANS_GENERIC_NSARRAY(type) NSArray<type>
#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary<key_type, object_key>
#else
#define ANS_GENERIC_NSARRAY(type) NSArray
#define ANS_GENERIC_NSDICTIONARY(key_type,object_key) NSDictionary
#endif

@ -1,210 +0,0 @@
//
// Answers.h
// Crashlytics
//
// Copyright (c) 2015 Crashlytics, Inc. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "ANSCompatibility.h"
NS_ASSUME_NONNULL_BEGIN
/**
* This class exposes the Answers Events API, allowing you to track key
* user user actions and metrics in your app.
*/
@interface Answers : NSObject
/**
* Log a Sign Up event to see users signing up for your app in real-time, understand how
* many users are signing up with different methods and their success rate signing up.
*
* @param signUpMethodOrNil The method by which a user logged in, e.g. Twitter or Digits.
* @param signUpSucceededOrNil The ultimate success or failure of the login
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logSignUpWithMethod:(nullable NSString *)signUpMethodOrNil
success:(nullable NSNumber *)signUpSucceededOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Log In event to see users logging into your app in real-time, understand how many
* users are logging in with different methods and their success rate logging into your app.
*
* @param loginMethodOrNil The method by which a user logged in, e.g. email, Twitter or Digits.
* @param loginSucceededOrNil The ultimate success or failure of the login
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logLoginWithMethod:(nullable NSString *)loginMethodOrNil
success:(nullable NSNumber *)loginSucceededOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Share event to see users sharing from your app in real-time, letting you
* understand what content they're sharing from the type or genre down to the specific id.
*
* @param shareMethodOrNil The method by which a user shared, e.g. email, Twitter, SMS.
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logShareWithMethod:(nullable NSString *)shareMethodOrNil
contentName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Invite Event to track how users are inviting other users into
* your application.
*
* @param inviteMethodOrNil The method of invitation, e.g. GameCenter, Twitter, email.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logInviteWithMethod:(nullable NSString *)inviteMethodOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Purchase event to see your revenue in real-time, understand how many users are making purchases, see which
* items are most popular, and track plenty of other important purchase-related metrics.
*
* @param itemPriceOrNil The purchased item's price.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param purchaseSucceededOrNil Was the purchase successful or unsuccessful
* @param itemNameOrNil The human-readable form of the item's name. Example:
* @param itemTypeOrNil The type, or genre of the item. Example: Song
* @param itemIdOrNil The machine-readable, unique item identifier Example: SKU
* @param customAttributesOrNil A dictionary of custom attributes to associate with this purchase.
*/
+ (void)logPurchaseWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil
currency:(nullable NSString *)currencyOrNil
success:(nullable NSNumber *)purchaseSucceededOrNil
itemName:(nullable NSString *)itemNameOrNil
itemType:(nullable NSString *)itemTypeOrNil
itemId:(nullable NSString *)itemIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Level Start Event to track where users are in your game.
*
* @param levelNameOrNil The level name
* @param customAttributesOrNil A dictionary of custom attributes to associate with this level start event.
*/
+ (void)logLevelStart:(nullable NSString *)levelNameOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Level End event to track how users are completing levels in your game.
*
* @param levelNameOrNil The name of the level completed, E.G. "1" or "Training"
* @param scoreOrNil The score the user completed the level with.
* @param levelCompletedSuccesfullyOrNil A boolean representing whether or not the level was completed successfully.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logLevelEnd:(nullable NSString *)levelNameOrNil
score:(nullable NSNumber *)scoreOrNil
success:(nullable NSNumber *)levelCompletedSuccesfullyOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log an Add to Cart event to see users adding items to a shopping cart in real-time, understand how
* many users start the purchase flow, see which items are most popular, and track plenty of other important
* purchase-related metrics.
*
* @param itemPriceOrNil The purchased item's price.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param itemNameOrNil The human-readable form of the item's name. Example:
* @param itemTypeOrNil The type, or genre of the item. Example: Song
* @param itemIdOrNil The machine-readable, unique item identifier Example: SKU
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logAddToCartWithPrice:(nullable NSDecimalNumber *)itemPriceOrNil
currency:(nullable NSString *)currencyOrNil
itemName:(nullable NSString *)itemNameOrNil
itemType:(nullable NSString *)itemTypeOrNil
itemId:(nullable NSString *)itemIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Start Checkout event to see users moving through the purchase funnel in real-time, understand how many
* users are doing this and how much they're spending per checkout, and see how it related to other important
* purchase-related metrics.
*
* @param totalPriceOrNil The total price of the cart.
* @param currencyOrNil The ISO4217 currency code. Example: USD
* @param itemCountOrNil The number of items in the cart.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logStartCheckoutWithPrice:(nullable NSDecimalNumber *)totalPriceOrNil
currency:(nullable NSString *)currencyOrNil
itemCount:(nullable NSNumber *)itemCountOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Rating event to see users rating content within your app in real-time and understand what
* content is most engaging, from the type or genre down to the specific id.
*
* @param ratingOrNil The integer rating given by the user.
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logRating:(nullable NSNumber *)ratingOrNil
contentName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Content View event to see users viewing content within your app in real-time and
* understand what content is most engaging, from the type or genre down to the specific id.
*
* @param contentNameOrNil The human readable name for this piece of content.
* @param contentTypeOrNil The type of content shared.
* @param contentIdOrNil The unique identifier for this piece of content. Useful for finding the top shared item.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logContentViewWithName:(nullable NSString *)contentNameOrNil
contentType:(nullable NSString *)contentTypeOrNil
contentId:(nullable NSString *)contentIdOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Search event allows you to see users searching within your app in real-time and understand
* exactly what they're searching for.
*
* @param queryOrNil The user's query.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event.
*/
+ (void)logSearchWithQuery:(nullable NSString *)queryOrNil
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
/**
* Log a Custom Event to see user actions that are uniquely important for your app in real-time, to see how often
* they're performing these actions with breakdowns by different categories you add. Use a human-readable name for
* the name of the event, since this is how the event will appear in Answers.
*
* @param eventName The human-readable name for the event.
* @param customAttributesOrNil A dictionary of custom attributes to associate with this event. Attribute keys
* must be <code>NSString</code> and values must be <code>NSNumber</code> or <code>NSString</code>.
* @discussion How we treat <code>NSNumbers</code>:
* We will provide information about the distribution of values over time.
*
* How we treat <code>NSStrings</code>:
* NSStrings are used as categorical data, allowing comparison across different category values.
* Strings are limited to a maximum length of 100 characters, attributes over this length will be
* truncated.
*
* When tracking the Tweet views to better understand user engagement, sending the tweet's length
* and the type of media present in the tweet allows you to track how tweet length and the type of media influence
* engagement.
*/
+ (void)logCustomEventWithName:(NSString *)eventName
customAttributes:(nullable ANS_GENERIC_NSDICTIONARY(NSString *, id) *)customAttributesOrNil;
@end
NS_ASSUME_NONNULL_END