编程语言
551
集成 Sentry 来监控应用崩溃问题
Sentry 是一款开源的崩溃监控工具,可帮助开发人员实时监控和修复导致应用崩溃的 BUG。最重要的是,它同时支持对原生代码和 React Native 代码的监控。
Sentry 可私有部署
本文讲述如何将 Sentryopen in new window 和 CI / CD 集成。
- 在打包时注入 Commit SHA,方便我们 checkout 出问题的代码
- 上传符号表(RN - jsbundle.map, iOS - dSYM, Android - mapping.txt)
安装 sentry-cli
curl -sL https://sentry.io/get-cli/ | bash
本文使用的 sentry-cli 版本已经更新到 1.67.2 以上,使用 sentry-cli update 命令即可升级 sentry-cli
登录和创建 Sentry Project
登录(省略一百个字)
创建 Sentry Project
- 打开 All 选项卡,选择 React-Native
- 更改项目名, 并点击 Create Projec
调整 iOS 项目
- 删除 Upload Debug Symbols to Sentry 这个 Build Phase
- 展开 Bundle React Native code and images 这个 Build Phase,将里面的脚本替换成如下内容
if [ -z "$CI" ]; then export NODE_BINARY=node ../node_modules/react-native/scripts/react-native-xcode.sh else export NODE_BINARY=node sourcemap_path="$SRCROOT/build/main.jsbundle.map" sourcemap_sources_root="$SRCROOT/../" export EXTRA_PACKAGER_ARGS="--sourcemap-output $sourcemap_path --sourcemap-sources-root $sourcemap_sources_root" echo $EXTRA_PACKAGER_ARGS >&2 ../node_modules/react-native/scripts/react-native-xcode.sh bundle_path="$CONFIGURATION_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH/main.jsbundle" build_dir="$SRCROOT/build" if [ -e "$bundle_path" ]; then cp "$bundle_path" "$build_dir" else echo "可能 ts 代码有错误,无法通过编译。" exit 3 fi fi
- 修改 ios/fastlane/Fastfile 文件,添加一个 lane
lane :upload_debug_symbol_to_sentry do |options| file_name, basename, version_name, build_number, dir_name = app_info bundle_output = "#{ENV['PWD']}/build/main.jsbundle" sourcemap_output = "#{ENV['PWD']}/build/main.jsbundle.map" release = "#{app_identifier}@#{version_name}+#{build_number}" sh(%( sentry-cli --log-level INFO releases new #{release} )) # upload_sourcemaps sh(%( sentry-cli --log-level INFO releases files #{release} upload-sourcemaps --dist #{build_number} --rewrite #{sourcemap_output} )) # upload_bundle sh(%( sentry-cli --log-level INFO releases files #{release} upload --dist #{build_number}#{bundle_output} "~/main.jsbundle" )) sh(%(sentry-cli releases finalize #{release})) # upload_sdym sh(%( sentry-cli --log-level INFO upload-dif -t dsym --no-bin #{dir_name} )) end
调整 Android 项目
- 修改 android/sentry.properties
- cli.executable=node_modules/@sentry/cli/bin/sentry-cli
- 修改 android/build.gradle 文件
buildscript { dependencies { + classpath 'io.sentry:sentry-android-gradle-plugin:2.1.0' } }
- 修改 anroid/app/build.gradle 文件
+ apply plugin: 'io.sentry.android.gradle' - apply from: "../../node_modules/@sentry/react-native/sentry.gradle" + sentry { + uploadNativeSymbols=false + autoUpload=false + }
调整 js 代码
- 修改 app.json 文件
{ "name": "MyApp", "displayName": "MyApp", "COMMIT_SHORT_SHA": "xxxxxxxx", "VERSION_NAME": "1.0.0", "VERSION_CODE": 1, "CI": false }
- 修改 AppInfo.ts 文件
VERSION_NAME
和 VERSION_CODE
不再通过桥接原生的方式获取,而是通过环境变量
export { VERSION_NAME, VERSION_CODE, COMMIT_SHORT_SHA, CI } from '../app.json'
- 修改 index.js 文件
Sentry 项目创建成功后,会打开一个安装引导页面,将该页面拖到底部,拷贝如下代码到你的 index.js 文件中
// index.js import { AppRegistry } from 'react-native' import App from './App' import { ENVIRONMENT, APPLICATION_ID, VERSION_NAME, VERSION_CODE, COMMIT_SHORT_SHA, CI } from './app/AppInfo' import * as Sentry from '@sentry/react-native' if (CI) { Sentry.init({ dsn: 'https://2d17bb1ffde34fec963d29b4c3a29f99@sentry.io/1446147', enableAutoSessionTracking: true, environment: ENVIRONMENT, release: `${APPLICATION_ID}@${VERSION_NAME}+${VERSION_CODE}`, dist: `${VERSION_CODE}`, }) Sentry.setTag('commit', COMMIT_SHORT_SHA) } AppRegistry.registerComponent(appName, () => App)
- 修改 App.tsx 文件,添加一些能导致崩溃的代码
// App.tsx import React from 'react' import { StyleSheet, Text, View, TouchableOpacity, Image } from 'react-native' import * as Sentry from '@sentry/react-native' import { Navigator, withNavigationItem, InjectedProps } from 'hybrid-navigation' import { ENVIRONMENT, VERSION_NAME, VERSION_CODE, COMMIT_SHORT_SHA } from './AppInfo' export default withNavigationItem({ rightBarButtonItem: { icon: Image.resolveAssetSource(require('./images/ic_settings.png')), action: (navigator: Navigator) => { navigator.push('Blank') }, }, })(App) function App({ sceneId }: InjectedProps) { const version = `${VERSION_NAME}-${VERSION_CODE}` function sentryNativeCrash() { Sentry.nativeCrash() } function jsCrash() { const array = ['x', 'y', 'z', 'a'] const a = array[9].length + 8 console.log(`${Number(a) + 1}`) } function throwError() { throw new Error('主动抛出异常') } function reject() { Promise.reject(new Error('promise 被拒绝了')) } return ( <View style={[styles.container]}> <Text style={styles.welcome}> 环境: {`${ENVIRONMENT}`} 版本: {version} commit: {COMMIT_SHORT_SHA} </Text> <Text style={styles.welcome}>按下一个按钮,让 APP 崩溃!</Text> <TouchableOpacity onPress={sentryNativeCrash} activeOpacity={0.2} style={styles.button}> <Text style={styles.buttonText}>Sentry native crash</Text> </TouchableOpacity> <TouchableOpacity onPress={jsCrash} activeOpacity={0.2} style={styles.button}> <Text style={styles.buttonText}>数组越界</Text> </TouchableOpacity> <TouchableOpacity onPress={throwError} activeOpacity={0.2} style={styles.button}> <Text style={styles.buttonText}>主动抛出异常</Text> </TouchableOpacity> <TouchableOpacity onPress={reject} activeOpacity={0.2} style={styles.button}> <Text style={styles.buttonText}>promise reject</Text> </TouchableOpacity> </View> ) } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'flex-start', alignItems: 'stretch', paddingTop: 16, }, welcome: { backgroundColor: 'transparent', fontSize: 17, textAlign: 'center', margin: 8, }, button: { alignItems: 'center', justifyContent: 'center', height: 40, }, buttonText: { backgroundColor: 'transparent', color: 'rgb(34,88,220)', }, })
修改 CI 脚本
- 注入 Commit SHA
在 ci 文件夹中添加 sha.js 文件
// sha.js const fs = require('fs') const path = require('path') const { VERSION_NAME, VERSION_CODE, CI } = require('./config') const file = path.resolve(__dirname, '../app.json') const data = fs.readFileSync(file, 'utf8') const app = JSON.parse(data) app.COMMIT_SHORT_SHA = process.env.CI_COMMIT_SHORT_SHA || 'xxxxxxxx' app.VERSION_NAME = VERSION_NAME app.VERSION_CODE = VERSION_CODE app.CI = !!process.env.CI fs.writeFileSync(file, JSON.stringify(app))
- 修改 ci/config.js 文件,添加若干常量
// config.js // Android js bundle 原始目录 const JS_BUNDLE_SOURCE_DIR = path.resolve(BUILD_DIR, `generated/assets/react/${ENVIRONMENT}/release/`) // AndroidManifest.xml const MANIFEST_FILENAME = 'AndroidManifest.xml' // AndroidManifest.xml 原始路径 const MANIFEST_SOURCE_PATH = path.resolve( BUILD_DIR, `intermediates/merged_manifests/${ENVIRONMENT}Release/arm64-v8a/${MANIFEST_FILENAME}`, ) // sentry properties 所在路径 const SENTRY_PROPERTIES_PATH = path.resolve(__dirname, `../${PLATFORM}/sentry.properties`) const SENTRY_DEBUG_META_FILENAME = 'sentry-debug-meta.properties' // sentry-debug-meta.properties 原始路径 const SENTRY_DEBUG_META_SOURCE_PATH = path.resolve( BUILD_DIR, `intermediates/merged_assets/${ENVIRONMENT}Release/out/${SENTRY_DEBUG_META_FILENAME}`, ) const MAPPING_FILENAME = 'mapping.txt' // android mapping.txt 原始路径 const MAPPING_FILE_SOURCE_PATH = path.resolve(BUILD_DIR, `outputs/mapping/${ENVIRONMENT}/release/${MAPPING_FILENAME}`)
- 修改 ci/pack/android.js 文件,在文件末尾增加如下内容
// ci/pack/android.js // jsbundle copy(JS_BUNDLE_SOURCE_DIR, ARTIFACTS_DIR) // AndroidManifest.xml fs.copyFileSync(MANIFEST_SOURCE_PATH, path.resolve(ARTIFACTS_DIR, MANIFEST_FILENAME), COPYFILE_EXCL) // sentry-debug-meta.properties fs.copyFileSync(SENTRY_DEBUG_META_SOURCE_PATH, path.resolve(ARTIFACTS_DIR, SENTRY_DEBUG_META_FILENAME), COPYFILE_EXCL) // mapping.txt fs.copyFileSync(MAPPING_FILE_SOURCE_PATH, path.resolve(ARTIFACTS_DIR, MAPPING_FILENAME), COPYFILE_EXCL)
- 创建 ci/sentry/android.js 文件,关键内容如下
// 上传 js bundle map 文件 sh( `sentry-cli --log-level INFO react-native gradle \ --bundle ${ARTIFACTS_DIR}/index.android.bundle \ --sourcemap ${ARTIFACTS_DIR}/index.android.bundle.map \ --release ${release} \ --dist ${VERSION_CODE}`, { env: { ...process.env, SENTRY_PROPERTIES: SENTRY_PROPERTIES_PATH }, }, )
- 创建 ci/sentry/ios.js 文件,关键内容如下
sh('bundle exec fastlane upload_debug_symbol_to_sentry', { env: { ...process.env, SENTRY_PROPERTIES: SENTRY_PROPERTIES_PATH, }, cwd: workdir, })
- 修改 .gitlab-ci.yml 文件,修改两个 job,关键内容如下
deploy:ios:sentry: stage: deploy dependencies: - build:ios script: - node ./ci/sentry ios only: - schedules tags: - ios except: variables: - $ANDROID_ONLY deploy:android:sentry: stage: deploy dependencies: - build:android script: - node ./ci/sentry android only: - schedules tags: - android except: variables: - $IOS_ONLY
测试
至此,我们已经集成了 Sentry,并实现通过 CI 打包时,将会上传符号表到 Sentry,现在就让我们上传代码到 GitLab,点击 Play 按钮打个包来测试一下吧。
一旦发生异常,Sentry 将收到报告,不仅定位到了出错的地方,还附带 Breadcrumbs,即崩溃前的日志。
我们可以在 TAGS 一栏中找到 commit 和 environment,这正是我们通过以下代码所设置的
Sentry.init({ environment: ENVIRONMENT, }) Sentry.setTag('commit', COMMIT_SHORT_SHA)
environment 可以告诉我们,是哪个环境出了问题,commit 可以告诉我们,是基于那个 commit 打的包出了问题,方便我们 checkout 出问题的代码。