diff --git a/CHANGELOG.md b/CHANGELOG.md index 75f0cbb74d..609ba94f27 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,261 @@ > [!IMPORTANT] -> If you are upgrading to the `6.x` versions of the Sentry React Native SDK from `5.x` or below, +> If you are upgrading to the `7.x` versions of the Sentry React Native SDK from `6.x` or below, > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Important Changes + +- The `_experiments.enableLogs` and `_experiments.beforeSendLog` options were removed, use the top-level `enableLogs` and `beforeSendLog` options instead. ([#5122](https://github.com/getsentry/sentry-react-native/pull/5122)) + +```js +// before +Sentry.init({ + _experiments: { + enableLogs: true, + beforeSendLog: log => { + return log; + }, + }, +}); + +// after +Sentry.init({ + enableLogs: true, + beforeSendLog: log => { + return log; + }, +}); +``` + +### Dependencies + +- Bump JavaScript SDK from v10.7.0 to v10.8.0 ([#5123](https://github.com/getsentry/sentry-react-native/pull5123)) + - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#1080) + - [diff](https://github.com/getsentry/sentry-javascript/compare/10.7.0...10.8.0) + +## 7.0.0-rc.2 + +### Important Changes + +- Ensure IP address is only inferred by Relay if `sendDefaultPii` is `true` ([#5111](https://github.com/getsentry/sentry-react-native/pull/5111)) + +This release includes a fix for a [behaviour change](https://docs.sentry.io/platforms/javascript/migration/v8-to-v9/#behavior-changes) +that was originally introduced with v9 of the JavaScript SDK included in v7.0.0-beta.0: User IP Addresses should only be added to Sentry events automatically, +if `sendDefaultPii` was set to `true`. + +We apologize for any inconvenience caused! + +### Features + +- Logs now contains more attributes like release, os and device information ([#5032](https://github.com/getsentry/sentry-react-native/pull/5032)) + +### Dependencies + +- Bump Android SDK from v8.17.0 to v8.20.0 ([#5034](https://github.com/getsentry/sentry-react-native/pull/5034), [#5063](https://github.com/getsentry/sentry-react-native/pull/5063), [#5106](https://github.com/getsentry/sentry-react-native/pull/5106)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8200) + - [diff](https://github.com/getsentry/sentry-java/compare/8.17.0...8.20.0) +- Bump JavaScript SDK from v9.22.0 to v10.7.0 ([#5111](https://github.com/getsentry/sentry-react-native/pull/5111)) + - [changelog](https://github.com/getsentry/sentry-javascript/blob/develop/CHANGELOG.md#1070) + - [diff](https://github.com/getsentry/sentry-javascript/compare/9.22.0...10.7.0) + + +## 7.0.0-rc.1 + +### Various fixes & improvements + +- fix(sdk): Set `{{auto}}` if `user.ip_address` is `undefined` and `sendDefaultPii: true` (#4466) by @krystofwoldrich +- fix(appStart): Align span description with other platforms (#4636) by @krystofwoldrich + +## 7.0.0-beta.2 + +### Features + +- Automatically detect Release name and version for Expo Web ([#4967](https://github.com/getsentry/sentry-react-native/pull/4967)) + +### Changes + +- Expose `featureFlagsIntegration` ([#4984](https://github.com/getsentry/sentry-react-native/pull/4984)) + +### Breaking changes + +- Tags formatting logic updated ([#4965](https://github.com/getsentry/sentry-react-native/pull/4965)) +Here are the altered/unaltered types, make sure to update your UI filters and alerts. + + Unaltered: string, null, number, and undefined values remain unchanged. + + Altered: Boolean values are now capitalized: true -> True, false -> False. + +### Fixes + +- tags with symbol are now logged ([#4965](https://github.com/getsentry/sentry-react-native/pull/4965)) +- ignoreError now filters Native errors ([#4948](https://github.com/getsentry/sentry-react-native/pull/4948)) + +You can use strings to filter errors or RegEx for filtering with a pattern. + +example: + +```typescript + ignoreErrors: [ + '1234', // Will filter any error message that contains 1234. + '.*1234', // Will not filter as regex, instead will filter messages that contains '.*1234" + /.*1234/, // Regex will filter any error message that ends with 1234 + /.*1234.*/ // Regex will filter any error message that contains 1234. + ] +``` + +### Dependencies + +- Bump Android SDK from v8.14.0 to v8.17.0 ([#4953](https://github.com/getsentry/sentry-react-native/pull/4953), [#4955](https://github.com/getsentry/sentry-react-native/pull/4955), [#4987](https://github.com/getsentry/sentry-react-native/pull/4987)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8170) + - [diff](https://github.com/getsentry/sentry-java/compare/8.14.0...8.17.0) + +## 7.0.0-beta.1 + +### Upgrading from 6.x to 7.0 + +Version 7 of the Sentry React Native SDK primarily introduces API cleanup and version support changes based on the Sentry Javascript SDK version 9. This update contains behavioral changes that will not be caught by type checkers, linters, or tests, so we recommend carefully reading through the entire migration guide instead of relying on automatic tooling. + +Version 7 of the SDK is compatible with Sentry self-hosted versions 24.4.2 or higher (unchanged from v6). Lower versions may continue to work, but may not support all features. + +### Major Changes + +- Set `{{auto}}` if `user.ip_address` is `undefined` and `sendDefaultPii: true` ([#4466](https://github.com/getsentry/sentry-react-native/pull/4466)) +- `Sentry.captureUserFeedback` removed, use `Sentry.captureFeedback` instead ([#4855](https://github.com/getsentry/sentry-react-native/pull/4855)) + +### Major Changes from Sentry JS SDK v9 + +- Exceptions from `captureConsoleIntegration` are now marked as handled: true by default +- `shutdownTimeout` moved from `core` to `@sentry/react-native` +- `hasTracingEnabled` was renamed to `hasSpansEnabled` +- You can no longer drop spans or return null on `beforeSendSpan` hook +- Fork `scope` if custom scope is passed to `startSpanManual` or `startSpan` + +#### Removed types + +- TransactionNamingScheme +- Request +- Scope (prefer using the Scope class) + +#### Other removed items. + +- `autoSessionTracking` from options. + To enable session tracking, ensure that `enableAutoSessionTracking` is enabled. +- `enableTracing`. Instead, set `tracesSampleRate` to a value greater than `zero` to `enable tracing`, `0` to keep tracing integrations active without sampling, or `undefined` to disable the performance integration. +- `getCurrentHub()`, `Hub`, and `getCurrentHubShim()` +- `spanId` from propagation `context` +- metrics API +- `transactionContext` from `samplingContext` +- `@sentry/utils` package, the exports were moved to `@sentry/core` +- Standalone `Client` interface & deprecate `BaseClient` + +### Features + +- Add experimental support for Log tracing ([#4827](https://github.com/getsentry/sentry-react-native/pull/4827)) + +To enable it add the following code to your Sentry Options: + +```typescript +Sentry.init({ + // other options... + _experiments: { + enableLogs: true, + }, +}); +``` + +You can also filter the logs being collected by adding beforeSendLogs into `_experiments` + +```typescript +Sentry.init({ + // other options... + _experiments: { + enableLogs: true, + beforeSendLog: (log) => { + return log; + }, + } +}); +``` + +### Changes + +- Expose `logger` and `consoleLoggingIntegration` ([#4930](https://github.com/getsentry/sentry-react-native/pull/4930)) +- Remove deprecated `appOwnership` constant use in Expo Go detection ([#4893](https://github.com/getsentry/sentry-react-native/pull/4893)) +- Disable AppStart and NativeFrames in unsupported environments (web, Expo Go) ([#4897](https://github.com/getsentry/sentry-react-native/pull/4897)) + +### Self Hosted + +- It is recommended to use Sentry Self Hosted version `25.2.0` or new for React Native V7 or newer + +### Dependencies + +- Bump Android SDK from v8.13.2 to v8.14.0 ([#4929](https://github.com/getsentry/sentry-react-native/pull/4929), [#4934](https://github.com/getsentry/sentry-react-native/pull/4934)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8140) + - [diff](https://github.com/getsentry/sentry-java/compare/8.13.2...8.14.0) + +## 7.0.0-beta.0 + +### Upgrading from 6.x to 7.0 + +Version 7 of the Sentry React Native SDK primarily introduces API cleanup and version support changes based on the Sentry Javascript SDK version 9. This update contains behavioral changes that will not be caught by type checkers, linters, or tests, so we recommend carefully reading through the entire migration guide instead of relying on automatic tooling. + +Version 7 of the SDK is compatible with Sentry self-hosted versions 24.4.2 or higher (unchanged from v6). Lower versions may continue to work, but may not support all features. + +### Major Changes + +- Set `{{auto}}` if `user.ip_address` is `undefined` and `sendDefaultPii: true` ([#4466](https://github.com/getsentry/sentry-react-native/pull/4466)) +- `Sentry.captureUserFeedback` removed, use `Sentry.captureFeedback` instead ([#4855](https://github.com/getsentry/sentry-react-native/pull/4855)) + +### Major Changes from Sentry JS SDK v9 + +- Exceptions from `captureConsoleIntegration` are now marked as handled: true by default +- `shutdownTimeout` moved from `core` to `@sentry/react-native` +- `hasTracingEnabled` was renamed to `hasSpansEnabled` +- You can no longer drop spans or return null on `beforeSendSpan` hook +- Fork `scope` if custom scope is passed to `startSpanManual` or `startSpan` + +#### Removed types + +- TransactionNamingScheme +- Request +- Scope (prefer using the Scope class) + +#### Other removed items. + +- `autoSessionTracking` from options. + To enable session tracking, ensure that `enableAutoSessionTracking` is enabled. +- `enableTracing`. Instead, set `tracesSampleRate` to a value greater than `zero` to `enable tracing`, `0` to keep tracing integrations active without sampling, or `undefined` to disable the performance integration. +- `getCurrentHub()`, `Hub`, and `getCurrentHubShim()` +- `spanId` from propagation `context` +- metrics API +- `transactionContext` from `samplingContext` +- `@sentry/utils` package, the exports were moved to `@sentry/core` +- Standalone `Client` interface & deprecate `BaseClient` + +### Changes + +- Use `Replay` interface for `browserReplayIntegration` return type ([#4858](https://github.com/getsentry/sentry-react-native/pull/4858)) +- Allow using `browserReplayIntegration` without `isWeb` guard ([#4858](https://github.com/getsentry/sentry-react-native/pull/4858)) + - The integration returns noop in non-browser environments +- Use single `encodeUTF8` implementation through the SDK ([#4885](https://github.com/getsentry/sentry-react-native/pull/4885)) +- Use global `TextEncoder` (available with Hermes in React Native 0.74 or higher) to improve envelope encoding performance. ([#4874](https://github.com/getsentry/sentry-react-native/pull/4874)) +- `breadcrumbsIntegration` disables React Native incompatible options automatically ([#4886](https://github.com/getsentry/sentry-react-native/pull/4886)) +- On React Native Web, `browserSessionIntegration` is added when `enableAutoSessionTracking` is set to `True` ([#4732](https://github.com/getsentry/sentry-react-native/pull/4732)) +- Change `Cold/Warm App Start` span description to `Cold/Warm Start` ([#4636](https://github.com/getsentry/sentry-react-native/pull/4636)) + +### Dependencies + +- Bump JavaScript SDK from v8.54.0 to v9.22.0 ([#4568](https://github.com/getsentry/sentry-react-native/pull/4568), [#4752](https://github.com/getsentry/sentry-react-native/pull/4752), [#4860](https://github.com/getsentry/sentry-react-native/pull/4860)) + - [changelog](https://github.com/getsentry/sentry-javascript/blob/9.22.0/CHANGELOG.md) + - [diff](https://github.com/getsentry/sentry-javascript/compare/8.54.0...9.22.0) +- Bump Android SDK from v7.20.1 to v8.13.2 ([#4490](https://github.com/getsentry/sentry-react-native/pull/4490), [#4847](https://github.com/getsentry/sentry-react-native/pull/4847)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8132) + - [diff](https://github.com/getsentry/sentry-java/compare/7.20.1...8.13.2) + ## 6.21.0 ### Important Changes @@ -247,6 +498,71 @@ We apologize for any inconvenience caused! - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2460) - [diff](https://github.com/getsentry/sentry-cli/compare/2.45.0...2.46.0) +## 7.0.0-alpha.0 + +### Upgrading from 6.x to 7.0 + +Version 7 of the Sentry React Native SDK primarily introduces API cleanup and version support changes based on the Sentry Javascript SDK version 9. This update contains behavioral changes that will not be caught by type checkers, linters, or tests, so we recommend carefully reading through the entire migration guide instead of relying on automatic tooling. + +Version 7 of the SDK is compatible with Sentry self-hosted versions 24.4.2 or higher (unchanged from v6). Lower versions may continue to work, but may not support all features. + +### Fixes + +- Expo Updates Context is passed to native after native init to be available for crashes ([#4808](https://github.com/getsentry/sentry-react-native/pull/4808)) +- Expo Updates Context values should all be lowercase ([#4809](https://github.com/getsentry/sentry-react-native/pull/4809)) +- Avoid duplicate network requests (fetch, xhr) by default ([#4816](https://github.com/getsentry/sentry-react-native/pull/4816)) + - `traceFetch` is disabled by default on mobile as RN uses a polyfill which will be traced by `traceXHR` + +### Major Changes + +- Set `{{auto}}` if `user.ip_address` is `undefined` and `sendDefaultPii: true` ([#4466](https://github.com/getsentry/sentry-react-native/pull/4466)) +- Exceptions from `captureConsoleIntegration` are now marked as handled: true by default +- `shutdownTimeout` moved from `core` to `@sentry/react-native` +- `hasTracingEnabled` was renamed to `hasSpansEnabled` +- You can no longer drop spans or return null on `beforeSendSpan` hook + +### Removed types + +- TransactionNamingScheme +- Request +- Scope (prefer using the Scope class) + +### Other removed items. + +- `autoSessionTracking` from options. + To enable session tracking, ensure that `enableAutoSessionTracking` is enabled. +- `enableTracing`. Instead, set `tracesSampleRate` to a value greater than `zero` to `enable tracing`, `0` to keep tracing integrations active without sampling, or `undefined` to disable the performance integration. +- `getCurrentHub()`, `Hub`, and `getCurrentHubShim()` +- `spanId` from propagation `context` +- metrics API +- `transactionContext` from `samplingContext` +- `@sentry/utils` package, the exports were moved to `@sentry/core` +- Standalone `Client` interface & deprecate `BaseClient` + +### Other Changes + +- Fork `scope` if custom scope is passed to `startSpanManual` or `startSpan` +- On React Native Web, `browserSessionIntegration` is added when `enableAutoSessionTracking` is set to `True` ([#4732](https://github.com/getsentry/sentry-react-native/pull/4732)) +- Change `Cold/Warm App Start` span description to `Cold/Warm Start` ([#4636](https://github.com/getsentry/sentry-react-native/pull/4636)) + +### Dependencies + +- Bump JavaScript SDK from v8.54.0 to v9.12.0 ([#4568](https://github.com/getsentry/sentry-react-native/pull/4568), [#4752](https://github.com/getsentry/sentry-react-native/pull/4752)) + - [changelog](https://github.com/getsentry/sentry-javascript/blob/9.12.0/CHANGELOG.md) + - [diff](https://github.com/getsentry/sentry-javascript/compare/8.54.0...9.12.0) +- Bump Android SDK from v7.20.1 to v8.11.1 ([#4490](https://github.com/getsentry/sentry-react-native/pull/4490)) + - [changelog](https://github.com/getsentry/sentry-java/blob/main/CHANGELOG.md#8111) + - [diff](https://github.com/getsentry/sentry-java/compare/7.20.1...8.11.1) +- Bump CLI from v2.43.1 to v2.45.0 ([#4804](https://github.com/getsentry/sentry-react-native/pull/4804), [#4818](https://github.com/getsentry/sentry-react-native/pull/4818)) + - [changelog](https://github.com/getsentry/sentry-cli/blob/master/CHANGELOG.md#2450) + - [diff](https://github.com/getsentry/sentry-cli/compare/2.43.1...2.45.0) +- Bump Bundler Plugins from v3.3.1 to v3.4.0 ([#4805](https://github.com/getsentry/sentry-react-native/pull/4805)) + - [changelog](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/CHANGELOG.md#340) + - [diff](https://github.com/getsentry/sentry-javascript-bundler-plugins/compare/3.3.1...3.4.0) +- Bump Cocoa SDK from v8.49.2 to v8.50.0 ([#4807](https://github.com/getsentry/sentry-react-native/pull/4807)) + - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8500) + - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.49.2...8.50.0) + ## 6.14.0 ### Fixes diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 68ef9e8b76..0dc2896344 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-e2e-tests", - "version": "6.21.0", + "version": "7.0.0-rc.2", "private": true, "description": "Sentry React Native End to End Tests Library", "main": "dist/index.js", @@ -13,8 +13,8 @@ "devDependencies": { "@babel/preset-env": "^7.25.3", "@babel/preset-typescript": "^7.18.6", - "@sentry/core": "8.55.0", - "@sentry/react-native": "6.21.0", + "@sentry/core": "10.8.0", + "@sentry/react-native": "7.0.0-rc.2", "@types/node": "^20.9.3", "@types/react": "^18.2.64", "appium": "2.4.1", diff --git a/dev-packages/e2e-tests/patch-scripts/rn.patch.app.build.gradle.js b/dev-packages/e2e-tests/patch-scripts/rn.patch.app.build.gradle.js index 8eec66240b..c6ae9712af 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.app.build.gradle.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.app.build.gradle.js @@ -4,15 +4,15 @@ const fs = require('fs'); const { argv } = require('process'); const parseArgs = require('minimist'); -const { logger } = require('@sentry/core'); -logger.enable(); +const { debug } = require('@sentry/core'); +debug.enable(); const args = parseArgs(argv.slice(2)); if (!args['app-build-gradle']) { throw new Error('Missing --app-build-gradle'); } -logger.info('Patching app/build.gradle', args['app-build-gradle']); +debug.log('Patching app/build.gradle', args['app-build-gradle']); const sentryGradlePatch = ` apply from: new File(["node", "--print", "require.resolve('@sentry/react-native/package.json')"].execute().text.trim(), "../sentry.gradle") @@ -26,7 +26,7 @@ if (!isPatched) { const patched = buildGradle.replace(reactNativeGradleRex, m => sentryGradlePatch + m); fs.writeFileSync(args['app-build-gradle'], patched); - logger.info('Patched app/build.gradle successfully!'); + debug.log('Patched app/build.gradle successfully!'); } else { - logger.info('app/build.gradle is already patched!'); + debug.log('app/build.gradle is already patched!'); } diff --git a/dev-packages/e2e-tests/patch-scripts/rn.patch.app.js b/dev-packages/e2e-tests/patch-scripts/rn.patch.app.js index 3fd9f5f825..2a6ac3b14d 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.app.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.app.js @@ -5,8 +5,8 @@ const path = require('path'); const { argv, env } = require('process'); const parseArgs = require('minimist'); -const { logger } = require('@sentry/core'); -logger.enable(); +const { debug } = require('@sentry/core'); +debug.enable(); const SENTRY_RELEASE = env.SENTRY_RELEASE; const SENTRY_DIST = env.SENTRY_DIST; @@ -16,7 +16,7 @@ if (!args.app) { throw new Error('Missing --app'); } -logger.info('Patching RN App.(js|tsx)', args.app); +debug.log('Patching RN App.(js|tsx)', args.app); const initPatch = ` import * as Sentry from '@sentry/react-native'; @@ -59,7 +59,7 @@ if (!isPatched) { .replace(exportDefaultRex, 'export default Sentry.wrap(App);'); fs.writeFileSync(appPath, patched); - logger.info('Patched RN App.(js|tsx) successfully!'); + debug.log('Patched RN App.(js|tsx) successfully!'); } else { - logger.info('App.(js|tsx) already patched!'); + debug.log('App.(js|tsx) already patched!'); } diff --git a/dev-packages/e2e-tests/patch-scripts/rn.patch.gradle.properties.js b/dev-packages/e2e-tests/patch-scripts/rn.patch.gradle.properties.js index 2292e52a3e..3d7ac3ff92 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.gradle.properties.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.gradle.properties.js @@ -4,8 +4,8 @@ const fs = require('fs'); const { argv } = require('process'); const parseArgs = require('minimist'); -const { logger } = require('@sentry/core'); -logger.enable(); +const { debug } = require('@sentry/core'); +debug.enable(); const args = parseArgs(argv.slice(2)); if (!args['gradle-properties']) { @@ -21,7 +21,7 @@ if (enableHermes === null) { throw new Error('Invalid engine'); } -logger.info('Patching gradle.properties', args['gradle-properties']); +debug.log('Patching gradle.properties', args['gradle-properties']); let content = fs.readFileSync(args['gradle-properties'], 'utf8'); const isHermesEnabled = content.includes('hermesEnabled=true'); @@ -31,9 +31,9 @@ if (enableHermes !== isHermesEnabled) { ? content.replace(/hermesEnabled=.*/g, patch) : content.concat(`\n${patch}`); if (enableHermes) { - logger.info('Patching gradle.properties for Hermes'); + debug.log('Patching gradle.properties for Hermes'); } else { - logger.info('Patching gradle.properties for JSC'); + debug.log('Patching gradle.properties for JSC'); } } diff --git a/dev-packages/e2e-tests/patch-scripts/rn.patch.metro.config.js b/dev-packages/e2e-tests/patch-scripts/rn.patch.metro.config.js index b92d2a983b..ea648ec449 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.metro.config.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.metro.config.js @@ -4,15 +4,15 @@ const fs = require('fs'); const { argv } = require('process'); const parseArgs = require('minimist'); -const { logger } = require('@sentry/core'); -logger.enable(); +const { debug } = require('@sentry/core'); +debug.enable(); const args = parseArgs(argv.slice(2)); if (!args.path) { throw new Error('Missing --path'); } -logger.info('Patching Metro config: ', args.path); +debug.log('Patching Metro config: ', args.path); const configFilePath = args.path; @@ -52,7 +52,7 @@ if (!isPatched) { } fs.writeFileSync(configFilePath, config.join('\n'), 'utf8'); - logger.info('Patched Metro config successfully!'); + debug.log('Patched Metro config successfully!'); } else { - logger.info('Metro config already patched!'); + debug.log('Metro config already patched!'); } diff --git a/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js b/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js index 44aed0693c..db43b54b54 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.podfile.js @@ -4,8 +4,8 @@ const fs = require('fs'); const { argv } = require('process'); const parseArgs = require('minimist'); -const { logger } = require('@sentry/core'); -logger.enable(); +const { debug } = require('@sentry/core'); +debug.enable(); const args = parseArgs(argv.slice(2)); if (!args['pod-file']) { @@ -21,7 +21,7 @@ if (enableHermes === null) { throw new Error('Invalid engine'); } -logger.info('Patching Podfile', args['pod-file']); +debug.log('Patching Podfile', args['pod-file']); const content = fs.readFileSync(args['pod-file'], 'utf8'); const isHermesEnabled = content.includes(':hermes_enabled => true,'); @@ -32,11 +32,11 @@ if (shouldPatch) { enableHermes ? ':hermes_enabled => true,' : ':hermes_enabled => false,', ); if (enableHermes) { - logger.info('Patching Podfile for Hermes'); + debug.log('Patching Podfile for Hermes'); } else { - logger.info('Patching Podfile for JSC'); + debug.log('Patching Podfile for JSC'); } fs.writeFileSync(args['pod-file'], patched); } else { - logger.info('Podfile is already patched!'); + debug.log('Podfile is already patched!'); } diff --git a/dev-packages/e2e-tests/patch-scripts/rn.patch.xcode.js b/dev-packages/e2e-tests/patch-scripts/rn.patch.xcode.js index d044817df2..072a885720 100755 --- a/dev-packages/e2e-tests/patch-scripts/rn.patch.xcode.js +++ b/dev-packages/e2e-tests/patch-scripts/rn.patch.xcode.js @@ -6,8 +6,8 @@ const { argv } = require('process'); const xcode = require('xcode'); const parseArgs = require('minimist'); const semver = require('semver'); -const { logger } = require('@sentry/core'); -logger.enable(); +const { debug } = require('@sentry/core'); +debug.enable(); const args = parseArgs(argv.slice(2)); if (!args.project) { @@ -17,7 +17,7 @@ if (!args['rn-version']) { throw new Error('Missing --rn-version'); } -logger.info('Patching Xcode project', args.project, 'for RN version', args['rn-version']); +debug.log('Patching Xcode project', args.project, 'for RN version', args['rn-version']); const newBundleScriptRNVersion = '0.69.0-rc.0'; @@ -29,7 +29,7 @@ const symbolsScript = ` `; const symbolsPatchRegex = /sentry-cli\s+(upload-dsym|debug-files upload)/; if (semver.satisfies(args['rn-version'], `< ${newBundleScriptRNVersion}`, { includePrerelease: true })) { - logger.info('Applying old bundle script patch'); + debug.log('Applying old bundle script patch'); bundleScript = ` export NODE_BINARY=node ../node_modules/@sentry/react-native/scripts/sentry-xcode.sh ../node_modules/react-native/scripts/react-native-xcode.sh @@ -37,7 +37,7 @@ export NODE_BINARY=node bundleScriptRegex = /(packager|scripts)\/react-native-xcode\.sh\b/; bundlePatchRegex = /sentry-cli\s+react-native[\s-]xcode/; } else if (semver.satisfies(args['rn-version'], `>= ${newBundleScriptRNVersion}`, { includePrerelease: true })) { - logger.info('Applying new bundle script patch'); + debug.log('Applying new bundle script patch'); bundleScript = ` WITH_ENVIRONMENT="../node_modules/react-native/scripts/xcode/with-environment.sh" REACT_NATIVE_XCODE="../node_modules/react-native/scripts/react-native-xcode.sh" @@ -62,38 +62,32 @@ for (const key in buildPhasesRaw) { } } -buildPhases.forEach((phase) => { +buildPhases.forEach(phase => { const isBundleReactNative = phase.shellScript.match(bundleScriptRegex); const isPatched = phase.shellScript.match(bundlePatchRegex); if (!isBundleReactNative) { return; } if (isPatched) { - logger.warn('Xcode project Bundle RN Build phase already patched'); + debug.warn('Xcode project Bundle RN Build phase already patched'); return; } phase.shellScript = JSON.stringify(bundleScript); - logger.info('Patched Xcode project Bundle RN Build phase'); + debug.log('Patched Xcode project Bundle RN Build phase'); }); -const isSymbolsPhase = (phase) => phase.shellScript.match(symbolsPatchRegex); +const isSymbolsPhase = phase => phase.shellScript.match(symbolsPatchRegex); const areSymbolsPatched = buildPhases.some(isSymbolsPhase); if (!areSymbolsPatched) { - project.addBuildPhase( - [], - 'PBXShellScriptBuildPhase', - 'Upload Debug Symbols to Sentry', - null, - { - shellPath: '/bin/sh', - shellScript: symbolsScript, - }, - ); - logger.info('Added Xcode project Upload Debug Symbols Build phase'); + project.addBuildPhase([], 'PBXShellScriptBuildPhase', 'Upload Debug Symbols to Sentry', null, { + shellPath: '/bin/sh', + shellScript: symbolsScript, + }); + debug.log('Added Xcode project Upload Debug Symbols Build phase'); } else { - logger.warn('Xcode project Upload Debug Symbols Build phase already patched'); + debug.warn('Xcode project Upload Debug Symbols Build phase already patched'); } fs.writeFileSync(args.project, project.writeSync()); -logger.info('Patched Xcode project successfully!'); +debug.log('Patched Xcode project successfully!'); diff --git a/dev-packages/type-check/package.json b/dev-packages/type-check/package.json index aaf9863e94..b8b94ba738 100644 --- a/dev-packages/type-check/package.json +++ b/dev-packages/type-check/package.json @@ -1,7 +1,7 @@ { "name": "sentry-react-native-type-check", "private": true, - "version": "6.21.0", + "version": "7.0.0-rc.2", "scripts": { "type-check": "./run-type-check.sh" } diff --git a/dev-packages/type-check/ts3.8-test/index.ts b/dev-packages/type-check/ts3.8-test/index.ts index 1e9fda3cd2..d6cc248482 100644 --- a/dev-packages/type-check/ts3.8-test/index.ts +++ b/dev-packages/type-check/ts3.8-test/index.ts @@ -3,6 +3,8 @@ declare global { interface IDBObjectStore {} interface Window { fetch: any; + setTimeout: any; + document: any; } interface ShadowRoot {} interface BufferSource {} @@ -19,6 +21,8 @@ declare global { redirectCount: number; } interface PerformanceEntry {} + interface Performance {} + interface PerformanceNavigationTiming {} } declare module 'react-native' { diff --git a/dev-packages/type-check/ts3.8-test/tsconfig.build.json b/dev-packages/type-check/ts3.8-test/tsconfig.build.json index 28d4363cb9..5b4837604c 100644 --- a/dev-packages/type-check/ts3.8-test/tsconfig.build.json +++ b/dev-packages/type-check/ts3.8-test/tsconfig.build.json @@ -3,7 +3,7 @@ "index.ts", ], "compilerOptions": { - "skipLibCheck": false, + "skipLibCheck": true, "noEmit": true, "importHelpers": true, "types": [], diff --git a/dev-packages/utils/package.json b/dev-packages/utils/package.json index 074b111e12..f18f9d1731 100644 --- a/dev-packages/utils/package.json +++ b/dev-packages/utils/package.json @@ -1,6 +1,6 @@ { "name": "sentry-react-native-samples-utils", - "version": "6.21.0", + "version": "7.0.0-rc.2", "description": "Internal Samples Utils", "main": "index.js", "license": "MIT", diff --git a/lerna.json b/lerna.json index b8f95da961..c4f3d1b0fd 100644 --- a/lerna.json +++ b/lerna.json @@ -1,6 +1,6 @@ { "$schema": "node_modules/lerna/schemas/lerna-schema.json", - "version": "6.21.0", + "version": "7.0.0-rc.2", "packages": [ "packages/*", "dev-packages/*", diff --git a/packages/core/.eslintrc.js b/packages/core/.eslintrc.js index 7bfc28b1e7..3ad06277ab 100644 --- a/packages/core/.eslintrc.js +++ b/packages/core/.eslintrc.js @@ -40,6 +40,7 @@ module.exports = { '@typescript-eslint/no-empty-function': 'off', '@typescript-eslint/no-explicit-any': 'off', '@typescript-eslint/unbound-method': 'off', + 'import/first': 'off', }, }, { diff --git a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt index 11b477a0b6..63b10812fe 100644 --- a/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt +++ b/packages/core/RNSentryAndroidTester/app/src/test/java/io/sentry/react/RNSentryModuleImplTest.kt @@ -185,4 +185,91 @@ class RNSentryModuleImplTest { assertEquals(breadcrumb, result) } + + @Test + fun `trySetIgnoreErrors sets only regex patterns`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "ignoreErrorsRegex", + com.facebook.react.bridge.JavaOnlyArray + .of("^Foo.*", "Bar$"), + ) + module.trySetIgnoreErrors(options, rnOptions) + assertEquals(listOf("^Foo.*", "Bar$"), options.ignoredErrors!!.map { it.filterString }) + } + + @Test + fun `trySetIgnoreErrors sets only string patterns`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "ignoreErrorsStr", + com.facebook.react.bridge.JavaOnlyArray + .of("ExactError", "AnotherError"), + ) + module.trySetIgnoreErrors(options, rnOptions) + assertEquals(listOf(".*\\QExactError\\E.*", ".*\\QAnotherError\\E.*"), options.ignoredErrors!!.map { it.filterString }) + } + + @Test + fun `trySetIgnoreErrors sets both regex and string patterns`() { + val options = SentryAndroidOptions() + val rnOptions = + JavaOnlyMap.of( + "ignoreErrorsRegex", + com.facebook.react.bridge.JavaOnlyArray + .of("^Foo.*"), + "ignoreErrorsStr", + com.facebook.react.bridge.JavaOnlyArray + .of("ExactError"), + ) + module.trySetIgnoreErrors(options, rnOptions) + assertEquals(listOf("^Foo.*", ".*\\QExactError\\E.*"), options.ignoredErrors!!.map { it.filterString }) + } + + @Test + fun `trySetIgnoreErrors sets nothing if neither is present`() { + val options = SentryAndroidOptions() + val rnOptions = JavaOnlyMap.of() + module.trySetIgnoreErrors(options, rnOptions) + assertNull(options.ignoredErrors) + } + + @Test + fun `trySetIgnoreErrors with string containing regex special characters should match literally if Pattern_quote is used`() { + val options = SentryAndroidOptions() + val special = "I like chocolate (and tomato)." + val rnOptions = + JavaOnlyMap.of( + "ignoreErrorsStr", + com.facebook.react.bridge.JavaOnlyArray + .of(special), + ) + module.trySetIgnoreErrors(options, rnOptions) + + assertEquals(listOf(".*\\QI like chocolate (and tomato).\\E.*"), options.ignoredErrors!!.map { it.filterString }) + + val regex = Regex(options.ignoredErrors!![0].filterString) + assertTrue(regex.matches("I like chocolate (and tomato).")) + assertTrue(regex.matches(" I like chocolate (and tomato). ")) + assertTrue(regex.matches("I like chocolate (and tomato). And vanilla.")) + } + + @Test + fun `trySetIgnoreErrors with string containing star should not match everything if Pattern_quote is used`() { + val options = SentryAndroidOptions() + val special = "Error*WithStar" + val rnOptions = + JavaOnlyMap.of( + "ignoreErrorsStr", + com.facebook.react.bridge.JavaOnlyArray + .of(special), + ) + module.trySetIgnoreErrors(options, rnOptions) + assertEquals(listOf(".*\\QError*WithStar\\E.*"), options.ignoredErrors!!.map { it.filterString }) + + val regex = Regex(options.ignoredErrors!![0].filterString) + assertTrue(regex.matches("Error*WithStar")) + } } diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift index 3b8765547f..ff3dccba6f 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryReplayOptionsTests.swift @@ -192,7 +192,7 @@ final class RNSentryReplayOptions: XCTestCase { XCTAssertEqual(actualOptions.sessionReplay.maskedViewClasses.count, 0) } - func testEnableViewRendererV2Default() { + func testEnableExperimentalViewRendererDefault() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", "replaysOnErrorSampleRate": 0.75 @@ -206,10 +206,10 @@ final class RNSentryReplayOptions: XCTestCase { let actualOptions = try! Options(dict: optionsDict as! [String: Any]) #endif - XCTAssertTrue(actualOptions.sessionReplay.enableViewRendererV2) + XCTAssertTrue(actualOptions.sessionReplay.enableExperimentalViewRenderer) } - func testEnableViewRendererV2True() { + func testEnableExperimentalViewRendererTrue() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", "replaysOnErrorSampleRate": 0.75, @@ -227,7 +227,7 @@ final class RNSentryReplayOptions: XCTestCase { XCTAssertTrue(actualOptions.sessionReplay.enableViewRendererV2) } - func testEnableViewRendererV2False() { + func testEnableExperimentalViewRendererFalse() { let optionsDict = ([ "dsn": "https://abc@def.ingest.sentry.io/1234567", "replaysOnErrorSampleRate": 0.75, diff --git a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m index 2020a8e5a3..b7940cad2b 100644 --- a/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m +++ b/packages/core/RNSentryCocoaTester/RNSentryCocoaTesterTests/RNSentryTests.m @@ -582,4 +582,119 @@ - (void)testFetchNativeStackFramesByInstructionsOnDeviceSymbolication XCTAssertTrue([actual isEqualToDictionary:expected]); } +- (void)testIgnoreErrorsDropsMatchingExceptionValue +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + NSDictionary *mockedOptions = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"ignoreErrorsRegex" : @[ @"IgnoreMe.*" ] + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedOptions error:&error]; + XCTAssertNotNil(options); + XCTAssertNil(error); + SentryEvent *event = [[SentryEvent alloc] init]; + SentryException *exception = [SentryException alloc]; + exception.value = @"IgnoreMe: This should be ignored"; + event.exceptions = @[ exception ]; + SentryEvent *result = options.beforeSend(event); + XCTAssertNil(result, @"Event with matching exception.value should be dropped"); +} + +- (void)testIgnoreErrorsDropsMatchingEventMessage +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + NSDictionary *mockedOptions = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"ignoreErrorsStr" : @[ @"DropThisError" ] + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedOptions error:&error]; + XCTAssertNotNil(options); + XCTAssertNil(error); + SentryEvent *event = [[SentryEvent alloc] init]; + SentryMessage *msg = [SentryMessage alloc]; + msg.message = @"DropThisError: should be dropped"; + event.message = msg; + SentryEvent *result = options.beforeSend(event); + XCTAssertNil(result, @"Event with matching event.message.formatted should be dropped"); +} + +- (void)testIgnoreErrorsDoesNotDropNonMatchingEvent +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + NSDictionary *mockedOptions = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"ignoreErrorsRegex" : @[ @"IgnoreMe.*" ] + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedOptions error:&error]; + XCTAssertNotNil(options); + XCTAssertNil(error); + SentryEvent *event = [[SentryEvent alloc] init]; + SentryException *exception = [SentryException alloc]; + exception.value = @"SomeOtherError: should not be ignored"; + event.exceptions = @[ exception ]; + SentryMessage *msg = [SentryMessage alloc]; + msg.message = @"SomeOtherMessage"; + event.message = msg; + SentryEvent *result = options.beforeSend(event); + XCTAssertNotNil(result, @"Event with non-matching error should not be dropped"); +} + +- (void)testIgnoreErrorsDropsMatchingExactString +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + NSDictionary *mockedOptions = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"ignoreErrorsStr" : @[ @"ExactError" ] + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedOptions error:&error]; + XCTAssertNotNil(options); + XCTAssertNil(error); + SentryEvent *event = [[SentryEvent alloc] init]; + SentryMessage *msg = [SentryMessage alloc]; + msg.message = @"ExactError"; + event.message = msg; + SentryEvent *result = options.beforeSend(event); + XCTAssertNil(result, @"Event with exactly matching string should be dropped"); +} + +- (void)testIgnoreErrorsRegexAndStringBothWork +{ + RNSentry *rnSentry = [[RNSentry alloc] init]; + NSError *error = nil; + NSDictionary *mockedOptions = @{ + @"dsn" : @"https://abc@def.ingest.sentry.io/1234567", + @"ignoreErrorsStr" : @[ @"ExactError" ], + @"ignoreErrorsRegex" : @[ @"IgnoreMe.*" ], + + }; + SentryOptions *options = [rnSentry createOptionsWithDictionary:mockedOptions error:&error]; + XCTAssertNotNil(options); + XCTAssertNil(error); + // Test regex match + SentryEvent *event1 = [[SentryEvent alloc] init]; + SentryException *exception = [SentryException alloc]; + exception.value = @"IgnoreMe: This should be ignored"; + event1.exceptions = @[ exception ]; + SentryEvent *result1 = options.beforeSend(event1); + XCTAssertNil(result1, @"Event with matching regex should be dropped"); + // Test exact string match + SentryEvent *event2 = [[SentryEvent alloc] init]; + SentryMessage *msg = [SentryMessage alloc]; + msg.message = @"ExactError"; + event2.message = msg; + SentryEvent *result2 = options.beforeSend(event2); + XCTAssertNil(result2, @"Event with exactly matching string should be dropped"); + // Test non-matching + SentryEvent *event3 = [[SentryEvent alloc] init]; + SentryMessage *msg3 = [SentryMessage alloc]; + msg3.message = @"OtherError"; + event3.message = msg3; + SentryEvent *result3 = options.beforeSend(event3); + XCTAssertNotNil(result3, @"Event with non-matching error should not be dropped"); +} + @end diff --git a/packages/core/android/build.gradle b/packages/core/android/build.gradle index 59d92f087f..0cc5290d84 100644 --- a/packages/core/android/build.gradle +++ b/packages/core/android/build.gradle @@ -54,5 +54,5 @@ android { dependencies { implementation 'com.facebook.react:react-native:+' - api 'io.sentry:sentry-android:7.22.6' + api 'io.sentry:sentry-android:8.20.0' } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java index ca6ecadacd..8ae3f204b8 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryModuleImpl.java @@ -31,12 +31,12 @@ import com.facebook.react.bridge.WritableNativeMap; import com.facebook.react.common.JavascriptException; import io.sentry.Breadcrumb; -import io.sentry.HubAdapter; import io.sentry.ILogger; import io.sentry.IScope; import io.sentry.ISentryExecutorService; import io.sentry.ISerializer; import io.sentry.Integration; +import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryDate; import io.sentry.SentryDateProvider; @@ -84,13 +84,16 @@ import java.net.URI; import java.net.URISyntaxException; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.CountDownLatch; +import java.util.regex.Pattern; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.TestOnly; @@ -280,6 +283,9 @@ protected void getSentryAndroidOptions( if (rnOptions.hasKey("enableNdk")) { options.setEnableNdk(rnOptions.getBoolean("enableNdk")); } + if (rnOptions.hasKey("enableLogs")) { + options.getLogs().setEnabled(rnOptions.getBoolean("enableLogs")); + } if (rnOptions.hasKey("spotlight")) { if (rnOptions.getType("spotlight") == ReadableType.Boolean) { options.setEnableSpotlight(rnOptions.getBoolean("spotlight")); @@ -315,6 +321,8 @@ protected void getSentryAndroidOptions( // we want to ignore it on the native side to avoid sending it twice. options.addIgnoredExceptionForType(JavascriptException.class); + trySetIgnoreErrors(options, rnOptions); + options.setBeforeSend( (event, hint) -> { setEventOriginTag(event); @@ -557,7 +565,7 @@ public void fetchNativeFrames(Promise promise) { } public void captureReplay(boolean isHardCrash, Promise promise) { - Sentry.getCurrentHub().getOptions().getReplayController().captureReplay(isHardCrash); + Sentry.getCurrentScopes().getOptions().getReplayController().captureReplay(isHardCrash); promise.resolve(getCurrentReplayId()); } @@ -653,7 +661,7 @@ public void fetchViewHierarchy(Promise promise) { return; } - ISerializer serializer = HubAdapter.getInstance().getOptions().getSerializer(); + ISerializer serializer = ScopesAdapter.getInstance().getOptions().getSerializer(); final @Nullable byte[] bytes = JsonSerializationUtils.bytesFrom(serializer, logger, viewHierarchy); if (bytes == null) { @@ -707,10 +715,6 @@ public void setUser(final ReadableMap userKeys, final ReadableMap userDataKeys) if (userKeys.hasKey("ip_address")) { userInstance.setIpAddress(userKeys.getString("ip_address")); } - - if (userKeys.hasKey("segment")) { - userInstance.setSegment(userKeys.getString("segment")); - } } if (userDataKeys != null) { @@ -872,8 +876,7 @@ private void initializeAndroidProfiler() { (int) SECONDS.toMicros(1) / profilingTracesHz, new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo), executorService, - logger, - buildInfo); + logger); } public WritableMap startProfiling(boolean platformProfilers) { @@ -897,7 +900,7 @@ public WritableMap startProfiling(boolean platformProfilers) { } public WritableMap stopProfiling() { - final boolean isDebug = HubAdapter.getInstance().getOptions().isDebug(); + final boolean isDebug = ScopesAdapter.getInstance().getOptions().isDebug(); final WritableMap result = new WritableNativeMap(); File output = null; try { @@ -982,8 +985,15 @@ private String readStringFromFile(File path) throws IOException { } } + public void fetchNativeLogAttributes(Promise promise) { + final @NotNull SentryOptions options = ScopesAdapter.getInstance().getOptions(); + final @Nullable Context context = this.getReactApplicationContext().getApplicationContext(); + final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope(); + fetchNativeLogContexts(promise, options, context, currentScope); + } + public void fetchNativeDeviceContexts(Promise promise) { - final @NotNull SentryOptions options = HubAdapter.getInstance().getOptions(); + final @NotNull SentryOptions options = ScopesAdapter.getInstance().getOptions(); final @Nullable Context context = this.getReactApplicationContext().getApplicationContext(); final @Nullable IScope currentScope = InternalSentrySdk.getCurrentScope(); fetchNativeDeviceContexts(promise, options, context, currentScope); @@ -1019,8 +1029,50 @@ protected void fetchNativeDeviceContexts( promise.resolve(deviceContext); } + // Basically fetchNativeDeviceContexts but filtered to only get contexts info. + protected void fetchNativeLogContexts( + Promise promise, + final @NotNull SentryOptions options, + final @Nullable Context osContext, + final @Nullable IScope currentScope) { + if (!(options instanceof SentryAndroidOptions) || osContext == null) { + promise.resolve(null); + return; + } + + Object contextsObj = + InternalSentrySdk.serializeScope(osContext, (SentryAndroidOptions) options, currentScope) + .get("contexts"); + + if (!(contextsObj instanceof Map)) { + promise.resolve(null); + return; + } + + @SuppressWarnings("unchecked") + Map contextsMap = (Map) contextsObj; + + Map contextItems = new HashMap<>(); + if (contextsMap.containsKey("os")) { + contextItems.put("os", contextsMap.get("os")); + } + + if (contextsMap.containsKey("device")) { + contextItems.put("device", contextsMap.get("device")); + } + + contextItems.put("release", options.getRelease()); + + Map logContext = new HashMap<>(); + logContext.put("contexts", contextItems); + Object filteredContext = RNSentryMapConverter.convertToWritable(logContext); + + promise.resolve(filteredContext); + } + public void fetchNativeSdkInfo(Promise promise) { - final @Nullable SdkVersion sdkVersion = HubAdapter.getInstance().getOptions().getSdkVersion(); + final @Nullable SdkVersion sdkVersion = + ScopesAdapter.getInstance().getOptions().getSdkVersion(); if (sdkVersion == null) { promise.resolve(null); } else { @@ -1108,14 +1160,14 @@ private void addPackages(SentryEvent event, SdkVersion sdk) { if (eventSdk != null && "sentry.javascript.react-native".equals(eventSdk.getName()) && sdk != null) { - List sentryPackages = sdk.getPackages(); + Set sentryPackages = sdk.getPackageSet(); if (sentryPackages != null) { for (SentryPackage sentryPackage : sentryPackages) { eventSdk.addPackage(sentryPackage.getName(), sentryPackage.getVersion()); } } - List integrations = sdk.getIntegrations(); + Set integrations = sdk.getIntegrationSet(); if (integrations != null) { for (String integration : integrations) { eventSdk.addIntegration(integration); @@ -1152,4 +1204,36 @@ private boolean isFrameMetricsAggregatorAvailable() { } return uri.getScheme() + "://" + uri.getHost(); } + + @TestOnly + protected void trySetIgnoreErrors(SentryAndroidOptions options, ReadableMap rnOptions) { + ReadableArray regErrors = null; + ReadableArray strErrors = null; + if (rnOptions.hasKey("ignoreErrorsRegex")) { + regErrors = rnOptions.getArray("ignoreErrorsRegex"); + } + if (rnOptions.hasKey("ignoreErrorsStr")) { + strErrors = rnOptions.getArray("ignoreErrorsStr"); + } + if (regErrors == null && strErrors == null) { + return; + } + + int regSize = regErrors != null ? regErrors.size() : 0; + int strSize = strErrors != null ? strErrors.size() : 0; + List list = new ArrayList<>(regSize + strSize); + if (regErrors != null) { + for (int i = 0; i < regErrors.size(); i++) { + list.add(regErrors.getString(i)); + } + } + if (strErrors != null) { + // Use the same behaviour of JavaScript instead of Android when dealing with strings. + for (int i = 0; i < strErrors.size(); i++) { + String pattern = ".*" + Pattern.quote(strErrors.getString(i)) + ".*"; + list.add(pattern); + } + } + options.setIgnoredErrors(list); + } } diff --git a/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java b/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java index 948661623c..19b2ecba6e 100644 --- a/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java +++ b/packages/core/android/src/main/java/io/sentry/react/RNSentryVersion.java @@ -2,7 +2,7 @@ class RNSentryVersion { static final String REACT_NATIVE_SDK_PACKAGE_NAME = "npm:@sentry/react-native"; - static final String REACT_NATIVE_SDK_PACKAGE_VERSION = "6.21.0"; + static final String REACT_NATIVE_SDK_PACKAGE_VERSION = "7.0.0-rc.2"; static final String NATIVE_SDK_NAME = "sentry.native.android.react-native"; static final String ANDROID_SDK_NAME = "sentry.java.android.react-native"; static final String REACT_NATIVE_SDK_NAME = "sentry.javascript.react-native"; diff --git a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 5b14f05c92..993969d830 100644 --- a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -127,6 +127,11 @@ public void disableNativeFramesTracking() { this.impl.disableNativeFramesTracking(); } + @Override + public void fetchNativeLogAttributes(Promise promise) { + this.impl.fetchNativeLogAttributes(promise); + } + @Override public void fetchNativeDeviceContexts(Promise promise) { this.impl.fetchNativeDeviceContexts(promise); diff --git a/packages/core/ios/RNSentry.mm b/packages/core/ios/RNSentry.mm index 8072422085..df1fd35bc4 100644 --- a/packages/core/ios/RNSentry.mm +++ b/packages/core/ios/RNSentry.mm @@ -71,6 +71,8 @@ @implementation RNSentry { bool sentHybridSdkDidBecomeActive; bool hasListeners; RNSentryTimeToDisplay *_timeToDisplay; + NSArray *_ignoreErrorPatternsStr; + NSArray *_ignoreErrorPatternsRegex; } - (dispatch_queue_t)methodQueue @@ -92,7 +94,6 @@ - (instancetype)init } RCT_EXPORT_MODULE() - RCT_EXPORT_METHOD(initNativeSdk : (NSDictionary *_Nonnull)options resolve : (RCTPromiseResolveBlock)resolve rejecter @@ -137,21 +138,98 @@ - (instancetype)init resolve(@YES); } +- (void)trySetIgnoreErrors:(NSMutableDictionary *)options +{ + NSArray *ignoreErrorsStr = nil; + NSArray *ignoreErrorsRegex = nil; + + id strArr = [options objectForKey:@"ignoreErrorsStr"]; + id regexArr = [options objectForKey:@"ignoreErrorsRegex"]; + if ([strArr isKindOfClass:[NSArray class]]) { + ignoreErrorsStr = (NSArray *)strArr; + } + if ([regexArr isKindOfClass:[NSArray class]]) { + ignoreErrorsRegex = (NSArray *)regexArr; + } + + NSMutableArray *strs = [NSMutableArray array]; + NSMutableArray *regexes = [NSMutableArray array]; + + if (ignoreErrorsStr != nil) { + for (id str in ignoreErrorsStr) { + if ([str isKindOfClass:[NSString class]]) { + [strs addObject:str]; + } + } + } + + if (ignoreErrorsRegex != nil) { + for (id pattern in ignoreErrorsRegex) { + if ([pattern isKindOfClass:[NSString class]]) { + NSError *error = nil; + NSRegularExpression *regex = + [NSRegularExpression regularExpressionWithPattern:pattern + options:0 + error:&error]; + if (regex && error == nil) { + [regexes addObject:regex]; + } + } + } + } + + _ignoreErrorPatternsStr = [strs count] > 0 ? [strs copy] : nil; + _ignoreErrorPatternsRegex = [regexes count] > 0 ? [regexes copy] : nil; +} + +- (BOOL)shouldIgnoreError:(NSString *)message +{ + if ((!_ignoreErrorPatternsStr && !_ignoreErrorPatternsRegex) || !message) { + return NO; + } + + for (NSString *str in _ignoreErrorPatternsStr) { + if ([message containsString:str]) { + return YES; + } + } + + for (NSRegularExpression *regex in _ignoreErrorPatternsRegex) { + NSRange range = NSMakeRange(0, message.length); + if ([regex firstMatchInString:message options:0 range:range]) { + return YES; + } + } + + return NO; +} + - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull)options error:(NSError *_Nonnull *_Nonnull)errorPointer { SentryBeforeSendEventCallback beforeSend = ^SentryEvent *(SentryEvent *event) { // We don't want to send an event after startup that came from a Unhandled JS Exception of - // react native Because we sent it already before the app crashed. + // React Native because we sent it already before the app crashed. if (nil != event.exceptions.firstObject.type && [event.exceptions.firstObject.type rangeOfString:@"Unhandled JS Exception"].location != NSNotFound) { return nil; } - [self setEventOriginTag:event]; + // Regex and Str are set when one of them has value so we only need to check one of them. + if (self->_ignoreErrorPatternsStr || self->_ignoreErrorPatternsRegex) { + for (SentryException *exception in event.exceptions) { + if ([self shouldIgnoreError:exception.value]) { + return nil; + } + } + if ([self shouldIgnoreError:event.message.message]) { + return nil; + } + } + [self setEventOriginTag:event]; return event; }; @@ -222,6 +300,8 @@ - (SentryOptions *_Nullable)createOptionsWithDictionary:(NSDictionary *_Nonnull) } } + [self trySetIgnoreErrors:mutableOptions]; + // Enable the App start and Frames tracking measurements if ([mutableOptions valueForKey:@"enableAutoPerformanceTracing"] != nil) { BOOL enableAutoPerformanceTracing = @@ -364,6 +444,58 @@ - (void)stopObserving return [self fetchNativeStackFramesBy:instructionsAddr symbolicate:dladdr]; } +RCT_EXPORT_METHOD(fetchNativeLogAttributes + : (RCTPromiseResolveBlock)resolve rejecter + : (RCTPromiseRejectBlock)reject) +{ + __block NSMutableDictionary *result = [NSMutableDictionary new]; + + [SentrySDKWrapper configureScope:^(SentryScope *_Nonnull scope) { + // Serialize to get contexts dictionary + NSDictionary *serializedScope = [scope serialize]; + NSDictionary *allContexts = serializedScope[@"context"]; // It's singular here, annoyingly + + NSMutableDictionary *contexts = [NSMutableDictionary new]; + + NSDictionary *device = allContexts[@"device"]; + if ([device isKindOfClass:[NSDictionary class]]) { + contexts[@"device"] = device; + } + + NSDictionary *os = allContexts[@"os"]; + if ([os isKindOfClass:[NSDictionary class]]) { + contexts[@"os"] = os; + } + + NSString *releaseName = [SentrySDK options].releaseName; + if (releaseName) { + contexts[@"release"] = releaseName; + } + // Merge extra context + NSDictionary *extraContext = [PrivateSentrySDKOnly getExtraContext]; + + if (extraContext) { + NSDictionary *extraDevice = extraContext[@"device"]; + if ([extraDevice isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *mergedDevice = + [contexts[@"device"] mutableCopy] ?: [NSMutableDictionary new]; + [mergedDevice addEntriesFromDictionary:extraDevice]; + contexts[@"device"] = mergedDevice; + } + + NSDictionary *extraOS = extraContext[@"os"]; + if ([extraOS isKindOfClass:[NSDictionary class]]) { + NSMutableDictionary *mergedOS = + [contexts[@"os"] mutableCopy] ?: [NSMutableDictionary new]; + [mergedOS addEntriesFromDictionary:extraOS]; + contexts[@"os"] = mergedOS; + } + } + result[@"contexts"] = contexts; + }]; + resolve(result); +} + RCT_EXPORT_METHOD(fetchNativeDeviceContexts : (RCTPromiseResolveBlock)resolve rejecter : (RCTPromiseRejectBlock)reject) diff --git a/packages/core/ios/RNSentryVersion.m b/packages/core/ios/RNSentryVersion.m index 4086729347..8c8f525dc5 100644 --- a/packages/core/ios/RNSentryVersion.m +++ b/packages/core/ios/RNSentryVersion.m @@ -3,4 +3,4 @@ NSString *const NATIVE_SDK_NAME = @"sentry.cocoa.react-native"; NSString *const REACT_NATIVE_SDK_NAME = @"sentry.javascript.react-native"; NSString *const REACT_NATIVE_SDK_PACKAGE_NAME = @"npm:@sentry/react-native"; -NSString *const REACT_NATIVE_SDK_PACKAGE_VERSION = @"6.21.0"; +NSString *const REACT_NATIVE_SDK_PACKAGE_VERSION = @"7.0.0-rc.2"; diff --git a/packages/core/package.json b/packages/core/package.json index ff9ddba51a..fef4408da2 100644 --- a/packages/core/package.json +++ b/packages/core/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": "6.21.0", + "version": "7.0.0-rc.2", "description": "Official Sentry SDK for react-native", "typings": "dist/js/index.d.ts", "types": "dist/js/index.d.ts", @@ -68,21 +68,20 @@ }, "dependencies": { "@sentry/babel-plugin-component-annotate": "4.2.0", - "@sentry/browser": "8.55.0", + "@sentry/browser": "10.8.0", "@sentry/cli": "2.53.0", - "@sentry/core": "8.55.0", - "@sentry/react": "8.55.0", - "@sentry/types": "8.55.0", - "@sentry/utils": "8.55.0" + "@sentry/core": "10.8.0", + "@sentry/react": "10.8.0", + "@sentry/types": "10.8.0" }, "devDependencies": { "@babel/core": "^7.25.2", "@expo/metro-config": "~0.20.0", "@mswjs/interceptors": "^0.25.15", "@react-native/babel-preset": "0.77.1", - "@sentry-internal/eslint-config-sdk": "8.55.0", - "@sentry-internal/eslint-plugin-sdk": "8.55.0", - "@sentry-internal/typescript": "8.55.0", + "@sentry-internal/eslint-config-sdk": "10.8.0", + "@sentry-internal/eslint-plugin-sdk": "10.8.0", + "@sentry-internal/typescript": "10.8.0", "@sentry/wizard": "6.3.0", "@testing-library/react-native": "^12.7.2", "@types/jest": "^29.5.13", @@ -111,7 +110,7 @@ "react": "18.3.1", "react-native": "0.77.1", "rimraf": "^4.1.1", - "ts-jest": "^29.1.1", + "ts-jest": "^29.3.1", "typescript": "4.9.5", "uglify-js": "^3.17.4", "uuid": "^9.0.1", diff --git a/packages/core/plugin/src/withSentry.ts b/packages/core/plugin/src/withSentry.ts index 70d4c8932b..2fdee7f063 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -1,6 +1,5 @@ import type { ConfigPlugin } from 'expo/config-plugins'; import { createRunOncePlugin } from 'expo/config-plugins'; - import { bold, sdkPackage, warnOnce } from './utils'; import { withSentryAndroid } from './withSentryAndroid'; import type { SentryAndroidGradlePluginOptions } from './withSentryAndroidGradlePlugin'; @@ -18,7 +17,7 @@ interface PluginProps { const withSentryPlugin: ConfigPlugin = (config, props) => { const sentryProperties = getSentryProperties(props); - if (props && props.authToken) { + if (props?.authToken) { // If not removed, the plugin config with the authToken will be written to the application package delete props.authToken; } @@ -50,8 +49,9 @@ const withSentryPlugin: ConfigPlugin = (config, props) => { const missingProjectMessage = '# no project found, falling back to SENTRY_PROJECT environment variable'; const missingOrgMessage = '# no org found, falling back to SENTRY_ORG environment variable'; -const existingAuthTokenMessage = `# DO NOT COMMIT the auth token, use SENTRY_AUTH_TOKEN instead, see https://docs.sentry.io/platforms/react-native/manual-setup/`; -const missingAuthTokenMessage = `# Using SENTRY_AUTH_TOKEN environment variable`; +const existingAuthTokenMessage = + '# DO NOT COMMIT the auth token, use SENTRY_AUTH_TOKEN instead, see https://docs.sentry.io/platforms/react-native/manual-setup/'; +const missingAuthTokenMessage = '# Using SENTRY_AUTH_TOKEN environment variable'; export function getSentryProperties(props: PluginProps | void): string | null { const { organization, project, authToken, url = 'https://sentry.io/' } = props ?? {}; diff --git a/packages/core/plugin/src/withSentryAndroid.ts b/packages/core/plugin/src/withSentryAndroid.ts index 9743bb1d6e..83c92b464b 100644 --- a/packages/core/plugin/src/withSentryAndroid.ts +++ b/packages/core/plugin/src/withSentryAndroid.ts @@ -1,7 +1,6 @@ import type { ConfigPlugin } from 'expo/config-plugins'; import { withAppBuildGradle, withDangerousMod } from 'expo/config-plugins'; import * as path from 'path'; - import { warnOnce, writeSentryPropertiesTo } from './utils'; export const withSentryAndroid: ConfigPlugin = (config, sentryProperties: string) => { diff --git a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts index dd8fa719a3..f39529d0ab 100644 --- a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts +++ b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts @@ -1,6 +1,5 @@ import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins'; import type { ExpoConfig } from '@expo/config-types'; - import { warnOnce } from './utils'; export interface SentryAndroidGradlePluginOptions { @@ -35,7 +34,7 @@ export function withSentryAndroidGradlePlugin( // Modify android/build.gradle const withSentryProjectBuildGradle = (config: ExpoConfig): ExpoConfig => { return withProjectBuildGradle(config, projectBuildGradle => { - if (!projectBuildGradle.modResults || !projectBuildGradle.modResults.contents) { + if (!projectBuildGradle.modResults?.contents) { warnOnce('android/build.gradle content is missing or undefined.'); return projectBuildGradle; } @@ -62,7 +61,7 @@ export function withSentryAndroidGradlePlugin( projectBuildGradle.modResults.contents = updatedContents; } } catch (error) { - warnOnce(`An error occurred while trying to modify build.gradle`); + warnOnce('An error occurred while trying to modify build.gradle'); } return projectBuildGradle; }); @@ -75,7 +74,7 @@ export function withSentryAndroidGradlePlugin( warnOnce('Cannot configure Sentry in android/app/build.gradle because it is not in Groovy.'); return appBuildGradle; } - const sentryPlugin = `apply plugin: "io.sentry.android.gradle"`; + const sentryPlugin = 'apply plugin: "io.sentry.android.gradle"'; const sentryConfig = ` sentry { autoUploadProguardMapping = ${autoUploadProguardMapping ? 'shouldSentryAutoUpload()' : 'false'} diff --git a/packages/core/plugin/src/withSentryIOS.ts b/packages/core/plugin/src/withSentryIOS.ts index db25261839..e10f820282 100644 --- a/packages/core/plugin/src/withSentryIOS.ts +++ b/packages/core/plugin/src/withSentryIOS.ts @@ -2,7 +2,6 @@ import type { ConfigPlugin, XcodeProject } from 'expo/config-plugins'; import { withDangerousMod, withXcodeProject } from 'expo/config-plugins'; import * as path from 'path'; - import { warnOnce, writeSentryPropertiesTo } from './utils'; type BuildPhase = { shellScript: string }; diff --git a/packages/core/scripts/expo-upload-sourcemaps.js b/packages/core/scripts/expo-upload-sourcemaps.js index b3783b5721..cc1cd623d1 100755 --- a/packages/core/scripts/expo-upload-sourcemaps.js +++ b/packages/core/scripts/expo-upload-sourcemaps.js @@ -177,7 +177,7 @@ if (!sentryOrg || !sentryProject || !sentryUrl) { console.log(`${SENTRY_URL} resolved to ${sentryUrl} from expo config.`); } else { - sentryUrl = `https://sentry.io/`; + sentryUrl = 'https://sentry.io/'; console.log( `Since it wasn't specified in the Expo config or environment variable, ${SENTRY_URL} now points to ${sentryUrl}.` ); @@ -217,7 +217,7 @@ for (const [assetGroupName, assets] of Object.entries(groupedAssets)) { } const isHermes = assets.find(asset => asset.endsWith('.hbc')); - const windowsCallback = process.platform === "win32" ? 'node ' : ''; + const windowsCallback = process.platform === 'win32' ? 'node ' : ''; execSync(`${windowsCallback}${sentryCliBin} sourcemaps upload ${isHermes ? '--debug-id-reference' : ''} ${assets.join(' ')}`, { env: { ...process.env, diff --git a/packages/core/src/js/NativeRNSentry.ts b/packages/core/src/js/NativeRNSentry.ts index 5b00b62116..cdfbb0d781 100644 --- a/packages/core/src/js/NativeRNSentry.ts +++ b/packages/core/src/js/NativeRNSentry.ts @@ -1,7 +1,6 @@ import type { Package } from '@sentry/core'; import type { TurboModule } from 'react-native'; import { TurboModuleRegistry } from 'react-native'; - import type { UnsafeObject } from './utils/rnlibrariesinterface'; // There has to be only one interface and it has to be named `Spec` @@ -25,6 +24,7 @@ export interface Spec extends TurboModule { fetchNativeRelease(): Promise; fetchNativeSdkInfo(): Promise; fetchNativeDeviceContexts(): Promise; + fetchNativeLogAttributes(): Promise; fetchNativeAppStart(): Promise; fetchNativeFrames(): Promise; initNativeSdk(options: UnsafeObject): Promise; diff --git a/packages/core/src/js/client.ts b/packages/core/src/js/client.ts index 6fcad6e513..fb8e8eea40 100644 --- a/packages/core/src/js/client.ts +++ b/packages/core/src/js/client.ts @@ -10,9 +10,16 @@ import type { TransportMakeRequestResponse, UserFeedback, } from '@sentry/core'; -import { BaseClient, dateTimestampInSeconds, logger, SentryError } from '@sentry/core'; +import { + _INTERNAL_flushLogsBuffer, + addAutoIpAddressToSession, + addAutoIpAddressToUser, + Client, + dateTimestampInSeconds, + debug, + SentryError, +} from '@sentry/core'; import { Alert } from 'react-native'; - import { getDevServer } from './integrations/debugsymbolicatorutils'; import { defaultSdkInfo } from './integrations/sdkinfo'; import { getDefaultSidecarUrl } from './integrations/spotlight'; @@ -25,14 +32,17 @@ import { mergeOutcomes } from './utils/outcome'; import { ReactNativeLibraries } from './utils/rnlibraries'; import { NATIVE } from './wrapper'; +const DEFAULT_FLUSH_INTERVAL = 5000; + /** * The Sentry React Native SDK Client. * * @see ReactNativeClientOptions for documentation on configuration options. * @see SentryClient for usage documentation. */ -export class ReactNativeClient extends BaseClient { +export class ReactNativeClient extends Client { private _outcomesBuffer: Outcome[]; + private _logFlushIdleTimeout: ReturnType | undefined; /** * Creates a new React Native SDK instance. @@ -48,6 +58,27 @@ export class ReactNativeClient extends BaseClient { super(options); this._outcomesBuffer = []; + + if (options.sendDefaultPii === true) { + this.on('postprocessEvent', addAutoIpAddressToUser); + this.on('beforeSendSession', addAutoIpAddressToSession); + } + + if (options.enableLogs) { + this.on('flush', () => { + _INTERNAL_flushLogsBuffer(this); + }); + + this.on('afterCaptureLog', () => { + if (this._logFlushIdleTimeout) { + clearTimeout(this._logFlushIdleTimeout); + } + + this._logFlushIdleTimeout = setTimeout(() => { + _INTERNAL_flushLogsBuffer(this); + }, DEFAULT_FLUSH_INTERVAL); + }); + } } /** @@ -116,13 +147,13 @@ export class ReactNativeClient extends BaseClient { // SentryError is thrown by SyncPromise shouldClearOutcomesBuffer = false; // If this is called asynchronously we want the _outcomesBuffer to be cleared - logger.error('SentryError while sending event, keeping outcomes buffer:', reason); + debug.error('SentryError while sending event, keeping outcomes buffer:', reason); } else { - logger.error('Error while sending event:', reason); + debug.error('Error while sending event:', reason); } }); } else { - logger.error('Transport disabled'); + debug.error('Transport disabled'); } if (shouldClearOutcomesBuffer) { @@ -188,7 +219,7 @@ export class ReactNativeClient extends BaseClient { this.emit('afterInit'); }) .then(undefined, error => { - logger.error('The OnReady callback threw an error: ', error); + debug.error('The OnReady callback threw an error: ', error); }); } diff --git a/packages/core/src/js/feedback/FeedbackButton.tsx b/packages/core/src/js/feedback/FeedbackButton.tsx index fbb546db8d..66ab86b07f 100644 --- a/packages/core/src/js/feedback/FeedbackButton.tsx +++ b/packages/core/src/js/feedback/FeedbackButton.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import type { NativeEventSubscription} from 'react-native'; import { Appearance, Image, Text, TouchableOpacity } from 'react-native'; - import { defaultButtonConfiguration } from './defaults'; import { defaultButtonStyles } from './FeedbackWidget.styles'; import { getTheme } from './FeedbackWidget.theme'; @@ -15,7 +14,7 @@ import { lazyLoadFeedbackIntegration } from './lazy'; * Implements a feedback button that opens the FeedbackForm. */ export class FeedbackButton extends React.Component { - private _themeListener: NativeEventSubscription; + private _themeListener: NativeEventSubscription | undefined; public constructor(props: FeedbackButtonProps) { super(props); @@ -58,8 +57,10 @@ export class FeedbackButton extends React.Component { onPress={showFeedbackWidget} accessibilityLabel={text.triggerAriaLabel} > - - {text.triggerLabel} + + + {text.triggerLabel} + ); } diff --git a/packages/core/src/js/feedback/FeedbackWidget.styles.ts b/packages/core/src/js/feedback/FeedbackWidget.styles.ts index 94df799d21..8620d8c9b3 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.styles.ts +++ b/packages/core/src/js/feedback/FeedbackWidget.styles.ts @@ -1,5 +1,4 @@ import type { ViewStyle } from 'react-native'; - import type { FeedbackWidgetTheme } from './FeedbackWidget.theme'; import type { FeedbackButtonStyles, FeedbackWidgetStyles } from './FeedbackWidget.types'; diff --git a/packages/core/src/js/feedback/FeedbackWidget.theme.ts b/packages/core/src/js/feedback/FeedbackWidget.theme.ts index aa8711a934..602b6bdea3 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.theme.ts +++ b/packages/core/src/js/feedback/FeedbackWidget.theme.ts @@ -1,5 +1,4 @@ import { Appearance } from 'react-native'; - import { getColorScheme, getFeedbackDarkTheme, getFeedbackLightTheme } from './integration'; /** diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index ad029377dd..2725b3747a 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -1,6 +1,6 @@ /* eslint-disable max-lines */ import type { SendFeedbackParams, User } from '@sentry/core'; -import { captureFeedback, getCurrentScope, getGlobalScope, getIsolationScope, lastEventId, logger } from '@sentry/core'; +import { captureFeedback, debug, getCurrentScope, getGlobalScope, getIsolationScope, lastEventId } from '@sentry/core'; import * as React from 'react'; import type { KeyboardTypeOptions , NativeEventSubscription} from 'react-native'; @@ -14,7 +14,6 @@ import { TouchableWithoutFeedback, View } from 'react-native'; - import { isExpoGo, isWeb, notWeb } from '../utils/environment'; import type { Screenshot } from '../wrapper'; import { getDataFromUri, NATIVE } from '../wrapper'; @@ -33,9 +32,7 @@ import { base64ToUint8Array, feedbackAlertDialog, isValidEmail } from './utils' * Implements a feedback form screen that sends feedback to Sentry using Sentry.captureFeedback. */ export class FeedbackWidget extends React.Component { - public static defaultProps: Partial = { - ...defaultConfiguration - } + public static defaultProps = defaultConfiguration; private static _savedState: Omit = { name: '', @@ -46,7 +43,7 @@ export class FeedbackWidget extends React.Component void = () => { const { name, email, description } = this.state; const { onSubmitSuccess, onSubmitError, onFormSubmitted } = this.props; - const text: FeedbackTextConfiguration = this.props; + const text = this.props; const trimmedName = name?.trim(); const trimmedEmail = email?.trim(); const trimmedDescription = description?.trim(); - if ((this.props.isNameRequired && !trimmedName) || (this.props.isEmailRequired && !trimmedEmail) || !trimmedDescription) { + if ( + (this.props.isNameRequired && !trimmedName) || + (this.props.isEmailRequired && !trimmedEmail) || + !trimmedDescription + ) { feedbackAlertDialog(text.errorTitle, text.formError); return; } - if (this.props.shouldValidateEmail && (this.props.isEmailRequired || trimmedEmail.length > 0) && !isValidEmail(trimmedEmail)) { + if ( + this.props.shouldValidateEmail && + (this.props.isEmailRequired || trimmedEmail.length > 0) && + !isValidEmail(trimmedEmail) + ) { feedbackAlertDialog(text.errorTitle, text.emailError); return; } - const attachments = this.state.filename && this.state.attachment - ? [ - { - filename: this.state.filename, - data: this.state.attachment, - }, - ] - : undefined; + const attachments = + this.state.filename && this.state.attachment + ? [ + { + filename: this.state.filename, + data: this.state.attachment, + }, + ] + : undefined; const eventId = lastEventId(); const userFeedback: SendFeedbackParams = { @@ -128,31 +134,36 @@ export class FeedbackWidget extends React.Component void = async () => { if (!this._hasScreenshot()) { - const imagePickerConfiguration: ImagePickerConfiguration = this.props; - if (imagePickerConfiguration.imagePicker) { - const launchImageLibrary = imagePickerConfiguration.imagePicker.launchImageLibraryAsync - // expo-image-picker library is available - ? () => imagePickerConfiguration.imagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], base64: isWeb() }) - // react-native-image-picker library is available - : imagePickerConfiguration.imagePicker.launchImageLibrary - ? () => imagePickerConfiguration.imagePicker.launchImageLibrary({ mediaType: 'photo', includeBase64: isWeb() }) - : null; + const { imagePicker } = this.props; + if (imagePicker) { + const launchImageLibrary = imagePicker.launchImageLibraryAsync + ? // expo-image-picker library is available + () => imagePicker.launchImageLibraryAsync?.({ mediaTypes: ['images'], base64: isWeb() }) + : // react-native-image-picker library is available + imagePicker.launchImageLibrary + ? () => imagePicker.launchImageLibrary?.({ mediaType: 'photo', includeBase64: isWeb() }) + : null; if (!launchImageLibrary) { - logger.warn('No compatible image picker library found. Please provide a valid image picker library.'); + debug.warn('No compatible image picker library found. Please provide a valid image picker library.'); if (__DEV__) { feedbackAlertDialog( 'Development note', @@ -163,31 +174,34 @@ export class FeedbackWidget extends React.Component 0) { + if (result?.assets && result.assets.length > 0) { if (isWeb()) { - const filename = result.assets[0].fileName; - const imageUri = result.assets[0].uri; - const base64 = result.assets[0].base64; - const data = base64ToUint8Array(base64); - if (data != null) { + const filename = result.assets[0]?.fileName; + const imageUri = result.assets[0]?.uri; + const base64 = result.assets[0]?.base64; + const data = base64 ? base64ToUint8Array(base64) : undefined; + if (data) { this.setState({ filename, attachment: data, attachmentUri: imageUri }); } else { - logger.error('Failed to read image data on the web'); + debug.error('Failed to read image data on the web'); } } else { - const filename = result.assets[0].fileName; - const imageUri = result.assets[0].uri; - getDataFromUri(imageUri).then((data) => { - if (data != null) { - this.setState({ filename, attachment: data, attachmentUri: imageUri }); - } else { - this._showImageRetrievalDevelopmentNote(); - logger.error('Failed to read image data from uri:', imageUri); - } - }).catch((error) => { - this._showImageRetrievalDevelopmentNote(); - logger.error('Failed to read image data from uri:', imageUri, 'error: ', error); - }); + const filename = result.assets[0]?.fileName; + const imageUri = result.assets[0]?.uri; + imageUri && + getDataFromUri(imageUri) + .then((data) => { + if (data != null) { + this.setState({ filename, attachment: data, attachmentUri: imageUri }); + } else { + this._showImageRetrievalDevelopmentNote(); + debug.error('Failed to read image data from uri:', imageUri); + } + }) + .catch((error) => { + this._showImageRetrievalDevelopmentNote(); + debug.error('Failed to read image data from uri:', imageUri, 'error: ', error); + }); } } } else { @@ -199,18 +213,18 @@ export class FeedbackWidget extends React.Component { this._showImageRetrievalDevelopmentNote(); - logger.error('Failed to read image data from uri:', uri, 'error: ', error); + debug.error('Failed to read image data from uri:', uri, 'error: ', error); }); }); } } else { this.setState({ filename: undefined, attachment: undefined, attachmentUri: undefined }); } - } + }; /** * Add a listener to the theme change event. @@ -253,7 +267,7 @@ export class FeedbackWidget extends React.Component + > - {text.formTitle} + + {text.formTitle} + {config.showBranding && ( - + )} {config.showName && ( - <> - - {text.nameLabel} - {config.isNameRequired && ` ${text.isRequiredLabel}`} - - this.setState({ name: value })} - /> - + <> + + {text.nameLabel} + {config.isNameRequired && ` ${text.isRequiredLabel}`} + + this.setState({ name: value })} + /> + )} {config.showEmail && ( - <> - - {text.emailLabel} - {config.isEmailRequired && ` ${text.isRequiredLabel}`} - - this.setState({ email: value })} - /> - + <> + + {text.emailLabel} + {config.isEmailRequired && ` ${text.isRequiredLabel}`} + + this.setState({ email: value })} + /> + )} @@ -325,25 +337,20 @@ export class FeedbackWidget extends React.Component this.setState({ description: value })} + onChangeText={value => this.setState({ description: value })} multiline /> {(config.enableScreenshot || imagePickerConfiguration.imagePicker || this._hasScreenshot()) && ( {this.state.attachmentUri && ( - + )} - {!this._hasScreenshot() - ? text.addScreenshotButtonLabel - : text.removeScreenshotButtonLabel} + {!this._hasScreenshot() ? text.addScreenshotButtonLabel : text.removeScreenshotButtonLabel} @@ -358,7 +365,9 @@ export class FeedbackWidget extends React.Component )} - {text.submitButtonLabel} + + {text.submitButtonLabel} + @@ -371,21 +380,23 @@ export class FeedbackWidget extends React.Component { if (screenshot.data != null) { - logger.debug('Setting captured screenshot:', screenshot.filename); - NATIVE.encodeToBase64(screenshot.data).then((base64String) => { - if (base64String != null) { - const dataUri = `data:${screenshot.contentType};base64,${base64String}`; - this.setState({ filename: screenshot.filename, attachment: screenshot.data, attachmentUri: dataUri }); - } else { - logger.error('Failed to read image data from:', screenshot.filename); - } - }).catch((error) => { - logger.error('Failed to read image data from:', screenshot.filename, 'error: ', error); - }); + debug.log('Setting captured screenshot:', screenshot.filename); + NATIVE.encodeToBase64(screenshot.data) + .then(base64String => { + if (base64String != null) { + const dataUri = `data:${screenshot.contentType};base64,${base64String}`; + this.setState({ filename: screenshot.filename, attachment: screenshot.data, attachmentUri: dataUri }); + } else { + debug.error('Failed to read image data from:', screenshot.filename); + } + }) + .catch(error => { + debug.error('Failed to read image data from:', screenshot.filename, 'error: ', error); + }); } else { - logger.error('Failed to read image data from:', screenshot.filename); + debug.error('Failed to read image data from:', screenshot.filename); } - } + }; private _saveFormState = (): void => { FeedbackWidget._savedState = { ...this.state }; diff --git a/packages/core/src/js/feedback/FeedbackWidget.types.ts b/packages/core/src/js/feedback/FeedbackWidget.types.ts index 22b6b0911f..d3878dcfb0 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.types.ts +++ b/packages/core/src/js/feedback/FeedbackWidget.types.ts @@ -21,38 +21,38 @@ export interface FeedbackGeneralConfiguration { * * @default true */ - showBranding?: boolean; + showBranding: boolean; /** * Should the email field be required? */ - isEmailRequired?: boolean; + isEmailRequired: boolean; /** * Should the email field be validated? */ - shouldValidateEmail?: boolean; + shouldValidateEmail: boolean; /** * Should the name field be required? */ - isNameRequired?: boolean; + isNameRequired: boolean; /** * Should the email input field be visible? Note: email will still be collected if set via `Sentry.setUser()` */ - showEmail?: boolean; + showEmail: boolean; /** * Should the name input field be visible? Note: name will still be collected if set via `Sentry.setUser()` */ - showName?: boolean; + showName: boolean; /** * This flag determines whether the "Add Screenshot" button is displayed * @default false */ - enableScreenshot?: boolean; + enableScreenshot: boolean; /** * This flag determines whether the "Take Screenshot" button is displayed @@ -77,32 +77,32 @@ export interface FeedbackTextConfiguration { /** * The label for the Feedback form cancel button that closes dialog */ - cancelButtonLabel?: string; + cancelButtonLabel: string; /** * The label for the Feedback form submit button that sends feedback */ - submitButtonLabel?: string; + submitButtonLabel: string; /** * The title of the Feedback form */ - formTitle?: string; + formTitle: string; /** * Label for the email input */ - emailLabel?: string; + emailLabel: string; /** * Placeholder text for Feedback email input */ - emailPlaceholder?: string; + emailPlaceholder: string; /** * Label for the message input */ - messageLabel?: string; + messageLabel: string; /** * Placeholder text for Feedback message input @@ -112,32 +112,32 @@ export interface FeedbackTextConfiguration { /** * Label for the name input */ - nameLabel?: string; + nameLabel: string; /** * Message after feedback was sent successfully */ - successMessageText?: string; + successMessageText: string; /** * Placeholder text for Feedback name input */ - namePlaceholder?: string; + namePlaceholder: string; /** * Text which indicates that a field is required */ - isRequiredLabel?: string; + isRequiredLabel: string; /** * The label for the button that adds a screenshot */ - addScreenshotButtonLabel?: string; + addScreenshotButtonLabel: string; /** * The label for the button that removes a screenshot */ - removeScreenshotButtonLabel?: string; + removeScreenshotButtonLabel: string; /** * The label for the button that shows the capture screenshot button @@ -147,27 +147,27 @@ export interface FeedbackTextConfiguration { /** * The title of the error dialog */ - errorTitle?: string; + errorTitle: string; /** * The error message when the form is invalid */ - formError?: string; + formError: string; /** * The error message when the email is invalid */ - emailError?: string; + emailError: string; /** * The error message when the capture screenshot fails */ - captureScreenshotError?: string; + captureScreenshotError: string; /** * Message when there is a generic error */ - genericError?: string; + genericError: string; } /** @@ -207,34 +207,34 @@ export interface FeedbackCallbacks { /** * Callback when form is opened */ - onFormOpen?: () => void; + onFormOpen: () => void; /** * Callback when form is closed and not submitted */ - onFormClose?: () => void; + onFormClose: () => void; /** * Callback when a screenshot is added */ - onAddScreenshot?: (addScreenshot: (uri: string) => void) => void; + onAddScreenshot: (addScreenshot: (uri: string) => void) => void; /** * Callback when feedback is successfully submitted * * After this you'll see a SuccessMessage on the screen for a moment. */ - onSubmitSuccess?: (data: FeedbackFormData) => void; + onSubmitSuccess: (data: FeedbackFormData) => void; /** * Callback when feedback is unsuccessfully submitted */ - onSubmitError?: (error: Error) => void; + onSubmitError: (error: Error) => void; /** * Callback when the feedback form is submitted successfully, and the SuccessMessage is complete, or dismissed */ - onFormSubmitted?: () => void; + onFormSubmitted: () => void; } /** diff --git a/packages/core/src/js/feedback/FeedbackWidgetManager.tsx b/packages/core/src/js/feedback/FeedbackWidgetManager.tsx index e554715586..505bf5e6da 100644 --- a/packages/core/src/js/feedback/FeedbackWidgetManager.tsx +++ b/packages/core/src/js/feedback/FeedbackWidgetManager.tsx @@ -1,5 +1,4 @@ -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { isWeb } from '../utils/environment'; import { lazyLoadAutoInjectFeedbackButtonIntegration,lazyLoadAutoInjectFeedbackIntegration, lazyLoadAutoInjectScreenshotButtonIntegration } from './lazy'; @@ -7,6 +6,10 @@ export const PULL_DOWN_CLOSE_THRESHOLD = 200; export const SLIDE_ANIMATION_DURATION = 200; export const BACKGROUND_ANIMATION_DURATION = 200; +const NOOP_SET_VISIBILITY = (): void => { + // No-op +}; + abstract class FeedbackManager { protected static _isVisible = false; protected static _setVisibility: (visible: boolean) => void; @@ -24,11 +27,11 @@ abstract class FeedbackManager { */ public static reset(): void { this._isVisible = false; - this._setVisibility = undefined; + this._setVisibility = NOOP_SET_VISIBILITY; } public static show(): void { - if (this._setVisibility) { + if (this._setVisibility !== NOOP_SET_VISIBILITY) { this._isVisible = true; this._setVisibility(true); } else { @@ -39,7 +42,7 @@ abstract class FeedbackManager { } public static hide(): void { - if (this._setVisibility) { + if (this._setVisibility !== NOOP_SET_VISIBILITY) { this._isVisible = false; this._setVisibility(false); } else { @@ -114,7 +117,7 @@ const resetFeedbackButtonManager = (): void => { const showScreenshotButton = (): void => { if (isWeb()) { - logger.warn('ScreenshotButton is not supported on Web.'); + debug.warn('ScreenshotButton is not supported on Web.'); return; } lazyLoadAutoInjectScreenshotButtonIntegration(); diff --git a/packages/core/src/js/feedback/FeedbackWidgetProvider.tsx b/packages/core/src/js/feedback/FeedbackWidgetProvider.tsx index 9e90ed785f..1c2c8bab73 100644 --- a/packages/core/src/js/feedback/FeedbackWidgetProvider.tsx +++ b/packages/core/src/js/feedback/FeedbackWidgetProvider.tsx @@ -1,14 +1,20 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import * as React from 'react'; import { type NativeEventSubscription, type NativeScrollEvent,type NativeSyntheticEvent, Animated, Appearance, Dimensions, Easing, Modal, PanResponder, Platform, ScrollView, View } from 'react-native'; - import { notWeb } from '../utils/environment'; import { FeedbackButton } from './FeedbackButton'; import { FeedbackWidget } from './FeedbackWidget'; import { modalSheetContainer,modalWrapper, topSpacer } from './FeedbackWidget.styles'; import { getTheme } from './FeedbackWidget.theme'; import type { FeedbackWidgetStyles } from './FeedbackWidget.types'; -import { BACKGROUND_ANIMATION_DURATION,FeedbackButtonManager, FeedbackWidgetManager, PULL_DOWN_CLOSE_THRESHOLD, ScreenshotButtonManager, SLIDE_ANIMATION_DURATION } from './FeedbackWidgetManager'; +import { + BACKGROUND_ANIMATION_DURATION, + FeedbackButtonManager, + FeedbackWidgetManager, + PULL_DOWN_CLOSE_THRESHOLD, + ScreenshotButtonManager, + SLIDE_ANIMATION_DURATION, +} from './FeedbackWidgetManager'; import { getFeedbackButtonOptions, getFeedbackOptions, getScreenshotButtonOptions } from './integration'; import { ScreenshotButton } from './ScreenshotButton'; import { isModalSupported, isNativeDriverSupportedForColorAnimations } from './utils'; @@ -44,7 +50,7 @@ export class FeedbackWidgetProvider extends React.Component { @@ -120,9 +126,9 @@ export class FeedbackWidgetProvider extends React.Component { - logger.info('FeedbackWidgetProvider componentDidUpdate'); + debug.log('FeedbackWidgetProvider componentDidUpdate'); }); } else if (prevState.isVisible && !this.state.isVisible) { this.state.backgroundOpacity.setValue(0); @@ -134,7 +140,7 @@ export class FeedbackWidgetProvider extends React.Component{this.props.children}; } @@ -154,25 +160,36 @@ export class FeedbackWidgetProvider extends React.Component} {isScreenshotButtonVisible && } - {isVisible && + {isVisible && ( - + + {...this._panResponder.panHandlers} + > - + + onFormSubmitted={this._handleClose} + /> - } + + )} ); } @@ -198,7 +215,7 @@ export class FeedbackWidgetProvider extends React.Component { // Change of the state unmount the component // which would cancel the animation diff --git a/packages/core/src/js/feedback/ScreenshotButton.tsx b/packages/core/src/js/feedback/ScreenshotButton.tsx index 40eae6168f..7f5bffd334 100644 --- a/packages/core/src/js/feedback/ScreenshotButton.tsx +++ b/packages/core/src/js/feedback/ScreenshotButton.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import type { NativeEventSubscription} from 'react-native'; import { Appearance, Image, Text, TouchableOpacity } from 'react-native'; - import type { Screenshot } from '../wrapper'; import { NATIVE } from '../wrapper'; import { defaultScreenshotButtonConfiguration } from './defaults'; @@ -38,7 +37,7 @@ export const getCapturedScreenshot = (): Screenshot | 'ErrorCapturingScreenshot' * Implements a screenshot button that takes a screenshot. */ export class ScreenshotButton extends React.Component { - private _themeListener: NativeEventSubscription; + private _themeListener: NativeEventSubscription | undefined; public constructor(props: ScreenshotButtonProps) { super(props); @@ -78,7 +77,7 @@ export class ScreenshotButton extends React.Component { return ( diff --git a/packages/core/src/js/feedback/defaults.ts b/packages/core/src/js/feedback/defaults.ts index 2158b69a41..59f2092f9f 100644 --- a/packages/core/src/js/feedback/defaults.ts +++ b/packages/core/src/js/feedback/defaults.ts @@ -23,7 +23,7 @@ const CAPTURE_SCREENSHOT_LABEL = 'Take a screenshot'; const REMOVE_SCREENSHOT_LABEL = 'Remove screenshot'; const GENERIC_ERROR_TEXT = 'Unable to send feedback due to an unexpected error.'; -export const defaultConfiguration: Partial = { +export const defaultConfiguration: FeedbackWidgetProps = { // FeedbackCallbacks onFormOpen: () => { // Does nothing by default diff --git a/packages/core/src/js/feedback/integration.ts b/packages/core/src/js/feedback/integration.ts index d450422aa3..7182205278 100644 --- a/packages/core/src/js/feedback/integration.ts +++ b/packages/core/src/js/feedback/integration.ts @@ -1,5 +1,4 @@ import { type Integration, getClient } from '@sentry/core'; - import type { FeedbackWidgetTheme } from './FeedbackWidget.theme'; import type { FeedbackButtonProps, FeedbackWidgetProps, ScreenshotButtonProps } from './FeedbackWidget.types'; @@ -15,7 +14,7 @@ type FeedbackIntegration = Integration & { }; export const feedbackIntegration = ( - initOptions: FeedbackWidgetProps & { + initOptions: Partial & { buttonOptions?: FeedbackButtonProps; screenshotButtonOptions?: ScreenshotButtonProps; colorScheme?: 'system' | 'light' | 'dark'; @@ -43,7 +42,7 @@ export const feedbackIntegration = ( }; }; -const _getClientIntegration = (): FeedbackIntegration => { +const _getClientIntegration = (): FeedbackIntegration | undefined => { return getClient()?.getIntegrationByName>(MOBILE_FEEDBACK_INTEGRATION_NAME); }; @@ -76,7 +75,7 @@ export const getScreenshotButtonOptions = (): Partial => export const getColorScheme = (): 'system' | 'light' | 'dark' => { const integration = _getClientIntegration(); - if (!integration) { + if (!integration?.colorScheme) { return 'system'; } diff --git a/packages/core/src/js/feedback/lazy.ts b/packages/core/src/js/feedback/lazy.ts index c3d2b2727d..6bfad02f56 100644 --- a/packages/core/src/js/feedback/lazy.ts +++ b/packages/core/src/js/feedback/lazy.ts @@ -1,5 +1,4 @@ import { getClient } from '@sentry/core'; - import { feedbackIntegration, MOBILE_FEEDBACK_INTEGRATION_NAME } from './integration'; /** diff --git a/packages/core/src/js/feedback/utils.ts b/packages/core/src/js/feedback/utils.ts index 6644bd7468..be839957ae 100644 --- a/packages/core/src/js/feedback/utils.ts +++ b/packages/core/src/js/feedback/utils.ts @@ -1,5 +1,4 @@ import { Alert } from 'react-native'; - import { isFabricEnabled, isWeb } from '../utils/environment'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; import { ReactNativeLibraries } from './../utils/rnlibraries'; @@ -15,7 +14,7 @@ declare global { */ export function isModalSupported(): boolean { const { major, minor } = ReactNativeLibraries.ReactNativeVersion?.version || {}; - return !(isFabricEnabled() && major === 0 && minor < 71); + return !(isFabricEnabled() && major === 0 && minor && minor < 71); } /** @@ -24,7 +23,7 @@ export function isModalSupported(): boolean { */ export function isNativeDriverSupportedForColorAnimations(): boolean { const { major, minor } = ReactNativeLibraries.ReactNativeVersion?.version || {}; - return major > 0 || minor >= 69; + return (major && major > 0) || (minor && minor >= 69) || false; } export const isValidEmail = (email: string): boolean => { diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 1e5a0f68ad..4a475a33c1 100644 --- a/packages/core/src/js/index.ts +++ b/packages/core/src/js/index.ts @@ -1,6 +1,5 @@ export type { Breadcrumb, - Request, SdkInfo, Event, Exception, @@ -46,7 +45,6 @@ export { getClient, setCurrentClient, addEventProcessor, - metricsDefault as metrics, lastEventId, } from '@sentry/core'; @@ -59,13 +57,20 @@ export { withProfiler, } from '@sentry/react'; +export { + logger, + consoleLoggingIntegration, + featureFlagsIntegration, + type FeatureFlagsIntegration, +} from '@sentry/browser'; + export * from './integrations/exports'; export { SDK_NAME, SDK_VERSION } from './version'; export type { ReactNativeOptions } from './options'; export { ReactNativeClient } from './client'; -export { init, wrap, nativeCrash, flush, close, captureUserFeedback, withScope, crashedLastRun } from './sdk'; +export { init, wrap, nativeCrash, flush, close, withScope, crashedLastRun } from './sdk'; export { TouchEventBoundary, withTouchEventBoundary } from './touchevents'; export { diff --git a/packages/core/src/js/integrations/appRegistry.ts b/packages/core/src/js/integrations/appRegistry.ts index 2467d73876..73041354c0 100644 --- a/packages/core/src/js/integrations/appRegistry.ts +++ b/packages/core/src/js/integrations/appRegistry.ts @@ -1,6 +1,5 @@ import type { Client, Integration } from '@sentry/core'; -import { getClient, logger } from '@sentry/core'; - +import { debug, getClient } from '@sentry/core'; import { isWeb } from '../utils/environment'; import { fillTyped } from '../utils/fill'; import { ReactNativeLibraries } from '../utils/rnlibraries'; @@ -23,7 +22,7 @@ export const appRegistryIntegration = (): Integration & { }, onRunApplication: (callback: () => void) => { if (callbacks.includes(callback)) { - logger.debug('[AppRegistryIntegration] Callback already registered.'); + debug.log('[AppRegistryIntegration] Callback already registered.'); return; } callbacks.push(callback); diff --git a/packages/core/src/js/integrations/breadcrumbs.ts b/packages/core/src/js/integrations/breadcrumbs.ts new file mode 100644 index 0000000000..da3b57eceb --- /dev/null +++ b/packages/core/src/js/integrations/breadcrumbs.ts @@ -0,0 +1,74 @@ +import { breadcrumbsIntegration as browserBreadcrumbsIntegration } from '@sentry/browser'; +import type { Integration } from '@sentry/core'; +import { isWeb } from '../utils/environment'; + +interface BreadcrumbsOptions { + /** + * Log calls to console.log, console.debug, and so on. + */ + console: boolean; + + /** + * Log all click and keypress events. + * + * Only available on web. In React Native this is a no-op. + */ + dom: + | boolean + | { + serializeAttribute?: string | string[]; + maxStringLength?: number; + }; + + /** + * Log HTTP requests done with the global Fetch API. + * + * Disabled by default in React Native because fetch is built on XMLHttpRequest. + * Enabled by default on web. + * + * Setting `fetch: true` and `xhr: true` will cause duplicates in React Native. + */ + fetch: boolean; + + /** + * Log calls to history.pushState and related APIs. + * + * Only available on web. In React Native this is a no-op. + */ + history: boolean; + + /** + * Log whenever we send an event to the server. + */ + sentry: boolean; + + /** + * Log HTTP requests done with the XHR API. + * + * Because React Native global fetch is built on XMLHttpRequest, + * this will also log `fetch` network requests. + * + * Setting `fetch: true` and `xhr: true` will cause duplicates in React Native. + */ + xhr: boolean; +} + +export const breadcrumbsIntegration = (options: Partial = {}): Integration => { + const _options: BreadcrumbsOptions = { + // FIXME: In mobile environment XHR is implemented by native APIs, which are instrumented by the Native SDK. + // This will cause duplicates in React Native. On iOS `NSURLSession` is instrumented by default. On Android + // `OkHttp` is only instrumented by SAGP. + xhr: true, + console: true, + sentry: true, + ...options, + fetch: options.fetch ?? (isWeb() ? true : false), + dom: isWeb() ? options.dom ?? true : false, + history: isWeb() ? options.history ?? true : false, + }; + + // Historically we had very little issue using the browser breadcrumbs integration + // and thus we don't cherry pick the implementation like for example the Sentry Deno SDK does. + // https://github.com/getsentry/sentry-javascript/blob/d007407c2e51d93d6d3933f9dea1e03ff3f4a4ab/packages/deno/src/integrations/breadcrumbs.ts#L34 + return browserBreadcrumbsIntegration(_options); +}; diff --git a/packages/core/src/js/integrations/debugsymbolicator.ts b/packages/core/src/js/integrations/debugsymbolicator.ts index 8529d0eeb6..8daafd0005 100644 --- a/packages/core/src/js/integrations/debugsymbolicator.ts +++ b/packages/core/src/js/integrations/debugsymbolicator.ts @@ -1,6 +1,5 @@ import type { Event, EventHint, Exception, Integration, StackFrame as SentryStackFrame } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import type { ExtendedError } from '../utils/error'; import { getFramesToPop, isErrorLike } from '../utils/error'; import type * as ReactNative from '../vendor/react-native'; @@ -71,7 +70,7 @@ async function symbolicate(rawStack: string, skipFirstFrames: number = 0): Promi const prettyStack = await symbolicateStackTrace(parsedStack); if (!prettyStack) { - logger.error('React Native DevServer could not symbolicate the stack trace.'); + debug.error('React Native DevServer could not symbolicate the stack trace.'); return null; } @@ -93,7 +92,7 @@ async function symbolicate(rawStack: string, skipFirstFrames: number = 0): Promi return await fetchSourceContext(sentryFrames); } catch (error) { if (error instanceof Error) { - logger.warn(`Unable to symbolicate stack trace: ${error.message}`); + debug.warn(`Unable to symbolicate stack trace: ${error.message}`); } return null; } @@ -131,7 +130,7 @@ async function convertReactNativeFramesToSentryFrames(frames: ReactNative.StackF * @param event Event * @param frames StackFrame[] */ -function replaceExceptionFramesInException(exception: Exception, frames: SentryStackFrame[]): void { +function replaceExceptionFramesInException(exception: Exception | undefined, frames: SentryStackFrame[]): void { if (exception?.stacktrace) { exception.stacktrace.frames = frames.reverse(); } @@ -143,7 +142,7 @@ function replaceExceptionFramesInException(exception: Exception, frames: SentryS * @param frames StackFrame[] */ function replaceThreadFramesInEvent(event: Event, frames: SentryStackFrame[]): void { - if (event.threads && event.threads.values && event.threads.values[0] && event.threads.values[0].stacktrace) { + if (event.threads?.values?.[0]?.stacktrace) { event.threads.values[0].stacktrace.frames = frames.reverse(); } } diff --git a/packages/core/src/js/integrations/debugsymbolicatorutils.ts b/packages/core/src/js/integrations/debugsymbolicatorutils.ts index 2b51171b39..18c595efca 100644 --- a/packages/core/src/js/integrations/debugsymbolicatorutils.ts +++ b/packages/core/src/js/integrations/debugsymbolicatorutils.ts @@ -1,6 +1,5 @@ import type { StackFrame as SentryStackFrame } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { ReactNativeLibraries } from '../utils/rnlibraries'; import { createStealthXhr, XHR_READYSTATE_DONE } from '../utils/xhr'; import type * as ReactNative from '../vendor/react-native'; @@ -20,7 +19,14 @@ export async function fetchSourceContext(frames: SentryStackFrame[]): Promise { // Ensures native errors and crashes have the same context as JS errors NATIVE.setContext(OTA_UPDATES_CONTEXT_KEY, expoUpdates); } catch (error) { - logger.error('Error setting Expo updates context:', error); + debug.error('Error setting Expo updates context:', error); } } diff --git a/packages/core/src/js/integrations/exports.ts b/packages/core/src/js/integrations/exports.ts index e87a88c615..4f4d0fb0ac 100644 --- a/packages/core/src/js/integrations/exports.ts +++ b/packages/core/src/js/integrations/exports.ts @@ -22,9 +22,11 @@ export { userInteractionIntegration } from '../tracing/integrations/userInteract export { createReactNativeRewriteFrames } from './rewriteframes'; export { appRegistryIntegration } from './appRegistry'; export { timeToDisplayIntegration } from '../tracing/integrations/timeToDisplayIntegration'; +export { breadcrumbsIntegration } from './breadcrumbs'; +export { primitiveTagIntegration } from './primitiveTagIntegration'; +export { logEnricherIntegration } from './logEnricherIntegration'; export { - breadcrumbsIntegration, browserApiErrorsIntegration, dedupeIntegration, functionToStringIntegration, diff --git a/packages/core/src/js/integrations/logEnricherIntegration.ts b/packages/core/src/js/integrations/logEnricherIntegration.ts new file mode 100644 index 0000000000..7b6b9dd786 --- /dev/null +++ b/packages/core/src/js/integrations/logEnricherIntegration.ts @@ -0,0 +1,67 @@ +/* eslint-disable complexity */ +import type { Integration, Log } from '@sentry/core'; +import { debug } from '@sentry/core'; +import type { ReactNativeClient } from '../client'; +import { NATIVE } from '../wrapper'; + +const INTEGRATION_NAME = 'LogEnricher'; + +export const logEnricherIntegration = (): Integration => { + return { + name: INTEGRATION_NAME, + setup(client: ReactNativeClient) { + client.on('afterInit', () => { + cacheLogContext().then( + () => { + client.on('beforeCaptureLog', (log: Log) => { + processLog(log); + }); + }, + reason => { + debug.log(reason); + }, + ); + }); + }, + }; +}; + +let NativeCache: Record | undefined = undefined; + +async function cacheLogContext(): Promise { + try { + const response = await NATIVE.fetchNativeLogAttributes(); + + NativeCache = { + ...(response?.contexts?.device && { + brand: response.contexts.device?.brand, + model: response.contexts.device?.model, + family: response.contexts.device?.family, + }), + ...(response?.contexts?.os && { + os: response.contexts.os.name, + version: response.contexts.os.version, + }), + ...(response?.contexts?.release && { + release: response.contexts.release, + }), + }; + } catch (e) { + return Promise.reject(`[LOGS]: Failed to prepare attributes from Native Layer: ${e}`); + } + return Promise.resolve(); +} + +function processLog(log: Log): void { + if (NativeCache === undefined) { + return; + } + + log.attributes = log.attributes ?? {}; + NativeCache.brand && (log.attributes['device.brand'] = NativeCache.brand); + NativeCache.model && (log.attributes['device.model'] = NativeCache.model); + NativeCache.family && (log.attributes['device.family'] = NativeCache.family); + NativeCache.os && (log.attributes['os.name'] = NativeCache.os); + NativeCache.version && (log.attributes['os.version'] = NativeCache.version); + NativeCache.release && (log.attributes['sentry.release'] = NativeCache.release); +} diff --git a/packages/core/src/js/integrations/modulesloader.ts b/packages/core/src/js/integrations/modulesloader.ts index 7a31154d33..d08ec6ebc9 100644 --- a/packages/core/src/js/integrations/modulesloader.ts +++ b/packages/core/src/js/integrations/modulesloader.ts @@ -1,6 +1,5 @@ import type { Event, Integration } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { NATIVE } from '../wrapper'; const INTEGRATION_NAME = 'ModulesLoader'; @@ -25,7 +24,7 @@ function createProcessEvent(): (event: Event) => Promise { try { modules = await NATIVE.fetchModules(); } catch (e) { - logger.log(`Failed to get modules from native: ${e}`); + debug.log(`Failed to get modules from native: ${e}`); } isSetup = true; } diff --git a/packages/core/src/js/integrations/nativelinkederrors.ts b/packages/core/src/js/integrations/nativelinkederrors.ts index 39d8d55879..727ef85638 100644 --- a/packages/core/src/js/integrations/nativelinkederrors.ts +++ b/packages/core/src/js/integrations/nativelinkederrors.ts @@ -11,7 +11,6 @@ import type { StackParser, } from '@sentry/core'; import { isInstanceOf, isPlainObject, isString } from '@sentry/core'; - import type { NativeStackFrames } from '../NativeRNSentry'; import { NATIVE } from '../wrapper'; @@ -43,7 +42,7 @@ export const nativeLinkedErrorsIntegration = (options: Partial True, false -> False. + * Symbols are stringified. + * + */ +export const primitiveTagIntegration = (): Integration => { + return { + name: INTEGRATION_NAME, + setup(client) { + client.on('beforeSendEvent', event => { + if (event.tags) { + Object.keys(event.tags).forEach(key => { + event.tags![key] = PrimitiveToString(event.tags![key]); + }); + } + }); + }, + afterAllSetup() { + if (NATIVE.enableNative) { + NATIVE._setPrimitiveProcessor((value: Primitive) => PrimitiveToString(value)); + } + }, + }; +}; diff --git a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts index df56da47cb..f838717603 100644 --- a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts +++ b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts @@ -3,11 +3,11 @@ import { addExceptionMechanism, addGlobalUnhandledRejectionInstrumentationHandler, captureException, + debug, getClient, getCurrentScope, - logger, } from '@sentry/core'; - +import type { ReactNativeClientOptions } from '../options'; import { isHermesEnabled, isWeb } from '../utils/environment'; import { createSyntheticError, isErrorLike } from '../utils/error'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; @@ -58,7 +58,7 @@ function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void { RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker && RN_GLOBAL_OBJ?.HermesInternal?.hasPromise?.() ) { - logger.log('Using Hermes native promise rejection tracking'); + debug.log('Using Hermes native promise rejection tracking'); RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({ allRejections: true, @@ -66,9 +66,9 @@ function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void { onHandled: promiseRejectionTrackingOptions.onHandled, }); - logger.log('Unhandled promise rejections will be caught by Sentry.'); + debug.log('Unhandled promise rejections will be caught by Sentry.'); } else if (isWeb()) { - logger.log('Using Browser JS promise rejection tracking for React Native Web'); + debug.log('Using Browser JS promise rejection tracking for React Native Web'); // Use Sentry's built-in global unhandled rejection handler addGlobalUnhandledRejectionInstrumentationHandler((error: unknown) => { @@ -85,10 +85,10 @@ function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void { checkPromiseAndWarn(); } else { // For JSC and other environments, patching was disabled by user configuration - logger.log('Unhandled promise rejections will not be caught by Sentry.'); + debug.log('Unhandled promise rejections will not be caught by Sentry.'); } } catch (e) { - logger.warn( + debug.warn( 'Failed to set up promise rejection tracking. ' + 'Unhandled promise rejections will not be caught by Sentry.' + 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', @@ -99,7 +99,7 @@ function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void { const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = { onUnhandled: (id, error: unknown, rejection = {}) => { if (__DEV__) { - logger.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${rejection}`); + debug.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${rejection}`); } // Marking the rejection as handled to avoid breaking crash rate calculations. @@ -113,7 +113,7 @@ const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = { }, onHandled: id => { if (__DEV__) { - logger.warn( + debug.warn( `Promise Rejection Handled (id: ${id})\n` + 'This means you can ignore any previous messages of the form ' + `"Possible Unhandled Promise Rejection (id: ${id}):"`, @@ -137,11 +137,11 @@ function setupErrorUtilsGlobalHandler(): void { const errorUtils = RN_GLOBAL_OBJ.ErrorUtils; if (!errorUtils) { - logger.warn('ErrorUtils not found. Can be caused by different environment for example react-native-web.'); + debug.warn('ErrorUtils not found. Can be caused by different environment for example react-native-web.'); return; } - const defaultHandler = errorUtils.getGlobalHandler && errorUtils.getGlobalHandler(); + const defaultHandler = errorUtils.getGlobalHandler?.(); // eslint-disable-next-line @typescript-eslint/no-explicit-any errorUtils.setGlobalHandler(async (error: any, isFatal?: boolean) => { @@ -149,7 +149,7 @@ function setupErrorUtilsGlobalHandler(): void { const shouldHandleFatal = isFatal && !__DEV__; if (shouldHandleFatal) { if (handlingFatal) { - logger.log('Encountered multiple fatals in a row. The latest:', error); + debug.log('Encountered multiple fatals in a row. The latest:', error); return; } handlingFatal = true; @@ -158,7 +158,7 @@ function setupErrorUtilsGlobalHandler(): void { const client = getClient(); if (!client) { - logger.error('Sentry client is missing, the error event might be lost.', error); + debug.error('Sentry client is missing, the error event might be lost.', error); // If there is no client something is fishy, anyway we call the default handler defaultHandler(error, isFatal); @@ -197,12 +197,12 @@ function setupErrorUtilsGlobalHandler(): void { return; } - void client.flush(client.getOptions().shutdownTimeout || 2000).then( + void client.flush((client.getOptions() as ReactNativeClientOptions).shutdownTimeout || 2000).then( () => { defaultHandler(error, isFatal); }, (reason: unknown) => { - logger.error('[ReactNativeErrorHandlers] Error while flushing the event cache after uncaught error.', reason); + debug.error('[ReactNativeErrorHandlers] Error while flushing the event cache after uncaught error.', reason); }, ); }); diff --git a/packages/core/src/js/integrations/reactnativeerrorhandlersutils.ts b/packages/core/src/js/integrations/reactnativeerrorhandlersutils.ts index 7453f696d6..9b2bb4790f 100644 --- a/packages/core/src/js/integrations/reactnativeerrorhandlersutils.ts +++ b/packages/core/src/js/integrations/reactnativeerrorhandlersutils.ts @@ -1,5 +1,4 @@ -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { ReactNativeLibraries } from '../utils/rnlibraries'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; @@ -14,7 +13,7 @@ import { RN_GLOBAL_OBJ } from '../utils/worldwide'; */ export function polyfillPromise(): void { if (!ReactNativeLibraries.Utilities) { - logger.warn('Could not polyfill Promise. React Native Libraries Utilities not found.'); + debug.warn('Could not polyfill Promise. React Native Libraries Utilities not found.'); return; } @@ -66,7 +65,7 @@ export function checkPromiseAndWarn(): void { const UsedPromisePolyfill = getPromisePolyfill(); if (ReactNativePromise !== PromisePackagePromise) { - logger.warn( + debug.warn( 'You appear to have multiple versions of the "promise" package installed. ' + 'This may cause unexpected behavior like undefined `Promise.allSettled`. ' + 'Please install the `promise` package manually using the exact version as the React Native package. ' + @@ -76,16 +75,16 @@ export function checkPromiseAndWarn(): void { // This only make sense if the user disabled the integration Polyfill if (UsedPromisePolyfill !== RN_GLOBAL_OBJ.Promise) { - logger.warn( + debug.warn( 'Unhandled promise rejections will not be caught by Sentry. ' + 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', ); } else { - logger.log('Unhandled promise rejections will be caught by Sentry.'); + debug.log('Unhandled promise rejections will be caught by Sentry.'); } } catch (e) { // Do Nothing - logger.warn( + debug.warn( 'Unhandled promise rejections will not be caught by Sentry. ' + 'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.', ); diff --git a/packages/core/src/js/integrations/reactnativeinfo.ts b/packages/core/src/js/integrations/reactnativeinfo.ts index b24e20f917..f45139f232 100644 --- a/packages/core/src/js/integrations/reactnativeinfo.ts +++ b/packages/core/src/js/integrations/reactnativeinfo.ts @@ -1,5 +1,4 @@ import type { Context, Event, EventHint, Integration } from '@sentry/core'; - import { getExpoGoVersion, getExpoSdkVersion, @@ -61,7 +60,7 @@ function processEvent(event: Event, hint: EventHint): Event { if (reactNativeContext.js_engine === 'hermes') { event.tags = { - hermes: 'true', + hermes: true, ...event.tags, }; } diff --git a/packages/core/src/js/integrations/release.ts b/packages/core/src/js/integrations/release.ts index f414f8a9ac..3682bb42b3 100644 --- a/packages/core/src/js/integrations/release.ts +++ b/packages/core/src/js/integrations/release.ts @@ -1,5 +1,4 @@ import type { BaseTransportOptions, Client, ClientOptions, Event, EventHint, Integration } from '@sentry/core'; - import { NATIVE } from '../wrapper'; const INTEGRATION_NAME = 'Release'; diff --git a/packages/core/src/js/integrations/rewriteframes.ts b/packages/core/src/js/integrations/rewriteframes.ts index 09ee8b6398..81c28a3bf9 100644 --- a/packages/core/src/js/integrations/rewriteframes.ts +++ b/packages/core/src/js/integrations/rewriteframes.ts @@ -1,7 +1,6 @@ import type { Integration, StackFrame } from '@sentry/core'; import { rewriteFramesIntegration } from '@sentry/core'; import { Platform } from 'react-native'; - import { isExpo, isHermesEnabled } from '../utils/environment'; export const ANDROID_DEFAULT_BUNDLE_NAME = 'app:///index.android.bundle'; diff --git a/packages/core/src/js/integrations/screenshot.ts b/packages/core/src/js/integrations/screenshot.ts index 6f504fa76e..3c45ada451 100644 --- a/packages/core/src/js/integrations/screenshot.ts +++ b/packages/core/src/js/integrations/screenshot.ts @@ -1,5 +1,4 @@ import type { Event, EventHint, Integration } from '@sentry/core'; - import type { ReactNativeClient } from '../client'; import type { Screenshot as ScreenshotAttachment } from '../wrapper'; import { NATIVE } from '../wrapper'; @@ -18,7 +17,7 @@ export const screenshotIntegration = (): Integration => { }; async function processEvent(event: Event, hint: EventHint, client: ReactNativeClient): Promise { - const hasException = event.exception && event.exception.values && event.exception.values.length > 0; + const hasException = event.exception?.values && event.exception.values.length > 0; if (!hasException || client.getOptions().beforeScreenshot?.(event, hint) === false) { return event; } diff --git a/packages/core/src/js/integrations/sdkinfo.ts b/packages/core/src/js/integrations/sdkinfo.ts index 6c4735379c..f8d54a136c 100644 --- a/packages/core/src/js/integrations/sdkinfo.ts +++ b/packages/core/src/js/integrations/sdkinfo.ts @@ -1,17 +1,9 @@ import type { Event, Integration, Package, SdkInfo as SdkInfoType } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { isExpoGo, notWeb } from '../utils/environment'; import { SDK_NAME, SDK_PACKAGE_NAME, SDK_VERSION } from '../version'; import { NATIVE } from '../wrapper'; -// TODO: Remove this on JS V10. -interface IpPatchedSdkInfo extends SdkInfoType { - settings?: { - infer_ip?: 'auto' | 'never'; - }; -} - const INTEGRATION_NAME = 'SdkInfo'; type DefaultSdkInfo = Pick, 'name' | 'packages' | 'version'>; @@ -26,7 +18,6 @@ export const defaultSdkInfo: DefaultSdkInfo = { ], version: SDK_VERSION, }; -let DefaultPii: boolean | undefined = undefined; /** Default SdkInfo instrumentation */ export const sdkInfoIntegration = (): Integration => { @@ -34,17 +25,6 @@ export const sdkInfoIntegration = (): Integration => { return { name: INTEGRATION_NAME, - setup(client) { - const options = client.getOptions(); - DefaultPii = options.sendDefaultPii; - if (DefaultPii) { - client.on('beforeSendEvent', event => { - if (event.user?.ip_address === '{{auto}}') { - delete event.user.ip_address; - } - }); - } - }, setupOnce: () => { // noop }, @@ -56,24 +36,15 @@ async function processEvent(event: Event, fetchNativeSdkInfo: () => Promise Promise { nativeSdkPackageCache = await NATIVE.fetchNativeSdkInfo(); isCached = true; } catch (e) { - logger.warn('Could not fetch native sdk info.', e); + debug.warn('Could not fetch native sdk info.', e); } return nativeSdkPackageCache; diff --git a/packages/core/src/js/integrations/spotlight.ts b/packages/core/src/js/integrations/spotlight.ts index b4f62e06da..01f35bb819 100644 --- a/packages/core/src/js/integrations/spotlight.ts +++ b/packages/core/src/js/integrations/spotlight.ts @@ -1,6 +1,5 @@ import type { BaseTransportOptions, Client, ClientOptions, Envelope, Integration } from '@sentry/core'; -import { logger, serializeEnvelope } from '@sentry/core'; - +import { debug, serializeEnvelope } from '@sentry/core'; import { ReactNativeLibraries } from '../utils/rnlibraries'; import { createStealthXhr, XHR_READYSTATE_DONE } from '../utils/xhr'; @@ -22,7 +21,7 @@ type SpotlightReactNativeIntegrationOptions = { export function spotlightIntegration({ sidecarUrl = getDefaultSidecarUrl(), }: SpotlightReactNativeIntegrationOptions = {}): Integration { - logger.info('[Spotlight] Using Sidecar URL', sidecarUrl); + debug.log('[Spotlight] Using Sidecar URL', sidecarUrl); return { name: 'Spotlight', @@ -57,7 +56,7 @@ function sendEnvelopesToSidecar(client: Client, sidecarUrl: string): void { const xhr = createStealthXhr(); if (!xhr) { - logger.error('[Spotlight] Sentry SDK can not create XHR object'); + debug.error('[Spotlight] Sentry SDK can not create XHR object'); return; } @@ -71,7 +70,7 @@ function sendEnvelopesToSidecar(client: Client, sidecarUrl: string): void { // The request has been completed successfully } else { // Handle the error - logger.error( + debug.error( "[Spotlight] Sentry SDK can't connect to Spotlight is it running? See https://spotlightjs.com to download it.", new Error(xhr.statusText), ); @@ -83,17 +82,23 @@ function sendEnvelopesToSidecar(client: Client, sidecarUrl: string): void { }); } +const DEFAULT_SIDECAR_URL = 'http://localhost:8969/stream'; + /** * Gets the default Spotlight sidecar URL. */ export function getDefaultSidecarUrl(): string { try { - const { url } = ReactNativeLibraries.Devtools?.getDevServer(); + const { url } = ReactNativeLibraries.Devtools?.getDevServer() ?? {}; + if (!url) { + return DEFAULT_SIDECAR_URL; + } + return `http://${getHostnameFromString(url)}:8969/stream`; } catch (_oO) { // We can't load devserver URL } - return 'http://localhost:8969/stream'; + return DEFAULT_SIDECAR_URL; } /** @@ -103,7 +108,7 @@ function getHostnameFromString(urlString: string): string | null { const regex = /^(?:\w+:)?\/\/([^/:]+)(:\d+)?(.*)$/; const matches = urlString.match(regex); - if (matches && matches[1]) { + if (matches?.[1]) { return matches[1]; } else { // Invalid URL format diff --git a/packages/core/src/js/integrations/viewhierarchy.ts b/packages/core/src/js/integrations/viewhierarchy.ts index 9b84ece273..f05e76705f 100644 --- a/packages/core/src/js/integrations/viewhierarchy.ts +++ b/packages/core/src/js/integrations/viewhierarchy.ts @@ -1,6 +1,5 @@ import type { Attachment, Event, EventHint, Integration } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { NATIVE } from '../wrapper'; const filename: string = 'view-hierarchy.json'; @@ -21,7 +20,7 @@ export const viewHierarchyIntegration = (): Integration => { }; async function processEvent(event: Event, hint: EventHint): Promise { - const hasException = event.exception && event.exception.values && event.exception.values.length > 0; + const hasException = event.exception?.values && event.exception.values.length > 0; if (!hasException) { return event; } @@ -30,7 +29,7 @@ async function processEvent(event: Event, hint: EventHint): Promise { try { viewHierarchy = await NATIVE.fetchViewHierarchy(); } catch (e) { - logger.error('Failed to get view hierarchy from native.', e); + debug.error('Failed to get view hierarchy from native.', e); } if (viewHierarchy) { diff --git a/packages/core/src/js/options.ts b/packages/core/src/js/options.ts index 1890fd36e9..df830dfdd2 100644 --- a/packages/core/src/js/options.ts +++ b/packages/core/src/js/options.ts @@ -1,15 +1,17 @@ import type { makeFetchTransport } from '@sentry/browser'; import type { CaptureContext, ClientOptions, Event, EventHint, Options } from '@sentry/core'; -import type { Profiler } from '@sentry/react'; +import type { BrowserOptions, Profiler } from '@sentry/react'; import type * as React from 'react'; import { Platform } from 'react-native'; - import type { TouchEventBoundaryProps } from './touchevents'; -import { getExpoConstants } from './utils/expomodules'; +import { isExpoGo } from './utils/environment'; type ProfilerProps = React.ComponentProps; type BrowserTransportOptions = Parameters[0]; +type BrowserExperiments = NonNullable; +type SharedExperimentsSubset = BrowserExperiments; + export interface BaseReactNativeOptions { /** * Enables native transport + device info + offline caching. @@ -234,6 +236,14 @@ export interface BaseReactNativeOptions { */ replaysOnErrorSampleRate?: number; + /** + * Controls how many milliseconds to wait before shutting down. The default is 2 seconds. Setting this too low can cause + * problems for sending events from command line applications. Setting it too + * high can cause the application to block for users with network connectivity + * problems. + */ + shutdownTimeout?: number; + /** * Defines the quality of the session replay. The higher the quality, the more accurate the replay * will be, but also more data to transfer and more CPU load. @@ -245,7 +255,7 @@ export interface BaseReactNativeOptions { /** * Options which are in beta, or otherwise not guaranteed to be stable. */ - _experiments?: { + _experiments?: SharedExperimentsSubset & { [key: string]: unknown; /** @@ -307,7 +317,7 @@ export interface ReactNativeClientOptions export interface ReactNativeWrapperOptions { /** Props for the root React profiler */ - profilerProps?: ProfilerProps; + profilerProps?: Omit; /** Props for the root touch event boundary */ touchEventBoundaryProps?: TouchEventBoundaryProps; @@ -329,8 +339,7 @@ export function shouldEnableNativeNagger(userOptions: unknown): boolean { return false; } - const expoConstants = getExpoConstants(); - if (expoConstants && expoConstants.appOwnership === 'expo') { + if (isExpoGo()) { // If the app is running in Expo Go, we don't want to nag return false; } diff --git a/packages/core/src/js/playground/examples.ts b/packages/core/src/js/playground/examples.ts index b06c4078c0..f82ddca83c 100644 --- a/packages/core/src/js/playground/examples.ts +++ b/packages/core/src/js/playground/examples.ts @@ -1,5 +1,4 @@ import { captureException } from '@sentry/core'; - import { NATIVE } from '../wrapper'; // This is a placeholder to match the example code with what Sentry SDK users would see. diff --git a/packages/core/src/js/playground/modal.tsx b/packages/core/src/js/playground/modal.tsx index 08d920aefe..62376bd908 100644 --- a/packages/core/src/js/playground/modal.tsx +++ b/packages/core/src/js/playground/modal.tsx @@ -1,5 +1,5 @@ /* eslint-disable max-lines */ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import * as React from 'react'; import { Animated, @@ -13,7 +13,6 @@ import { useColorScheme, View, } from 'react-native'; - import { getDevServer } from '../integrations/debugsymbolicatorutils'; import { isExpo, isExpoGo, isWeb } from '../utils/environment'; import { bug as bugAnimation, hi as hiAnimation, thumbsup as thumbsupAnimation } from './animations'; @@ -52,7 +51,7 @@ export const withSentryPlayground =

( ); }; - Wrapper.displayName = `withSentryPlayground()`; + Wrapper.displayName = 'withSentryPlayground()'; return Wrapper; }; @@ -191,7 +190,7 @@ export const SentryPlayground = ({ ); }; -const Animation = ({ id }: { id: string }): React.ReactElement => { +const Animation = ({ id }: { id: string }): React.ReactElement | null => { const shouldFallbackToImage = Platform.OS === 'android'; switch (id) { @@ -219,7 +218,7 @@ const Row = ({ last = false, // eslint-disable-next-line @typescript-eslint/no-empty-function action = () => {}, - actionDescription, + actionDescription = '', title, description, disabled = false, @@ -423,12 +422,17 @@ const lightStyles: typeof defaultDarkStyles = StyleSheet.create({ }); function openURLInBrowser(url: string): void { - // This doesn't work for Expo project with Web enabled - // disable-next-line @typescript-eslint/no-floating-promises - fetch(`${getDevServer().url}open-url`, { - method: 'POST', - body: JSON.stringify({ url }), - }).catch(e => { - logger.error('Error opening URL:', e); - }); + const devServer = getDevServer(); + if (devServer?.url) { + // This doesn't work for Expo project with Web enabled + // disable-next-line @typescript-eslint/no-floating-promises + fetch(`${devServer.url}open-url`, { + method: 'POST', + body: JSON.stringify({ url }), + }).catch(e => { + debug.error('Error opening URL:', e); + }); + } else { + debug.error('Dev server URL not available'); + } } diff --git a/packages/core/src/js/profiling/convertHermesProfile.ts b/packages/core/src/js/profiling/convertHermesProfile.ts index 39ed9ac752..475a68e05e 100644 --- a/packages/core/src/js/profiling/convertHermesProfile.ts +++ b/packages/core/src/js/profiling/convertHermesProfile.ts @@ -1,6 +1,5 @@ import type { FrameId, StackId, ThreadCpuFrame, ThreadCpuSample, ThreadCpuStack, ThreadId } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { MAX_PROFILE_DURATION_MS } from './constants'; import type * as Hermes from './hermes'; import { DEFAULT_BUNDLE_NAME } from './hermes'; @@ -26,7 +25,7 @@ const JS_THREAD_PRIORITY = 1; */ export function convertToSentryProfile(hermesProfile: Hermes.Profile): RawThreadCpuProfile | null { if (hermesProfile.samples.length === 0) { - logger.warn('[Profiling] No samples found in profile.'); + debug.warn('[Profiling] No samples found in profile.'); return null; } @@ -43,7 +42,7 @@ export function convertToSentryProfile(hermesProfile: Hermes.Profile): RawThread for (const sample of samples) { const sentryStackId = hermesStackToSentryStackMap.get(sample.stack_id); if (sentryStackId === undefined) { - logger.error(`[Profiling] Hermes Stack ID ${sample.stack_id} not found when mapping to Sentry Stack ID.`); + debug.error(`[Profiling] Hermes Stack ID ${sample.stack_id} not found when mapping to Sentry Stack ID.`); sample.stack_id = UNKNOWN_STACK_ID; } else { sample.stack_id = sentryStackId; @@ -82,18 +81,28 @@ export function mapSamples( hermesStacks: Set; jsThreads: Set; } { + const samples: ThreadCpuSample[] = []; const jsThreads = new Set(); const hermesStacks = new Set(); - const start = Number(hermesSamples[0].ts); - const samples: ThreadCpuSample[] = []; + const firstSample = hermesSamples[0]; + if (!firstSample) { + debug.warn('[Profiling] No samples found in profile.'); + return { + samples, + hermesStacks, + jsThreads, + }; + } + + const start = Number(firstSample.ts); for (const hermesSample of hermesSamples) { jsThreads.add(hermesSample.tid); hermesStacks.add(hermesSample.sf); const elapsed_since_start_ns = (Number(hermesSample.ts) - start) * 1e3; if (elapsed_since_start_ns >= maxElapsedSinceStartNs) { - logger.warn( + debug.warn( `[Profiling] Sample has elapsed time since start ${elapsed_since_start_ns}ns ` + `greater than the max elapsed time ${maxElapsedSinceStartNs}ns.`, ); @@ -130,8 +139,12 @@ function mapFrames(hermesStackFrames: Record 1) { - logger.warn( - '[Profiling] Multiple debug images found, but only one one bundle is supported. Using the first one...', - ); + debug.warn('[Profiling] Multiple debug images found, but only one one bundle is supported. Using the first one...'); + return []; + } + + if (!debugIdsKeys[0]) { + return []; + } + + const debugId = debugIdMap[debugIdsKeys[0]]; + if (!debugId) { + return []; } return [ { code_file: DEFAULT_BUNDLE_NAME, - debug_id: debugIdMap[debugIdsKeys[0]], + debug_id: debugId, type: 'sourcemap', }, ]; diff --git a/packages/core/src/js/profiling/hermes.ts b/packages/core/src/js/profiling/hermes.ts index d2ed0fec90..5e7dc9d6c6 100644 --- a/packages/core/src/js/profiling/hermes.ts +++ b/packages/core/src/js/profiling/hermes.ts @@ -1,5 +1,4 @@ import { Platform } from 'react-native'; - import { ANDROID_DEFAULT_BUNDLE_NAME, IOS_DEFAULT_BUNDLE_NAME } from '../integrations/rewriteframes'; export type StackFrameId = number; diff --git a/packages/core/src/js/profiling/integration.ts b/packages/core/src/js/profiling/integration.ts index 14c46721c1..884f82842a 100644 --- a/packages/core/src/js/profiling/integration.ts +++ b/packages/core/src/js/profiling/integration.ts @@ -1,8 +1,7 @@ /* eslint-disable complexity */ import type { Envelope, Event, Integration, Span, ThreadCpuProfile } from '@sentry/core'; -import { getActiveSpan, getClient, logger, spanIsSampled, uuid4 } from '@sentry/core'; +import { debug, getActiveSpan, getClient, spanIsSampled, uuid4 } from '@sentry/core'; import { Platform } from 'react-native'; - import type { ReactNativeClient } from '../client'; import { isHermesEnabled } from '../utils/environment'; import { isRootSpan } from '../utils/span'; @@ -60,7 +59,7 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = isReady = true; if (!isHermesEnabled()) { - logger.log('[Profiling] Hermes is not enabled, not adding profiling integration.'); + debug.log('[Profiling] Hermes is not enabled, not adding profiling integration.'); return; } @@ -82,7 +81,7 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = const profiledTransactions = findProfiledTransactionsFromEnvelope(envelope); if (!profiledTransactions.length) { - logger.log('[Profiling] no profiled transactions found in envelope'); + debug.log('[Profiling] no profiled transactions found in envelope'); return; } @@ -123,23 +122,23 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = const _shouldStartProfiling = (activeSpan: Span): boolean => { if (!spanIsSampled(activeSpan)) { - logger.log('[Profiling] Transaction is not sampled, skipping profiling'); + debug.log('[Profiling] Transaction is not sampled, skipping profiling'); return false; } const client = getClient(); - const options = client && client.getOptions(); + const options = client?.getOptions?.(); const profilesSampleRate = options && typeof options.profilesSampleRate === 'number' ? options.profilesSampleRate : undefined; if (profilesSampleRate === undefined) { - logger.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.'); + debug.log('[Profiling] Profiling disabled, enable it by setting `profilesSampleRate` option to SDK init call.'); return false; } // Check if we should sample this profile if (Math.random() > profilesSampleRate) { - logger.log('[Profiling] Skip profiling transaction due to sampling.'); + debug.log('[Profiling] Skip profiling transaction due to sampling.'); return false; } @@ -161,7 +160,7 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = startTimestampNs: profileStartTimestampNs, }; activeSpan.setAttribute('profile_id', _currentProfile.profile_id); - logger.log('[Profiling] started profiling: ', _currentProfile.profile_id); + debug.log('[Profiling] started profiling: ', _currentProfile.profile_id); }; /** @@ -173,7 +172,7 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = } if (span.spanContext().spanId !== _currentProfile?.span_id) { - logger.log( + debug.log( `[Profiling] Span (${span.spanContext().spanId}) ended is not the currently profiled span (${ _currentProfile?.span_id }). Not stopping profiling.`, @@ -195,14 +194,14 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = const profile = stopProfiling(_currentProfile.startTimestampNs); if (!profile) { - logger.warn('[Profiling] Stop failed. Cleaning up...'); + debug.warn('[Profiling] Stop failed. Cleaning up...'); _currentProfile = undefined; return; } PROFILE_QUEUE.add(_currentProfile.profile_id, profile); - logger.log('[Profiling] finished profiling: ', _currentProfile.profile_id); + debug.log('[Profiling] finished profiling: ', _currentProfile.profile_id); _currentProfile = undefined; }; @@ -210,7 +209,7 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = const profile_id = profiledTransaction?.contexts?.trace?.data?.profile_id; if (typeof profile_id !== 'string') { - logger.log('[Profiling] cannot find profile for a transaction without a profile context'); + debug.log('[Profiling] cannot find profile for a transaction without a profile context'); return null; } @@ -223,12 +222,12 @@ export const hermesProfilingIntegration = (initOptions: HermesProfilingOptions = PROFILE_QUEUE.delete(profile_id); if (!profile) { - logger.log(`[Profiling] cannot find profile ${profile_id} for transaction ${profiledTransaction.event_id}`); + debug.log(`[Profiling] cannot find profile ${profile_id} for transaction ${profiledTransaction.event_id}`); return null; } const profileWithEvent = enrichCombinedProfileWithEventContext(profile_id, profile, profiledTransaction); - logger.log(`[Profiling] Created profile ${profile_id} for transaction ${profiledTransaction.event_id}`); + debug.log(`[Profiling] Created profile ${profile_id} for transaction ${profiledTransaction.event_id}`); return profileWithEvent; }; diff --git a/packages/core/src/js/profiling/types.ts b/packages/core/src/js/profiling/types.ts index 871c975403..e483d4ccfc 100644 --- a/packages/core/src/js/profiling/types.ts +++ b/packages/core/src/js/profiling/types.ts @@ -1,5 +1,4 @@ import type { DebugImage, MeasurementUnit, Profile, ThreadCpuFrame, ThreadCpuProfile } from '@sentry/core'; - import type { NativeProfileEvent } from './nativeTypes'; export interface RawThreadCpuProfile extends ThreadCpuProfile { diff --git a/packages/core/src/js/profiling/utils.ts b/packages/core/src/js/profiling/utils.ts index c83342b50b..e532134244 100644 --- a/packages/core/src/js/profiling/utils.ts +++ b/packages/core/src/js/profiling/utils.ts @@ -1,7 +1,6 @@ /* eslint-disable complexity */ import type { Envelope, Event, ThreadCpuProfile } from '@sentry/core'; -import { forEachEnvelopeItem, logger } from '@sentry/core'; - +import { debug, forEachEnvelopeItem } from '@sentry/core'; import { getDefaultEnvironment } from '../utils/environment'; import { getDebugMetadata } from './debugid'; import type { @@ -22,7 +21,7 @@ export function isValidProfile(profile: ThreadCpuProfile): profile is RawThreadC // Log a warning if the profile has less than 2 samples so users can know why // they are not seeing any profiling data and we cant avoid the back and forth // of asking them to provide us with a dump of the profile data. - logger.log('[Profiling] Discarding profile because it contains less than 2 samples'); + debug.log('[Profiling] Discarding profile because it contains less than 2 samples'); } return false; } @@ -75,14 +74,14 @@ export function enrichCombinedProfileWithEventContext( return null; } - const trace_id = (event.contexts && event.contexts.trace && event.contexts.trace.trace_id) || ''; + const trace_id = event.contexts?.trace?.trace_id || ''; // Log a warning if the profile has an invalid traceId (should be uuidv4). // All profiles and transactions are rejected if this is the case and we want to // warn users that this is happening if they enable debug flag - if (trace_id && trace_id.length !== 32) { + if (trace_id?.length !== 32) { if (__DEV__) { - logger.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`); + debug.log(`[Profiling] Invalid traceId: ${trace_id} on profiled event`); } } @@ -97,25 +96,25 @@ export function enrichCombinedProfileWithEventContext( release: event.release || '', environment: event.environment || getDefaultEnvironment(), os: { - name: (event.contexts && event.contexts.os && event.contexts.os.name) || '', - version: (event.contexts && event.contexts.os && event.contexts.os.version) || '', - build_number: (event.contexts && event.contexts.os && event.contexts.os.build) || '', + name: event.contexts?.os?.name || '', + version: event.contexts?.os?.version || '', + build_number: event.contexts?.os?.build || '', }, device: { - locale: (event.contexts && event.contexts.device && (event.contexts.device.locale as string)) || '', - model: (event.contexts && event.contexts.device && event.contexts.device.model) || '', - manufacturer: (event.contexts && event.contexts.device && event.contexts.device.manufacturer) || '', - architecture: (event.contexts && event.contexts.device && event.contexts.device.arch) || '', - is_emulator: (event.contexts && event.contexts.device && event.contexts.device.simulator) || false, + locale: (event.contexts?.device && (event.contexts.device.locale as string)) || '', + model: event.contexts?.device?.model || '', + manufacturer: event.contexts?.device?.manufacturer || '', + architecture: event.contexts?.device?.arch || '', + is_emulator: event.contexts?.device?.simulator || false, }, transaction: { name: event.transaction || '', id: event.event_id || '', trace_id, - active_thread_id: (profile.transaction && profile.transaction.active_thread_id) || '', + active_thread_id: profile.transaction?.active_thread_id || '', }, debug_meta: { - images: [...getDebugMetadata(), ...((profile.debug_meta && profile.debug_meta.images) || [])], + images: [...getDebugMetadata(), ...(profile.debug_meta?.images || [])], }, }; } @@ -136,19 +135,15 @@ export function enrichAndroidProfileWithEventContext( build_id: profile.build_id || '', device_cpu_frequencies: [], - device_is_emulator: (event.contexts && event.contexts.device && event.contexts.device.simulator) || false, - device_locale: (event.contexts && event.contexts.device && (event.contexts.device.locale as string)) || '', - device_manufacturer: (event.contexts && event.contexts.device && event.contexts.device.manufacturer) || '', - device_model: (event.contexts && event.contexts.device && event.contexts.device.model) || '', - device_os_name: (event.contexts && event.contexts.os && event.contexts.os.name) || '', - device_os_version: (event.contexts && event.contexts.os && event.contexts.os.version) || '', + device_is_emulator: event.contexts?.device?.simulator || false, + device_locale: (event.contexts?.device && (event.contexts.device.locale as string)) || '', + device_manufacturer: event.contexts?.device?.manufacturer || '', + device_model: event.contexts?.device?.model || '', + device_os_name: event.contexts?.os?.name || '', + device_os_version: event.contexts?.os?.version || '', device_physical_memory_bytes: - (event.contexts && - event.contexts.device && - event.contexts.device.memory_size && - Number(event.contexts.device.memory_size).toString(10)) || - '', + (event.contexts?.device?.memory_size && Number(event.contexts.device.memory_size).toString(10)) || '', environment: event.environment || getDefaultEnvironment(), @@ -161,7 +156,7 @@ export function enrichAndroidProfileWithEventContext( transaction_id: event.event_id || '', transaction_name: event.transaction || '', - trace_id: (event.contexts && event.contexts.trace && event.contexts.trace.trace_id) || '', + trace_id: event.contexts?.trace?.trace_id || '', version_name: event.release || '', version_code: event.dist || '', diff --git a/packages/core/src/js/replay/CustomMask.tsx b/packages/core/src/js/replay/CustomMask.tsx index 4608dfbe04..b293dc8060 100644 --- a/packages/core/src/js/replay/CustomMask.tsx +++ b/packages/core/src/js/replay/CustomMask.tsx @@ -1,8 +1,7 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import * as React from 'react'; import type { HostComponent, ViewProps } from 'react-native'; import { UIManager, View } from 'react-native'; - import { isExpoGo } from '../utils/environment'; const NativeComponentRegistry: { @@ -34,11 +33,11 @@ const UnmaskFallback = (viewProps: ViewProps): React.ReactElement => { return ; }; -const hasViewManagerConfig = (nativeComponentName: string): boolean => UIManager.hasViewManagerConfig && UIManager.hasViewManagerConfig(nativeComponentName); +const hasViewManagerConfig = (nativeComponentName: string): boolean => UIManager.hasViewManagerConfig?.(nativeComponentName); const Mask = ((): HostComponent | React.ComponentType => { if (isExpoGo() || !hasViewManagerConfig(MaskNativeComponentName)) { - logger.warn(`[SentrySessionReplay] Can't load ${MaskNativeComponentName}.`); + debug.warn(`[SentrySessionReplay] Can't load ${MaskNativeComponentName}.`); return MaskFallback; } @@ -51,7 +50,7 @@ const Mask = ((): HostComponent | React.ComponentType => { const Unmask = ((): HostComponent | React.ComponentType => { if (isExpoGo() || !hasViewManagerConfig(UnmaskNativeComponentName)) { - logger.warn(`[SentrySessionReplay] Can't load ${UnmaskNativeComponentName}.`); + debug.warn(`[SentrySessionReplay] Can't load ${UnmaskNativeComponentName}.`); return UnmaskFallback; } diff --git a/packages/core/src/js/replay/browserReplay.ts b/packages/core/src/js/replay/browserReplay.ts index b72c0be69f..f316e42d57 100644 --- a/packages/core/src/js/replay/browserReplay.ts +++ b/packages/core/src/js/replay/browserReplay.ts @@ -1,8 +1,29 @@ import { replayIntegration } from '@sentry/react'; +import { notWeb } from '../utils/environment'; +import type { Replay } from './replayInterface'; + +/** + * ReplayConfiguration for browser replay integration. + * + * See the [Configuration documentation](https://docs.sentry.io/platforms/javascript/session-replay/configuration/) for more information. + */ +type ReplayConfiguration = Parameters[0]; + +// https://github.com/getsentry/sentry-javascript/blob/e00cb04f1bbf494067cd8475d392266ba296987a/packages/replay-internal/src/integration.ts#L109 +const INTEGRATION_NAME = 'Replay'; + +/** + * Browser Replay integration for React Native. + * + * See the [Browser Replay documentation](https://docs.sentry.io/platforms/javascript/session-replay/) for more information. + */ +const browserReplayIntegration = (options: ReplayConfiguration = {}): Replay => { + if (notWeb()) { + // This is required because `replayIntegration` browser check doesn't + // work for React Native. + return browserReplayIntegrationNoop(); + } -const browserReplayIntegration = ( - options: Parameters[0] = {}, -): ReturnType => { return replayIntegration({ ...options, mask: ['.sentry-react-native-mask', ...(options.mask || [])], @@ -10,4 +31,18 @@ const browserReplayIntegration = ( }); }; +const browserReplayIntegrationNoop = (): Replay => { + return { + name: INTEGRATION_NAME, + // eslint-disable-next-line @typescript-eslint/no-empty-function + start: () => {}, + // eslint-disable-next-line @typescript-eslint/no-empty-function + startBuffering: () => {}, + stop: () => Promise.resolve(), + flush: () => Promise.resolve(), + getReplayId: () => undefined, + getRecordingMode: () => undefined, + }; +}; + export { browserReplayIntegration }; diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts index 79aa1117ec..3530303c47 100644 --- a/packages/core/src/js/replay/mobilereplay.ts +++ b/packages/core/src/js/replay/mobilereplay.ts @@ -1,6 +1,5 @@ import type { Client, DynamicSamplingContext, Event, Integration } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { isHardCrash } from '../misc'; import { hasHooks } from '../utils/clientutils'; import { isExpoGo, notMobileOs } from '../utils/environment'; @@ -116,12 +115,12 @@ type MobileReplayIntegration = Integration & { */ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defaultOptions): MobileReplayIntegration => { if (isExpoGo()) { - logger.warn( + debug.warn( `[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported in Expo Go. Use EAS Build or \`expo prebuild\` to enable it.`, ); } if (notMobileOs()) { - logger.warn(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported on this platform.`); + debug.warn(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} is not supported on this platform.`); } if (isExpoGo() || notMobileOs()) { @@ -131,7 +130,7 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau const options = mergeOptions(initOptions); async function processEvent(event: Event): Promise { - const hasException = event.exception && event.exception.values && event.exception.values.length > 0; + const hasException = event.exception?.values && event.exception.values.length > 0; if (!hasException) { // Event is not an error, will not capture replay return event; @@ -139,7 +138,7 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau const recordingReplayId = NATIVE.getCurrentReplayId(); if (recordingReplayId) { - logger.debug( + debug.log( `[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} assign already recording replay ${recordingReplayId} for event ${event.event_id}.`, ); return event; @@ -147,7 +146,7 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau const replayId = await NATIVE.captureReplay(isHardCrash(event)); if (!replayId) { - logger.debug(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} not sampled for event ${event.event_id}.`); + debug.log(`[Sentry] ${MOBILE_REPLAY_INTEGRATION_NAME} not sampled for event ${event.event_id}.`); return event; } @@ -178,9 +177,6 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau // https://github.com/getsentry/sentry-javascript/blob/develop/packages/replay-internal/src/integration.ts#L45 return { name: MOBILE_REPLAY_INTEGRATION_NAME, - setupOnce() { - /* Noop */ - }, setup, processEvent, options: options, @@ -190,9 +186,6 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau const mobileReplayIntegrationNoop = (): MobileReplayIntegration => { return { name: MOBILE_REPLAY_INTEGRATION_NAME, - setupOnce() { - /* Noop */ - }, options: defaultOptions, }; }; diff --git a/packages/core/src/js/replay/networkUtils.ts b/packages/core/src/js/replay/networkUtils.ts index 6834294b33..68493f25d0 100644 --- a/packages/core/src/js/replay/networkUtils.ts +++ b/packages/core/src/js/replay/networkUtils.ts @@ -1,5 +1,4 @@ -import { RN_GLOBAL_OBJ } from '../utils/worldwide'; -import { utf8ToBytes } from '../vendor'; +import { encodeUTF8 } from '../utils/encode'; /** Convert a Content-Length header to number/undefined. */ export function parseContentLengthHeader(header: string | null | undefined): number | undefined { @@ -21,16 +20,16 @@ export function getBodySize(body: RequestBody): number | undefined { try { if (typeof body === 'string') { - return _encode(body).length; + return encodeUTF8(body).length; } if (body instanceof URLSearchParams) { - return _encode(body.toString()).length; + return encodeUTF8(body.toString()).length; } if (body instanceof FormData) { const formDataStr = _serializeFormData(body); - return _encode(formDataStr).length; + return encodeUTF8(formDataStr).length; } if (body instanceof Blob) { @@ -49,13 +48,6 @@ export function getBodySize(body: RequestBody): number | undefined { return undefined; } -function _encode(input: string): number[] | Uint8Array { - if (RN_GLOBAL_OBJ.TextEncoder) { - return new RN_GLOBAL_OBJ.TextEncoder().encode(input); - } - return utf8ToBytes(input); -} - function _serializeFormData(formData: FormData): string { // This is a bit simplified, but gives us a decent estimate // This converts e.g. { name: 'Anne Smith', age: 13 } to 'name=Anne+Smith&age=13' diff --git a/packages/core/src/js/replay/replayInterface.ts b/packages/core/src/js/replay/replayInterface.ts new file mode 100644 index 0000000000..0308a5a385 --- /dev/null +++ b/packages/core/src/js/replay/replayInterface.ts @@ -0,0 +1,57 @@ +import type { Integration, ReplayRecordingMode } from '@sentry/core'; + +// Based on Replay Class https://github.com/getsentry/sentry-javascript/blob/e00cb04f1bbf494067cd8475d392266ba296987a/packages/replay-internal/src/integration.ts#L50 + +/** + * Common interface for React Native Replay integrations. + * + * Both browser and mobile replay integrations should implement this interface + * to allow user manually control the replay. + */ +export interface Replay extends Integration { + /** + * Start a replay regardless of sampling rate. Calling this will always + * create a new session. Will log a message if replay is already in progress. + * + * Creates or loads a session, attaches listeners to varying events (DOM, + * PerformanceObserver, Recording, Sentry SDK, etc) + */ + start(): void; + + /** + * Start replay buffering. Buffers until `flush()` is called or, if + * `replaysOnErrorSampleRate` > 0, until an error occurs. + */ + startBuffering(): void; + + /** + * Currently, this needs to be manually called (e.g. for tests). Sentry SDK + * does not support a teardown + */ + stop(): Promise; + + /** + * If not in "session" recording mode, flush event buffer which will create a new replay. + * If replay is not enabled, a new session replay is started. + * Unless `continueRecording` is false, the replay will continue to record and + * behave as a "session"-based replay. + * + * Otherwise, queue up a flush. + */ + flush(options?: { continueRecording?: boolean }): Promise; + + /** + * Get the current session ID. + */ + getReplayId(): string | undefined; + + /** + * Get the current recording mode. This can be either `session` or `buffer`. + * + * `session`: Recording the whole session, sending it continuously + * `buffer`: Always keeping the last 60s of recording, requires: + * - having replaysOnErrorSampleRate > 0 to capture replay when an error occurs + * - or calling `flush()` to send the replay + */ + getRecordingMode(): ReplayRecordingMode | undefined; +} diff --git a/packages/core/src/js/replay/xhrUtils.ts b/packages/core/src/js/replay/xhrUtils.ts index 8118296ee5..deb0f3c88c 100644 --- a/packages/core/src/js/replay/xhrUtils.ts +++ b/packages/core/src/js/replay/xhrUtils.ts @@ -1,6 +1,5 @@ import type { Breadcrumb, BreadcrumbHint, SentryWrappedXMLHttpRequest, XhrBreadcrumbHint } from '@sentry/core'; import { dropUndefinedKeys } from '@sentry/core'; - import type { RequestBody } from './networkUtils'; import { getBodySize, parseContentLengthHeader } from './networkUtils'; diff --git a/packages/core/src/js/scopeSync.ts b/packages/core/src/js/scopeSync.ts index bc9f20c597..5c6537d89e 100644 --- a/packages/core/src/js/scopeSync.ts +++ b/packages/core/src/js/scopeSync.ts @@ -1,5 +1,5 @@ import type { Breadcrumb, Scope } from '@sentry/core'; - +import { logger } from '@sentry/react'; import { DEFAULT_BREADCRUMB_LEVEL } from './breadcrumb'; import { fillTyped } from './utils/fill'; import { convertToNormalizedObject } from './utils/normalize'; @@ -26,14 +26,14 @@ export function enableSyncToNative(scope: Scope): void { }); fillTyped(scope, 'setTag', original => (key, value): Scope => { - NATIVE.setTag(key, value as string); + NATIVE.setTag(key, NATIVE.primitiveProcessor(value)); return original.call(scope, key, value); }); fillTyped(scope, 'setTags', original => (tags): Scope => { // As native only has setTag, we just loop through each tag key. Object.keys(tags).forEach(key => { - NATIVE.setTag(key, tags[key] as string); + NATIVE.setTag(key, NATIVE.primitiveProcessor(tags[key])); }); return original.call(scope, tags); }); @@ -60,7 +60,11 @@ export function enableSyncToNative(scope: Scope): void { original.call(scope, mergedBreadcrumb, maxBreadcrumbs); const finalBreadcrumb = scope.getLastBreadcrumb(); - NATIVE.addBreadcrumb(finalBreadcrumb); + if (finalBreadcrumb) { + NATIVE.addBreadcrumb(finalBreadcrumb); + } else { + logger.warn('[ScopeSync] Last created breadcrumb is undefined. Skipping sync to native.'); + } return scope; }); diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx index 7d085a6725..7e70ae492f 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -1,12 +1,18 @@ /* eslint-disable complexity */ -import type { Breadcrumb, BreadcrumbHint, Integration, Scope, SendFeedbackParams, UserFeedback } from '@sentry/core'; -import { captureFeedback, getClient, getGlobalScope, getIntegrationsToSetup, getIsolationScope, initAndBind, logger, makeDsn, stackParserFromStackParserOptions, withScope as coreWithScope } from '@sentry/core'; +import type { Breadcrumb, BreadcrumbHint, Integration, Scope } from '@sentry/core'; import { - defaultStackParser, - makeFetchTransport, -} from '@sentry/react'; + debug, + getClient, + getGlobalScope, + getIntegrationsToSetup, + getIsolationScope, + initAndBind, + makeDsn, + stackParserFromStackParserOptions, + withScope as coreWithScope, +} from '@sentry/core'; +import { defaultStackParser, makeFetchTransport, Profiler } from '@sentry/react'; import * as React from 'react'; - import { ReactNativeClient } from './client'; import { FeedbackWidgetProvider } from './feedback/FeedbackWidgetProvider'; import { getDevServer } from './integrations/debugsymbolicatorutils'; @@ -18,7 +24,8 @@ import { TouchEventBoundary } from './touchevents'; import { ReactNativeProfiler } from './tracing'; import { useEncodePolyfill } from './transports/encodePolyfill'; import { DEFAULT_BUFFER_SIZE, makeNativeTransportFactory } from './transports/native'; -import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer } from './utils/environment'; +import { getDefaultEnvironment, isExpoGo, isRunningInMetroDevServer, isWeb } from './utils/environment'; +import { getDefaultRelease } from './utils/release'; import { safeFactory, safeTracesSampler } from './utils/safe'; import { NATIVE } from './wrapper'; @@ -63,13 +70,13 @@ export function init(passedOptions: ReactNativeOptions): void { enableSyncToNative(getIsolationScope()); } - const getURLFromDSN = (dsn: string | null): string | undefined => { + const getURLFromDSN = (dsn: string | undefined): string | undefined => { if (!dsn) { return undefined; } const dsnComponents = makeDsn(dsn); if (!dsnComponents) { - logger.error('Failed to extract url from DSN: ', dsn); + debug.error('Failed to extract url from DSN: ', dsn); return undefined; } const port = dsnComponents.port ? `:${dsnComponents.port}` : ''; @@ -105,6 +112,7 @@ export function init(passedOptions: ReactNativeOptions): void { const options: ReactNativeClientOptions = { ...DEFAULT_OPTIONS, ...passedOptions, + release: passedOptions.release ?? getDefaultRelease(), enableNative, enableNativeNagger: shouldEnableNativeNagger(passedOptions.enableNativeNagger), // If custom transport factory fails the SDK won't initialize @@ -143,8 +151,8 @@ export function init(passedOptions: ReactNativeOptions): void { initAndBind(ReactNativeClient, options); if (isExpoGo()) { - logger.info('Offline caching, native errors features are not available in Expo Go.'); - logger.info('Use EAS Build / Native Release Build to test these features.'); + debug.log('Offline caching, native errors features are not available in Expo Go.'); + debug.log('Use EAS Build / Native Release Build to test these features.'); } } @@ -156,18 +164,21 @@ export function wrap

>( options?: ReactNativeWrapperOptions ): React.ComponentType

{ const profilerProps = { - ...(options?.profilerProps ?? {}), + ...(options?.profilerProps), name: RootComponent.displayName ?? 'Root', + updateProps: {} }; - const RootApp: React.FC

= (appProps) => { + const ProfilerComponent = isWeb() ? Profiler : ReactNativeProfiler; + + const RootApp: React.FC

= appProps => { return ( - + - + ); }; @@ -199,7 +210,7 @@ export async function flush(): Promise { // eslint-disable-next-line no-empty } catch (_) { } - logger.error('Failed to flush the event queue.'); + debug.error('Failed to flush the event queue.'); return false; } @@ -215,24 +226,10 @@ export async function close(): Promise { await client.close(); } } catch (e) { - logger.error('Failed to close the SDK'); + debug.error('Failed to close the SDK'); } } -/** - * Captures user feedback and sends it to Sentry. - * @deprecated Use `Sentry.captureFeedback` instead. - */ -export function captureUserFeedback(feedback: UserFeedback): void { - const feedbackParams: SendFeedbackParams = { - name: feedback.name, - email: feedback.email, - message: feedback.comments, - associatedEventId: feedback.event_id, - }; - captureFeedback(feedbackParams); -} - /** * Creates a new scope with and executes the given operation within. * The scope is automatically removed once the operation @@ -251,7 +248,7 @@ export function withScope(callback: (scope: Scope) => T): T | undefined { try { return callback(scope); } catch (e) { - logger.error('Error while running withScope callback', e); + debug.error('Error while running withScope callback', e); return undefined; } }; diff --git a/packages/core/src/js/tools/ModulesCollector.ts b/packages/core/src/js/tools/ModulesCollector.ts index 9d942d1a07..cf5b921e4e 100644 --- a/packages/core/src/js/tools/ModulesCollector.ts +++ b/packages/core/src/js/tools/ModulesCollector.ts @@ -1,8 +1,8 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'; import { posix, sep } from 'path'; -logger.enable(); +debug.enable(); // eslint-disable-next-line @typescript-eslint/unbound-method const { dirname, join, resolve, sep: posixSep } = posix; @@ -63,7 +63,7 @@ export default class ModulesCollector { version: info.version, }; } catch (error) { - logger.error(`Failed to read ${pkgPath}`); + debug.error(`Failed to read ${pkgPath}`); } return upDirSearch(); // processed package.json file, continue up search @@ -90,36 +90,36 @@ export default class ModulesCollector { collect: (sources: unknown[], modulesPaths: string[]) => Record; }>): void { if (!sourceMapPath) { - logger.error('First argument `source-map-path` is missing!'); + debug.error('First argument `source-map-path` is missing!'); return; } if (!outputModulesPath) { - logger.error('Second argument `modules-output-path` is missing!'); + debug.error('Second argument `modules-output-path` is missing!'); return; } if (!modulesPaths || modulesPaths.length === 0) { - logger.error('Third argument `modules-paths` is missing!'); + debug.error('Third argument `modules-paths` is missing!'); return; } - logger.info('Reading source map from', sourceMapPath); - logger.info('Saving modules to', outputModulesPath); - logger.info('Resolving modules from paths', modulesPaths.join(', ')); + debug.log('Reading source map from', sourceMapPath); + debug.log('Saving modules to', outputModulesPath); + debug.log('Resolving modules from paths', modulesPaths.join(', ')); if (!existsSync(sourceMapPath)) { - logger.error(`Source map file does not exist at ${sourceMapPath}`); + debug.error(`Source map file does not exist at ${sourceMapPath}`); return; } for (const modulesPath of modulesPaths) { if (!existsSync(modulesPath)) { - logger.error(`Modules path does not exist at ${modulesPath}`); + debug.error(`Modules path does not exist at ${modulesPath}`); return; } } const map: { sources?: unknown } = JSON.parse(readFileSync(sourceMapPath, 'utf8')); if (!map.sources || !Array.isArray(map.sources)) { - logger.error(`Modules not collected. No sources found in the source map (${sourceMapPath})!`); + debug.error(`Modules not collected. No sources found in the source map (${sourceMapPath})!`); return; } @@ -131,6 +131,6 @@ export default class ModulesCollector { mkdirSync(outputModulesDirPath, { recursive: true }); } writeFileSync(outputModulesPath, JSON.stringify(modules, null, 2)); - logger.info(`Modules collected and saved to: ${outputModulesPath}`); + debug.log(`Modules collected and saved to: ${outputModulesPath}`); } } diff --git a/packages/core/src/js/tools/collectModules.ts b/packages/core/src/js/tools/collectModules.ts index ed386b2f50..15cca70dfb 100755 --- a/packages/core/src/js/tools/collectModules.ts +++ b/packages/core/src/js/tools/collectModules.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import { argv } from 'process'; - import ModulesCollector from './ModulesCollector'; const sourceMapPath: string | undefined = argv[2]; diff --git a/packages/core/src/js/tools/enableLogger.ts b/packages/core/src/js/tools/enableLogger.ts index 4dbc691916..9cd4d5be74 100644 --- a/packages/core/src/js/tools/enableLogger.ts +++ b/packages/core/src/js/tools/enableLogger.ts @@ -1,10 +1,10 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; /** * Enables debug logger when SENTRY_LOG_LEVEL=debug. */ export function enableLogger(): void { if (process.env.SENTRY_LOG_LEVEL === 'debug') { - logger.enable(); + debug.enable(); } } diff --git a/packages/core/src/js/tools/metroMiddleware.ts b/packages/core/src/js/tools/metroMiddleware.ts index aba23971a6..66b9e6157d 100644 --- a/packages/core/src/js/tools/metroMiddleware.ts +++ b/packages/core/src/js/tools/metroMiddleware.ts @@ -1,5 +1,5 @@ import type { StackFrame } from '@sentry/core'; -import { addContextToFrame, logger } from '@sentry/core'; +import { addContextToFrame, debug } from '@sentry/core'; import { readFile } from 'fs'; import type { IncomingMessage, ServerResponse } from 'http'; import type { InputConfigT, Middleware } from 'metro-config'; @@ -15,7 +15,7 @@ export const stackFramesContextMiddleware: Middleware = async ( request: IncomingMessage, response: ServerResponse, ): Promise => { - logger.debug('[@sentry/react-native/metro] Received request for stack frames context.'); + debug.log('[@sentry/react-native/metro] Received request for stack frames context.'); request.setEncoding('utf8'); const rawBody = await getRawBody(request); @@ -25,14 +25,14 @@ export const stackFramesContextMiddleware: Middleware = async ( try { body = JSON.parse(rawBody); } catch (e) { - logger.debug('[@sentry/react-native/metro] Could not parse request body.', e); + debug.log('[@sentry/react-native/metro] Could not parse request body.', e); badRequest(response, 'Invalid request body. Expected a JSON object.'); return; } const stack = body.stack; if (!Array.isArray(stack)) { - logger.debug('[@sentry/react-native/metro] Invalid stack frames.', stack); + debug.log('[@sentry/react-native/metro] Invalid stack frames.', stack); badRequest(response, 'Invalid stack frames. Expected an array.'); return; } @@ -41,7 +41,7 @@ export const stackFramesContextMiddleware: Middleware = async ( response.setHeader('Content-Type', 'application/json'); response.statusCode = 200; response.end(JSON.stringify({ stack: stackWithSourceContext })); - logger.debug('[@sentry/react-native/metro] Sent stack frames context.'); + debug.log('[@sentry/react-native/metro] Sent stack frames context.'); }; async function addSourceContext(frame: StackFrame): Promise { @@ -51,7 +51,7 @@ async function addSourceContext(frame: StackFrame): Promise { try { if (typeof frame.filename !== 'string') { - logger.warn('[@sentry/react-native/metro] Could not read source context for frame without filename.'); + debug.warn('[@sentry/react-native/metro] Could not read source context for frame without filename.'); return frame; } @@ -59,7 +59,7 @@ async function addSourceContext(frame: StackFrame): Promise { const lines = source.split('\n'); addContextToFrame(lines, frame); } catch (error) { - logger.warn('[@sentry/react-native/metro] Could not read source context for frame.', error); + debug.warn('[@sentry/react-native/metro] Could not read source context for frame.', error); } return frame; } diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index 79c9fd43b0..64897eea1b 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -1,18 +1,19 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import type { MetroConfig, MixedOutput, Module, ReadOnlyGraph } from 'metro'; import type { CustomResolutionContext, CustomResolver, Resolution } from 'metro-resolver'; import * as process from 'process'; import { env } from 'process'; - import { enableLogger } from './enableLogger'; +import { withSentryMiddleware } from './metroMiddleware'; import { setSentryBabelTransformerOptions, setSentryDefaultBabelTransformerPathEnv, } from './sentryBabelTransformerUtils'; -import { createSentryMetroSerializer, unstable_beforeAssetSerializationPlugin } from './sentryMetroSerializer'; +import { createSentryMetroSerializer, unstableBeforeAssetSerializationDebugIdPlugin } from './sentryMetroSerializer'; +import { unstableReleaseConstantsPlugin } from './sentryReleaseInjector'; import type { DefaultConfigOptions } from './vendor/expo/expoconfig'; + export * from './sentryMetroSerializer'; -import { withSentryMiddleware } from './metroMiddleware'; enableLogger(); @@ -44,6 +45,13 @@ export interface SentryExpoConfigOptions { * Pass a custom `getDefaultConfig` function to override the default Expo configuration getter. */ getDefaultConfig?: typeof getSentryExpoConfig; + + /** + * For Expo Web, inject `release` and `version` options from `app.json`, the Expo Application Config. + * + * @default true + */ + injectReleaseForWeb?: boolean; } /** @@ -93,7 +101,8 @@ export function getSentryExpoConfig( ...options, unstable_beforeAssetSerializationPlugins: [ ...(options.unstable_beforeAssetSerializationPlugins || []), - unstable_beforeAssetSerializationPlugin, + ...(options.injectReleaseForWeb ?? true ? [unstableReleaseConstantsPlugin(projectRoot)] : []), + unstableBeforeAssetSerializationDebugIdPlugin, ], }); @@ -140,8 +149,8 @@ export function withSentryBabelTransformer( config: MetroConfig, annotateReactComponents: true | { ignoredComponents?: string[] }, ): MetroConfig { - const defaultBabelTransformerPath = config.transformer && config.transformer.babelTransformerPath; - logger.debug('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath); + const defaultBabelTransformerPath = config.transformer?.babelTransformerPath; + debug.log('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath); if (!defaultBabelTransformerPath) { // This has to be console.warn because the options is enabled but won't be used @@ -263,17 +272,17 @@ export function withSentryFramesCollapsed(config: MetroConfig): MetroConfig { const collapseSentryInternalFrames = (frame: MetroFrame): boolean => typeof frame.file === 'string' && (frame.file.includes('node_modules/@sentry/core/cjs/instrument.js') || - frame.file.includes('node_modules/@sentry/core/cjs/logger.js')); + frame.file.includes('node_modules/@sentry/core/cjs/debug.js')); const customizeFrame = (frame: MetroFrame): MetroCustomizeFrameReturnValue => { const originalOrSentryCustomizeFrame = ( originalCustomization: MetroCustomizeFrame | undefined, ): MetroCustomizeFrame => ({ ...originalCustomization, - collapse: (originalCustomization && originalCustomization.collapse) || collapseSentryInternalFrames(frame), + collapse: originalCustomization?.collapse || collapseSentryInternalFrames(frame), }); - const maybePromiseCustomization = (originalCustomizeFrame && originalCustomizeFrame(frame)) || undefined; + const maybePromiseCustomization = originalCustomizeFrame?.(frame) || undefined; if (maybePromiseCustomization !== undefined && 'then' in maybePromiseCustomization) { return maybePromiseCustomization.then(originalCustomization => diff --git a/packages/core/src/js/tools/sentryBabelTransformerUtils.ts b/packages/core/src/js/tools/sentryBabelTransformerUtils.ts index 6484291cb9..6c77f42aef 100644 --- a/packages/core/src/js/tools/sentryBabelTransformerUtils.ts +++ b/packages/core/src/js/tools/sentryBabelTransformerUtils.ts @@ -1,7 +1,6 @@ import componentAnnotatePlugin from '@sentry/babel-plugin-component-annotate'; -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import * as process from 'process'; - import type { BabelTransformer, BabelTransformerArgs } from './vendor/metro/metroBabelTransformer'; export type SentryBabelTransformerOptions = { annotateReactComponents?: { ignoredComponents?: string[] } }; @@ -14,7 +13,7 @@ export const SENTRY_BABEL_TRANSFORMER_OPTIONS = 'SENTRY_BABEL_TRANSFORMER_OPTION */ export function setSentryDefaultBabelTransformerPathEnv(defaultBabelTransformerPath: string): void { process.env[SENTRY_DEFAULT_BABEL_TRANSFORMER_PATH] = defaultBabelTransformerPath; - logger.debug(`Saved default Babel transformer path ${defaultBabelTransformerPath}`); + debug.log(`Saved default Babel transformer path ${defaultBabelTransformerPath}`); } /** @@ -35,7 +34,7 @@ export function loadDefaultBabelTransformer(): BabelTransformer { ); } - logger.debug(`Loading default Babel transformer from ${defaultBabelTransformerPath}`); + debug.log(`Loading default Babel transformer from ${defaultBabelTransformerPath}`); // eslint-disable-next-line @typescript-eslint/no-var-requires return require(defaultBabelTransformerPath); } @@ -46,7 +45,7 @@ export function loadDefaultBabelTransformer(): BabelTransformer { export function setSentryBabelTransformerOptions(options: SentryBabelTransformerOptions): void { let optionsString: string | null = null; try { - logger.debug(`Stringifying Sentry Babel transformer options`, options); + debug.log('Stringifying Sentry Babel transformer options', options); optionsString = JSON.stringify(options); } catch (e) { // eslint-disable-next-line no-console @@ -57,7 +56,7 @@ export function setSentryBabelTransformerOptions(options: SentryBabelTransformer return; } - logger.debug(`Sentry Babel transformer options set to ${SENTRY_BABEL_TRANSFORMER_OPTIONS}`, optionsString); + debug.log(`Sentry Babel transformer options set to ${SENTRY_BABEL_TRANSFORMER_OPTIONS}`, optionsString); process.env[SENTRY_BABEL_TRANSFORMER_OPTIONS] = optionsString; } @@ -67,14 +66,12 @@ export function setSentryBabelTransformerOptions(options: SentryBabelTransformer export function getSentryBabelTransformerOptions(): SentryBabelTransformerOptions | undefined { const optionsString = process.env[SENTRY_BABEL_TRANSFORMER_OPTIONS]; if (!optionsString) { - logger.debug( - `Sentry Babel transformer options environment variable ${SENTRY_BABEL_TRANSFORMER_OPTIONS} is not set`, - ); + debug.log(`Sentry Babel transformer options environment variable ${SENTRY_BABEL_TRANSFORMER_OPTIONS} is not set`); return undefined; } try { - logger.debug(`Parsing Sentry Babel transformer options from ${optionsString}`); + debug.log(`Parsing Sentry Babel transformer options from ${optionsString}`); return JSON.parse(optionsString); } catch (e) { // eslint-disable-next-line no-console diff --git a/packages/core/src/js/tools/sentryMetroSerializer.ts b/packages/core/src/js/tools/sentryMetroSerializer.ts index b34e21a1e3..8e31f512b9 100644 --- a/packages/core/src/js/tools/sentryMetroSerializer.ts +++ b/packages/core/src/js/tools/sentryMetroSerializer.ts @@ -1,31 +1,28 @@ import * as crypto from 'crypto'; // eslint-disable-next-line import/no-extraneous-dependencies import type { MixedOutput, Module, ReadOnlyGraph } from 'metro'; -import type * as countLinesType from 'metro/private/lib/countLines'; - import type { Bundle, MetroSerializer, MetroSerializerOutput, SerializedBundle, VirtualJSOutput } from './utils'; -import { createDebugIdSnippet, createSet, determineDebugIdFromBundleSource, stringToUUID } from './utils'; +import { + createDebugIdSnippet, + createVirtualJSModule, + determineDebugIdFromBundleSource, + prependModule, + stringToUUID, +} from './utils'; import { createDefaultMetroSerializer } from './vendor/metro/utils'; -let countLines: typeof countLinesType; -try { - countLines = require('metro/private/lib/countLines'); -} catch (e) { - countLines = require('metro/src/lib/countLines'); -} - type SourceMap = Record; const DEBUG_ID_PLACE_HOLDER = '__debug_id_place_holder__'; const DEBUG_ID_MODULE_PATH = '__debugid__'; -const PRELUDE_MODULE_PATH = '__prelude__'; + const SOURCE_MAP_COMMENT = '//# sourceMappingURL='; const DEBUG_ID_COMMENT = '//# debugId='; /** * Adds Sentry Debug ID polyfill module to the bundle. */ -export function unstable_beforeAssetSerializationPlugin({ +export function unstableBeforeAssetSerializationDebugIdPlugin({ premodules, debugId, }: { @@ -45,7 +42,7 @@ export function unstable_beforeAssetSerializationPlugin({ } const debugIdModule = createDebugIdModule(debugId); - return [...addDebugIdModule(premodules, debugIdModule)]; + return prependModule(premodules, debugIdModule); } /** @@ -70,7 +67,7 @@ export const createSentryMetroSerializer = (customSerializer?: MetroSerializer): const debugIdModule = createDebugIdModule(DEBUG_ID_PLACE_HOLDER); options.sentryBundleCallback = createSentryBundleCallback(debugIdModule); - const modifiedPreModules = addDebugIdModule(preModules, debugIdModule); + const modifiedPreModules = prependModule(preModules, debugIdModule); // Run wrapped serializer const serializerResult = serializer(entryPoint, modifiedPreModules, graph, options); @@ -127,25 +124,6 @@ function createSentryBundleCallback(debugIdModule: Module & { s }; } -function addDebugIdModule( - preModules: readonly Module[], - debugIdModule: Module, -): readonly Module[] { - const modifiedPreModules = [...preModules]; - if ( - modifiedPreModules.length > 0 && - modifiedPreModules[0] !== undefined && - modifiedPreModules[0].path === PRELUDE_MODULE_PATH - ) { - // prelude module must be first as it measures the bundle startup time - modifiedPreModules.unshift(preModules[0] as Module); - modifiedPreModules[1] = debugIdModule; - } else { - modifiedPreModules.unshift(debugIdModule); - } - return modifiedPreModules; -} - async function extractSerializerResult(serializerResult: MetroSerializerOutput): Promise { if (typeof serializerResult === 'string') { return { code: serializerResult, map: '{}' }; @@ -164,27 +142,7 @@ async function extractSerializerResult(serializerResult: MetroSerializerOutput): } function createDebugIdModule(debugId: string): Module & { setSource: (code: string) => void } { - let debugIdCode = createDebugIdSnippet(debugId); - - return { - setSource: (code: string) => { - debugIdCode = code; - }, - dependencies: new Map(), - getSource: () => Buffer.from(debugIdCode), - inverseDependencies: createSet(), - path: DEBUG_ID_MODULE_PATH, - output: [ - { - type: 'js/script/virtual', - data: { - code: debugIdCode, - lineCount: countLines(debugIdCode), - map: [], - }, - }, - ], - }; + return createVirtualJSModule(DEBUG_ID_MODULE_PATH, createDebugIdSnippet(debugId)); } function calculateDebugId(bundle: Bundle): string { diff --git a/packages/core/src/js/tools/sentryReleaseInjector.ts b/packages/core/src/js/tools/sentryReleaseInjector.ts new file mode 100644 index 0000000000..527ecf4e60 --- /dev/null +++ b/packages/core/src/js/tools/sentryReleaseInjector.ts @@ -0,0 +1,46 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import type { MixedOutput, Module, ReadOnlyGraph } from 'metro'; +import type { VirtualJSOutput } from './utils'; +import { createVirtualJSModule, getExpoConfig, prependModule } from './utils'; + +const RELEASE_CONSTANTS_MODULE_PATH = '__sentryReleaseConstants__'; + +/** + * Adds Sentry Release constants to the bundle. + */ +export const unstableReleaseConstantsPlugin = + (projectRoot: string) => + ({ graph, premodules }: { graph: ReadOnlyGraph; premodules: Module[]; debugId?: string }): Module[] => { + const notWeb = graph.transformOptions.platform !== 'web'; + if (notWeb) { + return premodules; + } + + const { name, version } = getExpoConfig(projectRoot); + + if (!name || !version) { + return premodules; + } + + return prependModule( + premodules, + createSentryReleaseModule({ + name, + version, + }), + ); + }; + +function createSentryReleaseModule({ + name, + version, +}: { + name: string; + version: string; +}): Module & { setSource: (code: string) => void } { + return createVirtualJSModule(RELEASE_CONSTANTS_MODULE_PATH, createReleaseConstantsSnippet({ name, version })); +} + +function createReleaseConstantsSnippet({ name, version }: { name: string; version: string }): string { + return `var SENTRY_RELEASE;SENTRY_RELEASE={name: "${name}", version: "${version}"};`; +} diff --git a/packages/core/src/js/tools/utils.ts b/packages/core/src/js/tools/utils.ts index 10a73e43a9..8378334362 100644 --- a/packages/core/src/js/tools/utils.ts +++ b/packages/core/src/js/tools/utils.ts @@ -1,8 +1,17 @@ import * as crypto from 'crypto'; // eslint-disable-next-line import/no-extraneous-dependencies -import type { Module, ReadOnlyGraph, SerializerOptions } from 'metro'; +import type { MixedOutput, Module, ReadOnlyGraph, SerializerOptions } from 'metro'; +// eslint-disable-next-line import/no-extraneous-dependencies +import type * as countLinesType from 'metro/private/lib/countLines'; import type CountingSet from 'metro/src/lib/CountingSet'; // types are in src but exports are in private +let countLines: typeof countLinesType; +try { + countLines = require('metro/private/lib/countLines'); +} catch (e) { + countLines = require('metro/src/lib/countLines'); +} + // Variant of MixedOutput // https://github.com/facebook/metro/blob/9b85f83c9cc837d8cd897aa7723be7da5b296067/packages/metro/src/DeltaBundler/types.flow.js#L21 export type VirtualJSOutput = { @@ -125,3 +134,83 @@ function safeRequireCountingSetFromPrivate(): { default: new () => CountingSe } export const createSet = resolveSetCreator(); + +const PRELUDE_MODULE_PATH = '__prelude__'; + +/** + * Prepends the module after default required prelude modules. + */ +export function prependModule( + modules: readonly Module[], + module: Module, +): Module[] { + const modifiedPreModules = [...modules]; + if ( + modifiedPreModules.length > 0 && + modifiedPreModules[0] !== undefined && + modifiedPreModules[0].path === PRELUDE_MODULE_PATH + ) { + // prelude module must be first as it measures the bundle startup time + modifiedPreModules.unshift(modules[0] as Module); + modifiedPreModules[1] = module; + } else { + modifiedPreModules.unshift(module); + } + return modifiedPreModules; +} + +/** + * Creates a virtual JS module with the given path and code. + */ +export function createVirtualJSModule( + modulePath: string, + moduleCode: string, +): Module & { setSource: (code: string) => void } { + let sourceCode = moduleCode; + + return { + setSource: (code: string) => { + sourceCode = code; + }, + dependencies: new Map(), + getSource: () => Buffer.from(sourceCode), + inverseDependencies: createSet(), + path: modulePath, + output: [ + { + type: 'js/script/virtual', + data: { + code: sourceCode, + lineCount: countLines(sourceCode), + map: [], + }, + }, + ], + }; +} + +/** + * Tries to load Expo config using `@expo/config` package. + */ +export function getExpoConfig(projectRoot: string): Partial<{ + name: string; + version: string; +}> { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires, import/no-extraneous-dependencies + const expoConfig = require('@expo/config') as { + getConfig?: (projectRoot: string) => { exp: Record }; + }; + if (expoConfig.getConfig) { + const { exp } = expoConfig.getConfig(projectRoot); + return { + name: typeof exp.name === 'string' && exp.name ? exp.name : undefined, + version: typeof exp.version === 'string' && exp.version ? exp.version : undefined, + }; + } + } catch { + // @expo/config not available, do nothing + } + + return {}; +} diff --git a/packages/core/src/js/tools/vendor/metro/utils.ts b/packages/core/src/js/tools/vendor/metro/utils.ts index ba2817ee86..5b9ae21049 100644 --- a/packages/core/src/js/tools/vendor/metro/utils.ts +++ b/packages/core/src/js/tools/vendor/metro/utils.ts @@ -29,6 +29,7 @@ import type { MixedOutput, Module, ReadOnlyGraph } from 'metro'; import type * as baseJSBundleType from 'metro/private/DeltaBundler/Serializers/baseJSBundle'; import type * as sourceMapStringType from 'metro/private/DeltaBundler/Serializers/sourceMapString'; import type * as bundleToStringType from 'metro/private/lib/bundleToString'; +import type { MetroSerializer } from '../../utils'; let baseJSBundle: typeof baseJSBundleType; try { @@ -58,8 +59,6 @@ try { bundleToString = require('metro/src/lib/bundleToString'); } -import type { MetroSerializer } from '../../utils'; - type NewSourceMapStringExport = { // Since Metro v0.80.10 https://github.com/facebook/metro/compare/v0.80.9...v0.80.10#diff-1b836d1729e527a725305eef0cec22e44605af2700fa413f4c2489ea1a03aebcL28 sourceMapString: typeof sourceMapString; diff --git a/packages/core/src/js/touchevents.tsx b/packages/core/src/js/touchevents.tsx index 293391a834..aaf541dad1 100644 --- a/packages/core/src/js/touchevents.tsx +++ b/packages/core/src/js/touchevents.tsx @@ -1,9 +1,8 @@ import type { SeverityLevel } from '@sentry/core'; -import { addBreadcrumb, dropUndefinedKeys, getClient, logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; +import { addBreadcrumb, debug, dropUndefinedKeys, getClient, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import * as React from 'react'; import type { GestureResponderEvent } from 'react-native'; import { StyleSheet, View } from 'react-native'; - import { createIntegration } from './integrations/factory'; import { startUserInteractionSpan } from './tracing/integrations/userInteraction'; import { UI_ACTION_TOUCH } from './tracing/ops'; @@ -121,8 +120,12 @@ class TouchEventBoundary extends React.Component { const level = 'info' as SeverityLevel; const root = touchPath[0]; - const detail = label ? label : `${root.name}${root.file ? ` (${root.file})` : ''}`; + if (!root) { + debug.warn('[TouchEvents] No root component found in touch path.'); + return; + } + const detail = label ? label : `${root.name}${root.file ? ` (${root.file})` : ''}`; const crumb = { category: this.props.breadcrumbCategory, data: { path: touchPath }, @@ -132,7 +135,7 @@ class TouchEventBoundary extends React.Component { }; addBreadcrumb(crumb); - logger.log(`[TouchEvents] ${crumb.message}`); + debug.log(`[TouchEvents] ${crumb.message}`); } /** diff --git a/packages/core/src/js/tracing/gesturetracing.ts b/packages/core/src/js/tracing/gesturetracing.ts index 4dd012fe7b..67be06d644 100644 --- a/packages/core/src/js/tracing/gesturetracing.ts +++ b/packages/core/src/js/tracing/gesturetracing.ts @@ -1,6 +1,5 @@ import type { Breadcrumb } from '@sentry/core'; -import { addBreadcrumb, logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; - +import { addBreadcrumb, debug, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '@sentry/core'; import { startUserInteractionSpan } from './integrations/userInteraction'; import { UI_ACTION } from './ops'; import { SPAN_ORIGIN_AUTO_INTERACTION } from './origin'; @@ -47,17 +46,17 @@ export function sentryTraceGesture( ): GestureT { const gestureCandidate = gesture as unknown as BaseGesture | undefined | null; if (!gestureCandidate) { - logger.warn('[GestureTracing] Gesture can not be undefined'); + debug.warn('[GestureTracing] Gesture can not be undefined'); return gesture; } if (!gestureCandidate.handlers) { - logger.warn( + debug.warn( '[GestureTracing] Can not wrap gesture without handlers. If you want to wrap a gesture composition wrap individual gestures.', ); return gesture; } if (!label) { - logger.warn('[GestureTracing] Can not wrap gesture without name.'); + debug.warn('[GestureTracing] Can not wrap gesture without name.'); return gesture; } const name = @@ -123,7 +122,7 @@ function addGestureBreadcrumb( addBreadcrumb(crumb); - logger.log(`[GestureTracing] ${crumb.message}`); + debug.log(`[GestureTracing] ${crumb.message}`); } /** diff --git a/packages/core/src/js/tracing/integrations/appStart.ts b/packages/core/src/js/tracing/integrations/appStart.ts index b5576d8b2f..181737b188 100644 --- a/packages/core/src/js/tracing/integrations/appStart.ts +++ b/packages/core/src/js/tracing/integrations/appStart.ts @@ -1,16 +1,15 @@ /* eslint-disable complexity, max-lines */ import type { Client, Event, Integration, Span, SpanJSON, TransactionEvent } from '@sentry/core'; import { + debug, getCapturedScopesOnSpan, getClient, getCurrentScope, - logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, startInactiveSpan, timestampInSeconds, } from '@sentry/core'; - import { getAppRegistryIntegration } from '../../integrations/appRegistry'; import { APP_START_COLD as APP_START_COLD_MEASUREMENT, @@ -76,7 +75,7 @@ export function captureAppStart(): Promise { export async function _captureAppStart({ isManual }: { isManual: boolean }): Promise { const client = getClient(); if (!client) { - logger.warn('[AppStart] Could not capture App Start, missing client.'); + debug.warn('[AppStart] Could not capture App Start, missing client.'); return; } @@ -88,9 +87,9 @@ export async function _captureAppStart({ isManual }: { isManual: boolean }): Pro if (NATIVE.enableNative) { try { endFrames = await NATIVE.fetchNativeFrames(); - logger.debug('[AppStart] Captured end frames for app start.', endFrames); + debug.log('[AppStart] Captured end frames for app start.', endFrames); } catch (error) { - logger.debug('[AppStart] Failed to capture end frames for app start.', error); + debug.log('[AppStart] Failed to capture end frames for app start.', error); } } @@ -107,8 +106,8 @@ export async function _captureAppStart({ isManual }: { isManual: boolean }): Pro * Used automatically by `Sentry.wrap` and `Sentry.ReactNativeProfiler`. */ export function setRootComponentCreationTimestampMs(timestampMs: number): void { - appStartEndData?.timestampMs && logger.warn('Setting Root component creation timestamp after app start end is set.'); - rootComponentCreationTimestampMs && logger.warn('Overwriting already set root component creation timestamp.'); + appStartEndData?.timestampMs && debug.warn('Setting Root component creation timestamp after app start end is set.'); + rootComponentCreationTimestampMs && debug.warn('Overwriting already set root component creation timestamp.'); rootComponentCreationTimestampMs = timestampMs; isRootComponentCreationTimestampMsManual = true; } @@ -129,7 +128,7 @@ export function _setRootComponentCreationTimestampMs(timestampMs: number): void * @private */ export const _setAppStartEndData = (data: AppStartEndData): void => { - appStartEndData && logger.warn('Overwriting already set app start end data.'); + appStartEndData && debug.warn('Overwriting already set app start end data.'); appStartEndData = data; }; @@ -147,7 +146,7 @@ export function _clearRootComponentCreationTimestampMs(): void { */ function attachFrameDataToSpan(span: SpanJSON, frames: NativeFramesResponse): void { if (frames.totalFrames <= 0 && frames.slowFrames <= 0 && frames.totalFrames <= 0) { - logger.warn(`[AppStart] Detected zero slow or frozen frames. Not adding measurements to spanId (${span.span_id}).`); + debug.warn(`[AppStart] Detected zero slow or frozen frames. Not adding measurements to spanId (${span.span_id}).`); return; } span.data = span.data || {}; @@ -155,7 +154,7 @@ function attachFrameDataToSpan(span: SpanJSON, frames: NativeFramesResponse): vo span.data['frames.slow'] = frames.slowFrames; span.data['frames.frozen'] = frames.frozenFrames; - logger.debug('[AppStart] Attached frame data to span.', { + debug.log('[AppStart] Attached frame data to span.', { spanId: span.span_id, frameData: { total: frames.totalFrames, @@ -191,7 +190,7 @@ export const appStartIntegration = ({ if (!enableAppStartTracking) { isEnabled = false; - logger.warn('[AppStart] App start tracking is disabled.'); + debug.warn('[AppStart] App start tracking is disabled.'); } client.on('spanStart', recordFirstStartedActiveRootSpanId); @@ -207,11 +206,11 @@ export const appStartIntegration = ({ getAppRegistryIntegration(client)?.onRunApplication(() => { if (appStartDataFlushed) { - logger.log('[AppStartIntegration] Resetting app start data flushed flag based on runApplication call.'); + debug.log('[AppStartIntegration] Resetting app start data flushed flag based on runApplication call.'); appStartDataFlushed = false; firstStartedActiveRootSpanId = undefined; } else { - logger.log( + debug.log( '[AppStartIntegration] Waiting for initial app start was flush, before updating based on runApplication call.', ); } @@ -251,23 +250,30 @@ export const appStartIntegration = ({ */ const setFirstStartedActiveRootSpanId = (spanId: string | undefined): void => { firstStartedActiveRootSpanId = spanId; - logger.debug('[AppStart] First started active root span id recorded.', firstStartedActiveRootSpanId); + debug.log('[AppStart] First started active root span id recorded.', firstStartedActiveRootSpanId); }; async function captureStandaloneAppStart(): Promise { + if (!_client) { + // If client is not set, SDK was not initialized, logger is thus disabled + // eslint-disable-next-line no-console + console.warn('[AppStart] Could not capture App Start, missing client, call `Sentry.init` first.'); + return; + } + if (!standalone) { - logger.debug( + debug.log( '[AppStart] App start tracking is enabled. App start will be added to the first transaction as a child span.', ); return; } - logger.debug('[AppStart] App start tracking standalone root span (transaction).'); + debug.log('[AppStart] App start tracking standalone root span (transaction).'); if (!appStartEndData?.endFrames && NATIVE.enableNative) { try { const endFrames = await NATIVE.fetchNativeFrames(); - logger.debug('[AppStart] Captured end frames for standalone app start.', endFrames); + debug.log('[AppStart] Captured end frames for standalone app start.', endFrames); const currentTimestamp = appStartEndData?.timestampMs || timestampInSeconds() * 1000; _setAppStartEndData({ @@ -275,7 +281,7 @@ export const appStartIntegration = ({ endFrames, }); } catch (error) { - logger.debug('[AppStart] Failed to capture frames for standalone app start.', error); + debug.log('[AppStart] Failed to capture frames for standalone app start.', error); } } @@ -294,7 +300,7 @@ export const appStartIntegration = ({ const event = convertSpanToTransaction(span); if (!event) { - logger.warn('[AppStart] Failed to convert App Start span to transaction.'); + debug.warn('[AppStart] Failed to convert App Start span to transaction.'); return; } @@ -315,17 +321,17 @@ export const appStartIntegration = ({ } if (!firstStartedActiveRootSpanId) { - logger.warn('[AppStart] No first started active root span id recorded. Can not attach app start.'); + debug.warn('[AppStart] No first started active root span id recorded. Can not attach app start.'); return; } - if (!event.contexts || !event.contexts.trace) { - logger.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.'); + if (!event.contexts?.trace) { + debug.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.'); return; } if (firstStartedActiveRootSpanId !== event.contexts.trace.span_id) { - logger.warn( + debug.warn( '[AppStart] First started active root span id does not match the transaction event span id. Can not attached app start.', ); return; @@ -333,23 +339,23 @@ export const appStartIntegration = ({ const appStart = await NATIVE.fetchNativeAppStart(); if (!appStart) { - logger.warn('[AppStart] Failed to retrieve the app start metrics from the native layer.'); + debug.warn('[AppStart] Failed to retrieve the app start metrics from the native layer.'); return; } if (appStart.has_fetched) { - logger.warn('[AppStart] Measured app start metrics were already reported from the native layer.'); + debug.warn('[AppStart] Measured app start metrics were already reported from the native layer.'); return; } const appStartTimestampMs = appStart.app_start_timestamp_ms; if (!appStartTimestampMs) { - logger.warn('[AppStart] App start timestamp could not be loaded from the native layer.'); + debug.warn('[AppStart] App start timestamp could not be loaded from the native layer.'); return; } const appStartEndTimestampMs = appStartEndData?.timestampMs || getBundleStartTimestampMs(); if (!appStartEndTimestampMs) { - logger.warn( + debug.warn( '[AppStart] Javascript failed to record app start end. `_setAppStartEndData` was not called nor could the bundle start be found.', ); return; @@ -358,14 +364,14 @@ export const appStartIntegration = ({ const isAppStartWithinBounds = !!event.start_timestamp && appStartTimestampMs >= event.start_timestamp * 1_000 - MAX_APP_START_AGE_MS; if (!__DEV__ && !isAppStartWithinBounds) { - logger.warn('[AppStart] App start timestamp is too far in the past to be used for app start span.'); + debug.warn('[AppStart] App start timestamp is too far in the past to be used for app start span.'); return; } const appStartDurationMs = appStartEndTimestampMs - appStartTimestampMs; if (!__DEV__ && appStartDurationMs >= MAX_APP_START_DURATION_MS) { // Dev builds can have long app start waiting over minute for the first bundle to be produced - logger.warn('[AppStart] App start duration is over a minute long, not adding app start span.'); + debug.warn('[AppStart] App start duration is over a minute long, not adding app start span.'); return; } @@ -373,7 +379,7 @@ export const appStartIntegration = ({ // This can happen when MainActivity on Android is recreated, // and the app start end timestamp is not updated, for example // due to missing `Sentry.wrap(RootComponent)` call. - logger.warn( + debug.warn( '[AppStart] Last recorded app start end timestamp is before the app start timestamp.', 'This is usually caused by missing `Sentry.wrap(RootComponent)` call.', ); @@ -411,7 +417,7 @@ export const appStartIntegration = ({ const appStartEndTimestampSeconds = appStartEndTimestampMs / 1000; if (event.timestamp && event.timestamp < appStartEndTimestampSeconds) { - logger.debug( + debug.log( '[AppStart] Transaction event timestamp is before app start end. Adjusting transaction event timestamp.', ); event.timestamp = appStartEndTimestampSeconds; @@ -420,7 +426,7 @@ export const appStartIntegration = ({ const op = appStart.type === 'cold' ? APP_START_COLD_OP : APP_START_WARM_OP; const appStartSpanJSON: SpanJSON = createSpanJSON({ op, - description: appStart.type === 'cold' ? 'Cold App Start' : 'Warm App Start', + description: appStart.type === 'cold' ? 'Cold Start' : 'Warm Start', start_timestamp: appStartTimestampSeconds, timestamp: appStartEndTimestampSeconds, trace_id: event.contexts.trace.trace_id, @@ -441,7 +447,7 @@ export const appStartIntegration = ({ ]; children.push(...appStartSpans); - logger.debug('[AppStart] Added app start spans to transaction event.', JSON.stringify(appStartSpans, undefined, 2)); + debug.log('[AppStart] Added app start spans to transaction event.', JSON.stringify(appStartSpans, undefined, 2)); const measurementKey = appStart.type === 'cold' ? APP_START_COLD_MEASUREMENT : APP_START_WARM_MEASUREMENT; const measurementValue = { @@ -450,8 +456,8 @@ export const appStartIntegration = ({ }; event.measurements = event.measurements || {}; event.measurements[measurementKey] = measurementValue; - logger.debug( - `[AppStart] Added app start measurement to transaction event.`, + debug.log( + '[AppStart] Added app start measurement to transaction event.', JSON.stringify(measurementValue, undefined, 2), ); } @@ -468,7 +474,7 @@ export const appStartIntegration = ({ function setSpanDurationAsMeasurementOnTransactionEvent(event: TransactionEvent, label: string, span: SpanJSON): void { if (!span.timestamp || !span.start_timestamp) { - logger.warn('Span is missing start or end timestamp. Cam not set measurement on transaction event.'); + debug.warn('Span is missing start or end timestamp. Cam not set measurement on transaction event.'); return; } @@ -493,12 +499,12 @@ function createJSExecutionStartSpan( const bundleStartTimestampSeconds = bundleStartTimestampMs / 1000; if (bundleStartTimestampSeconds < parentSpan.start_timestamp) { - logger.warn('Bundle start timestamp is before the app start span start timestamp. Skipping JS execution span.'); + debug.warn('Bundle start timestamp is before the app start span start timestamp. Skipping JS execution span.'); return undefined; } if (!rootComponentCreationTimestampMs) { - logger.warn('Missing the root component first constructor call timestamp.'); + debug.warn('Missing the root component first constructor call timestamp.'); return createChildSpanJSON(parentSpan, { description: 'JS Bundle Execution Start', start_timestamp: bundleStartTimestampSeconds, diff --git a/packages/core/src/js/tracing/integrations/nativeFrames.ts b/packages/core/src/js/tracing/integrations/nativeFrames.ts index ceec914b88..58de636e0a 100644 --- a/packages/core/src/js/tracing/integrations/nativeFrames.ts +++ b/packages/core/src/js/tracing/integrations/nativeFrames.ts @@ -1,6 +1,5 @@ import type { Client, Event, Integration, Measurements, MeasurementUnit, Span } from '@sentry/core'; -import { logger, timestampInSeconds } from '@sentry/core'; - +import { debug, timestampInSeconds } from '@sentry/core'; import type { NativeFramesResponse } from '../../NativeRNSentry'; import { AsyncExpiringMap } from '../../utils/AsyncExpiringMap'; import { isRootSpan } from '../../utils/span'; @@ -60,7 +59,7 @@ export const createNativeFramesIntegrations = (enable: boolean | undefined): Int export const nativeFramesIntegration = (): Integration => { /** The native frames at the finish time of the most recent span. */ let _lastChildSpanEndFrames: NativeFramesResponseWithTimestamp | null = null; - const _spanToNativeFramesAtStartMap: AsyncExpiringMap = new AsyncExpiringMap({ + const _spanToNativeFramesAtStartMap: AsyncExpiringMap = new AsyncExpiringMap({ ttl: START_FRAMES_TIMEOUT_MS, }); const _spanToNativeFramesAtEndMap: AsyncExpiringMap = @@ -71,7 +70,7 @@ export const nativeFramesIntegration = (): Integration => { */ const setup = (client: Client): void => { if (!NATIVE.enableNative) { - logger.warn( + debug.warn( `[${INTEGRATION_NAME}] This is not available on the Web, Expo Go and other platforms without native modules.`, ); return undefined; @@ -88,14 +87,14 @@ export const nativeFramesIntegration = (): Integration => { } const spanId = rootSpan.spanContext().spanId; - logger.debug(`[${INTEGRATION_NAME}] Fetching frames for root span start (${spanId}).`); + debug.log(`[${INTEGRATION_NAME}] Fetching frames for root span start (${spanId}).`); _spanToNativeFramesAtStartMap.set( spanId, new Promise(resolve => { fetchNativeFrames() .then(frames => resolve(frames)) .then(undefined, error => { - logger.debug(`[${INTEGRATION_NAME}] Error while fetching native frames.`, error); + debug.log(`[${INTEGRATION_NAME}] Error while fetching native frames.`, error); resolve(null); }); }), @@ -113,7 +112,7 @@ export const nativeFramesIntegration = (): Integration => { return; } - logger.debug(`[${INTEGRATION_NAME}] Fetch frames for root span end (${spanId}).`); + debug.log(`[${INTEGRATION_NAME}] Fetch frames for root span end (${spanId}).`); _spanToNativeFramesAtEndMap.set( spanId, new Promise(resolve => { @@ -125,14 +124,14 @@ export const nativeFramesIntegration = (): Integration => { }); }) .then(undefined, error => { - logger.debug(`[${INTEGRATION_NAME}] Error while fetching native frames.`, error); + debug.log(`[${INTEGRATION_NAME}] Error while fetching native frames.`, error); resolve(null); }); }), ); return undefined; } else { - logger.debug(`[${INTEGRATION_NAME}] Fetch frames for child span end (${spanId}).`); + debug.log(`[${INTEGRATION_NAME}] Fetch frames for child span end (${spanId}).`); fetchNativeFrames() .then(frames => { _lastChildSpanEndFrames = { @@ -140,7 +139,7 @@ export const nativeFramesIntegration = (): Integration => { nativeFrames: frames, }; }) - .catch(error => logger.debug(`[${INTEGRATION_NAME}] Error while fetching native frames.`, error)); + .catch(error => debug.log(`[${INTEGRATION_NAME}] Error while fetching native frames.`, error)); } }; @@ -160,7 +159,7 @@ export const nativeFramesIntegration = (): Integration => { const spanId = event.contexts.trace.span_id; const startFrames = await _spanToNativeFramesAtStartMap.pop(spanId); if (!startFrames) { - logger.warn( + debug.warn( `[${INTEGRATION_NAME}] Start frames of transaction ${event.transaction} (eventId, ${event.event_id}) are missing, but the transaction already ended.`, ); return event; @@ -171,15 +170,15 @@ export const nativeFramesIntegration = (): Integration => { if (endFrames && isClose(endFrames.timestamp, event.timestamp)) { // Must be in the margin of error of the actual transaction finish time (finalEndTimestamp) - logger.debug(`[${INTEGRATION_NAME}] Using frames from root span end (spanId, ${spanId}).`); + debug.log(`[${INTEGRATION_NAME}] Using frames from root span end (spanId, ${spanId}).`); finalEndFrames = endFrames.nativeFrames; } else if (_lastChildSpanEndFrames && isClose(_lastChildSpanEndFrames.timestamp, event.timestamp)) { // Fallback to the last span finish if it is within the margin of error of the actual finish timestamp. // This should be the case for trimEnd. - logger.debug(`[${INTEGRATION_NAME}] Using native frames from last child span end (spanId, ${spanId}).`); + debug.log(`[${INTEGRATION_NAME}] Using native frames from last child span end (spanId, ${spanId}).`); finalEndFrames = _lastChildSpanEndFrames.nativeFrames; } else { - logger.warn( + debug.warn( `[${INTEGRATION_NAME}] Frames were collected within larger than margin of error delay for spanId (${spanId}). Dropping the inaccurate values.`, ); return event; @@ -205,13 +204,13 @@ export const nativeFramesIntegration = (): Integration => { measurements.frames_slow.value <= 0 && measurements.frames_total.value <= 0 ) { - logger.warn( + debug.warn( `[${INTEGRATION_NAME}] Detected zero slow or frozen frames. Not adding measurements to spanId (${spanId}).`, ); return event; } - logger.log( + debug.log( `[${INTEGRATION_NAME}] Adding measurements to ${traceOp} transaction ${event.transaction}: ${JSON.stringify( measurements, undefined, diff --git a/packages/core/src/js/tracing/integrations/stalltracking.ts b/packages/core/src/js/tracing/integrations/stalltracking.ts index e2b05c8d9d..288218696b 100644 --- a/packages/core/src/js/tracing/integrations/stalltracking.ts +++ b/packages/core/src/js/tracing/integrations/stalltracking.ts @@ -1,9 +1,8 @@ /* eslint-disable max-lines */ import type { Client, Integration, Measurements, MeasurementUnit, Span } from '@sentry/core'; -import { getRootSpan, logger, spanToJSON, timestampInSeconds } from '@sentry/core'; +import { debug, getRootSpan, spanToJSON, timestampInSeconds } from '@sentry/core'; import type { AppStateStatus } from 'react-native'; import { AppState } from 'react-native'; - import { STALL_COUNT, STALL_LONGEST_TIME, STALL_TOTAL_TIME } from '../../measurements'; import { isRootSpan } from '../../utils/span'; import { getLatestChildSpanEndTimestamp, isNearToNow, setSpanMeasurement } from '../utils'; @@ -125,7 +124,7 @@ export const stallTrackingIntegration = ({ } if (statsByRootSpan.has(rootSpan)) { - logger.error( + debug.error( '[StallTracking] Tried to start stall tracking on a transaction already being tracked. Measurements might be lost.', ); return; @@ -149,7 +148,7 @@ export const stallTrackingIntegration = ({ if (!transactionStats) { // Transaction has been flushed out somehow, we return null. - logger.log('[StallTracking] Stall measurements were not added to transaction due to exceeding the max count.'); + debug.log('[StallTracking] Stall measurements were not added to transaction due to exceeding the max count.'); statsByRootSpan.delete(rootSpan); _shouldStopTracking(); @@ -170,13 +169,13 @@ export const stallTrackingIntegration = ({ const latestChildSpanEnd = getLatestChildSpanEndTimestamp(rootSpan); if (latestChildSpanEnd !== endTimestamp) { - logger.log( + debug.log( '[StallTracking] Stall measurements not added due to a custom `endTimestamp` (root end is not equal to the latest child span end).', ); } if (!transactionStats.atTimestamp) { - logger.log( + debug.log( '[StallTracking] Stall measurements not added due to `endTimestamp` not being close to now. And no previous stats from child end were found.', ); } @@ -191,7 +190,7 @@ export const stallTrackingIntegration = ({ if (!statsOnFinish) { if (typeof endTimestamp !== 'undefined') { - logger.log( + debug.log( '[StallTracking] Stall measurements not added due to `endTimestamp` not being close to now.', 'endTimestamp', endTimestamp, @@ -241,7 +240,7 @@ export const stallTrackingIntegration = ({ const previousStats = statsByRootSpan.get(rootSpan); if (previousStats) { if (Math.abs(timestampInSeconds() - childSpanEndTime) > MARGIN_OF_ERROR_SECONDS) { - logger.log( + debug.log( '[StallTracking] Span end not logged due to end timestamp being outside the margin of error from now.', ); diff --git a/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts b/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts index 52cd915634..92b38370df 100644 --- a/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts +++ b/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts @@ -1,6 +1,5 @@ import type { Event, Integration, SpanJSON } from '@sentry/core'; -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { NATIVE } from '../../wrapper'; import { UI_LOAD_FULL_DISPLAY, UI_LOAD_INITIAL_DISPLAY } from '../ops'; import { SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from '../origin'; @@ -30,16 +29,16 @@ export const timeToDisplayIntegration = (): Integration => { return event; } - const rootSpanId = event.contexts.trace.span_id; + const rootSpanId = event.contexts?.trace?.span_id; if (!rootSpanId) { - logger.warn(`[${INTEGRATION_NAME}] No root span id found in transaction.`); + debug.warn(`[${INTEGRATION_NAME}] No root span id found in transaction.`); return event; } const transactionStartTimestampSeconds = event.start_timestamp; if (!transactionStartTimestampSeconds) { // This should never happen - logger.warn(`[${INTEGRATION_NAME}] No transaction start timestamp found in transaction.`); + debug.warn(`[${INTEGRATION_NAME}] No transaction start timestamp found in transaction.`); return event; } @@ -54,17 +53,19 @@ export const timeToDisplayIntegration = (): Integration => { }); const ttfdSpan = await addTimeToFullDisplay({ event, rootSpanId, transactionStartTimestampSeconds, ttidSpan }); - if (ttidSpan && ttidSpan.start_timestamp && ttidSpan.timestamp) { + if (ttidSpan?.start_timestamp && ttidSpan?.timestamp) { event.measurements['time_to_initial_display'] = { value: (ttidSpan.timestamp - ttidSpan.start_timestamp) * 1000, unit: 'millisecond', }; } - if (ttfdSpan && ttfdSpan.start_timestamp && ttfdSpan.timestamp) { + if (ttfdSpan?.start_timestamp && ttfdSpan?.timestamp) { const durationMs = (ttfdSpan.timestamp - ttfdSpan.start_timestamp) * 1000; if (isDeadlineExceeded(durationMs)) { - event.measurements['time_to_full_display'] = event.measurements['time_to_initial_display']; + if (event.measurements['time_to_initial_display']) { + event.measurements['time_to_full_display'] = event.measurements['time_to_initial_display']; + } } else { event.measurements['time_to_full_display'] = { value: durationMs, @@ -100,15 +101,17 @@ async function addTimeToInitialDisplay({ }): Promise { const ttidEndTimestampSeconds = await NATIVE.popTimeToDisplayFor(`ttid-${rootSpanId}`); + event.spans = event.spans || []; + let ttidSpan: SpanJSON | undefined = event.spans?.find(span => span.op === UI_LOAD_INITIAL_DISPLAY); if (ttidSpan && (ttidSpan.status === undefined || ttidSpan.status === 'ok') && !ttidEndTimestampSeconds) { - logger.debug(`[${INTEGRATION_NAME}] Ttid span already exists and is ok.`, ttidSpan); + debug.log(`[${INTEGRATION_NAME}] Ttid span already exists and is ok.`, ttidSpan); return ttidSpan; } if (!ttidEndTimestampSeconds) { - logger.debug(`[${INTEGRATION_NAME}] No manual ttid end timestamp found for span ${rootSpanId}.`); + debug.log(`[${INTEGRATION_NAME}] No manual ttid end timestamp found for span ${rootSpanId}.`); return addAutomaticTimeToInitialDisplay({ event, rootSpanId, @@ -117,10 +120,10 @@ async function addTimeToInitialDisplay({ }); } - if (ttidSpan && ttidSpan.status && ttidSpan.status !== 'ok') { + if (ttidSpan?.status && ttidSpan.status !== 'ok') { ttidSpan.status = 'ok'; ttidSpan.timestamp = ttidEndTimestampSeconds; - logger.debug(`[${INTEGRATION_NAME}] Updated existing ttid span.`, ttidSpan); + debug.log(`[${INTEGRATION_NAME}] Updated existing ttid span.`, ttidSpan); return ttidSpan; } @@ -135,7 +138,7 @@ async function addTimeToInitialDisplay({ [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }); - logger.debug(`[${INTEGRATION_NAME}] Added ttid span to transaction.`, ttidSpan); + debug.log(`[${INTEGRATION_NAME}] Added ttid span to transaction.`, ttidSpan); event.spans.push(ttidSpan); return ttidSpan; } @@ -156,7 +159,7 @@ async function addAutomaticTimeToInitialDisplay({ const hasBeenSeen = event.contexts?.trace?.data?.[SEMANTIC_ATTRIBUTE_ROUTE_HAS_BEEN_SEEN]; if (hasBeenSeen && !enableTimeToInitialDisplayForPreloadedRoutes) { - logger.debug( + debug.log( `[${INTEGRATION_NAME}] Route has been seen and time to initial display is disabled for preloaded routes.`, ); return undefined; @@ -164,7 +167,7 @@ async function addAutomaticTimeToInitialDisplay({ const ttidTimestampSeconds = ttidNativeTimestampSeconds ?? ttidFallbackTimestampSeconds; if (!ttidTimestampSeconds) { - logger.debug(`[${INTEGRATION_NAME}] No automatic ttid end timestamp found for span ${rootSpanId}.`); + debug.log(`[${INTEGRATION_NAME}] No automatic ttid end timestamp found for span ${rootSpanId}.`); return undefined; } @@ -204,20 +207,22 @@ async function addTimeToFullDisplay({ return undefined; } + event.spans = event.spans || []; + let ttfdSpan = event.spans?.find(span => span.op === UI_LOAD_FULL_DISPLAY); let ttfdAdjustedEndTimestampSeconds = ttfdEndTimestampSeconds; - const ttfdIsBeforeTtid = ttidSpan?.timestamp && ttfdEndTimestampSeconds < ttidSpan.timestamp; - if (ttfdIsBeforeTtid) { + const ttfdIsBeforeTtid = ttidSpan.timestamp && ttfdEndTimestampSeconds < ttidSpan.timestamp; + if (ttfdIsBeforeTtid && ttidSpan.timestamp) { ttfdAdjustedEndTimestampSeconds = ttidSpan.timestamp; } const durationMs = (ttfdAdjustedEndTimestampSeconds - transactionStartTimestampSeconds) * 1000; - if (ttfdSpan && ttfdSpan.status && ttfdSpan.status !== 'ok') { + if (ttfdSpan?.status && ttfdSpan.status !== 'ok') { ttfdSpan.status = 'ok'; ttfdSpan.timestamp = ttfdAdjustedEndTimestampSeconds; - logger.debug(`[${INTEGRATION_NAME}] Updated existing ttfd span.`, ttfdSpan); + debug.log(`[${INTEGRATION_NAME}] Updated existing ttfd span.`, ttfdSpan); return ttfdSpan; } @@ -233,7 +238,7 @@ async function addTimeToFullDisplay({ [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }); - logger.debug(`[${INTEGRATION_NAME}] Added ttfd span to transaction.`, ttfdSpan); + debug.log(`[${INTEGRATION_NAME}] Added ttfd span to transaction.`, ttfdSpan); event.spans.push(ttfdSpan); return ttfdSpan; } diff --git a/packages/core/src/js/tracing/integrations/userInteraction.ts b/packages/core/src/js/tracing/integrations/userInteraction.ts index 378181016c..3be486735c 100644 --- a/packages/core/src/js/tracing/integrations/userInteraction.ts +++ b/packages/core/src/js/tracing/integrations/userInteraction.ts @@ -1,13 +1,12 @@ import type { Integration, Span, StartSpanOptions } from '@sentry/core'; import { + debug, getActiveSpan, getClient, getCurrentScope, - logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, spanToJSON, } from '@sentry/core'; - import type { ReactNativeClientOptions } from '../../options'; import { onlySampleIfChildSpans } from '../onSpanEndUtils'; import { SPAN_ORIGIN_MANUAL_INTERACTION } from '../origin'; @@ -37,29 +36,29 @@ export const startUserInteractionSpan = (userInteractionId: { const tracing = getCurrentReactNativeTracingIntegration(); if (!tracing) { - logger.log(`[${INTEGRATION_NAME}] Tracing integration is not available. Can not start user interaction span.`); + debug.log(`[${INTEGRATION_NAME}] Tracing integration is not available. Can not start user interaction span.`); return undefined; } const options = client.getOptions() as ReactNativeClientOptions; const { elementId, op } = userInteractionId; if (!options.enableUserInteractionTracing) { - logger.log(`[${INTEGRATION_NAME}] User Interaction Tracing is disabled.`); + debug.log(`[${INTEGRATION_NAME}] User Interaction Tracing is disabled.`); return undefined; } if (!elementId) { - logger.log(`[${INTEGRATION_NAME}] User Interaction Tracing can not create transaction with undefined elementId.`); + debug.log(`[${INTEGRATION_NAME}] User Interaction Tracing can not create transaction with undefined elementId.`); return undefined; } if (!tracing.state.currentRoute) { - logger.log(`[${INTEGRATION_NAME}] User Interaction Tracing can not create transaction without a current route.`); + debug.log(`[${INTEGRATION_NAME}] User Interaction Tracing can not create transaction without a current route.`); return undefined; } const activeTransaction = getActiveSpan(); const activeTransactionIsNotInteraction = activeTransaction && !isSentryInteractionSpan(activeTransaction); if (activeTransaction && activeTransactionIsNotInteraction) { - logger.warn( + debug.warn( `[${INTEGRATION_NAME}] Did not create ${op} transaction because active transaction ${ spanToJSON(activeTransaction).description } exists on the scope.`, @@ -73,7 +72,7 @@ export const startUserInteractionSpan = (userInteractionId: { spanToJSON(activeTransaction).description === name && spanToJSON(activeTransaction).op === op ) { - logger.warn( + debug.warn( `[${INTEGRATION_NAME}] Did not create ${op} transaction because it the same transaction ${ spanToJSON(activeTransaction).description } already exists on the scope.`, @@ -94,6 +93,6 @@ export const startUserInteractionSpan = (userInteractionId: { }); newSpan.setAttribute(SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_ORIGIN_MANUAL_INTERACTION); onlySampleIfChildSpans(client, newSpan); - logger.log(`[${INTEGRATION_NAME}] User Interaction Tracing Created ${op} transaction ${name}.`); + debug.log(`[${INTEGRATION_NAME}] User Interaction Tracing Created ${op} transaction ${name}.`); return newSpan; }; diff --git a/packages/core/src/js/tracing/onSpanEndUtils.ts b/packages/core/src/js/tracing/onSpanEndUtils.ts index 40365b33fd..d35d9e7379 100644 --- a/packages/core/src/js/tracing/onSpanEndUtils.ts +++ b/packages/core/src/js/tracing/onSpanEndUtils.ts @@ -1,8 +1,7 @@ import type { Client, Span } from '@sentry/core'; -import { getSpanDescendants, logger, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core'; +import { debug, getSpanDescendants, SPAN_STATUS_ERROR, spanToJSON } from '@sentry/core'; import type { AppStateStatus } from 'react-native'; import { AppState } from 'react-native'; - import { isRootSpan, isSentrySpan } from '../utils/span'; /** @@ -19,7 +18,7 @@ export function onThisSpanEnd(client: Client, span: Span, callback: (span: Span) export const adjustTransactionDuration = (client: Client, span: Span, maxDurationMs: number): void => { if (!isRootSpan(span)) { - logger.warn('Not sampling empty back spans only works for Sentry Transactions (Root Spans).'); + debug.warn('Not sampling empty back spans only works for Sentry Transactions (Root Spans).'); return; } @@ -44,19 +43,19 @@ export const adjustTransactionDuration = (client: Client, span: Span, maxDuratio }); }; -export const ignoreEmptyBackNavigation = (client: Client | undefined, span: Span): void => { +export const ignoreEmptyBackNavigation = (client: Client | undefined, span: Span | undefined): void => { if (!client) { - logger.warn('Could not hook on spanEnd event because client is not defined.'); + debug.warn('Could not hook on spanEnd event because client is not defined.'); return; } if (!span) { - logger.warn('Could not hook on spanEnd event because span is not defined.'); + debug.warn('Could not hook on spanEnd event because span is not defined.'); return; } if (!isRootSpan(span) || !isSentrySpan(span)) { - logger.warn('Not sampling empty back spans only works for Sentry Transactions (Root Spans).'); + debug.warn('Not sampling empty back spans only works for Sentry Transactions (Root Spans).'); return; } @@ -79,7 +78,7 @@ export const ignoreEmptyBackNavigation = (client: Client | undefined, span: Span if (filtered.length <= 0) { // filter children must include at least one span not created by the navigation automatic instrumentation - logger.log( + debug.log( 'Not sampling transaction as route has been seen before. Pass ignoreEmptyBackNavigationTransactions = false to disable this feature.', ); // Route has been seen before and has no child spans. @@ -94,7 +93,7 @@ export const ignoreEmptyBackNavigation = (client: Client | undefined, span: Span */ export const onlySampleIfChildSpans = (client: Client, span: Span): void => { if (!isRootSpan(span) || !isSentrySpan(span)) { - logger.warn('Not sampling childless spans only works for Sentry Transactions (Root Spans).'); + debug.warn('Not sampling childless spans only works for Sentry Transactions (Root Spans).'); return; } @@ -107,7 +106,7 @@ export const onlySampleIfChildSpans = (client: Client, span: Span): void => { if (children.length <= 1) { // Span always has at lest one child, itself - logger.log(`Not sampling as ${spanToJSON(span).op} transaction has no child spans.`); + debug.log(`Not sampling as ${spanToJSON(span).op} transaction has no child spans.`); span['_sampled'] = false; } }); @@ -119,7 +118,7 @@ export const onlySampleIfChildSpans = (client: Client, span: Span): void => { export const cancelInBackground = (client: Client, span: Span): void => { const subscription = AppState.addEventListener('change', (newState: AppStateStatus) => { if (newState === 'background') { - logger.debug(`Setting ${spanToJSON(span).op} transaction to cancelled because the app is in the background.`); + debug.log(`Setting ${spanToJSON(span).op} transaction to cancelled because the app is in the background.`); span.setStatus({ code: SPAN_STATUS_ERROR, message: 'cancelled' }); span.end(); } @@ -128,8 +127,8 @@ export const cancelInBackground = (client: Client, span: Span): void => { subscription && client.on('spanEnd', (endedSpan: Span) => { if (endedSpan === span) { - logger.debug(`Removing AppState listener for ${spanToJSON(span).op} transaction.`); - subscription && subscription.remove && subscription.remove(); + debug.log(`Removing AppState listener for ${spanToJSON(span).op} transaction.`); + subscription?.remove?.(); } }); }; diff --git a/packages/core/src/js/tracing/reactnativenavigation.ts b/packages/core/src/js/tracing/reactnativenavigation.ts index 0b0f696c68..33a3275fb0 100644 --- a/packages/core/src/js/tracing/reactnativenavigation.ts +++ b/packages/core/src/js/tracing/reactnativenavigation.ts @@ -7,7 +7,6 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, spanToJSON, } from '@sentry/core'; - import type { EmitterSubscription } from '../utils/rnlibrariesinterface'; import { isSentrySpan } from '../utils/span'; import { ignoreEmptyBackNavigation } from './onSpanEndUtils'; @@ -129,7 +128,7 @@ export const reactNativeNavigationIntegration = ({ } latestNavigationSpan = startGenericIdleNavigationSpan( - tracing && tracing.options.beforeStartSpan + tracing?.options.beforeStartSpan ? tracing.options.beforeStartSpan(getDefaultIdleNavigationSpanOptions()) : getDefaultIdleNavigationSpanOptions(), idleSpanOptions, diff --git a/packages/core/src/js/tracing/reactnativeprofiler.tsx b/packages/core/src/js/tracing/reactnativeprofiler.tsx index ed5a9158e8..6a32b9971c 100644 --- a/packages/core/src/js/tracing/reactnativeprofiler.tsx +++ b/packages/core/src/js/tracing/reactnativeprofiler.tsx @@ -1,6 +1,5 @@ -import { logger, timestampInSeconds } from '@sentry/core'; +import { debug, timestampInSeconds } from '@sentry/core'; import { getClient, Profiler } from '@sentry/react'; - import { getAppRegistryIntegration } from '../integrations/appRegistry'; import { createIntegration } from '../integrations/factory'; import { _captureAppStart, _setRootComponentCreationTimestampMs } from '../tracing/integrations/appStart'; @@ -12,13 +11,15 @@ const ReactNativeProfilerGlobalState = { }, }; +type ProfilerConstructorProps = ConstructorParameters[0]; + /** * Custom profiler for the React Native app root. */ export class ReactNativeProfiler extends Profiler { public readonly name: string = 'ReactNativeProfiler'; - public constructor(props: ConstructorParameters[0]) { + public constructor(props: ProfilerConstructorProps) { _setRootComponentCreationTimestampMs(timestampInSeconds() * 1000); super(props); } @@ -47,13 +48,13 @@ export class ReactNativeProfiler extends Profiler { return; } - client.addIntegration && client.addIntegration(createIntegration(this.name)); + client.addIntegration?.(createIntegration(this.name)); const appRegistryIntegration = getAppRegistryIntegration(client); if (appRegistryIntegration && typeof appRegistryIntegration.onRunApplication === 'function') { appRegistryIntegration.onRunApplication(ReactNativeProfilerGlobalState.onRunApplicationHook); } else { - logger.warn('AppRegistryIntegration.onRunApplication not found or invalid.'); + debug.warn('AppRegistryIntegration.onRunApplication not found or invalid.'); } // eslint-disable-next-line @typescript-eslint/no-floating-promises diff --git a/packages/core/src/js/tracing/reactnativetracing.ts b/packages/core/src/js/tracing/reactnativetracing.ts index 8874f6769f..74e56c51b2 100644 --- a/packages/core/src/js/tracing/reactnativetracing.ts +++ b/packages/core/src/js/tracing/reactnativetracing.ts @@ -2,7 +2,6 @@ import { instrumentOutgoingRequests } from '@sentry/browser'; import type { Client, Event, Integration, StartSpanOptions } from '@sentry/core'; import { getClient } from '@sentry/core'; - import { isWeb } from '../utils/environment'; import { getDevServer } from './../integrations/debugsymbolicatorutils'; import { addDefaultOpForSpanFrom, addThreadInfoToSpan, defaultIdleOptions } from './span'; diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index 58afcf1f4a..28666b0f89 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -2,9 +2,9 @@ import type { Client, Integration, Span } from '@sentry/core'; import { addBreadcrumb, + debug, getClient, isPlainObject, - logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_OK, @@ -12,7 +12,6 @@ import { startInactiveSpan, timestampInSeconds, } from '@sentry/core'; - import { getAppRegistryIntegration } from '../integrations/appRegistry'; import { isSentrySpan } from '../utils/span'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; @@ -30,6 +29,7 @@ import { startIdleNavigationSpan as startGenericIdleNavigationSpan, } from './span'; import { addTimeToInitialDisplayFallback } from './timeToDisplayFallback'; + export const INTEGRATION_NAME = 'ReactNavigation'; const NAVIGATION_HISTORY_MAX_SIZE = 200; @@ -112,7 +112,7 @@ export const reactNavigationIntegration = ({ if (enableTimeToInitialDisplay) { NATIVE.initNativeReactNavigationNewFrameTracking().catch((reason: unknown) => { - logger.error(`${INTEGRATION_NAME} Failed to initialize native new frame tracking: ${reason}`); + debug.error(`${INTEGRATION_NAME} Failed to initialize native new frame tracking: ${reason}`); }); } @@ -140,7 +140,7 @@ export const reactNavigationIntegration = ({ // To avoid conflict with the initial transaction we check if it was already handled. // This ensures runApplication calls after the initial start are correctly traced. // This is used for example when Activity is (re)started on Android. - logger.log('[ReactNavigationIntegration] Starting new idle navigation span based on runApplication call.'); + debug.log('[ReactNavigationIntegration] Starting new idle navigation span based on runApplication call.'); startIdleNavigationSpan(); } }); @@ -159,7 +159,7 @@ export const reactNavigationIntegration = ({ const registerNavigationContainer = (maybeNewNavigationContainer: unknown): void => { if (RN_GLOBAL_OBJ.__sentry_rn_v5_registered) { - logger.debug(`${INTEGRATION_NAME} Instrumentation already exists, but registering again...`); + debug.log(`${INTEGRATION_NAME} Instrumentation already exists, but registering again...`); // In the past we have not allowed re-registering the navigation container to avoid unexpected behavior. // But this doesn't work for Android and re-recreating application main activity. // Where new navigation container is created and the old one is discarded. We need to re-register to @@ -174,13 +174,13 @@ export const reactNavigationIntegration = ({ } if (navigationContainer === newNavigationContainer) { - logger.log(`${INTEGRATION_NAME} Navigation container ref is the same as the one already registered.`); + debug.log(`${INTEGRATION_NAME} Navigation container ref is the same as the one already registered.`); return; } navigationContainer = newNavigationContainer as NavigationContainer; if (!navigationContainer) { - logger.warn(`${INTEGRATION_NAME} Received invalid navigation container ref!`); + debug.warn(`${INTEGRATION_NAME} Received invalid navigation container ref!`); return undefined; } @@ -194,7 +194,7 @@ export const reactNavigationIntegration = ({ } if (!latestNavigationSpan) { - logger.log(`${INTEGRATION_NAME} Navigation container registered, but integration has not been setup yet.`); + debug.log(`${INTEGRATION_NAME} Navigation container registered, but integration has not been setup yet.`); return undefined; } @@ -213,13 +213,14 @@ export const reactNavigationIntegration = ({ const startIdleNavigationSpan = (unknownEvent?: unknown): void => { const event = unknownEvent as UnsafeAction | undefined; if (useDispatchedActionData && event?.data.noop) { - logger.debug(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`); + debug.log(`${INTEGRATION_NAME} Navigation action is a noop, not starting navigation span.`); return; } const navigationActionType = useDispatchedActionData ? event?.data.action.type : undefined; if ( useDispatchedActionData && + navigationActionType && [ // Process common actions 'PRELOAD', @@ -230,18 +231,18 @@ export const reactNavigationIntegration = ({ 'TOGGLE_DRAWER', ].includes(navigationActionType) ) { - logger.debug(`${INTEGRATION_NAME} Navigation action is ${navigationActionType}, not starting navigation span.`); + debug.log(`${INTEGRATION_NAME} Navigation action is ${navigationActionType}, not starting navigation span.`); return; } if (latestNavigationSpan) { - logger.log(`${INTEGRATION_NAME} A transaction was detected that turned out to be a noop, discarding.`); + debug.log(`${INTEGRATION_NAME} A transaction was detected that turned out to be a noop, discarding.`); _discardLatestTransaction(); clearStateChangeTimeout(); } latestNavigationSpan = startGenericIdleNavigationSpan( - tracing && tracing.options.beforeStartSpan + tracing?.options.beforeStartSpan ? tracing.options.beforeStartSpan(getDefaultIdleNavigationSpanOptions()) : getDefaultIdleNavigationSpanOptions(), idleSpanOptions, @@ -252,12 +253,12 @@ export const reactNavigationIntegration = ({ ignoreEmptyBackNavigation(getClient(), latestNavigationSpan); } - if (enableTimeToInitialDisplay) { - NATIVE.setActiveSpanId(latestNavigationSpan?.spanContext().spanId); + if (enableTimeToInitialDisplay && latestNavigationSpan) { + NATIVE.setActiveSpanId(latestNavigationSpan.spanContext().spanId); navigationProcessingSpan = startInactiveSpan({ op: 'navigation.processing', name: 'Navigation dispatch to navigation cancelled or screen mounted', - startTime: latestNavigationSpan && spanToJSON(latestNavigationSpan).start_timestamp, + startTime: spanToJSON(latestNavigationSpan).start_timestamp, }); navigationProcessingSpan.setAttribute( SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, @@ -276,18 +277,18 @@ export const reactNavigationIntegration = ({ const previousRoute = latestRoute; if (!navigationContainer) { - logger.warn(`${INTEGRATION_NAME} Missing navigation container ref. Route transactions will not be sent.`); + debug.warn(`${INTEGRATION_NAME} Missing navigation container ref. Route transactions will not be sent.`); return undefined; } const route = navigationContainer.getCurrentRoute(); if (!route) { - logger.debug(`[${INTEGRATION_NAME}] Navigation state changed, but no route is rendered.`); + debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but no route is rendered.`); return undefined; } if (!latestNavigationSpan) { - logger.debug( + debug.log( `[${INTEGRATION_NAME}] Navigation state changed, but navigation transaction was not started on dispatch.`, ); return undefined; @@ -296,7 +297,7 @@ export const reactNavigationIntegration = ({ addTimeToInitialDisplayFallback(latestNavigationSpan.spanContext().spanId, NATIVE.getNewScreenTimeToDisplay()); if (previousRoute && previousRoute.key === route.key) { - logger.debug(`[${INTEGRATION_NAME}] Navigation state changed, but route is the same as previous.`); + debug.log(`[${INTEGRATION_NAME}] Navigation state changed, but route is the same as previous.`); pushRecentRouteKey(route.key); latestRoute = route; diff --git a/packages/core/src/js/tracing/span.ts b/packages/core/src/js/tracing/span.ts index ecb50cda76..88b54c3070 100644 --- a/packages/core/src/js/tracing/span.ts +++ b/packages/core/src/js/tracing/span.ts @@ -1,10 +1,10 @@ import type { Client, Scope, Span, SpanJSON, StartSpanOptions } from '@sentry/core'; import { - generatePropagationContext, + debug, + generateTraceId, getActiveSpan, getClient, getCurrentScope, - logger, SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SentryNonRecordingSpan, @@ -13,7 +13,6 @@ import { startIdleSpan as coreStartIdleSpan, } from '@sentry/core'; import { AppState } from 'react-native'; - import { isRootSpan } from '../utils/span'; import { adjustTransactionDuration, cancelInBackground } from './onSpanEndUtils'; import { @@ -54,14 +53,14 @@ export const startIdleNavigationSpan = ( ): Span | undefined => { const client = getClient(); if (!client) { - logger.warn(`[startIdleNavigationSpan] Can't create route change span, missing client.`); + debug.warn("[startIdleNavigationSpan] Can't create route change span, missing client."); return undefined; } const activeSpan = getActiveSpan(); clearActiveSpanFromScope(getCurrentScope()); if (activeSpan && isRootSpan(activeSpan) && isSentryInteractionSpan(activeSpan)) { - logger.log( + debug.log( `[startIdleNavigationSpan] Canceling ${ spanToJSON(activeSpan).op } transaction because of a new navigation root span.`, @@ -76,7 +75,7 @@ export const startIdleNavigationSpan = ( }; const idleSpan = startIdleSpan(finalStartSpanOptions, { finalTimeout, idleTimeout }); - logger.log( + debug.log( `[startIdleNavigationSpan] Starting ${finalStartSpanOptions.op || 'unknown op'} transaction "${ finalStartSpanOptions.name }" on scope`, @@ -101,17 +100,17 @@ export const startIdleSpan = ( ): Span => { const client = getClient(); if (!client) { - logger.warn(`[startIdleSpan] Can't create idle span, missing client.`); + debug.warn("[startIdleSpan] Can't create idle span, missing client."); return new SentryNonRecordingSpan(); } const currentAppState = AppState.currentState; if (currentAppState === 'background') { - logger.debug(`[startIdleSpan] App is already in background, not starting span for ${startSpanOption.name}`); + debug.log(`[startIdleSpan] App is already in background, not starting span for ${startSpanOption.name}`); return new SentryNonRecordingSpan(); } - getCurrentScope().setPropagationContext(generatePropagationContext()); + getCurrentScope().setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random() }); const span = coreStartIdleSpan(startSpanOption, { finalTimeout, idleTimeout }); cancelInBackground(client, span); @@ -134,7 +133,7 @@ export function getDefaultIdleNavigationSpanOptions(): StartSpanOptions { * Checks if the span is a Sentry User Interaction span. */ export function isSentryInteractionSpan(span: Span): boolean { - return [SPAN_ORIGIN_AUTO_INTERACTION, SPAN_ORIGIN_MANUAL_INTERACTION].includes(spanToJSON(span).origin); + return [SPAN_ORIGIN_AUTO_INTERACTION, SPAN_ORIGIN_MANUAL_INTERACTION].includes(spanToJSON(span).origin || ''); } export const SCOPE_SPAN_FIELD = '_sentrySpan'; diff --git a/packages/core/src/js/tracing/timeToDisplayFallback.ts b/packages/core/src/js/tracing/timeToDisplayFallback.ts index cd71b21df0..e854d43477 100644 --- a/packages/core/src/js/tracing/timeToDisplayFallback.ts +++ b/packages/core/src/js/tracing/timeToDisplayFallback.ts @@ -13,6 +13,6 @@ export const addTimeToInitialDisplayFallback = ( spanIdToTimeToInitialDisplayFallback.set(spanId, timestampSeconds); }; -export const getTimeToInitialDisplayFallback = async (spanId: string): Promise => { +export const getTimeToInitialDisplayFallback = async (spanId: string): Promise => { return spanIdToTimeToInitialDisplayFallback.get(spanId); }; diff --git a/packages/core/src/js/tracing/timetodisplay.tsx b/packages/core/src/js/tracing/timetodisplay.tsx index 6f2ee736fe..b5e02a1147 100644 --- a/packages/core/src/js/tracing/timetodisplay.tsx +++ b/packages/core/src/js/tracing/timetodisplay.tsx @@ -1,8 +1,7 @@ import type { Span,StartSpanOptions } from '@sentry/core'; -import { fill, getActiveSpan, getSpanDescendants, logger, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, spanToJSON, startInactiveSpan } from '@sentry/core'; +import { debug, fill, getActiveSpan, getSpanDescendants, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, SPAN_STATUS_ERROR, SPAN_STATUS_OK, spanToJSON, startInactiveSpan } from '@sentry/core'; import * as React from 'react'; import { useState } from 'react'; - import { SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from './origin'; import { getRNSentryOnDrawReporter } from './timetodisplaynative'; import { setSpanDurationAsMeasurement, setSpanDurationAsMeasurementOnSpan } from './utils'; @@ -85,13 +84,13 @@ export function startTimeToInitialDisplaySpan( ): Span | undefined { const activeSpan = getActiveSpan(); if (!activeSpan) { - logger.warn(`[TimeToDisplay] No active span found to attach ui.load.initial_display to.`); + debug.warn('[TimeToDisplay] No active span found to attach ui.load.initial_display to.'); return undefined; } const existingSpan = getSpanDescendants(activeSpan).find((span) => spanToJSON(span).op === 'ui.load.initial_display'); if (existingSpan) { - logger.debug(`[TimeToDisplay] Found existing ui.load.initial_display span.`); + debug.log('[TimeToDisplay] Found existing ui.load.initial_display span.'); return existingSpan } @@ -134,7 +133,7 @@ export function startTimeToFullDisplaySpan( ): Span | undefined { const activeSpan = getActiveSpan(); if (!activeSpan) { - logger.warn(`[TimeToDisplay] No active span found to attach ui.load.full_display to.`); + debug.warn('[TimeToDisplay] No active span found to attach ui.load.full_display to.'); return undefined; } @@ -142,13 +141,13 @@ export function startTimeToFullDisplaySpan( const initialDisplaySpan = descendantSpans.find((span) => spanToJSON(span).op === 'ui.load.initial_display'); if (!initialDisplaySpan) { - logger.warn(`[TimeToDisplay] No initial display span found to attach ui.load.full_display to.`); + debug.warn('[TimeToDisplay] No initial display span found to attach ui.load.full_display to.'); return undefined; } const existingSpan = descendantSpans.find((span) => spanToJSON(span).op === 'ui.load.full_display'); if (existingSpan) { - logger.debug(`[TimeToDisplay] Found existing ui.load.full_display span.`); + debug.log('[TimeToDisplay] Found existing ui.load.full_display span.'); return existingSpan; } @@ -169,7 +168,7 @@ export function startTimeToFullDisplaySpan( fullDisplaySpan.setStatus({ code: SPAN_STATUS_ERROR, message: 'deadline_exceeded' }); fullDisplaySpan.end(spanToJSON(initialDisplaySpan).timestamp); setSpanDurationAsMeasurement('time_to_full_display', fullDisplaySpan); - logger.warn(`[TimeToDisplay] Full display span deadline_exceeded.`); + debug.warn('[TimeToDisplay] Full display span deadline_exceeded.'); }, options.timeoutMs); fill(fullDisplaySpan, 'end', (originalEnd: Span['end']) => (endTimestamp?: Parameters[0]) => { @@ -202,32 +201,32 @@ export function updateInitialDisplaySpan( span?: Span; } = {}): void { if (!span) { - logger.warn(`[TimeToDisplay] No span found or created, possibly performance is disabled.`); + debug.warn('[TimeToDisplay] No span found or created, possibly performance is disabled.'); return; } if (!activeSpan) { - logger.warn(`[TimeToDisplay] No active span found to attach ui.load.initial_display to.`); + debug.warn('[TimeToDisplay] No active span found to attach ui.load.initial_display to.'); return; } if (spanToJSON(span).parent_span_id !== spanToJSON(activeSpan).span_id) { - logger.warn(`[TimeToDisplay] Initial display span is not a child of current active span.`); + debug.warn('[TimeToDisplay] Initial display span is not a child of current active span.'); return; } if (spanToJSON(span).timestamp) { - logger.warn(`[TimeToDisplay] ${spanToJSON(span).description} span already ended.`); + debug.warn(`[TimeToDisplay] ${spanToJSON(span).description} span already ended.`); return; } span.end(frameTimestampSeconds); span.setStatus({ code: SPAN_STATUS_OK }); - logger.debug(`[TimeToDisplay] ${spanToJSON(span).description} span updated with end timestamp.`); + debug.log(`[TimeToDisplay] ${spanToJSON(span).description} span updated with end timestamp.`); if (fullDisplayBeforeInitialDisplay.has(activeSpan)) { fullDisplayBeforeInitialDisplay.delete(activeSpan); - logger.debug(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`); + debug.log(`[TimeToDisplay] Updating full display with initial display (${span.spanContext().spanId}) end.`); updateFullDisplaySpan(frameTimestampSeconds, span); } @@ -237,7 +236,7 @@ export function updateInitialDisplaySpan( function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDisplaySpan?: Span): void { const activeSpan = getActiveSpan(); if (!activeSpan) { - logger.warn(`[TimeToDisplay] No active span found to update ui.load.full_display in.`); + debug.warn('[TimeToDisplay] No active span found to update ui.load.full_display in.'); return; } @@ -246,7 +245,7 @@ function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDispl const initialDisplayEndTimestamp = existingInitialDisplaySpan && spanToJSON(existingInitialDisplaySpan).timestamp; if (!initialDisplayEndTimestamp) { fullDisplayBeforeInitialDisplay.set(activeSpan, true); - logger.warn(`[TimeToDisplay] Full display called before initial display for active span (${activeSpan.spanContext().spanId}).`); + debug.warn(`[TimeToDisplay] Full display called before initial display for active span (${activeSpan.spanContext().spanId}).`); return; } @@ -254,25 +253,25 @@ function updateFullDisplaySpan(frameTimestampSeconds: number, passedInitialDispl isAutoInstrumented: true, }); if (!span) { - logger.warn(`[TimeToDisplay] No TimeToFullDisplay span found or created, possibly performance is disabled.`); + debug.warn('[TimeToDisplay] No TimeToFullDisplay span found or created, possibly performance is disabled.'); return; } const spanJSON = spanToJSON(span); if (spanJSON.timestamp) { - logger.warn(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span already ended.`); + debug.warn(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span already ended.`); return; } if (initialDisplayEndTimestamp > frameTimestampSeconds) { - logger.warn(`[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.`); + debug.warn('[TimeToDisplay] Using initial display end. Full display end frame timestamp is before initial display end.'); span.end(initialDisplayEndTimestamp); } else { span.end(frameTimestampSeconds); } span.setStatus({ code: SPAN_STATUS_OK }); - logger.debug(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span updated with end timestamp.`); + debug.log(`[TimeToDisplay] ${spanJSON.description} (${spanJSON.span_id}) span updated with end timestamp.`); setSpanDurationAsMeasurement('time_to_full_display', span); } @@ -325,6 +324,6 @@ function createTimeToDisplay({ return ; }; - TimeToDisplayWrapper.displayName = `TimeToDisplayWrapper`; + TimeToDisplayWrapper.displayName = 'TimeToDisplayWrapper'; return TimeToDisplayWrapper; } diff --git a/packages/core/src/js/tracing/timetodisplaynative.tsx b/packages/core/src/js/tracing/timetodisplaynative.tsx index d549fe8b87..217a922db8 100644 --- a/packages/core/src/js/tracing/timetodisplaynative.tsx +++ b/packages/core/src/js/tracing/timetodisplaynative.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; import type { HostComponent } from 'react-native'; import { UIManager, View } from 'react-native'; - import { isExpoGo } from '../utils/environment'; import { ReactNativeLibraries } from '../utils/rnlibraries'; import type { RNSentryOnDrawReporterProps } from './timetodisplaynative.types'; diff --git a/packages/core/src/js/tracing/utils.ts b/packages/core/src/js/tracing/utils.ts index 27b49306ac..ecd3e14128 100644 --- a/packages/core/src/js/tracing/utils.ts +++ b/packages/core/src/js/tracing/utils.ts @@ -1,8 +1,8 @@ import type { MeasurementUnit, Span, SpanJSON, TransactionSource } from '@sentry/core'; import { + debug, dropUndefinedKeys, getSpanDescendants, - logger, SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_UNIT, SEMANTIC_ATTRIBUTE_SENTRY_MEASUREMENT_VALUE, SEMANTIC_ATTRIBUTE_SENTRY_OP, @@ -12,7 +12,6 @@ import { timestampInSeconds, uuid4, } from '@sentry/core'; - import { RN_GLOBAL_OBJ } from '../utils/worldwide'; export const defaultTransactionSource: TransactionSource = 'component'; @@ -98,7 +97,7 @@ export function getLatestChildSpanEndTimestamp(span: Span): number | undefined { export function getBundleStartTimestampMs(): number | undefined { const bundleStartTime = RN_GLOBAL_OBJ.__BUNDLE_START_TIME__; if (!bundleStartTime) { - logger.warn('Missing the bundle start time on the global object.'); + debug.warn('Missing the bundle start time on the global object.'); return undefined; } diff --git a/packages/core/src/js/transports/encodePolyfill.ts b/packages/core/src/js/transports/encodePolyfill.ts index 6e84209ed0..44aad44f6a 100644 --- a/packages/core/src/js/transports/encodePolyfill.ts +++ b/packages/core/src/js/transports/encodePolyfill.ts @@ -1,14 +1,30 @@ +import { getSentryCarrier } from '../utils/carrier'; import { RN_GLOBAL_OBJ } from '../utils/worldwide'; import { utf8ToBytes } from '../vendor'; export const useEncodePolyfill = (): void => { - if (!RN_GLOBAL_OBJ.__SENTRY__) { - (RN_GLOBAL_OBJ.__SENTRY__ as Partial<(typeof RN_GLOBAL_OBJ)['__SENTRY__']>) = {}; - } + const carrier = getSentryCarrier(); - RN_GLOBAL_OBJ.__SENTRY__.encodePolyfill = encodePolyfill; + if (RN_GLOBAL_OBJ.TextEncoder) { + // Hermes for RN 0.74 and later includes native TextEncoder + // https://github.com/facebook/hermes/commit/8fb0496d426a8e50d00385148d5fb498a6daa312 + carrier.encodePolyfill = globalEncodeFactory(RN_GLOBAL_OBJ.TextEncoder); + } else { + carrier.encodePolyfill = encodePolyfill; + } }; +/* + * The default encode polyfill is available in Hermes for RN 0.74 and later. + * https://github.com/facebook/hermes/commit/8fb0496d426a8e50d00385148d5fb498a6daa312 + */ +export const globalEncodeFactory = (Encoder: EncoderClass) => (text: string) => new Encoder().encode(text); + +type EncoderClass = Required['TextEncoder']; + +/* + * Encode polyfill runs in JS and might cause performance issues when processing large payloads. (~2+ MB) + */ export const encodePolyfill = (text: string): Uint8Array => { const bytes = new Uint8Array(utf8ToBytes(text)); return bytes; diff --git a/packages/core/src/js/transports/native.ts b/packages/core/src/js/transports/native.ts index ea0ae88129..c8037ea986 100644 --- a/packages/core/src/js/transports/native.ts +++ b/packages/core/src/js/transports/native.ts @@ -6,7 +6,6 @@ import type { TransportMakeRequestResponse, } from '@sentry/core'; import { makePromiseBuffer } from '@sentry/core'; - import { NATIVE } from '../wrapper'; export const DEFAULT_BUFFER_SIZE = 30; diff --git a/packages/core/src/js/utils/AsyncExpiringMap.ts b/packages/core/src/js/utils/AsyncExpiringMap.ts index 3f3906c9cd..bba14e01f1 100644 --- a/packages/core/src/js/utils/AsyncExpiringMap.ts +++ b/packages/core/src/js/utils/AsyncExpiringMap.ts @@ -7,7 +7,7 @@ export class AsyncExpiringMap { private _ttl: number; private _cleanupIntervalMs: number; private _map: Map | null }>; - private _cleanupInterval: ReturnType; + private _cleanupInterval: ReturnType | undefined; public constructor({ cleanupInterval = 5_000, @@ -30,7 +30,7 @@ export class AsyncExpiringMap { this.startCleanup(); } - if (typeof promise !== 'object' || !('then' in promise)) { + if (typeof promise !== 'object' || !promise || !('then' in promise)) { this._map.set(key, { value: promise, expiresAt: Date.now() + this._ttl, promise: null }); return; } @@ -116,7 +116,7 @@ export class AsyncExpiringMap { */ public ttl(key: K): number | undefined { const entry = this._map.get(key); - if (entry && entry.expiresAt) { + if (entry?.expiresAt) { const remainingTime = entry.expiresAt - Date.now(); return remainingTime > 0 ? remainingTime : 0; } @@ -143,7 +143,9 @@ export class AsyncExpiringMap { * Clear all entries. */ public clear(): void { - clearInterval(this._cleanupInterval); + if (this._cleanupInterval) { + clearInterval(this._cleanupInterval); + } this._map.clear(); } @@ -151,7 +153,9 @@ export class AsyncExpiringMap { * Stop the cleanup interval. */ public stopCleanup(): void { - clearInterval(this._cleanupInterval); + if (this._cleanupInterval) { + clearInterval(this._cleanupInterval); + } } /** diff --git a/packages/core/src/js/utils/carrier.ts b/packages/core/src/js/utils/carrier.ts new file mode 100644 index 0000000000..707e33ae78 --- /dev/null +++ b/packages/core/src/js/utils/carrier.ts @@ -0,0 +1,13 @@ +import { getMainCarrier, SDK_VERSION as CORE_SDK_VERSION } from '@sentry/core'; + +/* + * Will either get the existing sentry carrier, or create a new one. + * Based on https://github.com/getsentry/sentry-javascript/blob/f0fc41f6166857cd97a695f5cc9a18caf6a0bf43/packages/core/src/carrier.ts#L49 + */ +export const getSentryCarrier = (): SentryCarrier => { + const carrier = getMainCarrier(); + const __SENTRY__ = (carrier.__SENTRY__ = carrier.__SENTRY__ || {}); + return (__SENTRY__[CORE_SDK_VERSION] = __SENTRY__[CORE_SDK_VERSION] || {}); +}; + +type SentryCarrier = Required>['__SENTRY__'][string]; diff --git a/packages/core/src/js/utils/encode.ts b/packages/core/src/js/utils/encode.ts new file mode 100644 index 0000000000..b1d2f2e189 --- /dev/null +++ b/packages/core/src/js/utils/encode.ts @@ -0,0 +1,14 @@ +import { useEncodePolyfill } from '../transports/encodePolyfill'; +import { getSentryCarrier } from './carrier'; + +/** + * Encode a string to UTF8 array. + */ +export function encodeUTF8(input: string): Uint8Array { + const carrier = getSentryCarrier(); + if (!carrier.encodePolyfill) { + useEncodePolyfill(); + } + + return carrier.encodePolyfill!(input); +} diff --git a/packages/core/src/js/utils/envelope.ts b/packages/core/src/js/utils/envelope.ts index f115ca4bd2..ca6cb9a6d0 100644 --- a/packages/core/src/js/utils/envelope.ts +++ b/packages/core/src/js/utils/envelope.ts @@ -22,13 +22,12 @@ export function createUserFeedbackEnvelope( const headers: EventEnvelope[0] = { event_id: feedback.event_id, sent_at: new Date().toISOString(), - ...(metadata && - metadata.sdk && { - sdk: { - name: metadata.sdk.name, - version: metadata.sdk.version, - }, - }), + ...(metadata?.sdk && { + sdk: { + name: metadata.sdk.name, + version: metadata.sdk.version, + }, + }), ...(!!tunnel && !!dsn && { dsn: dsnToString(dsn) }), }; const item = createUserFeedbackEnvelopeItem(feedback); diff --git a/packages/core/src/js/utils/environment.ts b/packages/core/src/js/utils/environment.ts index 93774ad50f..71c7586b6b 100644 --- a/packages/core/src/js/utils/environment.ts +++ b/packages/core/src/js/utils/environment.ts @@ -1,7 +1,6 @@ import { Platform } from 'react-native'; - import { RN_GLOBAL_OBJ } from '../utils/worldwide'; -import { getExpoConstants } from './expomodules'; +import { getExpoConstants, getExpoGo } from './expomodules'; import { ReactNativeLibraries } from './rnlibraries'; /** Checks if the React Native Hermes engine is running */ @@ -36,8 +35,8 @@ export function isExpo(): boolean { /** Check if JS runs in Expo Go */ export function isExpoGo(): boolean { - const expoConstants = getExpoConstants(); - return (expoConstants && expoConstants.appOwnership) === 'expo'; + const expoGo = getExpoGo(); + return !!expoGo; } /** Check Expo Go version if available */ @@ -76,11 +75,7 @@ export function notMobileOs(): boolean { /** Returns Hermes Version if hermes is present in the runtime */ export function getHermesVersion(): string | undefined { - return ( - RN_GLOBAL_OBJ.HermesInternal && - RN_GLOBAL_OBJ.HermesInternal.getRuntimeProperties && - RN_GLOBAL_OBJ.HermesInternal.getRuntimeProperties()['OSS Release Version'] - ); + return RN_GLOBAL_OBJ.HermesInternal?.getRuntimeProperties?.()['OSS Release Version']; } /** Returns default environment based on __DEV__ */ @@ -92,8 +87,7 @@ export function getDefaultEnvironment(): 'development' | 'production' { export function isRunningInMetroDevServer(): boolean { if ( typeof RN_GLOBAL_OBJ.process !== 'undefined' && - RN_GLOBAL_OBJ.process.env && - RN_GLOBAL_OBJ.process.env.___SENTRY_METRO_DEV_SERVER___ === 'true' + RN_GLOBAL_OBJ.process.env?.___SENTRY_METRO_DEV_SERVER___ === 'true' ) { return true; } diff --git a/packages/core/src/js/utils/expoglobalobject.ts b/packages/core/src/js/utils/expoglobalobject.ts index c36afde03c..e9bef4b2da 100644 --- a/packages/core/src/js/utils/expoglobalobject.ts +++ b/packages/core/src/js/utils/expoglobalobject.ts @@ -7,7 +7,6 @@ * https://github.com/expo/expo/blob/b51b5139f2caa2a9495e4132437d7ca612276158/packages/expo-manifests/src/Manifests.ts */ export interface ExpoConstants { - appOwnership?: 'standalone' | 'expo' | 'guest'; /** * Deprecated. But until removed we can use it as user ID to match the native SDKs. */ @@ -65,10 +64,13 @@ export interface ExpoUpdates { createdAt?: Date | null; } +export type ExpoGo = unknown; + export interface ExpoGlobalObject { modules?: { ExponentConstants?: ExpoConstants; ExpoDevice?: ExpoDevice; ExpoUpdates?: ExpoUpdates; + ExpoGo?: ExpoGo; }; } diff --git a/packages/core/src/js/utils/expomodules.ts b/packages/core/src/js/utils/expomodules.ts index 9f606a4f76..d1a973dd5e 100644 --- a/packages/core/src/js/utils/expomodules.ts +++ b/packages/core/src/js/utils/expomodules.ts @@ -1,4 +1,4 @@ -import type { ExpoConstants, ExpoDevice, ExpoUpdates } from './expoglobalobject'; +import type { ExpoConstants, ExpoDevice, ExpoGo, ExpoUpdates } from './expoglobalobject'; import { RN_GLOBAL_OBJ } from './worldwide'; /** @@ -21,3 +21,10 @@ export function getExpoDevice(): ExpoDevice | undefined { export function getExpoUpdates(): ExpoUpdates | undefined { return RN_GLOBAL_OBJ.expo?.modules?.ExpoUpdates ?? undefined; } + +/** + * Returns the Expo Go module if present + */ +export function getExpoGo(): ExpoGo | undefined { + return RN_GLOBAL_OBJ.expo?.modules?.ExpoGo ?? undefined; +} diff --git a/packages/core/src/js/utils/primitiveConverter.ts b/packages/core/src/js/utils/primitiveConverter.ts new file mode 100644 index 0000000000..c57073c73f --- /dev/null +++ b/packages/core/src/js/utils/primitiveConverter.ts @@ -0,0 +1,26 @@ +import type { Primitive } from '@sentry/core'; + +/** + * Converts primitive to string. + */ +export function PrimitiveToString(primitive: Primitive): string | undefined { + if (primitive === null) { + return ''; + } + + switch (typeof primitive) { + case 'string': + return primitive; + case 'boolean': + return primitive == true ? 'True' : 'False'; + case 'number': + case 'bigint': + return `${primitive}`; + case 'undefined': + return undefined; + case 'symbol': + return primitive.toString(); + default: + return primitive as string; + } +} diff --git a/packages/core/src/js/utils/release.ts b/packages/core/src/js/utils/release.ts new file mode 100644 index 0000000000..c4bb985d3f --- /dev/null +++ b/packages/core/src/js/utils/release.ts @@ -0,0 +1,33 @@ +import { notWeb } from './environment'; +import { RN_GLOBAL_OBJ } from './worldwide'; + +/** + * + */ +export function createReleaseFromGlobalReleaseConstants(): string | undefined { + const globalRelease = RN_GLOBAL_OBJ.SENTRY_RELEASE; + if (!globalRelease) { + return undefined; + } + + const { name, version } = globalRelease; + if (!name || !version) { + return undefined; + } + + return `${name}@${version}`; +} + +/** + * + */ +export function getDefaultRelease(): string | undefined { + if (notWeb()) { + // Mobile platforms use native release from the Release integration. + return undefined; + } + + // Web platforms (Expo Web) use the global release constants. + // Release set in the options is need for Session and Replay integrations. + return createReleaseFromGlobalReleaseConstants(); +} diff --git a/packages/core/src/js/utils/rnlibraries.ts b/packages/core/src/js/utils/rnlibraries.ts index b05167c2ef..45e2d3f630 100644 --- a/packages/core/src/js/utils/rnlibraries.ts +++ b/packages/core/src/js/utils/rnlibraries.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-var-requires */ import { AppRegistry, Platform, TurboModuleRegistry } from 'react-native'; - import type * as ReactNative from '../vendor/react-native'; import type { ReactNativeLibrariesInterface } from './rnlibrariesinterface'; diff --git a/packages/core/src/js/utils/safe.ts b/packages/core/src/js/utils/safe.ts index 911c0d3de1..7deb1d8d4c 100644 --- a/packages/core/src/js/utils/safe.ts +++ b/packages/core/src/js/utils/safe.ts @@ -1,5 +1,4 @@ -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import type { ReactNativeOptions } from '../options'; type DangerTypesWithoutCallSignature = @@ -24,7 +23,7 @@ export function safeFactory void): void { const map = listenersMap.get(eventType); if (!map) { - logger.warn(`EventEmitter was not initialized for event type: ${eventType}`); + debug.warn(`EventEmitter was not initialized for event type: ${eventType}`); return; } listenersMap.get(eventType)?.set(listener, true); @@ -93,19 +92,19 @@ export function createSentryEventEmitter( function createNoopSentryEventEmitter(): SentryEventEmitter { return { initAsync: () => { - logger.warn('Noop SentryEventEmitter: initAsync'); + debug.warn('Noop SentryEventEmitter: initAsync'); }, closeAllAsync: () => { - logger.warn('Noop SentryEventEmitter: closeAllAsync'); + debug.warn('Noop SentryEventEmitter: closeAllAsync'); }, addListener: () => { - logger.warn('Noop SentryEventEmitter: addListener'); + debug.warn('Noop SentryEventEmitter: addListener'); }, removeListener: () => { - logger.warn('Noop SentryEventEmitter: removeListener'); + debug.warn('Noop SentryEventEmitter: removeListener'); }, once: () => { - logger.warn('Noop SentryEventEmitter: once'); + debug.warn('Noop SentryEventEmitter: once'); }, }; } diff --git a/packages/core/src/js/utils/sentryeventemitterfallback.ts b/packages/core/src/js/utils/sentryeventemitterfallback.ts index 74f1ab58e8..5cb413d770 100644 --- a/packages/core/src/js/utils/sentryeventemitterfallback.ts +++ b/packages/core/src/js/utils/sentryeventemitterfallback.ts @@ -1,5 +1,4 @@ -import { logger, timestampInSeconds } from '@sentry/core'; - +import { debug, timestampInSeconds } from '@sentry/core'; import { NATIVE } from '../wrapper'; import type { NewFrameEvent, SentryEventEmitter } from './sentryeventemitter'; import { createSentryEventEmitter, NewFrameEventName } from './sentryeventemitter'; @@ -49,7 +48,7 @@ export function createSentryFallbackEventEmitter( nativeNewFrameTimestampSeconds = resolve ?? undefined; }) .catch(reason => { - logger.error('Failed to receive Native fallback timestamp.', reason); + debug.error('Failed to receive Native fallback timestamp.', reason); }); } @@ -73,20 +72,20 @@ export function createSentryFallbackEventEmitter( }; fallbackTimeout = setTimeout(() => { if (nativeNewFrameTimestampSeconds) { - logger.log('Native event emitter did not reply in time'); + debug.log('Native event emitter did not reply in time'); return listener({ newFrameTimestampInSeconds: nativeNewFrameTimestampSeconds, isFallback: true, }); } else if (animationFrameTimestampSeconds) { - logger.log('[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.'); + debug.log('[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.'); return listener({ newFrameTimestampInSeconds: animationFrameTimestampSeconds, isFallback: true, }); } else { emitter.removeListener(NewFrameEventName, internalListener); - logger.error('Failed to receive any fallback timestamp.'); + debug.error('Failed to receive any fallback timestamp.'); } }, fallbackTimeoutMs); diff --git a/packages/core/src/js/utils/worldwide.ts b/packages/core/src/js/utils/worldwide.ts index a6c530b09f..9e1893d441 100644 --- a/packages/core/src/js/utils/worldwide.ts +++ b/packages/core/src/js/utils/worldwide.ts @@ -1,8 +1,8 @@ import type { InternalGlobal } from '@sentry/core'; import { GLOBAL_OBJ } from '@sentry/core'; import type { ErrorUtils } from 'react-native/types'; - import type { ExpoGlobalObject } from './expoglobalobject'; + export interface HermesPromiseRejectionTrackingOptions { allRejections: boolean; onUnhandled: (id: string, error: unknown) => void; @@ -34,11 +34,18 @@ export interface ReactNativeInternalGlobal extends InternalGlobal { nativePerformanceNow?: () => number; TextEncoder?: TextEncoder; alert?: (message: string) => void; + SENTRY_RELEASE?: { + /** Used by Sentry Webpack Plugin, not used by RN, only to silence TS */ + id?: string; + name?: string; + version?: string; + }; } type TextEncoder = { - new (): TextEncoder; - encode(input?: string): Uint8Array; + new (): { + encode(input?: string): Uint8Array; + }; }; /** Get's the global object for the current JavaScript runtime */ diff --git a/packages/core/src/js/vendor/base64-js/fromByteArray.ts b/packages/core/src/js/vendor/base64-js/fromByteArray.ts index 51c046b0a4..11c771f1d5 100644 --- a/packages/core/src/js/vendor/base64-js/fromByteArray.ts +++ b/packages/core/src/js/vendor/base64-js/fromByteArray.ts @@ -28,10 +28,12 @@ const lookup: string[] = []; const code = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; for (let i = 0, len = code.length; i < len; ++i) { + // @ts-expect-error lookup[i] = code[i]; } function tripletToBase64(num: number): string { + // @ts-expect-error return lookup[(num >> 18) & 0x3f] + lookup[(num >> 12) & 0x3f] + lookup[(num >> 6) & 0x3f] + lookup[num & 0x3f]; } @@ -39,6 +41,7 @@ function encodeChunk(uint8: Uint8Array | number[], start: number, end: number): let tmp; const output = []; for (let i = start; i < end; i += 3) { + // @ts-expect-error tmp = ((uint8[i] << 16) & 0xff0000) + ((uint8[i + 1] << 8) & 0xff00) + (uint8[i + 2] & 0xff); output.push(tripletToBase64(tmp)); } @@ -63,9 +66,12 @@ export function base64StringFromByteArray(uint8: Uint8Array | number[]): string // pad the end with zeros, but make sure to not forget the extra bytes if (extraBytes === 1) { tmp = uint8[len - 1]; + // @ts-expect-error parts.push(`${lookup[tmp >> 2] + lookup[(tmp << 4) & 0x3f]}==`); } else if (extraBytes === 2) { + // @ts-expect-error tmp = (uint8[len - 2] << 8) + uint8[len - 1]; + // @ts-expect-error parts.push(`${lookup[tmp >> 10] + lookup[(tmp >> 4) & 0x3f] + lookup[(tmp << 2) & 0x3f]}=`); } diff --git a/packages/core/src/js/version.ts b/packages/core/src/js/version.ts index 77f9a255ec..9368fa91a0 100644 --- a/packages/core/src/js/version.ts +++ b/packages/core/src/js/version.ts @@ -1,3 +1,3 @@ export const SDK_PACKAGE_NAME = 'npm:@sentry/react-native'; export const SDK_NAME = 'sentry.javascript.react-native'; -export const SDK_VERSION = '6.21.0'; +export const SDK_VERSION = '7.0.0-rc.2'; diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index fdd04e8ac8..6829182298 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -6,12 +6,12 @@ import type { EnvelopeItem, Event, Package, + Primitive, SeverityLevel, User, } from '@sentry/core'; -import { logger, normalize, SentryError } from '@sentry/core'; +import { debug, normalize, SentryError } from '@sentry/core'; import { NativeModules, Platform } from 'react-native'; - import { isHardCrash } from './misc'; import type { NativeAppStartResponse, @@ -27,17 +27,18 @@ import type * as Hermes from './profiling/hermes'; import type { NativeAndroidProfileEvent, NativeProfileEvent } from './profiling/nativeTypes'; import type { MobileReplayOptions } from './replay/mobilereplay'; import type { RequiredKeysUser } from './user'; +import { encodeUTF8 } from './utils/encode'; import { isTurboModuleEnabled } from './utils/environment'; import { convertToNormalizedObject } from './utils/normalize'; import { ReactNativeLibraries } from './utils/rnlibraries'; -import { base64StringFromByteArray, utf8ToBytes } from './vendor'; +import { base64StringFromByteArray } from './vendor'; /** * Returns the RNSentry module. Dynamically resolves if NativeModule or TurboModule is used. */ export function getRNSentryModule(): Spec | undefined { return isTurboModuleEnabled() - ? ReactNativeLibraries.TurboModuleRegistry && ReactNativeLibraries.TurboModuleRegistry.get('RNSentry') + ? ReactNativeLibraries.TurboModuleRegistry?.get('RNSentry') : NativeModules.RNSentry; } @@ -52,6 +53,8 @@ export interface Screenshot { export type NativeSdkOptions = Partial & { devServerUrl: string | undefined; defaultSidecarUrl: string | undefined; + ignoreErrorsStr?: string[] | undefined; + ignoreErrorsRegex?: string[] | undefined; } & { mobileReplayOptions: MobileReplayOptions | undefined; }; @@ -64,6 +67,7 @@ interface SentryNativeWrapper { _NativeClientError: Error; _DisabledNativeError: Error; + _setPrimitiveProcessor: (processor: (value: Primitive) => void) => void; _processItem(envelopeItem: EnvelopeItem): EnvelopeItem; _processLevels(event: Event): Event; _processLevel(level: SeverityLevel): SeverityLevel; @@ -80,6 +84,7 @@ interface SentryNativeWrapper { fetchNativeRelease(): PromiseLike; fetchNativeDeviceContexts(): PromiseLike; + fetchNativeLogAttributes(): Promise; fetchNativeAppStart(): PromiseLike; fetchNativeFrames(): PromiseLike; fetchNativeSdkInfo(): PromiseLike; @@ -93,7 +98,7 @@ interface SentryNativeWrapper { clearBreadcrumbs(): void; setExtra(key: string, extra: unknown): void; setUser(user: User | null): void; - setTag(key: string, value: string): void; + setTag(key: string, value?: string): void; nativeCrash(): void; @@ -127,9 +132,11 @@ interface SentryNativeWrapper { setActiveSpanId(spanId: string): void; encodeToBase64(data: Uint8Array): Promise; + + primitiveProcessor(value: Primitive): string; } -const EOL = utf8ToBytes('\n'); +const EOL = encodeUTF8('\n'); /** * Our internal interface for calling native functions @@ -155,7 +162,7 @@ export const NATIVE: SentryNativeWrapper = { */ async sendEnvelope(envelope: Envelope): Promise { if (!this.enableNative) { - logger.warn('Event was skipped as native SDK is not enabled.'); + debug.warn('Event was skipped as native SDK is not enabled.'); return; } @@ -166,7 +173,7 @@ export const NATIVE: SentryNativeWrapper = { const [envelopeHeader, envelopeItems] = envelope; const headerString = JSON.stringify(envelopeHeader); - const headerBytes = utf8ToBytes(headerString); + const headerBytes = encodeUTF8(headerString); let envelopeBytes: Uint8Array = new Uint8Array(headerBytes.length + EOL.length); envelopeBytes.set(headerBytes); envelopeBytes.set(EOL, headerBytes.length); @@ -179,14 +186,14 @@ export const NATIVE: SentryNativeWrapper = { let bytesPayload: number[] | Uint8Array | undefined; if (typeof itemPayload === 'string') { bytesContentType = 'text/plain'; - bytesPayload = utf8ToBytes(itemPayload); + bytesPayload = encodeUTF8(itemPayload); } else if (itemPayload instanceof Uint8Array) { bytesContentType = typeof itemHeader.content_type === 'string' ? itemHeader.content_type : 'application/octet-stream'; bytesPayload = itemPayload; } else { - bytesContentType = 'application/json'; - bytesPayload = utf8ToBytes(JSON.stringify(itemPayload)); + bytesContentType = 'application/vnd.sentry.items.log+json'; + bytesPayload = encodeUTF8(JSON.stringify(itemPayload)); if (!hardCrashed) { hardCrashed = isHardCrash(itemPayload); } @@ -197,7 +204,7 @@ export const NATIVE: SentryNativeWrapper = { (itemHeader as BaseEnvelopeItemHeaders).length = bytesPayload.length; const serializedItemHeader = JSON.stringify(itemHeader); - const bytesItemHeader = utf8ToBytes(serializedItemHeader); + const bytesItemHeader = encodeUTF8(serializedItemHeader); const newBytes = new Uint8Array( envelopeBytes.length + bytesItemHeader.length + EOL.length + bytesPayload.length + EOL.length, ); @@ -225,14 +232,14 @@ export const NATIVE: SentryNativeWrapper = { if (!options.enableNative) { if (options.enableNativeNagger) { - logger.warn('Note: Native Sentry SDK is disabled.'); + debug.warn('Note: Native Sentry SDK is disabled.'); } this.enableNative = false; return false; } if (!options.autoInitializeNativeSdk) { if (options.enableNativeNagger) { - logger.warn( + debug.warn( 'Note: Native Sentry SDK was not initialized automatically, you will need to initialize it manually. If you wish to disable the native SDK and get rid of this warning, pass enableNative: false', ); } @@ -241,7 +248,7 @@ export const NATIVE: SentryNativeWrapper = { } if (!options.dsn) { - logger.warn( + debug.warn( 'Warning: No DSN was provided. The Sentry SDK will be disabled. Native SDK will also not be initalized.', ); this.enableNative = false; @@ -251,10 +258,22 @@ export const NATIVE: SentryNativeWrapper = { if (!this._isModuleLoaded(RNSentry)) { throw this._NativeClientError; } + const ignoreErrorsStr = options.ignoreErrors?.filter(item => typeof item === 'string') as string[] | undefined; + const ignoreErrorsRegex = options.ignoreErrors + ?.filter(item => item instanceof RegExp) + .map(item => (item as RegExp).source) as string[] | undefined; + + if (ignoreErrorsStr && ignoreErrorsStr.length > 0) { + options.ignoreErrorsStr = ignoreErrorsStr; + } + if (ignoreErrorsRegex && ignoreErrorsRegex.length > 0) { + options.ignoreErrorsRegex = ignoreErrorsRegex; + } // filter out all the options that would crash native. /* eslint-disable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */ - const { beforeSend, beforeBreadcrumb, beforeSendTransaction, integrations, ...filteredOptions } = options; + const { beforeSend, beforeBreadcrumb, beforeSendTransaction, integrations, ignoreErrors, ...filteredOptions } = + options; /* eslint-enable @typescript-eslint/unbound-method,@typescript-eslint/no-unused-vars */ const nativeIsReady = await RNSentry.initNativeSdk(filteredOptions); @@ -264,6 +283,19 @@ export const NATIVE: SentryNativeWrapper = { return nativeIsReady; }, + /** + * Fetches the attributes to be set into logs from Native + */ + async fetchNativeLogAttributes(): Promise { + if (!this.enableNative) { + throw this._DisabledNativeError; + } + if (!this._isModuleLoaded(RNSentry)) { + throw this._NativeClientError; + } + + return RNSentry.fetchNativeLogAttributes(); + }, /** * Fetches the release from native */ @@ -308,11 +340,11 @@ export const NATIVE: SentryNativeWrapper = { async fetchNativeAppStart(): Promise { if (!this.enableNative) { - logger.warn(this._DisabledNativeError); + debug.warn(this._DisabledNativeError); return null; } if (!this._isModuleLoaded(RNSentry)) { - logger.error(this._NativeClientError); + debug.error(this._NativeClientError); return null; } @@ -382,7 +414,7 @@ export const NATIVE: SentryNativeWrapper = { * @param key string * @param value string */ - setTag(key: string, value: string): void { + setTag(key: string, value?: string): void { if (!this.enableNative) { return; } @@ -421,7 +453,7 @@ export const NATIVE: SentryNativeWrapper = { const normalizedExtra = normalize(extra); stringifiedExtra = JSON.stringify(normalizedExtra); } catch (e) { - logger.error('Extra for key ${key} not passed to native SDK, because it contains non-stringifiable values', e); + debug.error('Extra for key ${key} not passed to native SDK, because it contains non-stringifiable values', e); } if (typeof stringifiedExtra === 'string') { @@ -485,7 +517,7 @@ export const NATIVE: SentryNativeWrapper = { try { normalizedContext = convertToNormalizedObject(context); } catch (e) { - logger.error('Context for key ${key} not passed to native SDK, because it contains non-serializable values', e); + debug.error('Context for key ${key} not passed to native SDK, because it contains non-serializable values', e); } if (normalizedContext) { @@ -539,11 +571,11 @@ export const NATIVE: SentryNativeWrapper = { async captureScreenshot(): Promise { if (!this.enableNative) { - logger.warn(this._DisabledNativeError); + debug.warn(this._DisabledNativeError); return null; } if (!this._isModuleLoaded(RNSentry)) { - logger.error(this._NativeClientError); + debug.error(this._NativeClientError); return null; } @@ -551,7 +583,7 @@ export const NATIVE: SentryNativeWrapper = { try { raw = await RNSentry.captureScreenshot(); } catch (e) { - logger.warn('Failed to capture screenshot', e); + debug.warn('Failed to capture screenshot', e); } if (raw) { @@ -586,9 +618,9 @@ export const NATIVE: SentryNativeWrapper = { const { started, error } = RNSentry.startProfiling(platformProfilers); if (started) { - logger.log('[NATIVE] Start Profiling'); + debug.log('[NATIVE] Start Profiling'); } else { - logger.error('[NATIVE] Start Profiling Failed', error); + debug.error('[NATIVE] Start Profiling Failed', error); } return !!started; @@ -608,14 +640,14 @@ export const NATIVE: SentryNativeWrapper = { const { profile, nativeProfile, androidProfile, error } = RNSentry.stopProfiling(); if (!profile || error) { - logger.error('[NATIVE] Stop Profiling Failed', error); + debug.error('[NATIVE] Stop Profiling Failed', error); return null; } if (Platform.OS === 'ios' && !nativeProfile) { - logger.warn('[NATIVE] Stop Profiling Failed: No Native Profile'); + debug.warn('[NATIVE] Stop Profiling Failed: No Native Profile'); } if (Platform.OS === 'android' && !androidProfile) { - logger.warn('[NATIVE] Stop Profiling Failed: No Android Profile'); + debug.warn('[NATIVE] Stop Profiling Failed: No Android Profile'); } try { @@ -625,7 +657,7 @@ export const NATIVE: SentryNativeWrapper = { androidProfile: androidProfile as NativeAndroidProfileEvent | undefined, }; } catch (e) { - logger.error('[NATIVE] Failed to parse Hermes Profile JSON', e); + debug.error('[NATIVE] Failed to parse Hermes Profile JSON', e); return null; } }, @@ -665,11 +697,11 @@ export const NATIVE: SentryNativeWrapper = { async captureReplay(isHardCrash: boolean): Promise { if (!this.enableNative) { - logger.warn(`[NATIVE] \`${this.captureReplay.name}\` is not available when native is disabled.`); + debug.warn(`[NATIVE] \`${this.captureReplay.name}\` is not available when native is disabled.`); return Promise.resolve(null); } if (!this._isModuleLoaded(RNSentry)) { - logger.warn(`[NATIVE] \`${this.captureReplay.name}\` is not available when native is not available.`); + debug.warn(`[NATIVE] \`${this.captureReplay.name}\` is not available when native is not available.`); return Promise.resolve(null); } @@ -678,11 +710,11 @@ export const NATIVE: SentryNativeWrapper = { getCurrentReplayId(): string | null { if (!this.enableNative) { - logger.warn(`[NATIVE] \`${this.getCurrentReplayId.name}\` is not available when native is disabled.`); + debug.warn(`[NATIVE] \`${this.getCurrentReplayId.name}\` is not available when native is disabled.`); return null; } if (!this._isModuleLoaded(RNSentry)) { - logger.warn(`[NATIVE] \`${this.getCurrentReplayId.name}\` is not available when native is not available.`); + debug.warn(`[NATIVE] \`${this.getCurrentReplayId.name}\` is not available when native is not available.`); return null; } @@ -717,7 +749,7 @@ export const NATIVE: SentryNativeWrapper = { const data: number[] = await RNSentry.getDataFromUri(uri); return new Uint8Array(data); } catch (error) { - logger.error('Error:', error); + debug.error('Error:', error); return null; } }, @@ -730,8 +762,8 @@ export const NATIVE: SentryNativeWrapper = { try { return RNSentry.popTimeToDisplayFor(key); } catch (error) { - logger.error('Error:', error); - return null; + debug.error('Error:', error); + return Promise.resolve(null); } }, @@ -743,7 +775,7 @@ export const NATIVE: SentryNativeWrapper = { try { RNSentry.setActiveSpanId(spanId); } catch (error) { - logger.error('Error:', error); + debug.error('Error:', error); return undefined; } }, @@ -758,11 +790,15 @@ export const NATIVE: SentryNativeWrapper = { const base64 = await RNSentry.encodeToBase64(byteArray); return base64 || null; } catch (error) { - logger.error('Error:', error); + debug.error('Error:', error); return Promise.resolve(null); } }, + primitiveProcessor: function (value: Primitive): string { + return value as string; + }, + /** * Gets the event from envelopeItem and applies the level filter to the selected event. * @param data An envelope item containing the event. @@ -808,7 +844,6 @@ export const NATIVE: SentryNativeWrapper = { * @param event * @returns Event with more widely supported Severity level strings */ - _processLevels(event: Event): Event { const processed: Event = { ...event, @@ -827,7 +862,6 @@ export const NATIVE: SentryNativeWrapper = { * @param level * @returns More widely supported Severity level strings */ - _processLevel(level: SeverityLevel): SeverityLevel { if (level == ('log' as SeverityLevel)) { return 'debug' as SeverityLevel; @@ -842,6 +876,10 @@ export const NATIVE: SentryNativeWrapper = { return !!module; }, + _setPrimitiveProcessor: function (processor: (value: Primitive) => any): void { + this.primitiveProcessor = processor; + }, + _DisabledNativeError: new SentryError('Native is disabled'), _NativeClientError: new SentryError("Native Client is not available, can't start on native."), diff --git a/packages/core/test/breadcrumb.test.ts b/packages/core/test/breadcrumb.test.ts index e0b7db4c60..9f1609727b 100644 --- a/packages/core/test/breadcrumb.test.ts +++ b/packages/core/test/breadcrumb.test.ts @@ -1,5 +1,4 @@ import type { Breadcrumb } from '@sentry/core'; - import { breadcrumbFromObject } from '../src/js/breadcrumb'; describe('Breadcrumb', () => { diff --git a/packages/core/test/client.test.ts b/packages/core/test/client.test.ts index 8cb4217356..14c35b533e 100644 --- a/packages/core/test/client.test.ts +++ b/packages/core/test/client.test.ts @@ -1,11 +1,20 @@ -import * as mockedtimetodisplaynative from './tracing/mockedtimetodisplaynative'; -jest.mock('../src/js/tracing/timetodisplaynative', () => mockedtimetodisplaynative); - import { defaultStackParser } from '@sentry/browser'; -import type { Envelope, Event, Outcome, Transport, TransportMakeRequestResponse } from '@sentry/core'; -import { rejectedSyncPromise, SentryError } from '@sentry/core'; +import type { + Envelope, + Event, + Outcome, + SessionAggregates, + Transport, + TransportMakeRequestResponse, +} from '@sentry/core'; +import { + addAutoIpAddressToSession, + addAutoIpAddressToUser, + makeSession, + rejectedSyncPromise, + SentryError, +} from '@sentry/core'; import * as RN from 'react-native'; - import { ReactNativeClient } from '../src/js/client'; import type { ReactNativeClientOptions } from '../src/js/options'; import { NativeTransport } from '../src/js/transports/native'; @@ -22,6 +31,9 @@ import { getMockUserFeedback, getSyncPromiseRejectOnFirstCall, } from './testutils'; +import * as mockedtimetodisplaynative from './tracing/mockedtimetodisplaynative'; + +jest.mock('../src/js/tracing/timetodisplaynative', () => mockedtimetodisplaynative); interface MockedReactNative { NativeModules: { @@ -625,6 +637,206 @@ describe('Tests ReactNativeClient', () => { client.recordDroppedEvent('before_send', 'error'); } }); + + describe('ipAddress', () => { + let mockTransportSend: jest.Mock; + let client: ReactNativeClient; + + beforeEach(() => { + mockTransportSend = jest.fn(() => Promise.resolve()); + client = new ReactNativeClient({ + ...DEFAULT_OPTIONS, + dsn: EXAMPLE_DSN, + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + sendDefaultPii: true, + }); + }); + + test('preserves ip_address null', () => { + client.captureEvent({ + user: { + ip_address: null, + }, + }); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].user).toEqual( + expect.objectContaining({ ip_address: null }), + ); + }); + + test('preserves ip_address value if set', () => { + client.captureEvent({ + user: { + ip_address: '203.45.167.89', + }, + }); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].user).toEqual( + expect.objectContaining({ ip_address: '203.45.167.89' }), + ); + }); + + test('adds ip_address {{auto}} to user if set to undefined', () => { + client.captureEvent({ + user: { + ip_address: undefined, + }, + }); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].user).toEqual( + expect.objectContaining({ ip_address: '{{auto}}' }), + ); + }); + + test('adds ip_address {{auto}} to user if not set', () => { + client.captureEvent({ + user: {}, + }); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].user).toEqual( + expect.objectContaining({ ip_address: '{{auto}}' }), + ); + }); + + test('adds ip_address {{auto}} to undefined user', () => { + client.captureEvent({}); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].user).toEqual( + expect.objectContaining({ ip_address: '{{auto}}' }), + ); + }); + + test('does not add ip_address {{auto}} to undefined user if sendDefaultPii is false', () => { + const { client, onSpy } = createClientWithSpy({ + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + sendDefaultPii: false, + }); + + client.captureEvent({}); + + expect(onSpy).not.toHaveBeenCalledWith('postprocessEvent', addAutoIpAddressToUser); + expect( + mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].user?.ip_address, + ).toBeUndefined(); + }); + + test('uses ip address hooks if sendDefaultPii is true', () => { + const { onSpy } = createClientWithSpy({ + sendDefaultPii: true, + }); + + expect(onSpy).toHaveBeenCalledWith('postprocessEvent', addAutoIpAddressToUser); + expect(onSpy).toHaveBeenCalledWith('beforeSendSession', addAutoIpAddressToSession); + }); + + test('does not add ip_address {{auto}} to session if sendDefaultPii is false', () => { + const { client, onSpy } = createClientWithSpy({ + release: 'test', // required for sessions to be sent + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + sendDefaultPii: false, + }); + + const session = makeSession(); + session.ipAddress = undefined; + client.captureSession(session); + + expect(onSpy).not.toHaveBeenCalledWith('beforeSendSession', addAutoIpAddressToSession); + expect( + mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].attrs.ip_address, + ).toBeUndefined(); + }); + + test('does not add ip_address {{auto}} to session aggregate if sendDefaultPii is false', () => { + const { client, onSpy } = createClientWithSpy({ + release: 'test', // required for sessions to be sent + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + sendDefaultPii: false, + }); + + const session: SessionAggregates = { + aggregates: [], + }; + client.sendSession(session); + + expect(onSpy).not.toHaveBeenCalledWith('beforeSendSession', addAutoIpAddressToSession); + expect( + mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].attrs.ip_address, + ).toBeUndefined(); + }); + + test('does not overwrite session aggregate ip_address if already set', () => { + const { client } = createClientWithSpy({ + release: 'test', // required for sessions to be sent + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + sendDefaultPii: true, + }); + + const session: SessionAggregates = { + aggregates: [], + attrs: { + ip_address: '123.45.67.89', + }, + }; + client.sendSession(session); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].attrs.ip_address).toBe( + '123.45.67.89', + ); + }); + + test('does add ip_address {{auto}} to session if sendDefaultPii is true', () => { + const { client } = createClientWithSpy({ + release: 'test', // required for sessions to be sent + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + sendDefaultPii: true, + }); + + const session = makeSession(); + session.ipAddress = undefined; + client.captureSession(session); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].attrs.ip_address).toBe( + '{{auto}}', + ); + }); + + test('does not overwrite session ip_address if already set', () => { + const { client } = createClientWithSpy({ + release: 'test', // required for sessions to be sent + transport: () => ({ + send: mockTransportSend, + flush: jest.fn(), + }), + sendDefaultPii: true, + }); + + const session = makeSession(); + session.ipAddress = '123.45.67.89'; + client.captureSession(session); + + expect(mockTransportSend.mock.calls[0][firstArg][envelopeItems][0][envelopeItemPayload].attrs.ip_address).toBe( + '123.45.67.89', + ); + }); + }); }); function mockedOptions(options: Partial): ReactNativeClientOptions { @@ -638,3 +850,23 @@ function mockedOptions(options: Partial): ReactNativeC ...options, }; } + +function createClientWithSpy(options: Partial) { + const onSpy = jest.fn(); + class SpyClient extends ReactNativeClient { + public on(hook: string, callback: unknown): () => void { + onSpy(hook, callback); + // @ts-expect-error - the public interface doesn't allow string and unknown + return super.on(hook, callback); + } + } + + return { + client: new SpyClient({ + ...DEFAULT_OPTIONS, + dsn: EXAMPLE_DSN, + ...options, + }), + onSpy, + }; +} diff --git a/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts b/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts index 28df575c78..bce3cfa916 100644 --- a/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts +++ b/packages/core/test/expo-plugin/withSentryAndroidGradlePlugin.test.ts @@ -1,5 +1,4 @@ import { withAppBuildGradle, withProjectBuildGradle } from '@expo/config-plugins'; - import { warnOnce } from '../../plugin/src/utils'; import type { SentryAndroidGradlePluginOptions } from '../../plugin/src/withSentryAndroidGradlePlugin'; import { @@ -97,7 +96,7 @@ describe('withSentryAndroidGradlePlugin', () => { }); it('warnOnce if failed to modify build.gradle', () => { - const invalidBuildGradle = `android {}`; + const invalidBuildGradle = 'android {}'; const options: SentryAndroidGradlePluginOptions = { enableAndroidGradlePlugin: true }; (withProjectBuildGradle as jest.Mock).mockImplementation((config, callback) => { diff --git a/packages/core/test/feedback.test.ts b/packages/core/test/feedback.test.ts index 6b1831934d..ca75063402 100644 --- a/packages/core/test/feedback.test.ts +++ b/packages/core/test/feedback.test.ts @@ -9,7 +9,6 @@ import { withIsolationScope, withScope, } from '@sentry/core'; - import { getDefaultTestClientOptions, TestClient } from './mocks/client'; describe('captureFeedback', () => { @@ -240,17 +239,17 @@ describe('captureFeedback', () => { const mockTransport = jest.spyOn(client.getTransport()!, 'send'); const traceId = '4C79F60C11214EB38604F4AE0781BFB2'; - const spanId = 'FA90FDEAD5F74052'; + const parentSpanId = 'FA90FDEAD5F74052'; const dsc = { trace_id: traceId, - span_id: spanId, sampled: 'true', }; getCurrentScope().setPropagationContext({ traceId, - spanId, + parentSpanId, dsc, + sampleRand: 1, }); const eventId = captureFeedback({ @@ -264,7 +263,7 @@ describe('captureFeedback', () => { expect(mockTransport).toHaveBeenCalledWith([ { event_id: eventId, - sent_at: expect.any(String), + sent_at: expect.toBeDateString(), }, [ [ @@ -274,7 +273,8 @@ describe('captureFeedback', () => { contexts: { trace: { trace_id: traceId, - span_id: spanId, + parent_span_id: parentSpanId, + span_id: expect.any(String), }, feedback: { message: 'test', @@ -297,7 +297,7 @@ describe('captureFeedback', () => { getDefaultTestClientOptions({ dsn: 'https://dsn@ingest.f00.f00/1', enableSend: true, - enableTracing: true, + tracesSampleRate: 1.0, // We don't care about transactions here... beforeSendTransaction() { return null; @@ -322,12 +322,12 @@ describe('captureFeedback', () => { expect(typeof eventId).toBe('string'); expect(span).toBeDefined(); - const { spanId, traceId } = span!.spanContext(); + const traceId = span!.spanContext().traceId; expect(mockTransport).toHaveBeenCalledWith([ { event_id: eventId, - sent_at: expect.any(String), + sent_at: expect.toBeDateString(), }, [ [ @@ -337,7 +337,7 @@ describe('captureFeedback', () => { contexts: { trace: { trace_id: traceId, - span_id: spanId, + span_id: expect.any(String), }, feedback: { message: 'test', @@ -360,7 +360,7 @@ describe('captureFeedback', () => { getDefaultTestClientOptions({ dsn: 'https://dsn@ingest.f00.f00/1', enableSend: true, - enableTracing: true, + tracesSampleRate: 1.0, // We don't care about transactions here... beforeSendTransaction() { return null; diff --git a/packages/core/test/feedback/FeedbackButton.test.tsx b/packages/core/test/feedback/FeedbackButton.test.tsx index 579bccc7ca..db2dd025a4 100644 --- a/packages/core/test/feedback/FeedbackButton.test.tsx +++ b/packages/core/test/feedback/FeedbackButton.test.tsx @@ -1,6 +1,5 @@ import { fireEvent, render, waitFor } from '@testing-library/react-native'; import * as React from 'react'; - import { FeedbackButton } from '../../src/js/feedback/FeedbackButton'; import type { FeedbackButtonProps, FeedbackButtonStyles } from '../../src/js/feedback/FeedbackWidget.types'; import { showFeedbackWidget } from '../../src/js/feedback/FeedbackWidgetManager'; diff --git a/packages/core/test/feedback/FeedbackWidget.test.tsx b/packages/core/test/feedback/FeedbackWidget.test.tsx index da7b13f585..1e424cde77 100644 --- a/packages/core/test/feedback/FeedbackWidget.test.tsx +++ b/packages/core/test/feedback/FeedbackWidget.test.tsx @@ -2,7 +2,6 @@ import { captureFeedback, getClient, setCurrentClient } from '@sentry/core'; import { fireEvent, render, waitFor } from '@testing-library/react-native'; import * as React from 'react'; import { Alert } from 'react-native'; - import { FeedbackWidget } from '../../src/js/feedback/FeedbackWidget'; import type { FeedbackWidgetProps, FeedbackWidgetStyles, ImagePicker } from '../../src/js/feedback/FeedbackWidget.types'; import { MOBILE_FEEDBACK_INTEGRATION_NAME } from '../../src/js/feedback/integration'; @@ -384,7 +383,7 @@ describe('FeedbackWidget', () => { it('calls launchImageLibraryAsync when the expo-image-picker library is integrated', async () => { const mockLaunchImageLibrary = jest.fn().mockResolvedValue({ - assets: [{ fileName: "mock-image.jpg", uri: "file:///mock/path/image.jpg" }], + assets: [{ fileName: 'mock-image.jpg', uri: 'file:///mock/path/image.jpg' }], }); const mockImagePicker: jest.Mocked = { launchImageLibraryAsync: mockLaunchImageLibrary, @@ -401,7 +400,7 @@ describe('FeedbackWidget', () => { it('calls launchImageLibrary when the react-native-image-picker library is integrated', async () => { const mockLaunchImageLibrary = jest.fn().mockResolvedValue({ - assets: [{ fileName: "mock-image.jpg", uri: "file:///mock/path/image.jpg" }], + assets: [{ fileName: 'mock-image.jpg', uri: 'file:///mock/path/image.jpg' }], }); const mockImagePicker: jest.Mocked = { launchImageLibrary: mockLaunchImageLibrary, diff --git a/packages/core/test/feedback/FeedbackWidgetManager.test.tsx b/packages/core/test/feedback/FeedbackWidgetManager.test.tsx index 21e10bff1e..0f2703e54c 100644 --- a/packages/core/test/feedback/FeedbackWidgetManager.test.tsx +++ b/packages/core/test/feedback/FeedbackWidgetManager.test.tsx @@ -1,8 +1,7 @@ -import { getClient, logger, setCurrentClient } from '@sentry/core'; +import { debug, getClient, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react-native'; import * as React from 'react'; import { Appearance, Text } from 'react-native'; - import { defaultConfiguration } from '../../src/js/feedback/defaults'; import { hideFeedbackButton,resetFeedbackButtonManager, resetFeedbackWidgetManager, showFeedbackButton, showFeedbackWidget } from '../../src/js/feedback/FeedbackWidgetManager'; import { FeedbackWidgetProvider } from '../../src/js/feedback/FeedbackWidgetProvider'; @@ -21,7 +20,7 @@ const consoleWarnSpy = jest.spyOn(console, 'warn'); const mockedIsModalSupported = isModalSupported as jest.MockedFunction; beforeEach(() => { - logger.error = jest.fn(); + debug.error = jest.fn(); }); describe('FeedbackWidgetManager', () => { @@ -60,7 +59,7 @@ describe('FeedbackWidgetManager', () => { expect(queryByTestId('feedback-form-modal')).toBeNull(); expect(getByText('App Components')).toBeTruthy(); - expect(logger.error).toHaveBeenLastCalledWith( + expect(debug.error).toHaveBeenLastCalledWith( 'FeedbackWidget Modal is not supported in React Native < 0.71 with Fabric renderer.', ); }); @@ -116,7 +115,7 @@ describe('FeedbackWidgetManager', () => { showFeedbackWidget(); - expect(consoleWarnSpy).toHaveBeenLastCalledWith(`[Sentry] FeedbackWidget requires 'Sentry.wrap(RootComponent)' to be called before 'showFeedbackWidget()'.`); + expect(consoleWarnSpy).toHaveBeenLastCalledWith('[Sentry] FeedbackWidget requires \'Sentry.wrap(RootComponent)\' to be called before \'showFeedbackWidget()\'.'); }); it('showFeedbackWidget does not warn about missing feedback provider when FeedbackWidgetProvider is used', () => { @@ -196,7 +195,7 @@ describe('FeedbackButtonManager', () => { it('showFeedbackButton warns about missing feedback provider', () => { showFeedbackButton(); - expect(consoleWarnSpy).toHaveBeenLastCalledWith(`[Sentry] FeedbackButton requires 'Sentry.wrap(RootComponent)' to be called before 'showFeedbackButton()'.`); + expect(consoleWarnSpy).toHaveBeenLastCalledWith('[Sentry] FeedbackButton requires \'Sentry.wrap(RootComponent)\' to be called before \'showFeedbackButton()\'.'); }); it('showFeedbackButton does not warn about missing feedback provider when FeedbackWidgetProvider is used', () => { diff --git a/packages/core/test/feedback/ScreenshotButton.test.tsx b/packages/core/test/feedback/ScreenshotButton.test.tsx index 2419860d1c..0bacceb208 100644 --- a/packages/core/test/feedback/ScreenshotButton.test.tsx +++ b/packages/core/test/feedback/ScreenshotButton.test.tsx @@ -2,7 +2,6 @@ import { getClient, setCurrentClient } from '@sentry/core'; import { fireEvent, render, waitFor } from '@testing-library/react-native'; import * as React from 'react'; import { Alert, Text } from 'react-native'; - import { FeedbackWidget } from '../../src/js/feedback/FeedbackWidget'; import type { ScreenshotButtonProps, ScreenshotButtonStyles } from '../../src/js/feedback/FeedbackWidget.types'; import { resetFeedbackButtonManager, resetFeedbackWidgetManager, resetScreenshotButtonManager, showFeedbackButton } from '../../src/js/feedback/FeedbackWidgetManager'; diff --git a/packages/core/test/integrations/appRegistry.test.ts b/packages/core/test/integrations/appRegistry.test.ts index 535616c378..44e7d6a3b8 100644 --- a/packages/core/test/integrations/appRegistry.test.ts +++ b/packages/core/test/integrations/appRegistry.test.ts @@ -1,5 +1,4 @@ import { getOriginalFunction } from '@sentry/core'; - import { appRegistryIntegration } from '../../src/js/integrations/appRegistry'; import * as Environment from '../../src/js/utils/environment'; import { ReactNativeLibraries } from '../../src/js/utils/rnlibraries'; diff --git a/packages/core/test/integrations/breadcrumbs.test.ts b/packages/core/test/integrations/breadcrumbs.test.ts new file mode 100644 index 0000000000..e7105667fe --- /dev/null +++ b/packages/core/test/integrations/breadcrumbs.test.ts @@ -0,0 +1,89 @@ +import { breadcrumbsIntegration as browserBreadcrumbsIntegration } from '@sentry/browser'; +import { breadcrumbsIntegration } from '../../src/js/integrations/breadcrumbs'; +import * as environment from '../../src/js/utils/environment'; + +jest.mock('@sentry/browser', () => ({ + breadcrumbsIntegration: jest.fn(), +})); + +describe('breadcrumbsIntegration', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('passes React Native defaults to browserBreadcrumbsIntegration', () => { + jest.spyOn(environment, 'isWeb').mockReturnValue(false); + + breadcrumbsIntegration(); + + expect(browserBreadcrumbsIntegration).toHaveBeenCalledWith({ + xhr: true, + console: true, + sentry: true, + dom: false, // DOM is not available in React Native + fetch: false, // fetch is built on XMLHttpRequest in React Native + history: false, // history is not available in React Native + }); + }); + + it('passes web defaults to browserBreadcrumbsIntegration when isWeb returns true', () => { + jest.spyOn(environment, 'isWeb').mockReturnValue(true); + + breadcrumbsIntegration(); + + expect(browserBreadcrumbsIntegration).toHaveBeenCalledWith({ + // Everything is enabled by default on web + xhr: true, + console: true, + sentry: true, + dom: true, + fetch: true, + history: true, + }); + }); + + it('respects custom options React Native options', () => { + jest.spyOn(environment, 'isWeb').mockReturnValue(false); + + breadcrumbsIntegration({ + xhr: false, + console: false, + sentry: false, + dom: {}, // Integration should not let user enable DOM breadcrumbs on React Native + fetch: true, // If user enables it, we should log fetch requests + history: true, // Integration should not let user enable history breadcrumbs on React Native + }); + + expect(browserBreadcrumbsIntegration).toHaveBeenCalledWith({ + xhr: false, + console: false, + sentry: false, + dom: false, + fetch: true, + history: false, + }); + }); + + it('respects custom options when isWeb returns true', () => { + jest.spyOn(environment, 'isWeb').mockReturnValue(true); + + breadcrumbsIntegration({ + // Everything can be disabled on web + xhr: false, + console: false, + sentry: false, + dom: false, + fetch: false, + history: false, + }); + + expect(browserBreadcrumbsIntegration).toHaveBeenCalledWith({ + xhr: false, + console: false, + sentry: false, + dom: false, + fetch: false, + history: false, + }); + }); +}); diff --git a/packages/core/test/integrations/debugsymbolicator.test.ts b/packages/core/test/integrations/debugsymbolicator.test.ts index 9ed5728319..265780d01f 100644 --- a/packages/core/test/integrations/debugsymbolicator.test.ts +++ b/packages/core/test/integrations/debugsymbolicator.test.ts @@ -1,7 +1,4 @@ -jest.mock('../../src/js/integrations/debugsymbolicatorutils'); - import type { Client, Event, EventHint, StackFrame } from '@sentry/core'; - import { debugSymbolicatorIntegration } from '../../src/js/integrations/debugsymbolicator'; import { fetchSourceContext, @@ -11,6 +8,8 @@ import { } from '../../src/js/integrations/debugsymbolicatorutils'; import type * as ReactNative from '../../src/js/vendor/react-native'; +jest.mock('../../src/js/integrations/debugsymbolicatorutils'); + async function processEvent(mockedEvent: Event, mockedHint: EventHint): Promise { return debugSymbolicatorIntegration().processEvent!(mockedEvent, mockedHint, {} as Client); } diff --git a/packages/core/test/integrations/devicecontext.test.ts b/packages/core/test/integrations/devicecontext.test.ts index 014b4939eb..2689582ee4 100644 --- a/packages/core/test/integrations/devicecontext.test.ts +++ b/packages/core/test/integrations/devicecontext.test.ts @@ -1,5 +1,4 @@ import type { Client, Event, EventHint, SeverityLevel } from '@sentry/core'; - import { deviceContextIntegration } from '../../src/js/integrations/devicecontext'; import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; diff --git a/packages/core/test/integrations/eventorigin.test.ts b/packages/core/test/integrations/eventorigin.test.ts index dd3e0cbd91..3b5ed14c1c 100644 --- a/packages/core/test/integrations/eventorigin.test.ts +++ b/packages/core/test/integrations/eventorigin.test.ts @@ -1,5 +1,4 @@ import type { Client } from '@sentry/core'; - import { eventOriginIntegration } from '../../src/js/integrations/eventorigin'; describe('Event Origin', () => { diff --git a/packages/core/test/integrations/expocontext.test.ts b/packages/core/test/integrations/expocontext.test.ts index 205fc35b57..47381e8926 100644 --- a/packages/core/test/integrations/expocontext.test.ts +++ b/packages/core/test/integrations/expocontext.test.ts @@ -1,5 +1,4 @@ import { type Client, type Event, getCurrentScope, getGlobalScope, getIsolationScope } from '@sentry/core'; - import { expoContextIntegration, getExpoUpdatesContext, diff --git a/packages/core/test/integrations/integrationsexecutionorder.test.ts b/packages/core/test/integrations/integrationsexecutionorder.test.ts index 08b4a4f5a6..09c1f3fb65 100644 --- a/packages/core/test/integrations/integrationsexecutionorder.test.ts +++ b/packages/core/test/integrations/integrationsexecutionorder.test.ts @@ -1,10 +1,10 @@ import * as mockWrapper from '../mockWrapper'; + jest.mock('../../src/js/wrapper', () => mockWrapper); jest.mock('../../src/js/utils/environment'); import { defaultStackParser } from '@sentry/browser'; import type { Integration } from '@sentry/core'; - import { ReactNativeClient } from '../../src/js/client'; import { getDefaultIntegrations } from '../../src/js/integrations/default'; import type { ReactNativeClientOptions } from '../../src/js/options'; diff --git a/packages/core/test/integrations/logEnricherIntegration.test.ts b/packages/core/test/integrations/logEnricherIntegration.test.ts new file mode 100644 index 0000000000..c15e39c2ab --- /dev/null +++ b/packages/core/test/integrations/logEnricherIntegration.test.ts @@ -0,0 +1,407 @@ +import type { Client, Log } from '@sentry/core'; +import { debug } from '@sentry/core'; +import { logEnricherIntegration } from '../../src/js/integrations/logEnricherIntegration'; +import type { NativeDeviceContextsResponse } from '../../src/js/NativeRNSentry'; +import { NATIVE } from '../../src/js/wrapper'; + +// Mock the NATIVE wrapper +jest.mock('../../src/js/wrapper'); +jest.mock('@sentry/core', () => ({ + ...jest.requireActual('@sentry/core'), + debug: { + log: jest.fn(), + }, +})); + +const mockLogger = debug as jest.Mocked; + +function on_beforeCaptureLogCount(client: jest.Mocked) { + const beforeCaptureLogCalls = client.on.mock.calls.filter( + ([eventName, _]) => eventName.toString() === 'beforeCaptureLog', + ); + + return beforeCaptureLogCalls.length; +} + +describe('LogEnricher Integration', () => { + let mockClient: jest.Mocked; + let mockOn: jest.Mock; + let mockFetchNativeLogAttributes: jest.Mock; + + const triggerAfterInit = () => { + const afterInitCallback = mockOn.mock.calls.find(call => call[0] === 'afterInit')?.[1] as (() => void) | undefined; + expect(afterInitCallback).toBeDefined(); + afterInitCallback!(); + }; + + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + + mockOn = jest.fn(); + mockFetchNativeLogAttributes = jest.fn(); + + mockClient = { + on: mockOn, + } as unknown as jest.Mocked; + + (NATIVE as jest.Mocked).fetchNativeLogAttributes = mockFetchNativeLogAttributes; + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + describe('setup', () => { + it('should set up the integration and register beforeCaptureLog handler after afterInit event', async () => { + const integration = logEnricherIntegration(); + + // Mock successful native response + const mockNativeResponse: NativeDeviceContextsResponse = { + contexts: { + device: { + brand: 'Apple', + model: 'iPhone 14', + family: 'iPhone', + } as Record, + os: { + name: 'iOS', + version: '16.0', + } as Record, + release: '1.0.0' as unknown as Record, + }, + }; + + mockFetchNativeLogAttributes.mockResolvedValue(mockNativeResponse); + + integration.setup(mockClient); + + // Initially, only afterInit handler should be registered + expect(mockOn).toHaveBeenCalledWith('afterInit', expect.any(Function)); + expect(mockOn).toHaveBeenCalledTimes(1); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + expect(mockOn).toHaveBeenCalledWith('beforeCaptureLog', expect.any(Function)); + expect(mockFetchNativeLogAttributes).toHaveBeenCalledTimes(1); + }); + + it('should handle native fetch failure gracefully', async () => { + const integration = logEnricherIntegration(); + + const errorMessage = 'Native fetch failed'; + mockFetchNativeLogAttributes.mockRejectedValue(new Error(errorMessage)); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + expect(mockLogger.log).toHaveBeenCalledWith( + expect.stringContaining('[LOGS]: Failed to prepare attributes from Native Layer'), + ); + expect(mockOn).toHaveBeenCalledTimes(1); + }); + + it('should handle null response from native layer', async () => { + const integration = logEnricherIntegration(); + + mockFetchNativeLogAttributes.mockResolvedValue(null); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + expect(mockOn).toHaveBeenCalledWith('beforeCaptureLog', expect.any(Function)); + + expect(on_beforeCaptureLogCount(mockClient)).toBe(1); + }); + }); + + describe('log processing', () => { + let logHandler: (log: Log) => void; + let mockLog: Log; + + beforeEach(async () => { + const integration = logEnricherIntegration(); + + const mockNativeResponse: NativeDeviceContextsResponse = { + contexts: { + device: { + brand: 'Apple', + model: 'iPhone 14', + family: 'iPhone', + } as Record, + os: { + name: 'iOS', + version: '16.0', + } as Record, + release: '1.0.0' as unknown as Record, + }, + }; + + mockFetchNativeLogAttributes.mockResolvedValue(mockNativeResponse); + + integration.setup(mockClient); + + // Simulate the afterInit event + triggerAfterInit(); + + // Wait for the async operations to complete + await jest.runAllTimersAsync(); + + // Extract the log handler + const beforeCaptureLogCall = mockOn.mock.calls.find(call => call[0] === 'beforeCaptureLog'); + expect(beforeCaptureLogCall).toBeDefined(); + logHandler = beforeCaptureLogCall![1] as (log: Log) => void; + + mockLog = { + message: 'Test log message', + level: 'info', + attributes: {}, + }; + }); + + it('should enrich log with device attributes', () => { + logHandler(mockLog); + + expect(mockLog.attributes).toEqual({ + 'device.brand': 'Apple', + 'device.model': 'iPhone 14', + 'device.family': 'iPhone', + 'os.name': 'iOS', + 'os.version': '16.0', + 'sentry.release': '1.0.0', + }); + }); + + it('should preserve existing log attributes', () => { + mockLog.attributes = { + existing: 'value', + 'custom.attr': 'custom-value', + }; + + logHandler(mockLog); + + expect(mockLog.attributes).toEqual({ + existing: 'value', + 'custom.attr': 'custom-value', + 'device.brand': 'Apple', + 'device.model': 'iPhone 14', + 'device.family': 'iPhone', + 'os.name': 'iOS', + 'os.version': '16.0', + 'sentry.release': '1.0.0', + }); + }); + + it('should handle log without attributes', () => { + mockLog.attributes = undefined; + + logHandler(mockLog); + + expect(mockLog.attributes).toEqual({ + 'device.brand': 'Apple', + 'device.model': 'iPhone 14', + 'device.family': 'iPhone', + 'os.name': 'iOS', + 'os.version': '16.0', + 'sentry.release': '1.0.0', + }); + }); + + it('should only add attributes that exist in cache', async () => { + const integration = logEnricherIntegration(); + + const partialNativeResponse: NativeDeviceContextsResponse = { + contexts: { + device: { + brand: 'Apple', + // model and family missing + } as Record, + os: { + name: 'iOS', + // version missing + } as Record, + // release missing + }, + }; + + mockFetchNativeLogAttributes.mockResolvedValue(partialNativeResponse); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + const beforeCaptureLogCall = mockOn.mock.calls.find(call => call[0] === 'beforeCaptureLog'); + expect(beforeCaptureLogCall).toBeDefined(); + const newLogHandler = beforeCaptureLogCall![1] as (log: Log) => void; + + newLogHandler(mockLog); + + expect(mockLog.attributes).toEqual({ + 'device.brand': 'Apple', + 'os.name': 'iOS', + }); + }); + + it('should not register beforeCaptureLog handler when native fetch fails', async () => { + const integration = logEnricherIntegration(); + + mockFetchNativeLogAttributes.mockRejectedValue(new Error('Failed')); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + expect(mockLogger.log).toHaveBeenCalledWith( + expect.stringContaining('[LOGS]: Failed to prepare attributes from Native Layer'), + ); + + // Default client count. + expect(on_beforeCaptureLogCount(mockClient)).toBe(1); + }); + + it('should handle empty contexts in native response', async () => { + const integration = logEnricherIntegration(); + + const emptyNativeResponse: NativeDeviceContextsResponse = { + contexts: {}, + }; + + mockFetchNativeLogAttributes.mockResolvedValue(emptyNativeResponse); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + const beforeCaptureLogCall = mockOn.mock.calls.find(call => call[0] === 'beforeCaptureLog'); + expect(beforeCaptureLogCall).toBeDefined(); + const emptyLogHandler = beforeCaptureLogCall![1] as (log: Log) => void; + + emptyLogHandler(mockLog); + + expect(mockLog.attributes).toEqual({}); + + expect(on_beforeCaptureLogCount(mockClient)).toBe(2); + }); + + it('should handle partial device context', async () => { + const integration = logEnricherIntegration(); + + const partialDeviceResponse: NativeDeviceContextsResponse = { + contexts: { + device: { + brand: 'Samsung', + model: 'Galaxy S21', + // family missing + } as Record, + }, + }; + + mockFetchNativeLogAttributes.mockResolvedValue(partialDeviceResponse); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + const beforeCaptureLogCall = mockOn.mock.calls.find(call => call[0] === 'beforeCaptureLog'); + expect(beforeCaptureLogCall).toBeDefined(); + const partialLogHandler = beforeCaptureLogCall![1] as (log: Log) => void; + + partialLogHandler(mockLog); + + expect(mockLog.attributes).toEqual({ + 'device.brand': 'Samsung', + 'device.model': 'Galaxy S21', + }); + + expect(on_beforeCaptureLogCount(mockClient)).toBe(2); + }); + + it('should handle partial OS context', async () => { + const integration = logEnricherIntegration(); + + const partialOsResponse: NativeDeviceContextsResponse = { + contexts: { + os: { + name: 'Android', + // version missing + } as Record, + }, + }; + + mockFetchNativeLogAttributes.mockResolvedValue(partialOsResponse); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + const beforeCaptureLogCall = mockOn.mock.calls.find(call => call[0] === 'beforeCaptureLog'); + expect(beforeCaptureLogCall).toBeDefined(); + const partialLogHandler = beforeCaptureLogCall![1] as (log: Log) => void; + + partialLogHandler(mockLog); + + expect(mockLog.attributes).toEqual({ + 'os.name': 'Android', + }); + + expect(on_beforeCaptureLogCount(mockClient)).toBe(2); + }); + }); + + describe('error handling', () => { + it('should handle errors', async () => { + const integration = logEnricherIntegration(); + + mockFetchNativeLogAttributes.mockRejectedValue(new Error('Failed to Initialize')); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + expect(mockLogger.log).toHaveBeenCalledWith( + expect.stringContaining('[LOGS]: Failed to prepare attributes from Native Layer'), + ); + expect(mockLogger.log).toHaveBeenCalledWith(expect.stringContaining('Failed to Initialize')); + + expect(on_beforeCaptureLogCount(mockClient)).toBe(0); + }); + + it('should handle malformed native response', async () => { + const integration = logEnricherIntegration(); + + const malformedResponse = { + someUnexpectedKey: 'value', + }; + + mockFetchNativeLogAttributes.mockResolvedValue(malformedResponse as NativeDeviceContextsResponse); + + integration.setup(mockClient); + + triggerAfterInit(); + + await jest.runAllTimersAsync(); + + expect(mockOn).toHaveBeenCalledWith('beforeCaptureLog', expect.any(Function)); + }); + }); +}); diff --git a/packages/core/test/integrations/modulesloader.test.ts b/packages/core/test/integrations/modulesloader.test.ts index c6fb43bb2f..50b99e66b3 100644 --- a/packages/core/test/integrations/modulesloader.test.ts +++ b/packages/core/test/integrations/modulesloader.test.ts @@ -1,5 +1,4 @@ import type { Client, Event, EventHint } from '@sentry/core'; - import { modulesLoaderIntegration } from '../../src/js/integrations/modulesloader'; import { NATIVE } from '../../src/js/wrapper'; diff --git a/packages/core/test/integrations/nativelinkederrors.test.ts b/packages/core/test/integrations/nativelinkederrors.test.ts index 9dd1a2da1b..60d0b1e6ff 100644 --- a/packages/core/test/integrations/nativelinkederrors.test.ts +++ b/packages/core/test/integrations/nativelinkederrors.test.ts @@ -1,6 +1,5 @@ import { defaultStackParser } from '@sentry/browser'; import type { Client, DebugImage, Event, EventHint, ExtendedError } from '@sentry/core'; - import { nativeLinkedErrorsIntegration } from '../../src/js/integrations/nativelinkederrors'; import type { NativeStackFrames } from '../../src/js/NativeRNSentry'; import { NATIVE } from '../../src/js/wrapper'; diff --git a/packages/core/test/integrations/primitiveTagIntegration.test.ts b/packages/core/test/integrations/primitiveTagIntegration.test.ts new file mode 100644 index 0000000000..e77a9aacd8 --- /dev/null +++ b/packages/core/test/integrations/primitiveTagIntegration.test.ts @@ -0,0 +1,95 @@ +import type { Client } from '@sentry/core'; +import { primitiveTagIntegration } from '../../src/js/integrations/primitiveTagIntegration'; +import { NATIVE } from '../../src/js/wrapper'; +import { setupTestClient } from '../mocks/client'; + +describe('primitiveTagIntegration', () => { + beforeEach(() => { + jest.clearAllMocks(); + setupTestClient(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('integration setup', () => { + it('sets up beforeSendEvent handler', () => { + const integration = primitiveTagIntegration(); + const mockClient = { + on: jest.fn(), + } as any; + + integration.setup!(mockClient); + + expect(mockClient.on).toHaveBeenCalledWith('beforeSendEvent', expect.any(Function)); + }); + }); + + describe('beforeSendEvent processing', () => { + let beforeSendEventHandler: (event: any) => void; + + beforeEach(() => { + const integration = primitiveTagIntegration(); + const mockClient = { + on: jest.fn((eventName, handler) => { + if (eventName === 'beforeSendEvent') { + beforeSendEventHandler = handler; + } + }), + } as any; + + integration.setup!(mockClient); + }); + + it('handles events without tags', () => { + const event = { message: 'test' }; + + expect(() => beforeSendEventHandler(event)).not.toThrow(); + expect(event).toEqual({ message: 'test' }); + }); + + it('handles events with empty tags object', () => { + const event = { tags: {} }; + + expect(() => beforeSendEventHandler(event)).not.toThrow(); + expect(event.tags).toEqual({}); + }); + + it('handles events with null tags', () => { + const event = { tags: null }; + + expect(() => beforeSendEventHandler(event)).not.toThrow(); + expect(event.tags).toBeNull(); + }); + }); + + describe('integration with native processor', () => { + it('sets primitiveProcessor to PrimitiveToString function', () => { + const integration = primitiveTagIntegration(); + NATIVE.enableNative = true; + jest.spyOn(NATIVE, '_setPrimitiveProcessor'); + + integration.afterAllSetup!({ getOptions: () => ({}) } as Client); + + expect(NATIVE._setPrimitiveProcessor).toHaveBeenCalledWith(expect.any(Function)); + + // Verify the function passed is PrimitiveToString + const passedFunction = (NATIVE._setPrimitiveProcessor as jest.Mock).mock.calls[0][0]; + expect(passedFunction(true)).toBe('True'); + expect(passedFunction(false)).toBe('False'); + expect(passedFunction(null)).toBe(''); + expect(passedFunction(42)).toBe('42'); + }); + + it('does not set processor when native is disabled', () => { + const integration = primitiveTagIntegration(); + NATIVE.enableNative = false; + jest.spyOn(NATIVE, '_setPrimitiveProcessor'); + + integration.afterAllSetup!({ getOptions: () => ({}) } as Client); + + expect(NATIVE._setPrimitiveProcessor).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts index 076008fc61..db5a8867f8 100644 --- a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts +++ b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts @@ -3,7 +3,6 @@ jest.mock('../../src/js/utils/environment'); import type { SeverityLevel } from '@sentry/core'; import { addGlobalUnhandledRejectionInstrumentationHandler, captureException, setCurrentClient } from '@sentry/core'; - import { reactNativeErrorHandlersIntegration } from '../../src/js/integrations/reactnativeerrorhandlers'; import { checkPromiseAndWarn, @@ -14,6 +13,8 @@ import { isHermesEnabled, isWeb } from '../../src/js/utils/environment'; import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; +jest.mock('../../src/js/integrations/reactnativeerrorhandlersutils'); + let errorHandlerCallback: ((error: Error, isFatal?: boolean) => Promise) | null = null; jest.mock('../../src/js/utils/worldwide', () => { @@ -87,50 +88,50 @@ describe('ReactNativeErrorHandlers', () => { describe('onError', () => { test('Sets up the global error handler', () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); - expect(RN_GLOBAL_OBJ.ErrorUtils.setGlobalHandler).toHaveBeenCalled(); + expect(RN_GLOBAL_OBJ.ErrorUtils!.setGlobalHandler).toHaveBeenCalled(); }); test('Sets handled:false on a fatal error', async () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); expect(errorHandlerCallback).not.toBeNull(); - await errorHandlerCallback(new Error('Test Error'), true); + await errorHandlerCallback!(new Error('Test Error'), true); await client.flush(); const event = client.event; expect(event?.level).toBe('fatal' as SeverityLevel); - expect(event?.exception?.values?.[0].mechanism?.handled).toBe(false); - expect(event?.exception?.values?.[0].mechanism?.type).toBe('onerror'); + expect(event?.exception?.values?.[0]?.mechanism?.handled).toBe(false); + expect(event?.exception?.values?.[0]?.mechanism?.type).toBe('onerror'); }); test('Does not set handled:false on a non-fatal error', async () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); expect(errorHandlerCallback).not.toBeNull(); - await errorHandlerCallback(new Error('Test Error'), false); + await errorHandlerCallback!(new Error('Test Error'), false); await client.flush(); const event = client.event; expect(event?.level).toBe('error' as SeverityLevel); - expect(event?.exception?.values?.[0].mechanism?.handled).toBe(true); - expect(event?.exception?.values?.[0].mechanism?.type).toBe('generic'); + expect(event?.exception?.values?.[0]?.mechanism?.handled).toBe(true); + expect(event?.exception?.values?.[0]?.mechanism?.type).toBe('generic'); }); test('Includes original exception in hint', async () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); expect(errorHandlerCallback).not.toBeNull(); - await errorHandlerCallback(new Error('Test Error'), false); + await errorHandlerCallback!(new Error('Test Error'), false); await client.flush(); const hint = client.hint; @@ -145,7 +146,7 @@ describe('ReactNativeErrorHandlers', () => { (isHermesEnabled as jest.Mock).mockReturnValue(false); const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); expect(polyfillPromise).toHaveBeenCalled(); expect(mockEnable).toHaveBeenCalledWith( @@ -179,7 +180,7 @@ describe('ReactNativeErrorHandlers', () => { (isHermesEnabled as jest.Mock).mockReturnValue(false); const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); const [options] = mockEnable.mock.calls[0]; const onUnhandledHandler = options.onUnhandled; @@ -208,7 +209,7 @@ describe('ReactNativeErrorHandlers', () => { test('uses native Hermes promise rejection tracking', () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); expect(mockEnablePromiseRejectionTracker).toHaveBeenCalledTimes(1); expect(mockEnablePromiseRejectionTracker).toHaveBeenCalledWith( @@ -224,7 +225,7 @@ describe('ReactNativeErrorHandlers', () => { test('captures unhandled rejection with Hermes tracker', async () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); const [options] = mockEnablePromiseRejectionTracker.mock.calls[0]; const onUnhandledHandler = options.onUnhandled; @@ -254,7 +255,7 @@ describe('ReactNativeErrorHandlers', () => { test('uses addGlobalUnhandledRejectionInstrumentationHandler for React Native Web', () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); expect(addGlobalUnhandledRejectionInstrumentationHandler).toHaveBeenCalledTimes(1); expect(addGlobalUnhandledRejectionInstrumentationHandler).toHaveBeenCalledWith(expect.any(Function)); @@ -266,7 +267,7 @@ describe('ReactNativeErrorHandlers', () => { test('captures unhandled rejection with the callback', () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); const [callback] = (addGlobalUnhandledRejectionInstrumentationHandler as jest.Mock).mock.calls[0]; @@ -287,7 +288,7 @@ describe('ReactNativeErrorHandlers', () => { test('handles non-error rejection with synthetic error', () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); const [callback] = (addGlobalUnhandledRejectionInstrumentationHandler as jest.Mock).mock.calls[0]; @@ -316,7 +317,7 @@ describe('ReactNativeErrorHandlers', () => { test('uses existing polyfill for JSC environments', () => { const integration = reactNativeErrorHandlersIntegration(); - integration.setupOnce(); + integration.setupOnce!(); expect(polyfillPromise).toHaveBeenCalledTimes(1); expect(requireRejectionTracking).toHaveBeenCalledTimes(1); @@ -324,7 +325,7 @@ describe('ReactNativeErrorHandlers', () => { test('respects patchGlobalPromise option', () => { const integration = reactNativeErrorHandlersIntegration({ patchGlobalPromise: false }); - integration.setupOnce(); + integration.setupOnce!(); expect(polyfillPromise).not.toHaveBeenCalled(); expect(requireRejectionTracking).not.toHaveBeenCalled(); diff --git a/packages/core/test/integrations/reactnativeinfo.test.ts b/packages/core/test/integrations/reactnativeinfo.test.ts index f7f9f3bfb9..2a6ab284be 100644 --- a/packages/core/test/integrations/reactnativeinfo.test.ts +++ b/packages/core/test/integrations/reactnativeinfo.test.ts @@ -1,5 +1,4 @@ import type { Client, Event, EventHint } from '@sentry/core'; - import type { ReactNativeError } from '../../src/js/integrations/debugsymbolicator'; import type { ReactNativeContext } from '../../src/js/integrations/reactnativeinfo'; import { reactNativeInfoIntegration } from '../../src/js/integrations/reactnativeinfo'; @@ -61,7 +60,7 @@ describe('React Native Info', () => { }, }, tags: { - hermes: 'true', + hermes: true, }, }); }); @@ -72,7 +71,7 @@ describe('React Native Info', () => { const actualEvent = await executeIntegrationFor({}, {}); expectMocksToBeCalledOnce(); - expect(actualEvent?.tags?.hermes).toEqual('true'); + expect(actualEvent?.tags?.hermes).toBeTrue(); expect(actualEvent?.contexts?.react_native_context).toEqual( expect.objectContaining({ js_engine: 'hermes', diff --git a/packages/core/test/integrations/release.test.ts b/packages/core/test/integrations/release.test.ts index be3a4f1b10..6a282f7d9b 100644 --- a/packages/core/test/integrations/release.test.ts +++ b/packages/core/test/integrations/release.test.ts @@ -1,5 +1,4 @@ import type { Client } from '@sentry/core'; - import { nativeReleaseIntegration } from '../../src/js/integrations/release'; jest.mock('../../src/js/wrapper', () => ({ diff --git a/packages/core/test/integrations/rewriteframes.test.ts b/packages/core/test/integrations/rewriteframes.test.ts index 45eb9f094c..9230583513 100644 --- a/packages/core/test/integrations/rewriteframes.test.ts +++ b/packages/core/test/integrations/rewriteframes.test.ts @@ -2,7 +2,6 @@ import type { Exception } from '@sentry/browser'; import { defaultStackParser, eventFromException } from '@sentry/browser'; import type { Client, Event, EventHint } from '@sentry/core'; import { Platform } from 'react-native'; - import { createReactNativeRewriteFrames } from '../../src/js/integrations/rewriteframes'; import { isExpo, isHermesEnabled } from '../../src/js/utils/environment'; import { mockFunction } from '../testutils'; diff --git a/packages/core/test/integrations/sdkinfo.test.ts b/packages/core/test/integrations/sdkinfo.test.ts index ae41ea6893..b166491834 100644 --- a/packages/core/test/integrations/sdkinfo.test.ts +++ b/packages/core/test/integrations/sdkinfo.test.ts @@ -1,5 +1,4 @@ -import type { Client, Event, EventHint, Package } from '@sentry/core'; - +import type { Event, EventHint, Package } from '@sentry/core'; import { SDK_NAME, SDK_VERSION } from '../../src/js'; import { sdkInfoIntegration } from '../../src/js/integrations/sdkinfo'; import { NATIVE } from '../../src/js/wrapper'; @@ -86,93 +85,9 @@ describe('Sdk Info', () => { expect(processedEvent?.sdk?.name).toEqual(SDK_NAME); expect(processedEvent?.sdk?.version).toEqual(SDK_VERSION); }); - - it('Add none setting when defaultIp is undefined', async () => { - mockedFetchNativeSdkInfo = jest.fn().mockResolvedValue(null); - const mockEvent: Event = {}; - const processedEvent = await processEvent(mockEvent, {}, undefined); - - expect(processedEvent?.sdk?.name).toEqual(SDK_NAME); - expect(processedEvent?.sdk?.version).toEqual(SDK_VERSION); - // @ts-expect-error injected type. - expect(processedEvent?.sdk?.settings?.infer_ip).toEqual('never'); - }); - - it('Add none setting when defaultIp is false', async () => { - mockedFetchNativeSdkInfo = jest.fn().mockResolvedValue(null); - const mockEvent: Event = {}; - const processedEvent = await processEvent(mockEvent, {}, false); - - expect(processedEvent?.sdk?.name).toEqual(SDK_NAME); - expect(processedEvent?.sdk?.version).toEqual(SDK_VERSION); - // @ts-expect-error injected type. - expect(processedEvent?.sdk?.settings?.infer_ip).toEqual('never'); - }); - - it('Add auto setting when defaultIp is true', async () => { - mockedFetchNativeSdkInfo = jest.fn().mockResolvedValue(null); - const mockEvent: Event = {}; - const processedEvent = await processEvent(mockEvent, {}, true); - - expect(processedEvent?.sdk?.name).toEqual(SDK_NAME); - expect(processedEvent?.sdk?.version).toEqual(SDK_VERSION); - // @ts-expect-error injected type. - expect(processedEvent?.sdk?.settings?.infer_ip).toEqual('auto'); - }); - - it('removes ip_address if it is "{{auto}}"', () => { - const mockHandler = jest.fn(); - - const client = { - getOptions: () => ({ sendDefaultPii: true }), - on: (eventName: string, cb: (event: any) => void) => { - if (eventName === 'beforeSendEvent') { - mockHandler.mockImplementation(cb); - } - }, - }; - - sdkInfoIntegration().setup!(client as any); - - const testEvent = { user: { ip_address: '{{auto}}' } }; - mockHandler(testEvent); - - expect(testEvent.user.ip_address).toBeUndefined(); - }); - - it('keeps ip_address if it is not "{{auto}}"', () => { - const mockHandler = jest.fn(); - - const client = { - getOptions: () => ({ sendDefaultPii: true }), - on: (eventName: string, cb: (event: any) => void) => { - if (eventName === 'beforeSendEvent') { - mockHandler.mockImplementation(cb); - } - }, - }; - - sdkInfoIntegration().setup!(client as any); - - const testEvent = { user: { ip_address: '1.2.3.4' } }; - mockHandler(testEvent); - - expect(testEvent.user.ip_address).toBe('1.2.3.4'); - }); }); -function processEvent( - mockedEvent: Event, - mockedHint: EventHint = {}, - sendDefaultPii?: boolean, -): Event | null | PromiseLike { +function processEvent(mockedEvent: Event, mockedHint: EventHint = {}): Event | null | PromiseLike { const integration = sdkInfoIntegration(); - if (sendDefaultPii != null) { - const mockClient: jest.Mocked = { - getOptions: jest.fn().mockReturnValue({ sendDefaultPii: sendDefaultPii }), - on: jest.fn(), - } as any; - integration.setup!(mockClient); - } return integration.processEvent!(mockedEvent, mockedHint, {} as any); } diff --git a/packages/core/test/integrations/spotlight.test.ts b/packages/core/test/integrations/spotlight.test.ts index dba566f260..d65f2c4385 100644 --- a/packages/core/test/integrations/spotlight.test.ts +++ b/packages/core/test/integrations/spotlight.test.ts @@ -1,10 +1,10 @@ import type { HttpRequestEventMap } from '@mswjs/interceptors'; import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest'; import type { Client, Envelope } from '@sentry/core'; - import { spotlightIntegration } from '../../src/js/integrations/spotlight'; globalThis.XMLHttpRequest = require('xmlhttprequest').XMLHttpRequest; + const requestListener = jest.fn(); const interceptor = new XMLHttpRequestInterceptor(); interceptor.on('request', requestListener); diff --git a/packages/core/test/integrations/viewhierarchy.test.ts b/packages/core/test/integrations/viewhierarchy.test.ts index 69020ce8ac..574220d9c3 100644 --- a/packages/core/test/integrations/viewhierarchy.test.ts +++ b/packages/core/test/integrations/viewhierarchy.test.ts @@ -1,5 +1,4 @@ import type { Client, Event, EventHint } from '@sentry/core'; - import { viewHierarchyIntegration } from '../../src/js/integrations/viewhierarchy'; import { NATIVE } from '../../src/js/wrapper'; diff --git a/packages/core/test/mockWrapper.ts b/packages/core/test/mockWrapper.ts index c26c384e26..83d15681b5 100644 --- a/packages/core/test/mockWrapper.ts +++ b/packages/core/test/mockWrapper.ts @@ -16,6 +16,7 @@ const NATIVE: MockInterface = { _processLevel: jest.fn(), _serializeObject: jest.fn(), _isModuleLoaded: jest.fn(), + _setPrimitiveProcessor: jest.fn(), isNativeAvailable: jest.fn(), @@ -63,6 +64,7 @@ const NATIVE: MockInterface = { popTimeToDisplayFor: jest.fn(), setActiveSpanId: jest.fn(), encodeToBase64: jest.fn(), + primitiveProcessor: jest.fn(), }; NATIVE.isNativeAvailable.mockReturnValue(true); @@ -89,7 +91,7 @@ NATIVE.getCurrentReplayId.mockReturnValue(null); NATIVE.crashedLastRun.mockResolvedValue(false); NATIVE.popTimeToDisplayFor.mockResolvedValue(null); NATIVE.getNewScreenTimeToDisplay.mockResolvedValue(null); - +NATIVE.primitiveProcessor.mockReturnValue(''); export const getRNSentryModule = jest.fn(); export { NATIVE }; diff --git a/packages/core/test/mocks/client.ts b/packages/core/test/mocks/client.ts index 1a18e94365..5dd3c56b25 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -1,6 +1,6 @@ import type { Event, EventHint, Integration, Outcome, ParameterizedString, Session, SeverityLevel } from '@sentry/core'; import { - BaseClient, + Client, createTransport, getCurrentScope, getGlobalScope, @@ -9,7 +9,6 @@ import { resolvedSyncPromise, setCurrentClient, } from '@sentry/core'; - import type { ReactNativeClientOptions } from '../../src/js/options'; export function getDefaultTestClientOptions(options: Partial = {}): TestClientOptions { @@ -37,7 +36,7 @@ export interface TestClientOptions extends ReactNativeClientOptions { defaultIntegrations?: Integration[] | false; } -export class TestClient extends BaseClient { +export class TestClient extends Client { public static instance?: TestClient; public static sendEventCalled?: (event: Event) => void; @@ -88,7 +87,7 @@ export class TestClient extends BaseClient { super.sendEvent(event, hint); return; } - TestClient.sendEventCalled && TestClient.sendEventCalled(event); + TestClient.sendEventCalled?.(event); } public sendSession(session: Session): void { diff --git a/packages/core/test/playground/modal.test.tsx b/packages/core/test/playground/modal.test.tsx index 29aab1b945..4d1fd9198b 100644 --- a/packages/core/test/playground/modal.test.tsx +++ b/packages/core/test/playground/modal.test.tsx @@ -1,6 +1,5 @@ import { render } from '@testing-library/react-native'; import * as React from 'react'; - import { SentryPlayground } from '../../src/js/playground/modal'; describe('PlaygroundComponent', () => { diff --git a/packages/core/test/profiling/convertHermesProfile.test.ts b/packages/core/test/profiling/convertHermesProfile.test.ts index 807d662fcd..569f6778fd 100644 --- a/packages/core/test/profiling/convertHermesProfile.test.ts +++ b/packages/core/test/profiling/convertHermesProfile.test.ts @@ -1,5 +1,4 @@ import type { ThreadCpuSample } from '@sentry/core'; - import { convertToSentryProfile, mapSamples } from '../../src/js/profiling/convertHermesProfile'; import type * as Hermes from '../../src/js/profiling/hermes'; import type { RawThreadCpuProfile } from '../../src/js/profiling/types'; diff --git a/packages/core/test/profiling/hermes.test.ts b/packages/core/test/profiling/hermes.test.ts index 21255b95ff..f2c8d64a9c 100644 --- a/packages/core/test/profiling/hermes.test.ts +++ b/packages/core/test/profiling/hermes.test.ts @@ -1,5 +1,4 @@ import type { ThreadCpuFrame } from '@sentry/core'; - import { parseHermesJSStackFrame } from '../../src/js/profiling/convertHermesProfile'; describe('hermes', () => { diff --git a/packages/core/test/profiling/integration.test.ts b/packages/core/test/profiling/integration.test.ts index 43a7d3cc75..bd904136fb 100644 --- a/packages/core/test/profiling/integration.test.ts +++ b/packages/core/test/profiling/integration.test.ts @@ -1,11 +1,11 @@ import * as mockWrapper from '../mockWrapper'; + jest.mock('../../src/js/wrapper', () => mockWrapper); jest.mock('../../src/js/utils/environment'); jest.mock('../../src/js/profiling/debugid'); import type { Envelope, Event, Integration, Profile, Span, ThreadCpuProfile, Transport } from '@sentry/core'; import { getClient, spanToJSON } from '@sentry/core'; - import * as Sentry from '../../src/js'; import { getDebugMetadata } from '../../src/js/profiling/debugid'; import type { HermesProfilingOptions } from '../../src/js/profiling/integration'; @@ -42,7 +42,10 @@ describe('profiling integration', () => { type: 'sourcemap', }, ]); - jest.useFakeTimers(); + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); }); afterEach(async () => { @@ -380,7 +383,7 @@ function initTestClient( const transportSendMock = jest.fn, Parameters>(); const options: Sentry.ReactNativeOptions = { dsn: MOCK_DSN, - enableTracing: true, + tracesSampleRate: 1.0, enableNativeFramesTracking: false, profilesSampleRate: 1, integrations: integrations => { @@ -466,5 +469,5 @@ function addIntegrationAndForceSetupOnce(integration: Integration): void { } client.addIntegration(integration); - integration.setupOnce && integration.setupOnce(); + integration.setupOnce?.(); } diff --git a/packages/core/test/replay/browserReplay.test.ts b/packages/core/test/replay/browserReplay.test.ts new file mode 100644 index 0000000000..7914c08f62 --- /dev/null +++ b/packages/core/test/replay/browserReplay.test.ts @@ -0,0 +1,23 @@ +import { describe, test } from '@jest/globals'; +import * as SentryReact from '@sentry/react'; +import { spyOn } from 'jest-mock'; +import { browserReplayIntegration } from '../../src/js/replay/browserReplay'; +import * as environment from '../../src/js/utils/environment'; + +describe('Browser Replay', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + test('should not call replayIntegration if not web', () => { + spyOn(environment, 'notWeb').mockReturnValue(true); + spyOn(SentryReact, 'replayIntegration').mockImplementation(() => { + throw new Error('replayIntegration should not be called'); + }); + + const integration = browserReplayIntegration(); + + expect(integration).toBeDefined(); + expect(SentryReact.replayIntegration).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/core/test/replay/xhrUtils.test.ts b/packages/core/test/replay/xhrUtils.test.ts index 477e8bc661..f348875dc7 100644 --- a/packages/core/test/replay/xhrUtils.test.ts +++ b/packages/core/test/replay/xhrUtils.test.ts @@ -1,5 +1,4 @@ import type { Breadcrumb } from '@sentry/core'; - import { enrichXhrBreadcrumbsForMobileReplay } from '../../src/js/replay/xhrUtils'; describe('xhrUtils', () => { diff --git a/packages/core/test/scopeSync.test.ts b/packages/core/test/scopeSync.test.ts index 85d14fe27c..29c6efae29 100644 --- a/packages/core/test/scopeSync.test.ts +++ b/packages/core/test/scopeSync.test.ts @@ -1,10 +1,11 @@ -jest.mock('../src/js/wrapper', () => jest.requireActual('./mockWrapper')); import type { Breadcrumb } from '@sentry/core'; import * as SentryCore from '@sentry/core'; import { Scope } from '@sentry/core'; - import { enableSyncToNative } from '../src/js/scopeSync'; import { getDefaultTestClientOptions, TestClient } from './mocks/client'; + +jest.mock('../src/js/wrapper', () => jest.requireActual('./mockWrapper')); + import { NATIVE } from './mockWrapper'; jest.mock('../src/js/wrapper'); @@ -134,7 +135,6 @@ describe('ScopeSync', () => { it('setUser', () => { expect(SentryCore.getIsolationScope().setUser).not.toBe(setUserScopeSpy); - const user = { id: '123' }; SentryCore.setUser(user); expect(NATIVE.setUser).toHaveBeenCalledExactlyOnceWith({ id: '123' }); @@ -142,6 +142,7 @@ describe('ScopeSync', () => { }); it('setTag', () => { + jest.spyOn(NATIVE, 'primitiveProcessor').mockImplementation((value: SentryCore.Primitive) => value as string); expect(SentryCore.getIsolationScope().setTag).not.toBe(setTagScopeSpy); SentryCore.setTag('key', 'value'); @@ -150,6 +151,7 @@ describe('ScopeSync', () => { }); it('setTags', () => { + jest.spyOn(NATIVE, 'primitiveProcessor').mockImplementation((value: SentryCore.Primitive) => value as string); expect(SentryCore.getIsolationScope().setTags).not.toBe(setTagsScopeSpy); SentryCore.setTags({ key: 'value', second: 'bar' }); diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index 85b4b7e030..0c48cad447 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -1,17 +1,17 @@ import type { BaseTransportOptions, Breadcrumb, BreadcrumbHint, ClientOptions, Integration, Scope } from '@sentry/core'; -import { initAndBind, logger } from '@sentry/core'; +import { debug, initAndBind } from '@sentry/core'; import { makeFetchTransport } from '@sentry/react'; - import { getDevServer } from '../src/js/integrations/debugsymbolicatorutils'; import { init, withScope } from '../src/js/sdk'; import type { ReactNativeTracingIntegration } from '../src/js/tracing'; import { REACT_NATIVE_TRACING_INTEGRATION_NAME, reactNativeTracingIntegration } from '../src/js/tracing'; import { makeNativeTransport } from '../src/js/transports/native'; import { getDefaultEnvironment, isExpoGo, notWeb } from '../src/js/utils/environment'; +import { RN_GLOBAL_OBJ } from '../src/js/utils/worldwide'; import { NATIVE } from './mockWrapper'; import { firstArg, secondArg } from './testutils'; -jest.spyOn(logger, 'error'); +jest.spyOn(debug, 'error'); jest.mock('../src/js/wrapper', () => jest.requireActual('./mockWrapper')); jest.mock('../src/js/utils/environment'); jest.mock('@sentry/core', () => ({ @@ -140,6 +140,45 @@ describe('Tests the SDK functionality', () => { }); }); + describe('release', () => { + afterEach(() => { + (notWeb as jest.Mock).mockReset(); + RN_GLOBAL_OBJ.SENTRY_RELEASE = undefined; + }); + + it('uses release from global for web', () => { + RN_GLOBAL_OBJ.SENTRY_RELEASE = { + name: 'test', + version: '1.0.0', + }; + (notWeb as jest.Mock).mockImplementation(() => false); + init({}); + expect(usedOptions()?.release).toEqual('test@1.0.0'); + }); + + it('uses release from options for web', () => { + RN_GLOBAL_OBJ.SENTRY_RELEASE = { + name: 'test', + version: '1.0.0', + }; + (notWeb as jest.Mock).mockImplementation(() => false); + init({ + release: 'custom@2.0.0', + }); + expect(usedOptions()?.release).toEqual('custom@2.0.0'); + }); + + it('uses undefined for others', () => { + RN_GLOBAL_OBJ.SENTRY_RELEASE = { + name: 'test', + version: '1.0.0', + }; + (notWeb as jest.Mock).mockImplementation(() => true); + init({}); + expect(usedOptions()?.release).toBeUndefined(); + }); + }); + describe('transport options buffer size', () => { it('uses default transport options buffer size', () => { init({ @@ -620,6 +659,36 @@ describe('Tests the SDK functionality', () => { expectIntegration('HermesProfiling'); }); + it('adds browserSessionIntegration on web when enableAutoSessionTracking is set true', () => { + (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); + (notWeb as jest.Mock).mockImplementation(() => false); + init({ enableAutoSessionTracking: true }); + + expectIntegration('BrowserSession'); + }); + + it('no browserSessionIntegration on web when enableAutoSessionTracking is set false', () => { + (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); + (notWeb as jest.Mock).mockImplementation(() => false); + init({ enableAutoSessionTracking: false }); + + expectNotIntegration('BrowserSession'); + }); + + it('no browserSessionIntegration on web when enableAutoSessionTracking is not set', () => { + (NATIVE.isNativeAvailable as jest.Mock).mockImplementation(() => false); + (notWeb as jest.Mock).mockImplementation(() => false); + init({}); + + expectNotIntegration('BrowserSession'); + }); + + it('no browserSessionIntegration on mobile', () => { + init({ enableAutoSessionTracking: true }); + + expectNotIntegration('BrowserSession'); + }); + it('no spotlight integration by default', () => { init({}); @@ -649,6 +718,15 @@ describe('Tests the SDK functionality', () => { expectNotIntegration('AppStart'); }); + it('when tracing enabled app start without native (on web, Expo Go) integration is not added', () => { + init({ + tracesSampleRate: 0.5, + enableNative: false, + }); + + expectNotIntegration('AppStart'); + }); + it('no native frames integration by default', () => { init({}); @@ -672,6 +750,15 @@ describe('Tests the SDK functionality', () => { expectNotIntegration('NativeFrames'); }); + it('when tracing enabled (on web, Expo Go) native frames integration is not added', () => { + init({ + tracesSampleRate: 0.5, + enableNative: false, + }); + + expectNotIntegration('NativeFrames'); + }); + it('when tracing not set stall tracking the integration is not added', () => { init({}); diff --git a/packages/core/test/sdk.withclient.test.ts b/packages/core/test/sdk.withclient.test.ts index 693d1817a9..0d7258ef63 100644 --- a/packages/core/test/sdk.withclient.test.ts +++ b/packages/core/test/sdk.withclient.test.ts @@ -1,11 +1,10 @@ -import { logger, setCurrentClient } from '@sentry/core'; - +import { debug, setCurrentClient } from '@sentry/core'; import { crashedLastRun, flush } from '../src/js/sdk'; import { getDefaultTestClientOptions, TestClient } from './mocks/client'; import { NATIVE } from './mockWrapper'; jest.mock('../src/js/wrapper.ts', () => jest.requireActual('./mockWrapper')); -jest.spyOn(logger, 'error'); +jest.spyOn(debug, 'error'); describe('Tests the SDK functionality', () => { let client: TestClient; @@ -33,7 +32,7 @@ describe('Tests the SDK functionality', () => { expect(client.flush).toBeCalled(); expect(flushResult).toBe(false); - expect(logger.error).toBeCalledWith('Failed to flush the event queue.'); + expect(debug.error).toBeCalledWith('Failed to flush the event queue.'); }); }); diff --git a/packages/core/test/tools/collectModules.test.ts b/packages/core/test/tools/collectModules.test.ts index 3b65e3aa7a..16e779cde8 100644 --- a/packages/core/test/tools/collectModules.test.ts +++ b/packages/core/test/tools/collectModules.test.ts @@ -1,9 +1,9 @@ -import { logger } from '@sentry/core'; +import { debug } from '@sentry/core'; import { existsSync, readFileSync, rmdirSync, unlinkSync } from 'fs'; import { dirname } from 'path'; jest.mock('@sentry/core'); -(logger.enable as jest.Mock).mockImplementation(() => {}); +(debug.enable as jest.Mock).mockImplementation(() => {}); import ModulesCollector from '../../src/js/tools/ModulesCollector'; diff --git a/packages/core/test/tools/fixtures/mockBabelTransformer.js b/packages/core/test/tools/fixtures/mockBabelTransformer.js index 17628495a5..707b86771f 100644 --- a/packages/core/test/tools/fixtures/mockBabelTransformer.js +++ b/packages/core/test/tools/fixtures/mockBabelTransformer.js @@ -1,3 +1,7 @@ +var globals = require('@jest/globals'); + +var jest = globals.jest; + module.exports = { transform: jest.fn(), getCacheKey: jest.fn(), diff --git a/packages/core/test/tools/metroMiddleware.test.ts b/packages/core/test/tools/metroMiddleware.test.ts index b1743e27a8..426d3dea69 100644 --- a/packages/core/test/tools/metroMiddleware.test.ts +++ b/packages/core/test/tools/metroMiddleware.test.ts @@ -1,7 +1,6 @@ import { afterEach, beforeEach, describe, expect, it, jest } from '@jest/globals'; import type { StackFrame } from '@sentry/core'; import * as fs from 'fs'; - import * as metroMiddleware from '../../src/js/tools/metroMiddleware'; const { withSentryMiddleware, createSentryMetroMiddleware, stackFramesContextMiddleware } = metroMiddleware; diff --git a/packages/core/test/tools/metroconfig.test.ts b/packages/core/test/tools/metroconfig.test.ts index 2329509754..e4ab6d2529 100644 --- a/packages/core/test/tools/metroconfig.test.ts +++ b/packages/core/test/tools/metroconfig.test.ts @@ -1,7 +1,6 @@ import type { getDefaultConfig } from 'expo/metro-config'; import type { MetroConfig } from 'metro'; import * as process from 'process'; - import { getSentryExpoConfig, withSentryBabelTransformer, @@ -160,7 +159,7 @@ describe('metroconfig', () => { describe.each([ ['new Metro', false, '0.70.0'], ['old Metro', true, '0.67.0'], - ])(`on %s`, (description, oldMetro, metroVersion) => { + ])('on %s', (description, oldMetro, metroVersion) => { beforeEach(() => { jest.resetModules(); // Mock metro/package.json diff --git a/packages/core/test/tools/sentryBabelTransformer.test.ts b/packages/core/test/tools/sentryBabelTransformer.test.ts index 3d6fb99d20..2a7868a5f9 100644 --- a/packages/core/test/tools/sentryBabelTransformer.test.ts +++ b/packages/core/test/tools/sentryBabelTransformer.test.ts @@ -1,5 +1,4 @@ import * as process from 'process'; - import { createSentryBabelTransformer, SENTRY_BABEL_TRANSFORMER_OPTIONS, diff --git a/packages/core/test/tools/sentryMetroSerializer.test.ts b/packages/core/test/tools/sentryMetroSerializer.test.ts index 62e151b4d9..244a2b440f 100644 --- a/packages/core/test/tools/sentryMetroSerializer.test.ts +++ b/packages/core/test/tools/sentryMetroSerializer.test.ts @@ -5,7 +5,6 @@ import CountingSet from 'metro/private/lib/CountingSet'; // eslint-disable-next-line import/no-unresolved import * as countLines from 'metro/private/lib/countLines'; import { minify } from 'uglify-js'; - import { createSentryMetroSerializer } from '../../src/js/tools/sentryMetroSerializer'; import { type MetroSerializer, type VirtualJSOutput, createDebugIdSnippet } from '../../src/js/tools/utils'; diff --git a/packages/core/test/tools/sentryReleaseInjector.test.ts b/packages/core/test/tools/sentryReleaseInjector.test.ts new file mode 100644 index 0000000000..b4c506a70b --- /dev/null +++ b/packages/core/test/tools/sentryReleaseInjector.test.ts @@ -0,0 +1,49 @@ +import type { MixedOutput, Module, ReadOnlyGraph } from 'metro'; +import { unstableReleaseConstantsPlugin } from '../../src/js/tools/sentryReleaseInjector'; + +const mockedExpoConfigRequire = jest.fn(); + +jest.mock('@expo/config', () => ({ + getConfig: mockedExpoConfigRequire, +})); + +describe('Sentry Release Injector', () => { + beforeEach(() => { + mockedExpoConfigRequire.mockReturnValue({ + exp: { + name: 'TestApp', + version: '1.0.0', + }, + }); + }); + + test('unstableReleaseConstantsPlugin returns premodules if not web', () => { + const projectRoot = '/some/project/root'; + const graph = { + transformOptions: { platform: 'ios' }, + } as unknown as ReadOnlyGraph; + const premodules = [{ path: 'someModule.js' }] as Module[]; + + const plugin = unstableReleaseConstantsPlugin(projectRoot); + const result = plugin({ graph, premodules: [...premodules] }); + + expect(result).toEqual(premodules); + }); + + test('unstableReleaseConstantsPlugin returns premodules with Sentry release constants if web', () => { + const projectRoot = '/some/project/root'; + const graph = { + transformOptions: { platform: 'web' }, + } as unknown as ReadOnlyGraph; + const premodules = [{ path: 'someModule.js' }] as Module[]; + + const plugin = unstableReleaseConstantsPlugin(projectRoot); + const result = plugin({ graph, premodules }); + + expect(result.length).toBe(premodules.length + 1); + expect(result[0]?.path).toBe('__sentryReleaseConstants__'); + expect(result[0]?.getSource().toString()).toEqual( + 'var SENTRY_RELEASE;SENTRY_RELEASE={name: "TestApp", version: "1.0.0"};', + ); + }); +}); diff --git a/packages/core/test/tools/utils.test.ts b/packages/core/test/tools/utils.test.ts new file mode 100644 index 0000000000..b752668a03 --- /dev/null +++ b/packages/core/test/tools/utils.test.ts @@ -0,0 +1,100 @@ +import type { Module } from 'metro'; +import * as countLines from 'metro/private/lib/countLines'; +import type { VirtualJSOutput } from '../../src/js/tools/utils'; +import { createSet, getExpoConfig, prependModule } from '../../src/js/tools/utils'; + +const mockedExpoConfigRequire = jest.fn(); + +jest.mock('@expo/config', () => ({ + getConfig: mockedExpoConfigRequire, +})); + +describe('Sentry Metro Tools Utils', () => { + describe('prependModule', () => { + test('module is added to position 0 if no prelude module', () => { + const module = mockModule('testModule.js', 'console.log("test");'); + const preModules = [mockModule('otherModule.js', 'console.log("other");')]; + + const result = prependModule(preModules, module); + + expect(result[0]).toEqual(module); + expect(result[1]).toEqual(preModules[0]); + }); + + test('module is added after prelude', () => { + const module = mockModule('testModule.js', 'console.log("test");'); + const preludeModule = mockModule('__prelude__', 'console.log("prelude");'); + const preModules = [preludeModule, mockModule('otherModule.js', 'console.log("other");')]; + + const result = prependModule(preModules, module); + + expect(result[0]).toEqual(preludeModule); + expect(result[1]).toEqual(module); + expect(result[2]).toEqual(preModules[1]); + }); + }); + + describe('getExpoConfig', () => { + test('returns empty object if @expo/config is not available', () => { + mockedExpoConfigRequire.mockImplementation(() => { + throw new Error('Module not found'); + }); + + const result = getExpoConfig('/some/project/root'); + expect(result).toStrictEqual({}); + }); + + test('returns config with name and version', () => { + mockedExpoConfigRequire.mockReturnValue({ + exp: { + name: 'TestApp', + version: '1.0.0', + }, + }); + + const result = getExpoConfig('/some/project/root'); + expect(result).toEqual({ name: 'TestApp', version: '1.0.0' }); + }); + + test('returns object with undefined(s) if name or version is not a string', () => { + mockedExpoConfigRequire.mockReturnValue({ + exp: { + name: 123, + version: null, + }, + }); + + const result = getExpoConfig('/some/project/root'); + expect(result).toEqual({ + name: undefined, + version: undefined, + }); + }); + + test('returns empty object if getConfig is not available', () => { + mockedExpoConfigRequire.mockReturnValue({}); + + const result = getExpoConfig('/some/project/root'); + expect(result).toEqual({}); + }); + }); +}); + +function mockModule(path: string, code: string): Module { + return { + dependencies: new Map(), + getSource: () => Buffer.from(code), + inverseDependencies: createSet(), + path, + output: [ + { + type: 'js/script/virtual', + data: { + code, + lineCount: countLines(code), + map: [], + }, + }, + ], + }; +} diff --git a/packages/core/test/touchevents.test.tsx b/packages/core/test/touchevents.test.tsx index 4266f62d4b..32c5504386 100644 --- a/packages/core/test/touchevents.test.tsx +++ b/packages/core/test/touchevents.test.tsx @@ -3,7 +3,6 @@ */ import type { SeverityLevel } from '@sentry/core'; import * as core from '@sentry/core'; - import { TouchEventBoundary } from '../src/js/touchevents'; import { getDefaultTestClientOptions, TestClient } from './mocks/client'; diff --git a/packages/core/test/trace.test.ts b/packages/core/test/trace.test.ts index 0f84d36e20..94b81bcee5 100644 --- a/packages/core/test/trace.test.ts +++ b/packages/core/test/trace.test.ts @@ -1,5 +1,4 @@ import { setCurrentClient, spanToJSON, startSpan } from '@sentry/core'; - import { getDefaultTestClientOptions, TestClient } from './mocks/client'; describe('parentSpanIsAlwaysRootSpan', () => { diff --git a/packages/core/test/tracing/addTracingExtensions.test.ts b/packages/core/test/tracing/addTracingExtensions.test.ts index 4d4c5384c3..b413ffd429 100644 --- a/packages/core/test/tracing/addTracingExtensions.test.ts +++ b/packages/core/test/tracing/addTracingExtensions.test.ts @@ -1,5 +1,5 @@ +import type { Span } from '@sentry/core'; import { getCurrentScope, spanToJSON, startSpanManual } from '@sentry/core'; - import { reactNativeTracingIntegration } from '../../src/js'; import { type TestClient, setupTestClient } from '../mocks/client'; @@ -55,9 +55,12 @@ describe('Tracing extensions', () => { }); test('transaction start span passes correct values to the child', async () => { - const transaction = startSpanManual({ name: 'parent', op: 'custom', scope: getCurrentScope() }, span => span); - const span = startSpanManual({ name: 'child', scope: getCurrentScope() }, span => span); - span!.end(); + let childSpan: Span = undefined; + const transaction = startSpanManual({ name: 'parent', op: 'custom', scope: getCurrentScope() }, _span => { + childSpan = startSpanManual({ name: 'child', scope: getCurrentScope() }, __span => __span); + return _span; + }); + childSpan!.end(); transaction!.end(); await client.flush(); @@ -70,9 +73,9 @@ describe('Tracing extensions', () => { }), }), ); - expect(spanToJSON(span!)).toEqual( + expect(spanToJSON(childSpan!)).toEqual( expect.objectContaining({ - parent_span_id: transaction!.spanContext().spanId, + parent_span_id: spanToJSON(transaction!).span_id, }), ); }); diff --git a/packages/core/test/tracing/gesturetracing.test.ts b/packages/core/test/tracing/gesturetracing.test.ts index 10c5ccf39d..d0023bc1cc 100644 --- a/packages/core/test/tracing/gesturetracing.test.ts +++ b/packages/core/test/tracing/gesturetracing.test.ts @@ -1,6 +1,5 @@ import type { Breadcrumb } from '@sentry/core'; import { getActiveSpan, spanToJSON, startSpan } from '@sentry/core'; - import { UI_ACTION } from '../../src/js/tracing'; import { DEFAULT_BREADCRUMB_CATEGORY as DEFAULT_GESTURE_BREADCRUMB_CATEGORY, @@ -55,7 +54,10 @@ describe('GestureTracing', () => { beforeEach(() => { jest.clearAllMocks(); - jest.useFakeTimers(); + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); client = setupTestClient({ enableUserInteractionTracing: true, }); diff --git a/packages/core/test/tracing/idleNavigationSpan.test.ts b/packages/core/test/tracing/idleNavigationSpan.test.ts index 74ca24bb7c..28fc38fcaf 100644 --- a/packages/core/test/tracing/idleNavigationSpan.test.ts +++ b/packages/core/test/tracing/idleNavigationSpan.test.ts @@ -1,7 +1,6 @@ import type { Span } from '@sentry/core'; import { getActiveSpan, getCurrentScope, spanToJSON, startSpanManual } from '@sentry/core'; import type { AppState, AppStateStatus } from 'react-native'; - import type { ScopeWithMaybeSpan } from '../../src/js/tracing/span'; import { SCOPE_SPAN_FIELD, startIdleNavigationSpan } from '../../src/js/tracing/span'; import { NATIVE } from '../../src/js/wrapper'; diff --git a/packages/core/test/tracing/integrations/appStart.test.ts b/packages/core/test/tracing/integrations/appStart.test.ts index 042114d240..9d62708d06 100644 --- a/packages/core/test/tracing/integrations/appStart.test.ts +++ b/packages/core/test/tracing/integrations/appStart.test.ts @@ -8,7 +8,6 @@ import { setCurrentClient, timestampInSeconds, } from '@sentry/core'; - import { APP_START_COLD as APP_START_COLD_MEASUREMENT, APP_START_WARM as APP_START_WARM_MEASUREMENT, @@ -162,7 +161,7 @@ describe('App Start Integration', () => { const actualEvent = await captureStandAloneAppStart(); - const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); const bundleStartSpan = actualEvent!.spans!.find( ({ description }) => description === 'JS Bundle Execution Start', ); @@ -170,7 +169,7 @@ describe('App Start Integration', () => { expect(appStartRootSpan).toEqual( expect.objectContaining(>{ span_id: expect.any(String), - description: 'Cold App Start', + description: 'Cold Start', op: APP_START_COLD_OP, data: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_COLD_OP, @@ -200,7 +199,7 @@ describe('App Start Integration', () => { const actualEvent = await captureStandAloneAppStart(); - const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); const bundleStartSpan = actualEvent!.spans!.find( ({ description }) => description === 'JS Bundle Execution Before React Root', ); @@ -208,7 +207,7 @@ describe('App Start Integration', () => { expect(appStartRootSpan).toEqual( expect.objectContaining(>{ span_id: expect.any(String), - description: 'Cold App Start', + description: 'Cold Start', op: APP_START_COLD_OP, data: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_COLD_OP, @@ -239,13 +238,13 @@ describe('App Start Integration', () => { const actualEvent = await captureStandAloneAppStart(); - const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); const nativeSpan = actualEvent!.spans!.find(({ description }) => description === 'test native app start span'); expect(appStartRootSpan).toEqual( expect.objectContaining(>{ span_id: expect.any(String), - description: 'Cold App Start', + description: 'Cold Start', op: APP_START_COLD_OP, data: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: APP_START_COLD_OP, @@ -483,14 +482,14 @@ describe('App Start Integration', () => { const actualEvent = await processEvent(getMinimalTransactionEvent()); - const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); const bundleStartSpan = actualEvent!.spans!.find( ({ description }) => description === 'JS Bundle Execution Start', ); expect(appStartRootSpan).toEqual( expect.objectContaining(>{ - description: 'Cold App Start', + description: 'Cold Start', span_id: expect.any(String), op: APP_START_COLD_OP, origin: SPAN_ORIGIN_AUTO_APP_START, @@ -523,14 +522,14 @@ describe('App Start Integration', () => { const actualEvent = await processEvent(getMinimalTransactionEvent()); - const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); const bundleStartSpan = actualEvent!.spans!.find( ({ description }) => description === 'JS Bundle Execution Before React Root', ); expect(appStartRootSpan).toEqual( expect.objectContaining(>{ - description: 'Cold App Start', + description: 'Cold Start', span_id: expect.any(String), op: APP_START_COLD_OP, origin: SPAN_ORIGIN_AUTO_APP_START, @@ -563,14 +562,14 @@ describe('App Start Integration', () => { const actualEvent = await processEvent(getMinimalTransactionEvent()); - const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); const bundleStartSpan = actualEvent!.spans!.find( ({ description }) => description === 'JS Bundle Execution Before React Root', ); expect(appStartRootSpan).toEqual( expect.objectContaining(>{ - description: 'Cold App Start', + description: 'Cold Start', span_id: expect.any(String), op: APP_START_COLD_OP, origin: SPAN_ORIGIN_AUTO_APP_START, @@ -604,12 +603,12 @@ describe('App Start Integration', () => { const actualEvent = await processEvent(getMinimalTransactionEvent()); - const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartRootSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); const nativeSpan = actualEvent!.spans!.find(({ description }) => description === 'test native app start span'); expect(appStartRootSpan).toEqual( expect.objectContaining(>{ - description: 'Cold App Start', + description: 'Cold Start', span_id: expect.any(String), op: APP_START_COLD_OP, origin: SPAN_ORIGIN_AUTO_APP_START, @@ -803,7 +802,7 @@ describe('Frame Data Integration', () => { const actualEvent = await captureStandAloneAppStart(); - const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); expect(appStartSpan).toBeDefined(); expect(appStartSpan!.data).toEqual( @@ -830,7 +829,7 @@ describe('Frame Data Integration', () => { const actualEvent = await captureStandAloneAppStart(); - const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Warm App Start'); + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Warm Start'); expect(appStartSpan).toBeDefined(); expect(appStartSpan!.data).toEqual( @@ -859,7 +858,7 @@ describe('Frame Data Integration', () => { const actualEvent = await processEvent(getMinimalTransactionEvent()); - const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); expect(appStartSpan).toBeDefined(); expect(appStartSpan!.data).toEqual( @@ -888,7 +887,7 @@ describe('Frame Data Integration', () => { const actualEvent = await processEvent(getMinimalTransactionEvent()); - const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Warm App Start'); + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Warm Start'); expect(appStartSpan).toBeDefined(); expect(appStartSpan!.data).toEqual( @@ -917,7 +916,7 @@ describe('Frame Data Integration', () => { const actualEvent = await processEvent(getMinimalTransactionEvent()); - const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); expect(appStartSpan).toBeDefined(); expect(appStartSpan!.data).toEqual( @@ -939,7 +938,7 @@ describe('Frame Data Integration', () => { const actualEvent = await captureStandAloneAppStart(); - const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); expect(appStartSpan).toBeDefined(); expect(appStartSpan!.data).toEqual( @@ -963,7 +962,7 @@ describe('Frame Data Integration', () => { const actualEvent = await captureStandAloneAppStart(); - const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold App Start'); + const appStartSpan = actualEvent!.spans!.find(({ description }) => description === 'Cold Start'); expect(appStartSpan).toBeDefined(); expect(appStartSpan!.data).toEqual( @@ -1043,6 +1042,7 @@ function getMinimalTransactionEvent({ description: 'Test', span_id: '123', trace_id: '456', + data: {}, }, ], }; @@ -1077,7 +1077,7 @@ function expectEventWithAttachedColdAppStart({ spans: expect.arrayContaining([ { op: APP_START_COLD_OP, - description: 'Cold App Start', + description: 'Cold Start', start_timestamp: appStartTimeMilliseconds / 1000, timestamp: expect.any(Number), trace_id: expect.any(String), @@ -1097,6 +1097,7 @@ function expectEventWithAttachedColdAppStart({ description: 'Test', span_id: '123', trace_id: '456', + data: {}, }, ]), }); @@ -1133,7 +1134,7 @@ function expectEventWithAttachedWarmAppStart({ spans: expect.arrayContaining([ { op: APP_START_WARM_OP, - description: 'Warm App Start', + description: 'Warm Start', start_timestamp: appStartTimeMilliseconds / 1000, timestamp: expect.any(Number), trace_id: expect.any(String), @@ -1153,6 +1154,7 @@ function expectEventWithAttachedWarmAppStart({ description: 'Test', span_id: '123', trace_id: '456', + data: {}, }, ]), }); @@ -1190,7 +1192,7 @@ function expectEventWithStandaloneColdAppStart( spans: expect.arrayContaining([ { op: APP_START_COLD_OP, - description: 'Cold App Start', + description: 'Cold Start', start_timestamp: appStartTimeMilliseconds / 1000, timestamp: expect.any(Number), trace_id: expect.any(String), @@ -1241,7 +1243,7 @@ function expectEventWithStandaloneWarmAppStart( spans: expect.arrayContaining([ { op: APP_START_WARM_OP, - description: 'Warm App Start', + description: 'Warm Start', start_timestamp: appStartTimeMilliseconds / 1000, timestamp: expect.any(Number), trace_id: expect.any(String), diff --git a/packages/core/test/tracing/integrations/nativeframes.test.ts b/packages/core/test/tracing/integrations/nativeframes.test.ts index 839bdd5d1b..143ef6c787 100644 --- a/packages/core/test/tracing/integrations/nativeframes.test.ts +++ b/packages/core/test/tracing/integrations/nativeframes.test.ts @@ -1,6 +1,5 @@ import type { Event, Measurements } from '@sentry/core'; import { getCurrentScope, getGlobalScope, getIsolationScope, setCurrentClient, startSpan } from '@sentry/core'; - import { nativeFramesIntegration } from '../../../src/js'; import { NATIVE } from '../../../src/js/wrapper'; import { getDefaultTestClientOptions, TestClient } from '../../mocks/client'; @@ -17,7 +16,10 @@ jest.mock('../../../src/js/wrapper', () => { }; }); -jest.useFakeTimers({ advanceTimers: true }); +jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API +}); const mockDate = new Date(2024, 7, 14); // Set your desired mock date here const originalDateNow = Date.now; // Store the original Date.now function diff --git a/packages/core/test/tracing/integrations/stallTracking/stalltracking.background.test.ts b/packages/core/test/tracing/integrations/stallTracking/stalltracking.background.test.ts index 2358c77d1e..e83d0d505b 100644 --- a/packages/core/test/tracing/integrations/stallTracking/stalltracking.background.test.ts +++ b/packages/core/test/tracing/integrations/stallTracking/stalltracking.background.test.ts @@ -1,5 +1,4 @@ import type { AppStateStatus } from 'react-native'; - import { stallTrackingIntegration } from '../../../../src/js/tracing/integrations/stalltracking'; type StallTrackingWithTestProperties = ReturnType & { @@ -21,7 +20,10 @@ describe('BackgroundEventListener', () => { stallTracking['_internalState']['lastIntervalMs'] = currentTime; stallTracking['_internalState']['timeout'] = setTimeout(() => {}, LOOP_TIMEOUT_INTERVAL_MS); // Create a fake timeout to simulate a running interval stallTracking['_internalState']['isBackground'] = true; - jest.useFakeTimers(); // Enable fake timers to control timeouts + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); stallTracking['_internalState']['backgroundEventListener']('active' as AppStateStatus); // Check if _isBackground is set to false and _lastIntervalMs is updated correctly expect(stallTracking['_internalState']['isBackground']).toBe(false); @@ -40,7 +42,10 @@ describe('BackgroundEventListener', () => { stallTracking['_internalState']['timeout'] = null; // Mock _iteration stallTracking['_internalState']['iteration'] = jest.fn(); - jest.useFakeTimers(); // Enable fake timers to control timeouts + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); stallTracking['_internalState']['backgroundEventListener']('active' as AppStateStatus); expect(stallTracking['_internalState']['iteration']).not.toBeCalled(); @@ -50,7 +55,10 @@ describe('BackgroundEventListener', () => { stallTracking['_internalState']['timeout'] = setTimeout(() => {}, 500); // Mock _iteration stallTracking['_internalState']['iteration'] = jest.fn(); // Create a fake timeout to simulate a running interval - jest.useFakeTimers(); // Enable fake timers to control timeouts + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); stallTracking['_internalState']['backgroundEventListener']('active' as AppStateStatus); expect(stallTracking['_internalState']['iteration']).toBeCalled(); }); diff --git a/packages/core/test/tracing/integrations/stallTracking/stalltracking.iteration.test.ts b/packages/core/test/tracing/integrations/stallTracking/stalltracking.iteration.test.ts index 53c6c84439..eb6efec02a 100644 --- a/packages/core/test/tracing/integrations/stallTracking/stalltracking.iteration.test.ts +++ b/packages/core/test/tracing/integrations/stallTracking/stalltracking.iteration.test.ts @@ -19,7 +19,10 @@ describe('Iteration', () => { stallTracking['isTracking'] = false; stallTracking['_internalState']['isBackground'] = false; stallTracking['_internalState']['lastIntervalMs'] = Date.now() - 1000; // Force a timeout - jest.useFakeTimers(); + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); // Invokes the private _interaction function. stallTracking['_internalState']['iteration'](); expect(stallTracking['_internalState']['timeout']).toBeNull(); @@ -29,7 +32,10 @@ describe('Iteration', () => { stallTracking['isTracking'] = true; stallTracking['_internalState']['isBackground'] = true; stallTracking['_internalState']['lastIntervalMs'] = Date.now() - 1000; // Force a timeout - jest.useFakeTimers(); + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); // Invokes the private _interaction function. stallTracking['_internalState']['iteration'](); expect(stallTracking['_internalState']['timeout']).toBeNull(); @@ -38,7 +44,10 @@ describe('Iteration', () => { const stallTracking = stallTrackingIntegration() as StallTrackingWithTestProperties; stallTracking['isTracking'] = true; stallTracking['_internalState']['isBackground'] = false; - jest.useFakeTimers(); + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); stallTracking['_internalState']['lastIntervalMs'] = Date.now(); // Force a timeout // Invokes the private _interaction function. stallTracking['_internalState']['iteration'](); @@ -50,7 +59,10 @@ describe('Iteration', () => { const _minimumStallThreshold = 100; // Call _iteration with totalTimeTaken >= LOOP_TIMEOUT_INTERVAL_MS + _minimumStallThreshold const totalTimeTaken = LOOP_TIMEOUT_INTERVAL_MS + _minimumStallThreshold; - jest.useFakeTimers(); + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); stallTracking['_internalState']['lastIntervalMs'] = Date.now() - totalTimeTaken; stallTracking['_internalState']['statsByTransaction'] = new Map(); stallTracking['_internalState']['iteration'](); diff --git a/packages/core/test/tracing/integrations/stallTracking/stalltracking.test.ts b/packages/core/test/tracing/integrations/stallTracking/stalltracking.test.ts index 12fc59f1ab..e357984fe7 100644 --- a/packages/core/test/tracing/integrations/stallTracking/stalltracking.test.ts +++ b/packages/core/test/tracing/integrations/stallTracking/stalltracking.test.ts @@ -9,12 +9,14 @@ import { startSpanManual, timestampInSeconds, } from '@sentry/core'; - import { stallTrackingIntegration } from '../../../../src/js/tracing/integrations/stalltracking'; import { getDefaultTestClientOptions, TestClient } from '../../../mocks/client'; import { expectNonZeroStallMeasurements, expectStallMeasurements } from './stalltrackingutils'; -jest.useFakeTimers({ advanceTimers: true }); +jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['Date', 'performance'], // Keep real Date/performance APIs +}); const expensiveOperation = () => { const expensiveObject: { value: string[] } = { diff --git a/packages/core/test/tracing/integrations/userInteraction.test.ts b/packages/core/test/tracing/integrations/userInteraction.test.ts index c152017a63..19a65c4071 100644 --- a/packages/core/test/tracing/integrations/userInteraction.test.ts +++ b/packages/core/test/tracing/integrations/userInteraction.test.ts @@ -8,7 +8,6 @@ import { startSpanManual, } from '@sentry/core'; import type { AppState, AppStateStatus } from 'react-native'; - import { startUserInteractionSpan, userInteractionIntegration, @@ -63,7 +62,10 @@ describe('User Interaction Tracing', () => { let mockedUserInteractionId: { elementId: string | undefined; op: string }; beforeEach(() => { - jest.useFakeTimers(); + jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API + }); NATIVE.enableNative = true; mockedAppState.isAvailable = true; mockedAppState.currentState = 'active'; @@ -256,15 +258,20 @@ describe('User Interaction Tracing', () => { }); test('do not start UI event transaction if active transaction on scope', () => { - const activeTransaction = startSpanManual( - { name: 'activeTransactionOnScope', scope: getCurrentScope() }, - (span: Span) => span, - ); - expect(activeTransaction).toBeDefined(); - expect(activeTransaction).toBe(getActiveSpan()); + const placeholderCallback: (span: Span, finish: () => void) => void = (span, finish) => { + // @ts-expect-error no direct access to _name + expect(span._name).toBe('activeTransactionOnScope'); - startUserInteractionSpan(mockedUserInteractionId); - expect(activeTransaction).toBe(getActiveSpan()); + expect(span).toBe(getActiveSpan()); + + startUserInteractionSpan(mockedUserInteractionId); + + expect(span).toBe(getActiveSpan()); + + finish(); + }; + + startSpanManual({ name: 'activeTransactionOnScope', scope: getCurrentScope() }, placeholderCallback); }); test('UI event transaction is canceled when routing transaction starts', () => { diff --git a/packages/core/test/tracing/mockedtimetodisplaynative.tsx b/packages/core/test/tracing/mockedtimetodisplaynative.tsx index 6fbc773b53..12c9b7e6ae 100644 --- a/packages/core/test/tracing/mockedtimetodisplaynative.tsx +++ b/packages/core/test/tracing/mockedtimetodisplaynative.tsx @@ -1,6 +1,5 @@ import * as React from 'react'; import { View } from 'react-native'; - import type { RNSentryOnDrawReporterProps } from '../../src/js/tracing/timetodisplaynative.types'; import { NATIVE } from '../mockWrapper'; diff --git a/packages/core/test/tracing/reactnativenavigation.test.ts b/packages/core/test/tracing/reactnativenavigation.test.ts index ebc264d91f..5ecb73b416 100644 --- a/packages/core/test/tracing/reactnativenavigation.test.ts +++ b/packages/core/test/tracing/reactnativenavigation.test.ts @@ -9,7 +9,6 @@ import { spanToJSON, } from '@sentry/core'; import type { EmitterSubscription } from 'react-native'; - import { reactNativeTracingIntegration } from '../../src/js'; import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NATIVE_NAVIGATION } from '../../src/js/tracing/origin'; import type { @@ -44,7 +43,10 @@ interface MockEventsRegistry extends EventsRegistry { onBottomTabPressed(event: BottomTabPressedEvent): void; } -jest.useFakeTimers({ advanceTimers: true }); +jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API +}); describe('React Native Navigation Instrumentation', () => { let mockEventsRegistry: MockEventsRegistry; diff --git a/packages/core/test/tracing/reactnativetracing.test.ts b/packages/core/test/tracing/reactnativetracing.test.ts index 861b94ba1a..7aab318b6d 100644 --- a/packages/core/test/tracing/reactnativetracing.test.ts +++ b/packages/core/test/tracing/reactnativetracing.test.ts @@ -13,6 +13,11 @@ jest.mock('../../src/js/wrapper', () => { }; }); +import { reactNativeTracingIntegration } from '../../src/js/tracing/reactnativetracing'; +import { isWeb } from '../../src/js/utils/environment'; +import type { TestClient } from '../mocks/client'; +import { setupTestClient } from '../mocks/client'; + jest.mock('../../src/js/tracing/utils', () => { const originalUtils = jest.requireActual('../../src/js/tracing/utils'); @@ -32,10 +37,6 @@ jest.mock('@sentry/core', () => { }); jest.mock('../../src/js/utils/environment'); -import { reactNativeTracingIntegration } from '../../src/js/tracing/reactnativetracing'; -import { isWeb } from '../../src/js/utils/environment'; -import type { TestClient } from '../mocks/client'; -import { setupTestClient } from '../mocks/client'; describe('ReactNativeTracing', () => { let client: TestClient; diff --git a/packages/core/test/tracing/reactnavigation.stalltracking.test.ts b/packages/core/test/tracing/reactnavigation.stalltracking.test.ts index 0fbc3b8627..0d6e5fda9a 100644 --- a/packages/core/test/tracing/reactnavigation.stalltracking.test.ts +++ b/packages/core/test/tracing/reactnavigation.stalltracking.test.ts @@ -4,7 +4,6 @@ jest.mock('../../src/js/tracing/utils', () => ({ })); import { getCurrentScope, getGlobalScope, getIsolationScope, setCurrentClient, startSpanManual } from '@sentry/core'; - import { reactNativeTracingIntegration, reactNavigationIntegration } from '../../src/js'; import { stallTrackingIntegration } from '../../src/js/tracing/integrations/stalltracking'; import { isNearToNow } from '../../src/js/tracing/utils'; @@ -13,7 +12,10 @@ import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { expectStallMeasurements } from './integrations/stallTracking/stalltrackingutils'; import { createMockNavigationAndAttachTo } from './reactnavigationutils'; -jest.useFakeTimers({ advanceTimers: 1 }); +jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API +}); describe('StallTracking with ReactNavigation', () => { let client: TestClient; diff --git a/packages/core/test/tracing/reactnavigation.test.ts b/packages/core/test/tracing/reactnavigation.test.ts index 54a203000f..0b7ce0a1aa 100644 --- a/packages/core/test/tracing/reactnavigation.test.ts +++ b/packages/core/test/tracing/reactnavigation.test.ts @@ -10,7 +10,6 @@ import { setCurrentClient, spanToJSON, } from '@sentry/core'; - import { nativeFramesIntegration, reactNativeTracingIntegration } from '../../src/js'; import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION } from '../../src/js/tracing/origin'; import type { NavigationRoute } from '../../src/js/tracing/reactnavigation'; @@ -44,7 +43,10 @@ jest.mock('../../src/js/wrapper.ts', () => jest.requireActual('../mockWrapper.ts jest.mock('./../../src/js/integrations/debugsymbolicatorutils', () => ({ getDevServer: jest.fn(), })); -jest.useFakeTimers({ advanceTimers: true }); +jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'], // Keep real performance API +}); class MockNavigationContainer { currentRoute: NavigationRoute | undefined = dummyRoute; diff --git a/packages/core/test/tracing/reactnavigation.ttid.test.tsx b/packages/core/test/tracing/reactnavigation.ttid.test.tsx index 7830012e0d..89bcc1015d 100644 --- a/packages/core/test/tracing/reactnavigation.ttid.test.tsx +++ b/packages/core/test/tracing/reactnavigation.ttid.test.tsx @@ -1,11 +1,11 @@ import type { Scope, Span, SpanJSON, TransactionEvent, Transport } from '@sentry/core'; import { getActiveSpan, spanToJSON, timestampInSeconds } from '@sentry/core'; import * as TestRenderer from '@testing-library/react-native' -import * as React from "react"; - +import * as React from 'react'; import * as mockWrapper from '../mockWrapper'; import * as mockedSentryEventEmitter from '../utils/mockedSentryeventemitterfallback'; import * as mockedtimetodisplaynative from './mockedtimetodisplaynative'; + jest.mock('../../src/js/wrapper', () => mockWrapper); jest.mock('../../src/js/utils/environment'); jest.mock('../../src/js/utils/sentryeventemitterfallback', () => mockedSentryEventEmitter); @@ -24,6 +24,7 @@ import { nowInSeconds, secondInFutureTimestampMs } from '../testutils'; import { mockRecordedTimeToDisplay } from './mockedtimetodisplaynative'; import { createMockNavigationAndAttachTo } from './reactnavigationutils'; + const SCOPE_SPAN_FIELD = '_sentrySpan'; type ScopeWithMaybeSpan = Scope & { @@ -258,7 +259,7 @@ describe('React Navigation - TTID', () => { type: 'transaction', spans: expect.arrayContaining([ expect.objectContaining>({ - description: 'Cold App Start', + description: 'Cold Start', }), expect.objectContaining>({ data: { @@ -299,7 +300,7 @@ describe('React Navigation - TTID', () => { type: 'transaction', spans: expect.arrayContaining([ expect.objectContaining>({ - description: 'Cold App Start', + description: 'Cold Start', }), expect.objectContaining>({ data: { @@ -330,7 +331,7 @@ describe('React Navigation - TTID', () => { type: 'transaction', spans: expect.arrayContaining([ expect.objectContaining>({ - description: 'Cold App Start', + description: 'Cold Start', }), ]), measurements: expect.objectContaining['measurements']>({ @@ -681,7 +682,7 @@ function initSentry(sut: ReturnType): const transportSendMock = jest.fn, Parameters>(); const options: Sentry.ReactNativeOptions = { dsn: MOCK_DSN, - enableTracing: true, + tracesSampleRate: 1.0, enableStallTracking: false, integrations: [ sut, diff --git a/packages/core/test/tracing/timetodisplay.test.tsx b/packages/core/test/tracing/timetodisplay.test.tsx index ebf3d8fef5..4c6f6913ac 100644 --- a/packages/core/test/tracing/timetodisplay.test.tsx +++ b/packages/core/test/tracing/timetodisplay.test.tsx @@ -1,21 +1,18 @@ -import { getCurrentScope, getGlobalScope, getIsolationScope, logger , setCurrentClient, spanToJSON, startSpanManual } from '@sentry/core'; -jest.spyOn(logger, 'warn'); +import type { Event, Measurements, Span, SpanJSON} from '@sentry/core'; +import { debug , getCurrentScope, getGlobalScope, getIsolationScope, setCurrentClient, spanToJSON, startSpanManual } from '@sentry/core'; + +jest.spyOn(debug, 'warn'); import * as mockWrapper from '../mockWrapper'; + jest.mock('../../src/js/wrapper', () => mockWrapper); import * as mockedtimetodisplaynative from './mockedtimetodisplaynative'; -jest.mock('../../src/js/tracing/timetodisplaynative', () => mockedtimetodisplaynative); -jest.mock('../../src/js/utils/environment', () => ({ - isWeb: jest.fn().mockReturnValue(false), - isTurboModuleEnabled: jest.fn().mockReturnValue(false), -})); +jest.mock('../../src/js/tracing/timetodisplaynative', () => mockedtimetodisplaynative); -import type { Event, Measurements, Span, SpanJSON} from '@sentry/core'; import { render } from '@testing-library/react-native'; -import * as React from "react"; - +import * as React from 'react'; import { timeToDisplayIntegration } from '../../src/js/tracing/integrations/timeToDisplayIntegration'; import { SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from '../../src/js/tracing/origin'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP, SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN } from '../../src/js/tracing/semanticAttributes'; @@ -24,9 +21,17 @@ import { startTimeToFullDisplaySpan, startTimeToInitialDisplaySpan, TimeToFullDi import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { nowInSeconds, secondAgoTimestampMs, secondInFutureTimestampMs } from '../testutils'; +jest.mock('../../src/js/utils/environment', () => ({ + isWeb: jest.fn().mockReturnValue(false), + isTurboModuleEnabled: jest.fn().mockReturnValue(false), +})); + const { mockRecordedTimeToDisplay, getMockedOnDrawReportedProps, clearMockedOnDrawReportedProps } = mockedtimetodisplaynative; -jest.useFakeTimers({advanceTimers: true}); +jest.useFakeTimers({ + advanceTimers: true, + doNotFake: ['performance'] // Keep real performance API +}); describe('TimeToDisplay', () => { let client: TestClient; @@ -301,7 +306,7 @@ function getFullDisplaySpanJSON(spans: SpanJSON[]) { function expectFinishedInitialDisplaySpan(event: Event) { expect(getInitialDisplaySpanJSON(event.spans!)).toEqual(expect.objectContaining>({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: "ui.load.initial_display", + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.load.initial_display', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY, [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, @@ -317,7 +322,7 @@ function expectFinishedInitialDisplaySpan(event: Event) { function expectFinishedFullDisplaySpan(event: Event) { expect(getFullDisplaySpanJSON(event.spans!)).toEqual(expect.objectContaining>({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: "ui.load.full_display", + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.load.full_display', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY, [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, @@ -334,7 +339,7 @@ function expectFinishedFullDisplaySpan(event: Event) { function expectDeadlineExceededFullDisplaySpan(event: Event) { expect(getFullDisplaySpanJSON(event.spans!)).toEqual(expect.objectContaining>({ data: { - [SEMANTIC_ATTRIBUTE_SENTRY_OP]: "ui.load.full_display", + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'ui.load.full_display', [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY, [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, diff --git a/packages/core/test/tracing/timetodisplaynative.web.test.tsx b/packages/core/test/tracing/timetodisplaynative.web.test.tsx index 507c33a745..6956bc81a2 100644 --- a/packages/core/test/tracing/timetodisplaynative.web.test.tsx +++ b/packages/core/test/tracing/timetodisplaynative.web.test.tsx @@ -1,9 +1,15 @@ jest.mock('react-native', () => { const RN = jest.requireActual('react-native'); + // Fixes TypeError: Cannot set property UIManager of # which has only a getter + delete RN.UIManager; RN.UIManager = {}; + return RN; }); +import { getRNSentryOnDrawReporter } from '../../src/js/tracing/timetodisplaynative'; +import { ReactNativeLibraries } from '../../src/js/utils/rnlibraries'; + jest.mock('../../src/js/utils/rnlibraries', () => { const webLibrary = jest.requireActual('../../src/js/utils/rnlibraries.web'); return { @@ -11,9 +17,6 @@ jest.mock('../../src/js/utils/rnlibraries', () => { }; }); -import { getRNSentryOnDrawReporter } from '../../src/js/tracing/timetodisplaynative'; -import { ReactNativeLibraries } from '../../src/js/utils/rnlibraries'; - describe('timetodisplaynative', () => { test('requireNativeComponent to be undefined', () => { expect(ReactNativeLibraries).toBeDefined(); diff --git a/packages/core/test/transports/encodePolyfill.test.ts b/packages/core/test/transports/encodePolyfill.test.ts new file mode 100644 index 0000000000..6bc319f97f --- /dev/null +++ b/packages/core/test/transports/encodePolyfill.test.ts @@ -0,0 +1,33 @@ +import { SDK_VERSION } from '@sentry/core'; +import { encodePolyfill, useEncodePolyfill } from '../../src/js/transports/encodePolyfill'; +import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide'; + +const OriginalTextEncoder = RN_GLOBAL_OBJ.TextEncoder; + +const restoreTextEncoder = (): void => { + RN_GLOBAL_OBJ.TextEncoder = OriginalTextEncoder; +}; + +describe('useEncodePolyfill', () => { + afterEach(() => { + restoreTextEncoder(); + }); + + test('should use global encode factory if TextEncoder is available', () => { + RN_GLOBAL_OBJ.TextEncoder = MockedTextEncoder; + useEncodePolyfill(); + expect(RN_GLOBAL_OBJ.__SENTRY__?.[SDK_VERSION]?.encodePolyfill?.('')).toEqual(new Uint8Array([1, 2, 3])); + }); + + test('should use encode polyfill if TextEncoder is not available', () => { + RN_GLOBAL_OBJ.TextEncoder = undefined; + useEncodePolyfill(); + expect(RN_GLOBAL_OBJ.__SENTRY__?.[SDK_VERSION]?.encodePolyfill).toBe(encodePolyfill); + }); +}); + +class MockedTextEncoder { + public encode(_text: string): Uint8Array { + return new Uint8Array([1, 2, 3]); + } +} diff --git a/packages/core/test/transports/native.test.ts b/packages/core/test/transports/native.test.ts index 267f2b4d59..72a0c936ca 100644 --- a/packages/core/test/transports/native.test.ts +++ b/packages/core/test/transports/native.test.ts @@ -1,5 +1,4 @@ import type { Envelope } from '@sentry/core'; - import { NativeTransport } from '../../src/js/transports/native'; jest.mock('../../src/js/wrapper', () => ({ diff --git a/packages/core/test/utils/PrimitiveConverter.test.ts b/packages/core/test/utils/PrimitiveConverter.test.ts new file mode 100644 index 0000000000..0583eccd73 --- /dev/null +++ b/packages/core/test/utils/PrimitiveConverter.test.ts @@ -0,0 +1,49 @@ +import { PrimitiveToString } from '../../src/js/utils/primitiveConverter'; + +describe('Primitive to String', () => { + it('Doesnt change strings', () => { + expect(PrimitiveToString('1234')).toBe('1234'); + expect(PrimitiveToString('1234,1')).toBe('1234,1'); + expect(PrimitiveToString('abc')).toBe('abc'); + }); + + it('Converts boolean to uppercase', () => { + expect(PrimitiveToString(false)).toBe('False'); + expect(PrimitiveToString(true)).toBe('True'); + }); + + it('Keeps undefined', () => { + expect(PrimitiveToString(undefined)).toBeUndefined(); + }); + + it('Converts null to empty', () => { + expect(PrimitiveToString(null)).toBe(''); + }); + + test.each([ + [0, '0'], + [1, '1'], + [12345, '12345'], + [Number.MIN_VALUE, `${Number.MIN_VALUE}`], + [Number.MAX_VALUE, `${Number.MAX_VALUE}`], + [Number.MIN_SAFE_INTEGER, `${Number.MIN_SAFE_INTEGER}`], + [Number.MAX_SAFE_INTEGER, `${Number.MAX_SAFE_INTEGER}`], + ])('Converts %p to "%s"', (input, expected) => { + expect(PrimitiveToString(input)).toBe(expected); + }); + + test.each([ + [BigInt('0'), '0'], + [BigInt('1'), '1'], + [BigInt('-1'), '-1'], + [BigInt('123456789012345678901234567890'), '123456789012345678901234567890'], + [BigInt('-98765432109876543210987654321'), '-98765432109876543210987654321'], + ])('converts bigint %p to "%s"', (input, expected) => { + expect(PrimitiveToString(input)).toBe(expected); + }); + + it('Symbol to String', () => { + const symbol = Symbol('a symbol'); + expect(PrimitiveToString(symbol)).toBe('Symbol(a symbol)'); + }); +}); diff --git a/packages/core/test/utils/ignorerequirecyclelogs.test.ts b/packages/core/test/utils/ignorerequirecyclelogs.test.ts index 11f62b9d2c..52b87e691d 100644 --- a/packages/core/test/utils/ignorerequirecyclelogs.test.ts +++ b/packages/core/test/utils/ignorerequirecyclelogs.test.ts @@ -1,5 +1,4 @@ import { LogBox } from 'react-native'; - import { ignoreRequireCycleLogs } from '../../src/js/utils/ignorerequirecyclelogs'; jest.mock('react-native', () => ({ diff --git a/packages/core/test/utils/mockedSentryeventemitterfallback.ts b/packages/core/test/utils/mockedSentryeventemitterfallback.ts index 74ebd4f53a..9ad6dcdf5d 100644 --- a/packages/core/test/utils/mockedSentryeventemitterfallback.ts +++ b/packages/core/test/utils/mockedSentryeventemitterfallback.ts @@ -1,6 +1,5 @@ import { timestampInSeconds } from '@sentry/core'; import * as EventEmitter from 'events'; - import type { NewFrameEvent } from '../../src/js/utils/sentryeventemitter'; import type { SentryEventEmitterFallback } from '../../src/js/utils/sentryeventemitterfallback'; import type { MockInterface } from '../testutils'; diff --git a/packages/core/test/utils/safe.test.ts b/packages/core/test/utils/safe.test.ts index 89e063bd35..0e781ef313 100644 --- a/packages/core/test/utils/safe.test.ts +++ b/packages/core/test/utils/safe.test.ts @@ -38,14 +38,31 @@ describe('safe', () => { test('calls given function with correct args', () => { const mockFn = jest.fn(); const actualSafeFunction = safeTracesSampler(mockFn); - actualSafeFunction?.({ name: 'foo', transactionContext: { name: 'foo' } }); + const expectedInheritOrSampleWith = function (fallbackSampleRate: number): number { + return fallbackSampleRate; + }; + actualSafeFunction?.({ + name: 'foo', + transactionContext: { name: 'foo' }, + inheritOrSampleWith: expectedInheritOrSampleWith, + }); expect(mockFn).toBeCalledTimes(1); - expect(mockFn).toBeCalledWith({ name: 'foo', transactionContext: { name: 'foo' } }); + expect(mockFn).toBeCalledWith({ + name: 'foo', + transactionContext: { name: 'foo' }, + inheritOrSampleWith: expectedInheritOrSampleWith, + }); }); test('calls given function amd return its result', () => { const mockFn = jest.fn(() => 0.5); const actualSafeFunction = safeTracesSampler(mockFn); - const actualResult = actualSafeFunction?.({ name: 'foo', transactionContext: { name: 'foo' } }); + const actualResult = actualSafeFunction?.({ + name: 'foo', + transactionContext: { name: 'foo' }, + inheritOrSampleWith: function (fallbackSampleRate: number): number { + return fallbackSampleRate; + }, + }); expect(mockFn).toBeCalledTimes(1); expect(actualResult).toBe(0.5); }); @@ -58,7 +75,13 @@ describe('safe', () => { throw 'Test error'; }); const actualSafeFunction = safeTracesSampler(mockFn); - const actualResult = actualSafeFunction?.({ name: 'foo', transactionContext: { name: 'foo' } }); + const actualResult = actualSafeFunction?.({ + name: 'foo', + transactionContext: { name: 'foo' }, + inheritOrSampleWith: function (fallbackSampleRate: number): number { + return fallbackSampleRate; + }, + }); expect(mockFn).toBeCalledTimes(1); expect(actualResult).toEqual(0); }); diff --git a/packages/core/test/utils/sentryeventemitterfallback.test.ts b/packages/core/test/utils/sentryeventemitterfallback.test.ts index 28353fa490..c4e2de2c58 100644 --- a/packages/core/test/utils/sentryeventemitterfallback.test.ts +++ b/packages/core/test/utils/sentryeventemitterfallback.test.ts @@ -1,5 +1,4 @@ -import { logger } from '@sentry/core'; - +import { debug } from '@sentry/core'; import { NewFrameEventName } from '../../src/js/utils/sentryeventemitter'; import { createSentryFallbackEventEmitter } from '../../src/js/utils/sentryeventemitterfallback'; @@ -12,9 +11,9 @@ jest.mock('../../src/js/wrapper', () => jest.requireActual('../mockWrapper')); import { NATIVE } from '../../src/js/wrapper'; -jest.spyOn(logger, 'warn'); -jest.spyOn(logger, 'log'); -jest.spyOn(logger, 'error'); +jest.spyOn(debug, 'warn'); +jest.spyOn(debug, 'log'); +jest.spyOn(debug, 'error'); describe('SentryEventEmitterFallback', () => { let emitter: ReturnType; @@ -48,7 +47,7 @@ describe('SentryEventEmitterFallback', () => { await Promise.resolve(); await expect(NATIVE.getNewScreenTimeToDisplay).toHaveBeenCalled(); - expect(logger.error).not.toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); + expect(debug.error).not.toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); // Simulate retries and timer jest.runAllTimers(); @@ -58,7 +57,7 @@ describe('SentryEventEmitterFallback', () => { newFrameTimestampInSeconds: fallbackTime, isFallback: true, }); - expect(logger.log).toHaveBeenCalledWith( + expect(debug.log).toHaveBeenCalledWith( expect.stringContaining( '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', ), @@ -81,7 +80,7 @@ describe('SentryEventEmitterFallback', () => { await Promise.resolve(); await expect(NATIVE.getNewScreenTimeToDisplay).toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); + expect(debug.error).toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); // Simulate retries and timer jest.runAllTimers(); @@ -91,7 +90,7 @@ describe('SentryEventEmitterFallback', () => { newFrameTimestampInSeconds: fallbackTime, isFallback: true, }); - expect(logger.log).toHaveBeenCalledWith( + expect(debug.log).toHaveBeenCalledWith( expect.stringContaining( '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', ), @@ -113,7 +112,7 @@ describe('SentryEventEmitterFallback', () => { await Promise.resolve(); await expect(NATIVE.getNewScreenTimeToDisplay).toHaveBeenCalled(); - expect(logger.error).toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); + expect(debug.error).toHaveBeenCalledWith('Failed to receive Native fallback timestamp.', expect.any(Error)); // Simulate retries and timer jest.runAllTimers(); @@ -123,7 +122,7 @@ describe('SentryEventEmitterFallback', () => { newFrameTimestampInSeconds: fallbackTime, isFallback: true, }); - expect(logger.log).toHaveBeenCalledWith( + expect(debug.log).toHaveBeenCalledWith( expect.stringContaining( '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', ), @@ -152,7 +151,7 @@ describe('SentryEventEmitterFallback', () => { newFrameTimestampInSeconds: fallbackTime, isFallback: true, }); - expect(logger.log).toHaveBeenCalledWith( + expect(debug.log).toHaveBeenCalledWith( expect.stringContaining( '[Sentry] Native event emitter did not reply in time. Using JavaScript fallback emitter.', ), @@ -211,7 +210,7 @@ describe('SentryEventEmitterFallback', () => { newFrameTimestampInSeconds: 67890, isFallback: undefined, }); - expect(logger.log).not.toBeCalled(); + expect(debug.log).not.toBeCalled(); }); it('should retry up to maxRetries and emit fallback if no response', async () => { @@ -223,13 +222,13 @@ describe('SentryEventEmitterFallback', () => { // Wait for the next event loop to allow startListenerAsync to call NATIVE.getNewScreenTimeToDisplay await Promise.resolve(); - expect(logger.log).not.toHaveBeenCalled(); + expect(debug.log).not.toHaveBeenCalled(); // Simulate retries and timer jest.runAllTimers(); expect(listener).toHaveBeenCalledWith(expect.objectContaining({ isFallback: true })); - expect(logger.log).toHaveBeenCalledWith(expect.stringContaining('Native event emitter did not reply in time')); + expect(debug.log).toHaveBeenCalledWith(expect.stringContaining('Native event emitter did not reply in time')); jest.useRealTimers(); }); diff --git a/packages/core/test/wrap.mocked.test.tsx b/packages/core/test/wrap.mocked.test.tsx new file mode 100644 index 0000000000..ee9fda34bb --- /dev/null +++ b/packages/core/test/wrap.mocked.test.tsx @@ -0,0 +1,144 @@ +// We can't test wrap with mock and non mocked components, otherwise it will break the RN testing library. +import { render } from '@testing-library/react-native'; +import * as React from 'react'; +import type { ReactNativeWrapperOptions } from 'src/js/options'; +import * as environment from '../src/js/utils/environment'; + +jest.doMock('../src/js/touchevents', () => { + return { + TouchEventBoundary: ({ children }: { children: React.ReactNode }) => ( + // eslint-disable-next-line react/no-unknown-property +
{children}
+ ), + }; +}); + +jest.doMock('../src/js/tracing', () => { + return { + ReactNativeProfiler: jest.fn(({ children }: { children: React.ReactNode }) => ( + // eslint-disable-next-line react/no-unknown-property +
{children}
+ )), + }; +}); + +jest.doMock('@sentry/react', () => { + return { + Profiler: jest.fn(({ children }: { children: React.ReactNode }) => ( + // eslint-disable-next-line react/no-unknown-property +
{children}
+ )), + }; +}); + +jest.doMock('../src/js/feedback/FeedbackWidgetProvider', () => { + return { + FeedbackWidgetProvider: ({ children }: { children: React.ReactNode }) => ( + // eslint-disable-next-line react/no-unknown-property +
{children}
+ ), + }; +}); + +import { wrap } from '../src/js/sdk'; +import { ReactNativeProfiler } from '../src/js/tracing'; + +describe('Sentry.wrap', () => { + const DummyComponent: React.FC<{ value?: string }> = ({ value }) =>
{value}
; + + it('should not enforce any keys on the wrapped component', () => { + const Mock: React.FC<{ test: 23 }> = () => <>; + const ActualWrapped = wrap(Mock); + + expect(typeof ActualWrapped.defaultProps).toBe(typeof Mock.defaultProps); + }); + + it('wraps components with Sentry wrappers', () => { + const Wrapped = wrap(DummyComponent); + const renderResult = render(); + + expect(renderResult.toJSON()).toMatchInlineSnapshot(` +
+
+
+
+ wrapped +
+
+
+
+`); + }); + + it('wraps components with JS React Profiler on web', () => { + jest.spyOn(environment, 'isWeb').mockReturnValueOnce(true); + + const Wrapped = wrap(DummyComponent); + const renderResult = render(); + + expect(renderResult.toJSON()).toMatchInlineSnapshot(` +
+
+
+
+ wrapped +
+
+
+
+`); + }); + + describe('ReactNativeProfiler', () => { + it('uses given options when set', () => { + const options: ReactNativeWrapperOptions = { + profilerProps: { disabled: false, includeRender: true, includeUpdates: true }, + }; + const Wrapped = wrap(DummyComponent, options); + render(); + + expect(ReactNativeProfiler).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'Root', + disabled: false, + includeRender: true, + includeUpdates: true, + }), + expect.anything(), + ); + + expect(ReactNativeProfiler).not.toHaveBeenCalledWith( + expect.objectContaining({ + updateProps: expect.anything(), + }), + ); + }); + + it('ignore updateProps when set', () => { + const { wrap } = jest.requireActual('../src/js/sdk'); + + const Wrapped = wrap(DummyComponent, { updateProps: ['prop'] }); + render(); + + expect(ReactNativeProfiler).toHaveBeenCalledWith( + expect.objectContaining({ + name: 'Root', + updateProps: {}, + }), + expect.anything(), + ); + }); + }); +}); diff --git a/packages/core/test/wrap.test.tsx b/packages/core/test/wrap.test.tsx index f949b36d38..5d967b7852 100644 --- a/packages/core/test/wrap.test.tsx +++ b/packages/core/test/wrap.test.tsx @@ -1,36 +1,29 @@ -import { logger, setCurrentClient } from '@sentry/core'; +// We can't test wrap with mock and non mocked components, otherwise it will break the RN testing library. +import { debug, setCurrentClient } from '@sentry/core'; import { render } from '@testing-library/react-native'; import * as React from 'react'; import { Text } from 'react-native'; - import * as AppRegistry from '../src/js/integrations/appRegistry'; import { wrap } from '../src/js/sdk'; import { getDefaultTestClientOptions, TestClient } from './mocks/client'; -describe('Sentry.wrap', () => { - it('should not enforce any keys on the wrapped component', () => { - const Mock: React.FC<{ test: 23 }> = () => <>; - const ActualWrapped = wrap(Mock); - - expect(typeof ActualWrapped.defaultProps).toBe(typeof Mock.defaultProps); - }); - - it('should wrap the component and init with a warning when getAppRegistryIntegration returns undefined', () => { - logger.warn = jest.fn(); - const getAppRegistryIntegration = jest.spyOn(AppRegistry, 'getAppRegistryIntegration').mockReturnValueOnce(undefined); - const Mock: React.FC = () => Test; - const client = new TestClient( - getDefaultTestClientOptions(), - ); - setCurrentClient(client); +describe('ReactNativeProfiler', () => { + it('should wrap the component and init with a warning when getAppRegistryIntegration returns undefined', () => { + debug.warn = jest.fn(); + const getAppRegistryIntegration = jest.spyOn(AppRegistry, 'getAppRegistryIntegration').mockReturnValueOnce(undefined); + const Mock: React.FC = () => Test; + const client = new TestClient( + getDefaultTestClientOptions(), + ); + setCurrentClient(client); - client.init(); - const ActualWrapped = wrap(Mock); + client.init(); + const ActualWrapped = wrap(Mock); - const { getByText } = render(); + const { getByText } = render(); - expect(getAppRegistryIntegration).toHaveBeenCalled(); - expect(logger.warn).toHaveBeenCalledWith('AppRegistryIntegration.onRunApplication not found or invalid.'); - expect(getByText('Test')).toBeTruthy(); + expect(getAppRegistryIntegration).toHaveBeenCalled(); + expect(debug.warn).toHaveBeenCalledWith('AppRegistryIntegration.onRunApplication not found or invalid.'); + expect(getByText('Test')).toBeTruthy(); + }); }); -}); diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index f0cc9be167..8fc14c89ea 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -1,7 +1,6 @@ import type { Event, EventEnvelope, EventItem, SeverityLevel } from '@sentry/core'; -import { createEnvelope, logger } from '@sentry/core'; +import { createEnvelope, debug } from '@sentry/core'; import * as RN from 'react-native'; - import type { Spec } from '../src/js/NativeRNSentry'; import type { ReactNativeOptions } from '../src/js/options'; import { base64StringFromByteArray, utf8ToBytes } from '../src/js/vendor'; @@ -109,7 +108,7 @@ describe('Tests Native Wrapper', () => { }); test('warns if there is no dsn', async () => { - logger.warn = jest.fn(); + debug.warn = jest.fn(); await NATIVE.initNativeSdk({ enableNative: true, @@ -119,13 +118,13 @@ describe('Tests Native Wrapper', () => { }); expect(RNSentry.initNativeSdk).not.toBeCalled(); - expect(logger.warn).toHaveBeenLastCalledWith( + expect(debug.warn).toHaveBeenLastCalledWith( 'Warning: No DSN was provided. The Sentry SDK will be disabled. Native SDK will also not be initalized.', ); }); test('does not call native module with enableNative: false', async () => { - logger.warn = jest.fn(); + debug.warn = jest.fn(); await NATIVE.initNativeSdk({ dsn: 'test', @@ -138,7 +137,7 @@ describe('Tests Native Wrapper', () => { expect(RNSentry.initNativeSdk).not.toBeCalled(); expect(NATIVE.enableNative).toBe(false); - expect(logger.warn).toHaveBeenLastCalledWith('Note: Native Sentry SDK is disabled.'); + expect(debug.warn).toHaveBeenLastCalledWith('Note: Native Sentry SDK is disabled.'); }); test('filter beforeSend when initializing Native SDK', async () => { @@ -215,7 +214,7 @@ describe('Tests Native Wrapper', () => { test('does not initialize with autoInitializeNativeSdk: false', async () => { NATIVE.enableNative = false; - logger.warn = jest.fn(); + debug.warn = jest.fn(); await NATIVE.initNativeSdk({ dsn: 'test', @@ -259,7 +258,7 @@ describe('Tests Native Wrapper', () => { }); test('enableNative: false takes precedence over autoInitializeNativeSdk: false', async () => { - logger.warn = jest.fn(); + debug.warn = jest.fn(); await NATIVE.initNativeSdk({ devServerUrl: undefined, @@ -282,6 +281,37 @@ describe('Tests Native Wrapper', () => { expect(RNSentry.setContext).not.toBeCalled(); expect(RNSentry.setExtra).not.toBeCalled(); }); + + test('sets ignoreErrorsStr and ignoreErrorsRegex correctly when ignoreErrors contains strings and regex', async () => { + const regex1 = /foo/; + const regex2 = new RegExp('bar'); + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + ignoreErrors: ['string1', regex1, 'string2', regex2], + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + expect(RNSentry.initNativeSdk).toBeCalled(); + const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction).mock.calls[0][0]; + expect(initParameter.ignoreErrorsStr).toEqual(['string1', 'string2']); + expect(initParameter.ignoreErrorsRegex).toEqual([regex1.source, regex2.source]); + }); + + test('does not set ignoreErrorsStr or ignoreErrorsRegex if ignoreErrors is not provided', async () => { + await NATIVE.initNativeSdk({ + dsn: 'test', + enableNative: true, + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + expect(RNSentry.initNativeSdk).toBeCalled(); + const initParameter = (RNSentry.initNativeSdk as jest.MockedFunction).mock.calls[0][0]; + expect(initParameter.ignoreErrorsStr).toBeUndefined(); + expect(initParameter.ignoreErrorsRegex).toBeUndefined(); + }); }); describe('sendEnvelope', () => { @@ -305,7 +335,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":87}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":87}\n' + '{"event_id":"event0","message":"test","sdk":{"name":"test-sdk-name","version":"2.1.3"}}\n', ), ), @@ -337,7 +367,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":93}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":93}\n' + '{"event_id":"event0","sdk":{"name":"test-sdk-name","version":"2.1.3"},"instance":{"value":0}}\n', ), ), @@ -380,7 +410,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":50}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":50}\n' + '{"event_id":"event0","message":{"message":"test"}}\n', ), ), @@ -419,7 +449,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":124}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":124}\n' + '{"event_id":"event0","exception":{"values":[{"mechanism":{"handled":true,"type":""}}]},"breadcrumbs":[{"message":"crumb!"}]}\n', ), ), @@ -448,7 +478,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":58}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":58}\n' + '{"event_id":"event0","breadcrumbs":[{"message":"crumb!"}]}\n', ), ), @@ -487,7 +517,7 @@ describe('Tests Native Wrapper', () => { base64StringFromByteArray( utf8ToBytes( '{"event_id":"event0","sent_at":"123"}\n' + - '{"type":"event","content_type":"application/json","length":132}\n' + + '{"type":"event","content_type":"application/vnd.sentry.items.log+json","length":132}\n' + '{"event_id":"event0","exception":{"values":[{"mechanism":{"handled":false,"type":"onerror"}}]},"breadcrumbs":[{"message":"crumb!"}]}\n', ), ), @@ -808,4 +838,101 @@ describe('Tests Native Wrapper', () => { expect(result).toBeNull(); }); }); + + describe('primitiveProcessor and _setPrimitiveProcessor', () => { + describe('primitiveProcessor', () => { + it('default primitiveProcessor returns value as string', () => { + expect(NATIVE.primitiveProcessor('test')).toBe('test'); + expect(NATIVE.primitiveProcessor(123)).toBe(123); + expect(NATIVE.primitiveProcessor(true)).toBe(true); + expect(NATIVE.primitiveProcessor(null)).toBe(null); + expect(NATIVE.primitiveProcessor(undefined)).toBe(undefined); + }); + + it('handles all primitive types correctly', () => { + const testCases = [ + { input: 'string', expected: 'string' }, + { input: 42, expected: 42 }, + { input: true, expected: true }, + { input: false, expected: false }, + { input: null, expected: null }, + { input: undefined, expected: undefined }, + { input: BigInt(123), expected: BigInt(123) }, + ]; + + testCases.forEach(({ input, expected }) => { + expect(NATIVE.primitiveProcessor(input)).toBe(expected); + }); + }); + }); + + describe('_setPrimitiveProcessor', () => { + it('sets primitiveProcessor to the provided function', () => { + const mockProcessor = jest.fn(value => `processed_${value}`); + + NATIVE._setPrimitiveProcessor(mockProcessor); + + expect(NATIVE.primitiveProcessor).toBe(mockProcessor); + }); + + it('allows custom processing of primitive values', () => { + const customProcessor = (value: any) => { + if (typeof value === 'boolean') { + return value ? 'YES' : 'NO'; + } + if (value === null) { + return 'NULL'; + } + return String(value); + }; + + NATIVE._setPrimitiveProcessor(customProcessor); + + expect(NATIVE.primitiveProcessor(true)).toBe('YES'); + expect(NATIVE.primitiveProcessor(false)).toBe('NO'); + expect(NATIVE.primitiveProcessor(null)).toBe('NULL'); + expect(NATIVE.primitiveProcessor(42)).toBe('42'); + expect(NATIVE.primitiveProcessor('test')).toBe('test'); + }); + + it('can be chained with PrimitiveToString for consistent formatting', () => { + const { PrimitiveToString } = require('../src/js/utils/primitiveConverter'); + + NATIVE._setPrimitiveProcessor(PrimitiveToString); + + expect(NATIVE.primitiveProcessor(true)).toBe('True'); + expect(NATIVE.primitiveProcessor(false)).toBe('False'); + expect(NATIVE.primitiveProcessor(null)).toBe(''); + expect(NATIVE.primitiveProcessor(42)).toBe('42'); + expect(NATIVE.primitiveProcessor('test')).toBe('test'); + expect(NATIVE.primitiveProcessor(undefined)).toBeUndefined(); + }); + + it('can be reset to default behavior', () => { + const customProcessor = jest.fn(); + NATIVE._setPrimitiveProcessor(customProcessor); + expect(NATIVE.primitiveProcessor).toBe(customProcessor); + + const defaultProcessor = (value: any) => value; + NATIVE._setPrimitiveProcessor(defaultProcessor); + expect(NATIVE.primitiveProcessor).toBe(defaultProcessor); + }); + + it('works with primitiveTagIntegration', () => { + const { primitiveTagIntegration } = require('../src/js/integrations/primitiveTagIntegration'); + + const client = { + on: jest.fn(), + }; + + const integration = primitiveTagIntegration(); + integration.setup(client); + integration.afterAllSetup(); + + expect(NATIVE.primitiveProcessor(true)).toBe('True'); + expect(NATIVE.primitiveProcessor(false)).toBe('False'); + expect(NATIVE.primitiveProcessor(null)).toBe(''); + }); + }); + }); }); diff --git a/packages/core/tsconfig.build.json b/packages/core/tsconfig.build.json index 0b6ab9e09f..ac956235ba 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -27,9 +27,7 @@ "target": "es6", "module": "es6", "skipLibCheck": true, - "allowSyntheticDefaultImports": true, "strictBindCallApply": true, - "strictNullChecks": false, "importHelpers": false } } diff --git a/packages/core/tsconfig.build.tools.json b/packages/core/tsconfig.build.tools.json index ac4f55f2cd..ac86a9a64d 100644 --- a/packages/core/tsconfig.build.tools.json +++ b/packages/core/tsconfig.build.tools.json @@ -14,7 +14,6 @@ "target": "es6", "module": "CommonJS", "skipLibCheck": true, - "allowSyntheticDefaultImports": true, "importHelpers": false } } diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 76c7318349..33d561135e 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -14,7 +14,6 @@ "plugin/**/*.ts" ], "exclude": ["dist"], - "allowSyntheticDefaultImports": true, "compilerOptions": { "rootDir": ".", "jsx": "react", diff --git a/performance-tests/TestAppPlain/package.json b/performance-tests/TestAppPlain/package.json index 6a53c80edd..28fa52091d 100644 --- a/performance-tests/TestAppPlain/package.json +++ b/performance-tests/TestAppPlain/package.json @@ -1,6 +1,6 @@ { "name": "TestAppPlain", - "version": "6.21.0", + "version": "7.0.0-rc.2", "private": true, "scripts": { "android": "react-native run-android", diff --git a/performance-tests/TestAppSentry/package.json b/performance-tests/TestAppSentry/package.json index 0ee43550a4..5f02a3313f 100644 --- a/performance-tests/TestAppSentry/package.json +++ b/performance-tests/TestAppSentry/package.json @@ -1,6 +1,6 @@ { "name": "TestAppSentry", - "version": "6.21.0", + "version": "7.0.0-rc.2", "private": true, "scripts": { "android": "react-native run-android", @@ -8,7 +8,7 @@ "start": "react-native start" }, "dependencies": { - "@sentry/react-native": "6.21.0", + "@sentry/react-native": "7.0.0-rc.2", "react": "18.1.0", "react-native": "0.70.15" }, diff --git a/samples/expo/app.json b/samples/expo/app.json index 192ffb6e27..ef33b7506c 100644 --- a/samples/expo/app.json +++ b/samples/expo/app.json @@ -5,7 +5,7 @@ "jsEngine": "hermes", "newArchEnabled": true, "scheme": "sentry-expo-sample", - "version": "6.21.0", + "version": "7.0.0-rc.2", "orientation": "portrait", "icon": "./assets/icon.png", "userInterfaceStyle": "light", diff --git a/samples/expo/app/(tabs)/index.tsx b/samples/expo/app/(tabs)/index.tsx index fe6a630adb..1a624c8f15 100644 --- a/samples/expo/app/(tabs)/index.tsx +++ b/samples/expo/app/(tabs)/index.tsx @@ -1,7 +1,6 @@ -import { Button, StyleSheet } from 'react-native'; -import Constants from 'expo-constants'; +import { Button, ScrollView, StyleSheet } from 'react-native'; import * as Sentry from '@sentry/react-native'; -import { reloadAppAsync } from 'expo'; +import { reloadAppAsync, isRunningInExpoGo } from 'expo'; import * as DevClient from 'expo-dev-client'; import { Text, View } from '@/components/Themed'; @@ -11,113 +10,190 @@ import * as WebBrowser from 'expo-web-browser'; import { useUpdates } from 'expo-updates'; import { isWeb } from '../../utils/isWeb'; -const isRunningInExpoGo = Constants.appOwnership === 'expo'; - export default function TabOneScreen() { const { currentlyRunning } = useUpdates(); return ( - - - Welcome to Sentry Expo Sample App! - Update ID: {currentlyRunning.updateId} - Channel: {currentlyRunning.channel} - Runtime Version: {currentlyRunning.runtimeVersion} -