diff --git a/.circleci/config.yml b/.circleci/config.yml index b63f6ab54f..82bc3ab811 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -111,7 +111,9 @@ commands: key: v2-npm-{{ checksum "package.json" }}-{{ arch }} - run: name: Getting JavaScript dependencies - command: NODE_ENV=development npm ci --ignore-scripts + command: | + NODE_ENV=development npm ci --ignore-scripts + node node_modules/\@sentry/cli/scripts/install.js - save_cache: name: Save npm cache key: v2-npm-{{ checksum "package.json" }}-{{ arch }} diff --git a/app/database/manager/index.ts b/app/database/manager/index.ts index 7ca4111626..c1d2819c17 100644 --- a/app/database/manager/index.ts +++ b/app/database/manager/index.ts @@ -110,7 +110,7 @@ class DatabaseManager { return this.appDatabase; } catch (e) { - // TODO : report to sentry? Show something on the UI ? + logError('Unable to create the App Database!!', e); } return undefined; @@ -164,8 +164,6 @@ class DatabaseManager { return serverDatabase; } catch (e) { - // TODO : report to sentry? Show something on the UI ? - logError('Error initializing database', e); } } @@ -249,7 +247,7 @@ class DatabaseManager { } } } catch (e) { - // TODO : report to sentry? Show something on the UI ? + logError('Error adding server to App database', e); } }; diff --git a/app/screens/home/channel_list/channel_list.tsx b/app/screens/home/channel_list/channel_list.tsx index ca19b8f08b..e82543506b 100644 --- a/app/screens/home/channel_list/channel_list.tsx +++ b/app/screens/home/channel_list/channel_list.tsx @@ -12,10 +12,12 @@ import {Edge, SafeAreaView, useSafeAreaInsets} from 'react-native-safe-area-cont import FreezeScreen from '@components/freeze_screen'; import TeamSidebar from '@components/team_sidebar'; import {Navigation as NavigationConstants, Screens} from '@constants'; +import {useServerUrl} from '@context/server'; import {useTheme} from '@context/theme'; import {useIsTablet} from '@hooks/device'; import {resetToTeams} from '@screens/navigation'; import NavigationStore from '@store/navigation_store'; +import {addSentryContext} from '@utils/sentry'; import AdditionalTabletView from './additional_tablet_view'; import CategoriesList from './categories_list'; @@ -50,6 +52,7 @@ const ChannelListScreen = (props: ChannelProps) => { const isFocused = useIsFocused(); const navigation = useNavigation(); const insets = useSafeAreaInsets(); + const serverUrl = useServerUrl(); const params = route.params as {direction: string}; const canAddOtherServers = managedConfig?.allowOtherServers !== 'false'; @@ -111,6 +114,10 @@ const ChannelListScreen = (props: ChannelProps) => { return () => back.remove(); }, [handleBackPress]); + useEffect(() => { + addSentryContext(serverUrl); + }, [serverUrl]); + return ( {} diff --git a/app/utils/log.ts b/app/utils/log.ts index fb325a6854..38acd662a0 100644 --- a/app/utils/log.ts +++ b/app/utils/log.ts @@ -1,22 +1,39 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import keyMirror from '@utils/key_mirror'; + +const SentryLevels = keyMirror({debug: null, info: null, warning: null, error: null}); + export function logError(...args: any[]) { // eslint-disable-next-line no-console console.error(...args); + addBreadcrumb(SentryLevels.error, ...args); } export function logWarning(...args: any[]) { // eslint-disable-next-line no-console console.warn(...args); + addBreadcrumb(SentryLevels.warning, ...args); } export function logInfo(...args: any[]) { // eslint-disable-next-line no-console console.log(...args); + addBreadcrumb(SentryLevels.info, ...args); } export function logDebug(...args: any[]) { // eslint-disable-next-line no-console console.debug(...args); + addBreadcrumb(SentryLevels.debug, ...args); } + +const addBreadcrumb = (logLevel: keyof typeof SentryLevels, ...args: any[]) => { + const Sentry = require('@sentry/react-native'); + Sentry.addBreadcrumb({ + level: logLevel, + message: args.join(','), + type: 'console-log', + }); +}; diff --git a/app/utils/sentry.ts b/app/utils/sentry.ts index 49ebeb7fa6..87c05e17d7 100644 --- a/app/utils/sentry.ts +++ b/app/utils/sentry.ts @@ -1,13 +1,18 @@ // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. +import {Database} from '@nozbe/watermelondb'; import {Breadcrumb} from '@sentry/types'; import {Platform} from 'react-native'; +import {Navigation} from 'react-native-navigation'; import Config from '@assets/config.json'; +import DatabaseManager from '@database/manager'; +import {getConfig} from '@queries/servers/system'; +import {getCurrentUser} from '@queries/servers/user'; import {ClientError} from './client_error'; -import {logError, logWarning} from './log'; +import {logWarning} from './log'; export const BREADCRUMB_UNCAUGHT_APP_ERROR = 'uncaught-app-error'; export const BREADCRUMB_UNCAUGHT_NON_ERROR = 'uncaught-non-error'; @@ -17,6 +22,7 @@ export const LOGGER_JAVASCRIPT_WARNING = 'javascript_warning'; export const LOGGER_NATIVE = 'native'; let Sentry: any; + export function initializeSentry() { if (!Config.SentryEnabled) { return; @@ -33,7 +39,21 @@ export function initializeSentry() { return; } - Sentry.init({dsn, ...Config.SentryOptions}); + Sentry.init({ + dsn, + tracesSampleRate: 0.2, + integrations: [ + new Sentry.ReactNativeTracing({ + + // Pass instrumentation to be used as `routingInstrumentation` + routingInstrumentation: new Sentry.ReactNativeNavigationInstrumentation( + Navigation, + ), + }), + ], + sendDefaultPii: false, + ...Config.SentryOptions, + }); } function getDsn() { @@ -55,12 +75,7 @@ export function captureException(error: Error | string, logger: string) { logWarning('captureException called with missing arguments', error, logger); return; } - - // TODO: Get current server config and other relevant data - - capture(() => { - Sentry.captureException(error, {logger}); - }); + Sentry.captureException(error, {logger}); } export function captureJSException(error: Error | ClientError, isFatal: boolean) { @@ -120,128 +135,70 @@ function captureClientErrorAsBreadcrumb(error: ClientError, isFatal: boolean) { } } -// Wrapper function to any calls to Sentry so that we can gather any necessary extra data -// before sending. -function capture(captureFunc: () => void, config?: ClientConfig) { - if (config?.EnableDiagnostics !== 'true') { - return; - } - - try { - let hasUserContext = false; - const userContext = getUserContext(); - if (userContext) { - hasUserContext = true; - Sentry.setUserContext(userContext); - } - - const extraContext = getExtraContext(); - if (Object.keys(extraContext).length) { - Sentry.setExtraContext(extraContext); - } - - const buildTags = getBuildTags(); - if (buildTags) { - Sentry.setTagsContext(buildTags); - } - - if (hasUserContext) { - logWarning('Capturing with Sentry at ' + getDsn() + '...'); - - captureFunc(); - } else { - logWarning('No user context, skipping capture'); - } - } catch (e) { - // Don't want this to get into an infinite loop again... - logError('Exception occurred while sending to Sentry'); - logError(e); - } -} - -function getUserContext() { - // TODO: Get current user data from active database +const getUserContext = async (database: Database) => { const currentUser = { id: 'currentUserId', locale: 'en', roles: 'multi-server-test-role', }; - if (!currentUser) { - return null; - } + const user = await getCurrentUser(database); return { - userID: currentUser.id, // This can be changed to id after we upgrade to Sentry >= 0.14.10, + userID: user?.id ?? currentUser.id, email: '', username: '', - extra: { - locale: currentUser.locale, - roles: currentUser.roles, - }, + locale: user?.locale ?? currentUser.locale, + roles: user?.roles ?? currentUser.roles, }; -} +}; -function getExtraContext() { - const context = {}; +const getExtraContext = async (database: Database) => { + const context = { + config: {}, + currentChannel: {}, + currentTeam: {}, + }; - // TODO: Add context based on the active database - - // 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, - // }; - // } + const config = await getConfig(database); + if (config) { + context.config = { + BuildDate: config.BuildDate, + BuildEnterpriseReady: config.BuildEnterpriseReady, + BuildHash: config.BuildHash, + BuildHashEnterprise: config.BuildHashEnterprise, + BuildNumber: config.BuildNumber, + }; + } return context; -} +}; -function getBuildTags() { - let tags; +const getBuildTags = async (database: Database) => { + const tags = { + serverBuildHash: '', + serverBuildNumber: '', + }; - // TODO: Add context based on the active database - - // const config = getConfig(state); - // if (config) { - // tags = { - // serverBuildHash: config.BuildHash, - // serverBuildNumber: config.BuildNumber, - // }; - // } + const config = await getConfig(database); + if (config) { + tags.serverBuildHash = config.BuildHash; + tags.serverBuildNumber = config.BuildNumber; + } return tags; -} +}; + +export const addSentryContext = async (serverUrl: string) => { + const database = DatabaseManager.serverDatabases[serverUrl]?.database; + if (database) { + const userContext = await getUserContext(database); + Sentry.setContext('User-Information', userContext); + + const buildContext = await getBuildTags(database); + Sentry.setContext('App-Build Information', buildContext); + + const extraContext = await getExtraContext(database); + Sentry.setContext('Server-Information', extraContext); + } +}; diff --git a/ios/bundleReactNative.sh b/ios/bundleReactNative.sh index 63ec1bcd03..250ef1d5d5 100755 --- a/ios/bundleReactNative.sh +++ b/ios/bundleReactNative.sh @@ -12,7 +12,8 @@ if [[ "${SENTRY_ENABLED}" = "true" ]]; then ./makeSentryProperties.sh export SENTRY_PROPERTIES=sentry.properties - ../node_modules/@sentry/cli/bin/sentry-cli react-native xcode ./react-native-xcode.sh + ../node_modules/@sentry/cli/bin/sentry-cli react-native xcode \ + ../node_modules/react-native/scripts/react-native-xcode.sh else echo "Sentry native integration is not enabled" ../node_modules/react-native/scripts/react-native-xcode.sh diff --git a/ios/uploadDebugSymbols.sh b/ios/uploadDebugSymbols.sh index f72ff24438..348deb8d9d 100755 --- a/ios/uploadDebugSymbols.sh +++ b/ios/uploadDebugSymbols.sh @@ -6,7 +6,8 @@ if [[ "${SENTRY_ENABLED}" = "true" ]]; then ./makeSentryProperties.sh export SENTRY_PROPERTIES=sentry.properties - ../node_modules/@sentry/cli/bin/sentry-cli upload-dsym +../node_modules/@sentry/cli/bin/sentry-cli upload-dif "$DWARF_DSYM_FOLDER_PATH" + else echo "Not uploading debugging symbols to Sentry because Sentry is disabled" fi