diff --git a/.eslintrc.js b/.eslintrc.js index be8fadebc6..822a22f6f1 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -12,7 +12,15 @@ module.exports = { settings: { version: 'detect', // React version. "detect" automatically picks the version you have installed. }, - ignorePatterns: ['test/react-native/versions/**/*', 'coverage/**/*', 'test/typescript/**/*', 'metro.d.ts'], + ignorePatterns: [ + 'test/react-native/versions/**/*', + 'coverage/**/*', + 'test/typescript/**/*', + 'metro.d.ts', + 'samples/**/*', + 'plugin/build/**/*', + 'expo.d.ts', + ], overrides: [ { // Typescript Files @@ -37,7 +45,7 @@ module.exports = { // Scripts files: ['scripts/*'], parserOptions: { - ecmaVersion: 2015, + ecmaVersion: 2018, }, rules: { 'no-console': 'off', diff --git a/.github/workflows/buildandtest.yml b/.github/workflows/buildandtest.yml index 45079f20c0..f5b2bd006f 100644 --- a/.github/workflows/buildandtest.yml +++ b/.github/workflows/buildandtest.yml @@ -107,6 +107,11 @@ jobs: with: name: ts3.8 path: ts3.8 + - name: Archive Expo Plugin + uses: actions/upload-artifact@v3 + with: + name: expo-plugin + path: plugin/build - name: Pack run: yarn pack - name: Archive Artifacts @@ -155,6 +160,37 @@ jobs: working-directory: test/typescript/ts3.8 run: yarn type-check + job_circular_dep_check: + name: Circular Dependency Check + runs-on: ubuntu-latest + needs: [job_build] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 18 + - name: Cache Dependencies + uses: actions/cache@v3 + id: cache + with: + path: node_modules + key: ${{ runner.os }}-${{ github.sha }} + - name: Install Dependencies + if: ${{ steps.cache.outputs['cache-hit'] != 'true' }} + run: yarn install + - name: Download dist + uses: actions/download-artifact@v3 + with: + name: dist + path: dist + - name: Download Expo Plugin + uses: actions/download-artifact@v3 + with: + name: expo-plugin + path: plugin/build + - name: Run madge + run: yarn circularDepCheck + job_bundle: name: Bundle runs-on: ubuntu-latest diff --git a/.npmignore b/.npmignore index 2603c8292e..6a301a4784 100644 --- a/.npmignore +++ b/.npmignore @@ -18,7 +18,14 @@ !scripts/sentry-xcode.sh !scripts/sentry-xcode-debug-files.sh !scripts/sentry_utils.rb +!scripts/expo-upload-sourcemaps.js # Metro !/metro.js !/metro.d.ts + +# Expo +!/expo.js +!/expo.d.ts +!/app.plugin.js +!/plugin/build/**/* diff --git a/.vscode/settings.json b/.vscode/settings.json index 446a4a85c2..70e541174d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,9 +2,7 @@ "editor.formatOnType": true, "editor.formatOnPaste": false, "editor.formatOnSave": true, - "editor.rulers": [ - 120 - ], + "editor.rulers": [120], "editor.tabSize": 2, "files.autoSave": "onWindowChange", "files.trimTrailingWhitespace": true, diff --git a/CHANGELOG.md b/CHANGELOG.md index 132a607820..dd18fedff0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,112 @@ ## Unreleased -### Fixes +This release ships with a beta version of our new built-in Expo SDK 50 support, +which replaces the deprecated `sentry-expo` package. To learn more, +see [the Expo guide](https://docs.sentry.io/platforms/react-native/manual-setup/expo/). + +### Features + +- New `@sentry/react-native/expo` Expo config plugin ([#3429](https://github.com/getsentry/sentry-react-native/pull/3429)) + + ```js + const { withSentry } = require('@sentry/react-native/expo'); + + const config = {...}; + + module.exports = withSentry(config, { + url: 'https://www.sentry.io/', + authToken: 'example-token', // Or use SENTRY_AUTH_TOKEN env + project: 'project-slug', // Or use SENTRY_PROJECT env + organization: 'org-slug', // Or use SENTRY_ORG env + }); + ``` + + - And `Sentry.init` in `App.js` + + ```js + import * as Sentry from '@sentry/react-native'; + + Sentry.init({ + dsn: '__DSN__', + }); + ``` + +- New `getSentryExpoConfig` for simple Metro configuration ([#3454](https://github.com/getsentry/sentry-react-native/pull/3454), [#3501](https://github.com/getsentry/sentry-react-native/pull/3501), [#3514](https://github.com/getsentry/sentry-react-native/pull/3514)) + - This function is a drop in replacement for `getDefaultConfig` from `expo/metro-config` + + ```js + // const { getDefaultConfig } = require("expo/metro-config"); + const { getSentryExpoConfig } = require("@sentry/react-native/metro"); + + // const config = getDefaultConfig(__dirname); + const config = getSentryExpoConfig(config, {}); + ``` +- New `npx sentry-expo-upload-sourcemaps` for simple EAS Update (`npx expo export`) source maps upload ([#3491](https://github.com/getsentry/sentry-react-native/pull/3491), [#3510](https://github.com/getsentry/sentry-react-native/pull/3510), [#3515](https://github.com/getsentry/sentry-react-native/pull/3515), [#3507](https://github.com/getsentry/sentry-react-native/pull/3507)) + + ```bash + SENTRY_PROJECT=project-slug \ + SENTRY_ORG=org-slug \ + SENTRY_AUTH_TOKEN=super-secret-token \ + npx sentry-expo-upload-sourcemaps dist + ``` + +### Others + +- Update `sentry-xcode.sh` scripts with Node modules resolution ([#3450](https://github.com/getsentry/sentry-react-native/pull/3450)) + - RN SDK and Sentry CLI are dynamically resolved if override is not supplied +- Resolve Default Integrations based on current platform ([#3465](https://github.com/getsentry/sentry-react-native/pull/3465)) + - Native Integrations are only added if Native Module is available + - Web Integrations only for React Native Web builds +- Remove Native Modules warning from platform where the absence is expected ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466)) +- Add Expo Context information using Expo Native Modules ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466)) - Errors from InternalBytecode.js are no longer marked as in_app ([#3518](https://github.com/getsentry/sentry-react-native/pull/3518)) +## 5.16.0-alpha.4 + +### Fixes + +- Make `getSentryExpoConfig` options parameter optional ([#3514](https://github.com/getsentry/sentry-react-native/pull/3514)) +- Use `@sentry/react-native/expo` as plugin name in `expo-upload-sourcemaps.js` ([#3515](https://github.com/getsentry/sentry-react-native/pull/3515)) + +## 5.16.0-alpha.3 + +This release is compatible with `expo@50.0.0-preview.6` and newer. + +### Features + +- `withSentryExpoSerializers` changes to `getSentryExpoConfig` ([#3501](https://github.com/getsentry/sentry-react-native/pull/3501)) + - `getSentryExpoConfig` accepts the same parameters as `getDefaultConfig` from `expo/metro-config` and returns Metro configuration + - This also works for EAS Updates (and expo export). Debug ID is generated by `expo/metro-config` and used by Sentry. + + ```js + const { getSentryExpoConfig } = require("@sentry/react-native/metro"); + const config = getSentryExpoConfig(config, {}); + ``` + +- Add `npx sentry-expo-upload-sourcemaps` for simple EAS Update (expo export) source maps upload to Sentry ([#3491](https://github.com/getsentry/sentry-react-native/pull/3491), [#3510](https://github.com/getsentry/sentry-react-native/pull/3510)) + + ```bash + SENTRY_PROJECT=project-slug \ + SENTRY_ORG=org-slug \ + SENTRY_AUTH_TOKEN=super-secret-token \ + npx sentry-expo-upload-sourcemaps dist + ``` + +- Sentry CLI binary path in `scripts/expo-upload-sourcemaps.js` is resolved dynamically ([#3507](https://github.com/getsentry/sentry-react-native/pull/3507)) + - Or can be overwritten by `SENTRY_CLI_EXECUTABLE` env + +- Resolve Default Integrations based on current platform ([#3465](https://github.com/getsentry/sentry-react-native/pull/3465)) + - Native Integrations are only added if Native Module is available + - Web Integrations only for React Native Web builds +- Remove Native Modules warning from platform where the absence is expected ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466)) +- Add Expo Context information using Expo Native Modules ([#3466](https://github.com/getsentry/sentry-react-native/pull/3466)) + +### Fixes + +- Includes fixes from version 5.15.2 + ## 5.15.2 ### Fixes @@ -13,6 +115,32 @@ - Stop sending navigation route params for auto-generated transactions, as they may contain PII or other sensitive data ([#3487](https://github.com/getsentry/sentry-react-native/pull/3487)) - Further details and other strategies to mitigate this issue can be found on our [trouble shooting guide page](https://docs.sentry.io/platforms/react-native/troubleshooting/#routing-transaction-data-contains-sensitive-information) +## 5.16.0-alpha.2 + +### Features + +- Add `withSentryExpoSerializers` for easy configurable `metro.config.js` ([#3454](https://github.com/getsentry/sentry-react-native/pull/3454)) + + This Serializer doesn't support EAS Updates (and expo export) commands yet. Debug IDs needed for source maps resolution in Sentry + are generated only during native builds. + + ```js + const { getDefaultConfig } = require('expo/metro-config'); + const { withSentryExpoSerializers } = require("@sentry/react-native/metro"); + + const config = getDefaultConfig(__dirname); + module.exports = withSentryExpoSerializers(config); + ``` + + Note that this will remove any existing `customSerializer`. Guide for advanced setups [can be found here](https://docs.sentry.io/platforms/react-native/manual-setup/metro). + +### Fixes + +- Expo SDK minimum version is 49 ([#3453](https://github.com/getsentry/sentry-react-native/pull/3453)) +- Remove RN Internal imports for RN Web builds ([#3462](https://github.com/getsentry/sentry-react-native/pull/3462)) +- Remove circular dependencies inside of the SDK ([#3464](https://github.com/getsentry/sentry-react-native/pull/3464)) +- Includes fixes from version 5.15.1 + ## 5.15.1 ### Fixes @@ -25,6 +153,48 @@ - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2230) - [diff](https://github.com/getsentry/sentry-cli/compare/2.21.3...2.23.0) +## 5.16.0-alpha.1 + +### Features + +- Add `@sentry/react-native/expo` Expo config plugin ([#3429](https://github.com/getsentry/sentry-react-native/pull/3429)) + + This Release introduces the first alpha version of our new SDK for Expo. + At this time, the SDK is considered experimental and things might break and change in future versions. + + The core of the SDK is Expo plugin which you can easily add to your App config: + + ```js + const { withSentry } = require('@sentry/react-native/expo'); + + const config = {...}; + + module.exports = withSentry(config, { + url: 'https://www.sentry.io/', + authToken: 'example-token', // Or use SENTRY_AUTH_TOKEN env + project: 'project-slug', // Or use SENTRY_PROJECT env + organization: 'org-slug', // Or use SENTRY_ORG env + }); + ``` + + - And `Sentry.init` in `App.js` + + ```js + import * as Sentry from '@sentry/react-native'; + + Sentry.init({ + dsn: '__DSN__', + }); + ``` + +- Update `sentry-xcode.sh` scripts with Node modules resolution ([#3450](https://github.com/getsentry/sentry-react-native/pull/3450)) + - RN SDK and Sentry CLI are dynamically resolved if override is not supplied + +### Fixes + +- Transform shipped JSX for both react-native and web ([#3428](https://github.com/getsentry/sentry-react-native/pull/3428)) + - Removes builds errors when using react-native-web with Webpack + ## 5.15.0 ### Features diff --git a/app.plugin.js b/app.plugin.js new file mode 100644 index 0000000000..3ae4fa6e4d --- /dev/null +++ b/app.plugin.js @@ -0,0 +1 @@ +module.exports = require('./expo'); diff --git a/expo.d.ts b/expo.d.ts new file mode 100644 index 0000000000..020de8e4bb --- /dev/null +++ b/expo.d.ts @@ -0,0 +1 @@ +export * from './plugin/build'; diff --git a/expo.js b/expo.js new file mode 100644 index 0000000000..3c7d11b615 --- /dev/null +++ b/expo.js @@ -0,0 +1 @@ +module.exports = require('./plugin/build'); diff --git a/package.json b/package.json index 3003bb707a..dd7a931270 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@sentry/react-native", "homepage": "https://github.com/getsentry/sentry-react-native", "repository": "https://github.com/getsentry/sentry-react-native", - "version": "5.15.2", + "version": "5.16.0-alpha.4", "description": "Official Sentry SDK for react-native", "typings": "dist/js/index.d.ts", "types": "dist/js/index.d.ts", @@ -18,26 +18,33 @@ }, "main": "dist/js/index.js", "scripts": { - "build": "yarn build:sdk && yarn downlevel && yarn build:tools", + "build": "yarn build:sdk && yarn downlevel && yarn build:tools && yarn build:plugin", "build:sdk": "tsc -p tsconfig.build.json", "build:sdk:watch": "tsc -p tsconfig.build.json -w --preserveWatchOutput", "build:tools": "tsc -p tsconfig.build.tools.json", + "build:tools:watch": "tsc -p tsconfig.build.tools.json -w --preserveWatchOutput", + "build:plugin": "EXPO_NONINTERACTIVE=true expo-module build plugin", "downlevel": "downlevel-dts dist ts3.8/dist --to=3.8", - "clean": "rimraf dist coverage", + "clean": "rimraf dist coverage && yarn clean:plugin", + "clean:plugin": "expo-module clean plugin", + "circularDepCheck": "madge --circular dist/js/index.js && madge --circular metro.js && madge --circular expo.js", "test": "yarn test:sdk && yarn test:tools", "test:sdk": "jest", "test:tools": "jest --config jest.config.tools.js", "fix": "yarn fix:eslint && yarn fix:prettier", "fix:eslint": "eslint --config .eslintrc.js --fix .", - "fix:prettier": "prettier --write \"{src,test,scripts}/**/**.ts\"", + "fix:prettier": "prettier --write \"{src,test,scripts,plugin/src}/**/**.ts\"", "lint": "yarn lint:eslint && yarn lint:prettier", "lint:eslint": "eslint --config .eslintrc.js .", - "lint:prettier": "prettier --check \"{src,test,scripts}/**/**.ts\"", + "lint:prettier": "prettier --check \"{src,test,scripts,plugin/src}/**/**.ts\"", "test:watch": "jest --watch", "run-ios": "cd samples/react-native && yarn react-native run-ios", "run-android": "cd samples/react-native && yarn react-native run-android", "yalc:add:sentry-javascript": "yalc add @sentry/browser @sentry/core @sentry/hub @sentry/integrations @sentry/react @sentry/types @sentry/utils" }, + "bin": { + "sentry-expo-upload-sourcemaps": "scripts/expo-upload-sourcemaps.js" + }, "keywords": [ "react-native", "sentry", @@ -54,6 +61,7 @@ "When bumping make sure to update the version of react, i.e: https://github.com/facebook/react-native/blob/v0.56.0/package.json" ], "peerDependencies": { + "expo": ">=49.0.0", "react": ">=17.0.0", "react-native": ">=0.65.0" }, @@ -68,6 +76,8 @@ "@sentry/utils": "7.81.1" }, "devDependencies": { + "@babel/core": "^7.23.5", + "@expo/metro-config": "0.16.0", "@sentry-internal/eslint-config-sdk": "7.81.1", "@sentry-internal/eslint-plugin-sdk": "7.81.1", "@sentry-internal/typescript": "7.80.0", @@ -82,8 +92,11 @@ "eslint": "^7.6.0", "eslint-plugin-react": "^7.20.6", "eslint-plugin-react-native": "^3.8.1", + "expo": "50.0.0-preview.8", + "expo-module-scripts": "^3.1.0", "jest": "^29.6.2", "jest-environment-jsdom": "^29.6.2", + "madge": "^6.1.0", "metro": "0.76", "prettier": "^2.0.5", "react": "18.2.0", @@ -110,5 +123,10 @@ "android": { "javaPackageName": "io.sentry.react" } + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } } } diff --git a/plugin/src/index.ts b/plugin/src/index.ts new file mode 100644 index 0000000000..8edc33ead6 --- /dev/null +++ b/plugin/src/index.ts @@ -0,0 +1,5 @@ +import { withSentry } from './withSentry'; + +export { withSentry }; + +export default withSentry; diff --git a/plugin/src/utils.ts b/plugin/src/utils.ts new file mode 100644 index 0000000000..22320d247f --- /dev/null +++ b/plugin/src/utils.ts @@ -0,0 +1,20 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +export function writeSentryPropertiesTo(filepath: string, sentryProperties: string): void { + if (!fs.existsSync(filepath)) { + throw new Error(`Directory '${filepath}' does not exist.`); + } + + fs.writeFileSync(path.resolve(filepath, 'sentry.properties'), sentryProperties); +} + +const sdkPackage: { + name: string; + version: string; + // eslint-disable-next-line @typescript-eslint/no-var-requires +} = require('../../package.json'); + +const SDK_PACKAGE_NAME = sdkPackage.name; + +export { sdkPackage, SDK_PACKAGE_NAME }; diff --git a/plugin/src/withSentry.ts b/plugin/src/withSentry.ts new file mode 100644 index 0000000000..f1a4022fd4 --- /dev/null +++ b/plugin/src/withSentry.ts @@ -0,0 +1,70 @@ +import type { ConfigPlugin } from 'expo/config-plugins'; +import { createRunOncePlugin, WarningAggregator } from 'expo/config-plugins'; + +import { SDK_PACKAGE_NAME, sdkPackage } from './utils'; +import { withSentryAndroid } from './withSentryAndroid'; +import { withSentryIOS } from './withSentryIOS'; + +interface PluginProps { + organization?: string; + project?: string; + authToken?: string; + url?: string; +} + +const withSentryPlugin: ConfigPlugin = (config, props) => { + const sentryProperties = getSentryProperties(props); + let cfg = config; + if (sentryProperties !== null) { + try { + cfg = withSentryAndroid(cfg, sentryProperties); + } catch (e) { + WarningAggregator.addWarningAndroid( + SDK_PACKAGE_NAME, + `There was a problem configuring sentry-expo in your native Android project: ${e}`, + ); + } + try { + cfg = withSentryIOS(cfg, sentryProperties); + } catch (e) { + WarningAggregator.addWarningIOS( + SDK_PACKAGE_NAME, + `There was a problem configuring sentry-expo in your native iOS project: ${e}`, + ); + } + } + return cfg; +}; + +const missingAuthTokenMessage = '# auth.token is configured through SENTRY_AUTH_TOKEN environment variable'; +const missingProjectMessage = '# no project found, falling back to SENTRY_PROJECT environment variable'; +const missingOrgMessage = '# no org found, falling back to SENTRY_ORG environment variable'; + +export function getSentryProperties(props: PluginProps | void): string | null { + const { organization, project, authToken, url = 'https://sentry.io/' } = props ?? {}; + // eslint-disable-next-line no-prototype-builtins + const missingProperties = ['organization', 'project'].filter(each => !props?.hasOwnProperty(each)); + + if (missingProperties.length) { + const warningMessage = `Missing Sentry configuration properties: ${missingProperties.join( + ', ', + )} in config plugin. Builds will fall back to environment variables. See: https://docs.sentry.io/platforms/react-native/manual-setup/.`; + WarningAggregator.addWarningAndroid(SDK_PACKAGE_NAME, warningMessage); + WarningAggregator.addWarningIOS(SDK_PACKAGE_NAME, warningMessage); + } + + return `defaults.url=${url} +${organization ? `defaults.org=${organization}` : missingOrgMessage} +${project ? `defaults.project=${project}` : missingProjectMessage} +${ + authToken + ? `# Configure this value through \`SENTRY_AUTH_TOKEN\` environment variable instead. See: https://docs.sentry.io/platforms/react-native/manual-setup/\nauth.token=${authToken}` + : missingAuthTokenMessage +} +`; +} + +// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access +const withSentry = createRunOncePlugin(withSentryPlugin, sdkPackage.name, sdkPackage.version); + +export { withSentry }; diff --git a/plugin/src/withSentryAndroid.ts b/plugin/src/withSentryAndroid.ts new file mode 100644 index 0000000000..74e187be4f --- /dev/null +++ b/plugin/src/withSentryAndroid.ts @@ -0,0 +1,52 @@ +import type { ConfigPlugin } from 'expo/config-plugins'; +import { WarningAggregator, withAppBuildGradle, withDangerousMod } from 'expo/config-plugins'; +import * as path from 'path'; + +import { SDK_PACKAGE_NAME, writeSentryPropertiesTo } from './utils'; + +export const withSentryAndroid: ConfigPlugin = (config, sentryProperties: string) => { + const cfg = withAppBuildGradle(config, config => { + if (config.modResults.language === 'groovy') { + config.modResults.contents = modifyAppBuildGradle(config.modResults.contents); + } else { + throw new Error('Cannot configure Sentry in the app gradle because the build.gradle is not groovy'); + } + return config; + }); + return withDangerousMod(cfg, [ + 'android', + config => { + writeSentryPropertiesTo(path.resolve(config.modRequest.projectRoot, 'android'), sentryProperties); + return config; + }, + ]); +}; + +const resolveSentryReactNativePackageJsonPath = + '["node", "--print", "require(\'path\').dirname(require.resolve(\'@sentry/react-native/package.json\'))"].execute().text.trim()'; + +/** + * Writes to projectDirectory/android/app/build.gradle, + * adding the relevant @sentry/react-native script. + */ +export function modifyAppBuildGradle(buildGradle: string): string { + if (buildGradle.includes('sentry.gradle')) { + return buildGradle; + } + + // Use the same location that sentry-wizard uses + // See: https://github.com/getsentry/sentry-wizard/blob/e9b4522f27a852069c862bd458bdf9b07cab6e33/lib/Steps/Integrations/ReactNative.ts#L232 + const pattern = /^android {/m; + + if (!buildGradle.match(pattern)) { + WarningAggregator.addWarningAndroid( + SDK_PACKAGE_NAME, + 'Could not find `^android {` in `android/app/build.gradle`. Please open a bug report at https://github.com/getsentry/sentry-react-native.', + ); + return buildGradle; + } + + const applyFrom = `apply from: new File(${resolveSentryReactNativePackageJsonPath}, "sentry.gradle")`; + + return buildGradle.replace(pattern, match => `${applyFrom}\n\n${match}`); +} diff --git a/plugin/src/withSentryIOS.ts b/plugin/src/withSentryIOS.ts new file mode 100644 index 0000000000..60460fc516 --- /dev/null +++ b/plugin/src/withSentryIOS.ts @@ -0,0 +1,71 @@ +/* eslint-disable @typescript-eslint/no-unsafe-member-access */ +import type { ConfigPlugin, XcodeProject } from 'expo/config-plugins'; +import { WarningAggregator, withDangerousMod, withXcodeProject } from 'expo/config-plugins'; +import * as path from 'path'; + +import { SDK_PACKAGE_NAME, writeSentryPropertiesTo } from './utils'; + +type BuildPhase = { shellScript: string }; + +const SENTRY_REACT_NATIVE_XCODE_PATH = + "`\"$NODE_BINARY\" --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode.sh'\"`"; +const SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH = + "`${NODE_BINARY:-node} --print \"require('path').dirname(require.resolve('@sentry/react-native/package.json')) + '/scripts/sentry-xcode-debug-files.sh'\"`"; + +export const withSentryIOS: ConfigPlugin = (config, sentryProperties: string) => { + const cfg = withXcodeProject(config, config => { + const xcodeProject: XcodeProject = config.modResults; + + const sentryBuildPhase = xcodeProject.pbxItemByComment( + 'Upload Debug Symbols to Sentry', + 'PBXShellScriptBuildPhase', + ); + if (!sentryBuildPhase) { + xcodeProject.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_DEBUG_FILES_PATH}`, + }); + } + + const bundleReactNativePhase = xcodeProject.pbxItemByComment( + 'Bundle React Native code and images', + 'PBXShellScriptBuildPhase', + ); + modifyExistingXcodeBuildScript(bundleReactNativePhase); + + return config; + }); + + return withDangerousMod(cfg, [ + 'ios', + config => { + writeSentryPropertiesTo(path.resolve(config.modRequest.projectRoot, 'ios'), sentryProperties); + return config; + }, + ]); +}; + +export function modifyExistingXcodeBuildScript(script: BuildPhase): void { + if ( + !script.shellScript.match(/(packager|scripts)\/react-native-xcode\.sh\b/) || + script.shellScript.includes('sentry-xcode.sh') || + script.shellScript.includes('@sentry') + ) { + WarningAggregator.addWarningIOS( + SDK_PACKAGE_NAME, + "Unable to modify build script 'Bundle React Native code and images'. Please open a bug report at https://github.com/expo/sentry-expo.", + ); + return; + } + + const code = JSON.parse(script.shellScript); + script.shellScript = JSON.stringify(addSentryWithBundledScriptsToBundleShellScript(code)); +} + +export function addSentryWithBundledScriptsToBundleShellScript(script: string): string { + return script.replace( + /^.*?(packager|scripts)\/react-native-xcode\.sh\s*(\\'\\\\")?/m, + // eslint-disable-next-line no-useless-escape + (match: string) => `/bin/sh ${SENTRY_REACT_NATIVE_XCODE_PATH} ${match}`, + ); +} diff --git a/plugin/tsconfig.json b/plugin/tsconfig.json new file mode 100644 index 0000000000..354bddb433 --- /dev/null +++ b/plugin/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "expo-module-scripts/tsconfig.plugin", + "compilerOptions": { + "outDir": "build", + "rootDir": "src" + }, + "include": ["./src"], + "exclude": ["**/__mocks__/*", "**/__tests__/*"] +} diff --git a/samples/expo/.gitignore b/samples/expo/.gitignore new file mode 100644 index 0000000000..f534ebd0c8 --- /dev/null +++ b/samples/expo/.gitignore @@ -0,0 +1,45 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ + +# Native +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# Generated by expo-cli +/android +/ios + +# @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb +# The following patterns were generated by expo-cli + +expo-env.d.ts +# @end expo-cli \ No newline at end of file diff --git a/samples/expo/app.json b/samples/expo/app.json new file mode 100644 index 0000000000..59cc1cbcf2 --- /dev/null +++ b/samples/expo/app.json @@ -0,0 +1,52 @@ +{ + "expo": { + "name": "sentry-react-native-expo-sample", + "slug": "sentry-react-native-expo-sample", + "jsEngine": "hermes", + "scheme": "sentry-expo-sample", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "splash": { + "image": "./assets/splash.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "assetBundlePatterns": [ + "**/*" + ], + "ios": { + "supportsTablet": true, + "bundleIdentifier": "io.sentry.expo.sample" + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + }, + "package": "io.sentry.expo.sample" + }, + "web": { + "bundler": "metro", + "output": "static", + "favicon": "./assets/favicon.png" + }, + "experiments": { + "tsconfigPaths": true, + "typedRoutes": true + }, + "plugins": [ + [ + "../../expo", + { + "url": "https://sentry.io/", + "project": "sentry-react-native", + "organization": "sentry-sdks" + } + ], + "./withSampleSentry.js", + "expo-router" + ] + } +} diff --git a/samples/expo/app/(tabs)/_layout.tsx b/samples/expo/app/(tabs)/_layout.tsx new file mode 100644 index 0000000000..30914fbc0c --- /dev/null +++ b/samples/expo/app/(tabs)/_layout.tsx @@ -0,0 +1,59 @@ +import React from 'react'; +import FontAwesome from '@expo/vector-icons/FontAwesome'; +import { Link, Tabs } from 'expo-router'; +import { Pressable } from 'react-native'; + +import Colors from '@/constants/Colors'; +import { useColorScheme } from '@/components/useColorScheme'; +import { useClientOnlyValue } from '@/components/useClientOnlyValue'; + +// You can explore the built-in icon families and icons on the web at https://icons.expo.fyi/ +function TabBarIcon(props: { + name: React.ComponentProps['name']; + color: string; +}) { + return ; +} + +export default function TabLayout() { + const colorScheme = useColorScheme(); + + return ( + + , + headerRight: () => ( + + + {({ pressed }) => ( + + )} + + + ), + }} + /> + , + }} + /> + + ); +} diff --git a/samples/expo/app/(tabs)/index.tsx b/samples/expo/app/(tabs)/index.tsx new file mode 100644 index 0000000000..a849a5b465 --- /dev/null +++ b/samples/expo/app/(tabs)/index.tsx @@ -0,0 +1,145 @@ +import { Button, StyleSheet } from 'react-native'; +import Constants from 'expo-constants'; +import * as Sentry from '@sentry/react-native'; + +import { Text, View } from '@/components/Themed'; +import { SENTRY_INTERNAL_DSN } from '@/utils/dsn'; +import { HttpClient } from '@sentry/integrations'; +import { setScopeProperties } from '@/utils/setScopeProperties'; + +const isRunningInExpoGo = Constants.appOwnership === 'expo' + +Sentry.init({ + // Replace the example DSN below with your own DSN: + dsn: SENTRY_INTERNAL_DSN, + debug: true, + environment: 'dev', + beforeSend: (event: Sentry.Event) => { + console.log('Event beforeSend:', event.event_id); + return event; + }, + beforeSendTransaction(event) { + console.log('Transaction beforeSend:', event.event_id); + return event; + }, + // This will be called with a boolean `didCallNativeInit` when the native SDK has been contacted. + onReady: ({ didCallNativeInit }) => { + console.log('onReady called with didCallNativeInit:', didCallNativeInit); + }, + integrations(integrations) { + integrations.push( + new HttpClient({ + // These options are effective only in JS. + // This array can contain tuples of `[begin, end]` (both inclusive), + // Single status codes, or a combinations of both. + // default: [[500, 599]] + failedRequestStatusCodes: [[400, 599]], + // This array can contain Regexes or strings, or combinations of both. + // default: [/.*/] + failedRequestTargets: [/.*/], + }), + ); + return integrations.filter(i => i.name !== 'Dedupe'); + }, + enableAutoSessionTracking: true, + // For testing, session close when 5 seconds (instead of the default 30) in the background. + sessionTrackingIntervalMillis: 5000, + // This will capture ALL TRACES and likely use up all your quota + enableTracing: true, + tracesSampleRate: 1.0, + tracePropagationTargets: ['localhost', /^\//, /^https:\/\//, /^http:\/\//], + attachStacktrace: true, + // Attach screenshots to events. + attachScreenshot: true, + // Attach view hierarchy to events. + attachViewHierarchy: true, + // Enables capture failed requests in JS and native. + enableCaptureFailedRequests: true, + // Sets the `release` and `dist` on Sentry events. Make sure this matches EXACTLY with the values on your sourcemaps + // otherwise they will not work. + // release: 'myapp@1.2.3+1', + // dist: `1`, + _experiments: { + profilesSampleRate: 0, + }, +}); + +export default function TabOneScreen() { + return ( + + Welcome to Sentry Expo Sample App! +