mattermost-mobile/app/utils/sentry/index.js
Miguel Alatzar 1efb01deda
[MM-16593] Upgrade to RN 0.61.2 (#3455)
* [MM-16593] [MM-17175] [MM-17164] [MM-17189] [MM-17181] Android - Upgrade to RN 0.60 (#3145)

* Upgrade to react-native 0.60

* Use @sentry/react-native

* Manually link @sentry/react-native

* Address review comments

* Jetify after install

* Call jetify from node_modules

* [MM-17785] iOS - Upgrade to RN 0.60 (#3153)

* Upgrade to react-native 0.60

* Use @sentry/react-native

* Manually link @sentry/react-native

* Address review comments

* Jetify after install

* Call jetify from node_modules

* Get app building for iOS

* Revert react-native-image-picker upgrade

* Minor version upgrade of react-native-image-picker

* [MM-17142] Convert all string refs to callbacks (#3217)

* Replace string refs

* Fix tests

* Don't use inline functions

* Fix mattermost-redux reversion from master merge

* [MM-18336] Upload Sentry debug symbols only when SENTRY_ENABLED is true (#3227)

* Upgrade @sentry/react-native

* Run Sentry gradle tasks only when enabled

* Upgrade @sentry/react-native and remove extra Sentry build phase

* [MM-17144] Use Hermes (#3226)

* Replace string refs

* Fix tests

* Don't use inline functions

* Fix mattermost-redux reversion from master merge

* Use Hermes

* bundleCommand ram-bundle no longer needed

* Require harmony-reflect only for Android

* Fix failing test

* Path react-native's splitLayoutProps (#3337)

* [MM-18867] [MM-17186] [MM-18866] [MM-19447] [MM-18967] Upgrade to RN 0.61.2 (#3423)

* Upgrade to RN 0.61.2

* Update rn-fetch-blob commit hash

* Update react-native-keyboard-tracking-view commit hash

* Use react-native-youtube with AVPlayer fix

* Fix jest mocks

* Use updated document picker API

* Remove unnecessary linking

* Revert "MM-17759 Add code highlighting (#3072)"

This reverts commit 26b999e885.

* Fix share extension

* Revert "Revert "MM-17759 Add code highlighting (#3072)""

This reverts commit 52aca776b12dee3abe8646d0c90480f8528e86c1.

* Address PR reviews

* Rename patch to match version

* Update react-native-youtube patch

* Update dependencies

* Fix RNDocViewer reference

* Update tests and revert to redux-persist 4.10.2

* Revert "Revert "Revert "MM-17759 Add code highlighting (#3072)"""

This reverts commit 5ef383be2619b1be6167c23f128ecb4b4ad25df9.

* Android fixes after dep upgrades

* Use fresco 2.0.0

* Use mattermost forks

* Use React-Core in Mattermost.scheme instead of react

* Remove packager (#3452)

* Remove Pods from source control

* Fix unit tests

* Add new line before entering the keystore in gradle.properties

* set ios as working directory for cocoapods

* Cache cocoapods on circleCI builds

* set ios as working dir

* fix cocoapods cache key

* Unify fastlane and npm dependencies command

* Use package-lock.json checksum for npm cache

* Fix package.json and use the checksum as the cache key

* Fix package.json and use the checksum as the cache key

* changes to circleci.yaml fastlane and removing pods from git

* Fix Mattermost.xcodeproj

* Update coocoapods to 1.7.5 and fix xcode header search paths

* Update package-lock.json

* Remove unused tooltip component

* Fix incorrect ref

* Disable Hermes (#3460)

* Revert "Remove packager (#3452)"

This reverts commit b2a79e184b3242124dfdb91ae095f6ce3f4eb986.

* Disable Hermes :'(

* Update preloaded modules

* Fix packages moduleNames and modulePaths, update snapshots and update mm-redux (ts version)

* remove document picker from modulePaths

* Fix package-lock.json

* Add eslint disable rules
2019-10-28 15:04:24 -07:00

315 lines
10 KiB
JavaScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Platform} from 'react-native';
import * as Sentry from '@sentry/react-native';
import Config from 'assets/config';
import {Client4} from 'mattermost-redux/client';
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_EXTENSION = 'extension';
export const LOGGER_JAVASCRIPT = 'javascript';
export const LOGGER_JAVASCRIPT_WARNING = 'javascript_warning';
export const LOGGER_NATIVE = 'native';
export const LOGGER_REDUX = 'redux';
export const BREADCRUMB_UNCAUGHT_APP_ERROR = 'uncaught-app-error';
export const BREADCRUMB_UNCAUGHT_NON_ERROR = 'uncaught-non-error';
export function initializeSentry() {
if (!Config.SentryEnabled) {
return;
}
const dsn = getDsn();
if (!dsn) {
console.warn('Sentry is enabled, but not configured on this platform'); // eslint-disable-line no-console
return;
}
Sentry.init({dsn, ...Config.SentryOptions});
}
function getDsn() {
if (Platform.OS === 'android') {
return Config.SentryDsnAndroid;
} else if (Platform.OS === 'ios') {
return Config.SentryDsnIos;
}
return '';
}
export function captureJSException(error, isFatal, store) {
if (!error || !store) {
console.warn('captureJSException called with missing arguments', error, store); // eslint-disable-line no-console
return;
}
if (error instanceof Error) {
captureException(error, LOGGER_JAVASCRIPT, store);
} else {
captureNonErrorAsBreadcrumb(error, isFatal);
}
}
export function captureException(error, logger, store) {
if (!error || !logger || !store) {
console.warn('captureException called with missing arguments', error, logger, store); // eslint-disable-line no-console
return;
}
capture(() => {
Sentry.captureException(error, {logger});
}, store);
}
export function captureExceptionWithoutState(err, logger) {
if (!Config.SentryEnabled) {
return;
}
if (err && logger) {
try {
Sentry.captureException(err, {logger});
} catch (error) {
// do nothing...
}
}
}
export function captureMessage(message, logger, store) {
if (message && logger && store) {
capture(() => {
Sentry.captureMessage(message, {logger});
}, store);
}
}
export function captureNonErrorAsBreadcrumb(obj, isFatal) {
if (!obj || typeof obj !== 'object') {
console.warning('Invalid object passed to captureNonErrorAsBreadcrumb', obj); // eslint-disable-line no-console
return;
}
const isAppError = Boolean(obj.server_error_id);
const breadcrumb = {
category: isAppError ? BREADCRUMB_UNCAUGHT_APP_ERROR : BREADCRUMB_UNCAUGHT_NON_ERROR,
data: {
isFatal: String(isFatal),
},
level: 'warn',
};
if (obj.message) {
breadcrumb.message = obj.message;
} else if (obj.intl && obj.intl.defaultMessage) {
breadcrumb.message = obj.intl.defaultMessage;
} else {
breadcrumb.message = 'no message provided';
}
if (obj.server_error_id) {
breadcrumb.data.server_error_id = obj.server_error_id;
}
if (obj.status_code) {
breadcrumb.data.status_code = obj.status_code;
}
if (obj.url) {
const match = (/^(?:https?:\/\/)[^/]+(\/.*)$/);
if (match && match.length >= 2) {
breadcrumb.data.url = match[1];
}
}
try {
Sentry.addBreadcrumb(breadcrumb);
} catch (e) {
// Do nothing since this is only here to make sure we don't crash when handling an exception
console.warn('Failed to capture breadcrumb of non-error', e); // eslint-disable-line no-console
}
}
// 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 && config.EnableDiagnostics != null && config.EnableDiagnostics !== 'true') {
return;
}
let hasUserContext = false;
const userContext = getUserContext(state);
if (Object.keys(userContext).length) {
hasUserContext = true;
Sentry.setUserContext(userContext);
}
const extraContext = getExtraContext(state);
if (Object.keys(extraContext).length) {
Sentry.setExtraContext(extraContext);
}
const buildTags = getBuildTags(state);
if (Object.keys(buildTags).length) {
Sentry.setTagsContext(buildTags);
}
if (hasUserContext) {
console.warn('Capturing with Sentry at ' + getDsn() + '...'); // eslint-disable-line no-console
captureFunc();
} else {
console.warn('No user context, skipping capture'); // eslint-disable-line no-console
}
} 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;
}
// Given a URL from an API request, return a URL that has any parts removed that are either sensitive or that would
// prevent properly grouping the messages in Sentry.
export function cleanUrlForLogging(original) {
let url = original;
// Trim the host name
url = url.substring(Client4.getUrl().length);
// Filter the query string
const index = url.indexOf('?');
if (index !== -1) {
url = url.substring(0, index);
}
// A non-exhaustive whitelist to exclude parts of the URL that are unimportant (eg IDs) or may be sentsitive
// (eg email addresses). We prefer filtering out fields that aren't recognized because there should generally
// be enough left over for debugging.
//
// Note that new API routes don't need to be added here since this shouldn't be happening for newly added routes.
const whitelist = [
'api', 'v4', 'users', 'teams', 'scheme', 'name', 'members', 'channels', 'posts', 'reactions', 'commands',
'files', 'preferences', 'hooks', 'incoming', 'outgoing', 'oauth', 'apps', 'emoji', 'brand', 'image',
'data_retention', 'jobs', 'plugins', 'roles', 'system', 'timezones', 'schemes', 'redirect_location', 'patch',
'mfa', 'password', 'reset', 'send', 'active', 'verify', 'terms_of_service', 'login', 'logout', 'ids',
'usernames', 'me', 'username', 'email', 'default', 'sessions', 'revoke', 'all', 'audits', 'device', 'status',
'search', 'switch', 'authorized', 'authorize', 'deauthorize', 'tokens', 'disable', 'enable', 'exists', 'unread',
'invite', 'batch', 'stats', 'import', 'schemeRoles', 'direct', 'group', 'convert', 'view', 'search_autocomplete',
'thread', 'info', 'flagged', 'pinned', 'pin', 'unpin', 'opengraph', 'actions', 'thumbnail', 'preview', 'link',
'delete', 'logs', 'ping', 'config', 'client', 'license', 'websocket', 'webrtc', 'token', 'regen_token',
'autocomplete', 'execute', 'regen_secret', 'policy', 'type', 'cancel', 'reload', 'environment', 's3_test', 'file',
'caches', 'invalidate', 'database', 'recycle', 'compliance', 'reports', 'cluster', 'ldap', 'test', 'sync', 'saml',
'certificate', 'public', 'private', 'idp', 'elasticsearch', 'purge_indexes', 'analytics', 'old', 'webapp', 'fake',
];
url = url.split('/').map((part) => {
if (part !== '' && whitelist.indexOf(part) === -1) {
return '<filtered>';
}
return part;
}).join('/');
if (index !== -1) {
// Add this on afterwards since it wouldn't pass the whitelist
url += '?<filtered>';
}
return url;
}