diff --git a/CHANGELOG.md b/CHANGELOG.md index d114256a3e..9661856452 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,12 +2,18 @@ > [!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 +### Upgrading from 6.x to 7.0 + +Version 7 of the Sentry React Native SDK primarily introduces API cleanup and version support changes. 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 - Avoid silent failure when JS bundle was not created due to Sentry Xcode scripts failure ([#4690](https://github.com/getsentry/sentry-react-native/pull/4690)) @@ -16,6 +22,12 @@ ### 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 Bundler Plugins from v3.2.2 to v3.3.1 ([#4693](https://github.com/getsentry/sentry-react-native/pull/4693), [#4707](https://github.com/getsentry/sentry-react-native/pull/4707), [#4720](https://github.com/getsentry/sentry-react-native/pull/4720), [#4721](https://github.com/getsentry/sentry-react-native/pull/4721)) - [changelog](https://github.com/getsentry/sentry-javascript-bundler-plugins/blob/main/CHANGELOG.md#331) - [diff](https://github.com/getsentry/sentry-javascript-bundler-plugins/compare/3.2.2...3.3.1) @@ -26,6 +38,37 @@ - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8490) - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.48.0...8.49.0) +### 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)) + ## 6.11.0-beta.0 ### Features @@ -181,6 +224,10 @@ To learn more about the available configuration options visit [the documentation](https://docs.sentry.io/platforms/react-native/manual-setup/expo/gradle). +### Changes + +Change `Cold/Warm App Start` span description to `Cold/Warm Start` ([#4636](https://github.com/getsentry/sentry-react-native/pull/4636)) + ### Fixes - Remove `error:` prefix from `collect-modules.sh` to avoid failing iOS builds ([#4570](https://github.com/getsentry/sentry-react-native/pull/4570)) @@ -191,9 +238,6 @@ ### Dependencies -- Bump Android SDK from v7.20.1 to v7.22.0 ([#4529](https://github.com/getsentry/sentry-react-native/pull/4529)) - - [changelog](https://github.com/getsentry/sentry-java/blob/7.x.x/CHANGELOG.md#7220) - - [diff](https://github.com/getsentry/sentry-java/compare/7.20.1...7.22.0) - Bump Cocoa SDK from v8.44.0 to v8.45.0 ([#4537](https://github.com/getsentry/sentry-react-native/pull/4537)) - [changelog](https://github.com/getsentry/sentry-cocoa/blob/main/CHANGELOG.md#8450) - [diff](https://github.com/getsentry/sentry-cocoa/compare/8.44.0...8.45.0) diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 69ceadf907..1e4a666698 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -13,7 +13,7 @@ "devDependencies": { "@babel/preset-env": "^7.25.3", "@babel/preset-typescript": "^7.18.6", - "@sentry/core": "8.54.0", + "@sentry/core": "9.12.0", "@sentry/react-native": "6.11.0-beta.0", "@types/node": "^20.9.3", "@types/react": "^18.2.64", 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/packages/core/android/build.gradle b/packages/core/android/build.gradle index 292f7f6417..5cc7aab628 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.5' + api 'io.sentry:sentry-android:8.11.1' } 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 8a6beaa024..8cae8e2418 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 @@ -30,12 +30,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; @@ -87,6 +87,7 @@ import java.util.List; import java.util.Map; import java.util.Properties; +import java.util.Set; import java.util.concurrent.CountDownLatch; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -516,7 +517,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()); } @@ -612,7 +613,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) { @@ -666,10 +667,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) { @@ -831,8 +828,7 @@ private void initializeAndroidProfiler() { (int) SECONDS.toMicros(1) / profilingTracesHz, new SentryFrameMetricsCollector(reactApplicationContext, logger, buildInfo), executorService, - logger, - buildInfo); + logger); } public WritableMap startProfiling(boolean platformProfilers) { @@ -856,7 +852,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 { @@ -942,7 +938,7 @@ private String readStringFromFile(File path) throws IOException { } 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); @@ -979,7 +975,8 @@ protected void fetchNativeDeviceContexts( } 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 { @@ -1058,14 +1055,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); diff --git a/packages/core/package.json b/packages/core/package.json index b670ae9b78..e18fef732f 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -66,21 +66,20 @@ }, "dependencies": { "@sentry/babel-plugin-component-annotate": "3.3.1", - "@sentry/browser": "8.54.0", + "@sentry/browser": "9.12.0", "@sentry/cli": "2.43.0", - "@sentry/core": "8.54.0", - "@sentry/react": "8.54.0", - "@sentry/types": "8.54.0", - "@sentry/utils": "8.54.0" + "@sentry/core": "9.12.0", + "@sentry/react": "9.12.0", + "@sentry/types": "9.12.0" }, "devDependencies": { "@babel/core": "^7.25.2", "@expo/metro-config": "0.19.5", "@mswjs/interceptors": "^0.25.15", "@react-native/babel-preset": "0.77.1", - "@sentry-internal/eslint-config-sdk": "8.54.0", - "@sentry-internal/eslint-plugin-sdk": "8.54.0", - "@sentry-internal/typescript": "8.54.0", + "@sentry-internal/eslint-config-sdk": "9.12.0", + "@sentry-internal/eslint-plugin-sdk": "9.12.0", + "@sentry-internal/typescript": "9.12.0", "@sentry/wizard": "4.7.0", "@testing-library/react-native": "^12.7.2", "@types/jest": "^29.5.13", @@ -110,7 +109,7 @@ "react-native": "0.77.1", "react-test-renderer": "^18.3.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..332957e871 100644 --- a/packages/core/plugin/src/withSentry.ts +++ b/packages/core/plugin/src/withSentry.ts @@ -18,7 +18,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; } diff --git a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts index 27a9a4d904..3154f25aad 100644 --- a/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts +++ b/packages/core/plugin/src/withSentryAndroidGradlePlugin.ts @@ -35,7 +35,7 @@ export function withSentryAndroidGradlePlugin( const withSentryProjectBuildGradle = (config: any): any => { return withProjectBuildGradle(config, (projectBuildGradle: any) => { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - if (!projectBuildGradle.modResults || !projectBuildGradle.modResults.contents) { + if (!projectBuildGradle.modResults?.contents) { warnOnce('android/build.gradle content is missing or undefined.'); return config; } diff --git a/packages/core/src/js/client.ts b/packages/core/src/js/client.ts index d838167093..4c2911fbbe 100644 --- a/packages/core/src/js/client.ts +++ b/packages/core/src/js/client.ts @@ -10,7 +10,14 @@ import type { TransportMakeRequestResponse, UserFeedback, } from '@sentry/core'; -import { BaseClient, dateTimestampInSeconds, logger, SentryError } from '@sentry/core'; +import { + addAutoIpAddressToSession, + addAutoIpAddressToUser, + BaseClient, + dateTimestampInSeconds, + logger, + SentryError, +} from '@sentry/core'; import { Alert } from 'react-native'; import { getDevServer } from './integrations/debugsymbolicatorutils'; @@ -48,6 +55,11 @@ export class ReactNativeClient extends BaseClient { super(options); this._outcomesBuffer = []; + + if (options.sendDefaultPii === true) { + this.on('postprocessEvent', addAutoIpAddressToUser); + this.on('beforeSendSession', addAutoIpAddressToSession); + } } /** diff --git a/packages/core/src/js/feedback/FeedbackWidget.tsx b/packages/core/src/js/feedback/FeedbackWidget.tsx index 7448d9d8b4..95b6e295fb 100644 --- a/packages/core/src/js/feedback/FeedbackWidget.tsx +++ b/packages/core/src/js/feedback/FeedbackWidget.tsx @@ -26,9 +26,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: '', @@ -67,7 +65,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(); @@ -119,14 +117,14 @@ export class FeedbackWidget extends React.Component void = async () => { if (!this.state.filename && !this.state.attachment) { - const imagePickerConfiguration: ImagePickerConfiguration = this.props; - if (imagePickerConfiguration.imagePicker) { - const launchImageLibrary = imagePickerConfiguration.imagePicker.launchImageLibraryAsync + const { imagePicker } = this.props; + if (imagePicker) { + const launchImageLibrary = imagePicker.launchImageLibraryAsync // expo-image-picker library is available - ? () => imagePickerConfiguration.imagePicker.launchImageLibraryAsync({ mediaTypes: ['images'], base64: isWeb() }) + ? () => imagePicker.launchImageLibraryAsync?.({ mediaTypes: ['images'], base64: isWeb() }) // react-native-image-picker library is available - : imagePickerConfiguration.imagePicker.launchImageLibrary - ? () => imagePickerConfiguration.imagePicker.launchImageLibrary({ mediaType: 'photo', includeBase64: isWeb() }) + : 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.'); @@ -140,22 +138,22 @@ 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'); } } else { - const filename = result.assets[0].fileName; - const imageUri = result.assets[0].uri; - getDataFromUri(imageUri).then((data) => { - if (data != null) { + const filename = result.assets[0]?.fileName; + const imageUri = result.assets[0]?.uri; + imageUri && getDataFromUri(imageUri).then((data) => { + if (data) { this.setState({ filename, attachment: data, attachmentUri: imageUri }); } else { logger.error('Failed to read image data from uri:', imageUri); diff --git a/packages/core/src/js/feedback/FeedbackWidget.types.ts b/packages/core/src/js/feedback/FeedbackWidget.types.ts index af08c2ffc3..adb300784b 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; /** * Fill in email/name input fields with Sentry user context if it exists. @@ -71,32 +71,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 @@ -106,52 +106,52 @@ 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 and renders the image editor */ - addScreenshotButtonLabel?: string; + addScreenshotButtonLabel: string; /** * The label for the button that removes a screenshot and hides the image editor */ - removeScreenshotButtonLabel?: string; + removeScreenshotButtonLabel: string; /** * 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; /** * Message when there is a generic error */ - genericError?: string; + genericError: string; } /** @@ -161,34 +161,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 856298382e..4f380842e3 100644 --- a/packages/core/src/js/feedback/FeedbackWidgetManager.tsx +++ b/packages/core/src/js/feedback/FeedbackWidgetManager.tsx @@ -15,6 +15,10 @@ const PULL_DOWN_CLOSE_THRESHOLD = 200; const SLIDE_ANIMATION_DURATION = 200; const BACKGROUND_ANIMATION_DURATION = 200; +const NOOP_SET_VISIBILITY = (): void => { + // No-op +}; + class FeedbackWidgetManager { private static _isVisible = false; private static _setVisibility: (visible: boolean) => void; @@ -28,22 +32,24 @@ class FeedbackWidgetManager { */ 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 { // This message should be always shown otherwise it's not possible to use the widget. // eslint-disable-next-line no-console - console.warn('[Sentry] FeedbackWidget requires `Sentry.wrap(RootComponent)` to be called before `showFeedbackWidget()`.'); + console.warn( + '[Sentry] FeedbackWidget requires `Sentry.wrap(RootComponent)` to be called before `showFeedbackWidget()`.', + ); } } public static hide(): void { - if (this._setVisibility) { + if (this._setVisibility !== NOOP_SET_VISIBILITY) { this._isVisible = false; this._setVisibility(false); } else { diff --git a/packages/core/src/js/feedback/defaults.ts b/packages/core/src/js/feedback/defaults.ts index 90d9534874..3152c769fb 100644 --- a/packages/core/src/js/feedback/defaults.ts +++ b/packages/core/src/js/feedback/defaults.ts @@ -19,7 +19,7 @@ const ADD_SCREENSHOT_LABEL = 'Add 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 96280cf967..ad2bd0675b 100644 --- a/packages/core/src/js/feedback/integration.ts +++ b/packages/core/src/js/feedback/integration.ts @@ -8,7 +8,7 @@ type FeedbackIntegration = Integration & { options: Partial; }; -export const feedbackIntegration = (initOptions: FeedbackWidgetProps = {}): FeedbackIntegration => { +export const feedbackIntegration = (initOptions: Partial = {}): FeedbackIntegration => { return { name: MOBILE_FEEDBACK_INTEGRATION_NAME, options: initOptions, diff --git a/packages/core/src/js/feedback/utils.ts b/packages/core/src/js/feedback/utils.ts index 9c2826981d..05b8b471f1 100644 --- a/packages/core/src/js/feedback/utils.ts +++ b/packages/core/src/js/feedback/utils.ts @@ -15,7 +15,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); } export const isValidEmail = (email: string): boolean => { diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 3aa2bdb71d..fb931312d9 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, @@ -43,7 +42,6 @@ export { getClient, setCurrentClient, addEventProcessor, - metricsDefault as metrics, lastEventId, } from '@sentry/core'; diff --git a/packages/core/src/js/integrations/debugsymbolicator.ts b/packages/core/src/js/integrations/debugsymbolicator.ts index 8529d0eeb6..ed1c2901d3 100644 --- a/packages/core/src/js/integrations/debugsymbolicator.ts +++ b/packages/core/src/js/integrations/debugsymbolicator.ts @@ -131,7 +131,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 +143,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..d99f72b261 100644 --- a/packages/core/src/js/integrations/debugsymbolicatorutils.ts +++ b/packages/core/src/js/integrations/debugsymbolicatorutils.ts @@ -20,7 +20,14 @@ export async function fetchSourceContext(frames: SentryStackFrame[]): Promise { @@ -155,7 +156,7 @@ 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); }, diff --git a/packages/core/src/js/integrations/screenshot.ts b/packages/core/src/js/integrations/screenshot.ts index 6f504fa76e..11491689ee 100644 --- a/packages/core/src/js/integrations/screenshot.ts +++ b/packages/core/src/js/integrations/screenshot.ts @@ -18,7 +18,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/spotlight.ts b/packages/core/src/js/integrations/spotlight.ts index b4f62e06da..90af07ccca 100644 --- a/packages/core/src/js/integrations/spotlight.ts +++ b/packages/core/src/js/integrations/spotlight.ts @@ -83,17 +83,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 +109,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..c2c56fa4fc 100644 --- a/packages/core/src/js/integrations/viewhierarchy.ts +++ b/packages/core/src/js/integrations/viewhierarchy.ts @@ -21,7 +21,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; } diff --git a/packages/core/src/js/options.ts b/packages/core/src/js/options.ts index a95de6df4c..14cc743fc2 100644 --- a/packages/core/src/js/options.ts +++ b/packages/core/src/js/options.ts @@ -234,6 +234,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; + /** * Options which are in beta, or otherwise not guaranteed to be stable. */ diff --git a/packages/core/src/js/profiling/convertHermesProfile.ts b/packages/core/src/js/profiling/convertHermesProfile.ts index 39ed9ac752..241c6fd249 100644 --- a/packages/core/src/js/profiling/convertHermesProfile.ts +++ b/packages/core/src/js/profiling/convertHermesProfile.ts @@ -82,11 +82,21 @@ 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) { + logger.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); @@ -130,8 +140,12 @@ function mapFrames(hermesStackFrames: Record(); - const options = client && client.getOptions(); + const options = client?.getOptions?.(); const profilesSampleRate = options && typeof options.profilesSampleRate === 'number' ? options.profilesSampleRate : undefined; diff --git a/packages/core/src/js/profiling/utils.ts b/packages/core/src/js/profiling/utils.ts index c83342b50b..5c6d996e83 100644 --- a/packages/core/src/js/profiling/utils.ts +++ b/packages/core/src/js/profiling/utils.ts @@ -75,12 +75,12 @@ 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`); } @@ -97,25 +97,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 +136,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 +157,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..7d84ec1b8c 100644 --- a/packages/core/src/js/replay/CustomMask.tsx +++ b/packages/core/src/js/replay/CustomMask.tsx @@ -34,7 +34,7 @@ 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)) { diff --git a/packages/core/src/js/replay/mobilereplay.ts b/packages/core/src/js/replay/mobilereplay.ts index 4bdfdd7955..3b63e40957 100644 --- a/packages/core/src/js/replay/mobilereplay.ts +++ b/packages/core/src/js/replay/mobilereplay.ts @@ -103,7 +103,7 @@ export const mobileReplayIntegration = (initOptions: MobileReplayOptions = defau const options = { ...defaultOptions, ...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; diff --git a/packages/core/src/js/scopeSync.ts b/packages/core/src/js/scopeSync.ts index bc9f20c597..bc69ffe86d 100644 --- a/packages/core/src/js/scopeSync.ts +++ b/packages/core/src/js/scopeSync.ts @@ -1,4 +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'; @@ -60,7 +61,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 5edba50b48..c50f1c35f1 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -63,7 +63,7 @@ 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; } @@ -158,6 +158,7 @@ export function wrap

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

= (appProps) => { diff --git a/packages/core/src/js/tools/metroconfig.ts b/packages/core/src/js/tools/metroconfig.ts index e3c71b684f..73c9445b40 100644 --- a/packages/core/src/js/tools/metroconfig.ts +++ b/packages/core/src/js/tools/metroconfig.ts @@ -140,7 +140,7 @@ export function withSentryBabelTransformer( config: MetroConfig, annotateReactComponents: true | { ignoredComponents?: string[] }, ): MetroConfig { - const defaultBabelTransformerPath = config.transformer && config.transformer.babelTransformerPath; + const defaultBabelTransformerPath = config.transformer?.babelTransformerPath; logger.debug('Default Babel transformer path from `config.transformer`:', defaultBabelTransformerPath); if (!defaultBabelTransformerPath) { @@ -270,10 +270,10 @@ export function withSentryFramesCollapsed(config: MetroConfig): MetroConfig { 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/touchevents.tsx b/packages/core/src/js/touchevents.tsx index 293391a834..2280597264 100644 --- a/packages/core/src/js/touchevents.tsx +++ b/packages/core/src/js/touchevents.tsx @@ -121,8 +121,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) { + logger.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 }, diff --git a/packages/core/src/js/tracing/integrations/appStart.ts b/packages/core/src/js/tracing/integrations/appStart.ts index df0713bc57..aa2ce1c70e 100644 --- a/packages/core/src/js/tracing/integrations/appStart.ts +++ b/packages/core/src/js/tracing/integrations/appStart.ts @@ -211,6 +211,13 @@ export const appStartIntegration = ({ }; 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( '[AppStart] App start tracking is enabled. App start will be added to the first transaction as a child span.', @@ -260,7 +267,7 @@ export const appStartIntegration = ({ return; } - if (!event.contexts || !event.contexts.trace) { + if (!event.contexts?.trace) { logger.warn('[AppStart] Transaction event is missing trace context. Can not attach app start.'); return; } @@ -361,7 +368,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, diff --git a/packages/core/src/js/tracing/integrations/nativeFrames.ts b/packages/core/src/js/tracing/integrations/nativeFrames.ts index ceec914b88..e5dd20f98a 100644 --- a/packages/core/src/js/tracing/integrations/nativeFrames.ts +++ b/packages/core/src/js/tracing/integrations/nativeFrames.ts @@ -60,7 +60,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 = diff --git a/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts b/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts index 52cd915634..8b5d0ffc66 100644 --- a/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts +++ b/packages/core/src/js/tracing/integrations/timeToDisplayIntegration.ts @@ -30,7 +30,7 @@ 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.`); return event; @@ -54,17 +54,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,6 +102,8 @@ 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) { @@ -117,7 +121,7 @@ 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); @@ -204,17 +208,19 @@ 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); diff --git a/packages/core/src/js/tracing/onSpanEndUtils.ts b/packages/core/src/js/tracing/onSpanEndUtils.ts index 40365b33fd..cffa13a852 100644 --- a/packages/core/src/js/tracing/onSpanEndUtils.ts +++ b/packages/core/src/js/tracing/onSpanEndUtils.ts @@ -44,7 +44,7 @@ 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.'); return; @@ -129,7 +129,7 @@ export const cancelInBackground = (client: Client, span: Span): void => { client.on('spanEnd', (endedSpan: Span) => { if (endedSpan === span) { logger.debug(`Removing AppState listener for ${spanToJSON(span).op} transaction.`); - subscription && subscription.remove && subscription.remove(); + subscription?.remove?.(); } }); }; diff --git a/packages/core/src/js/tracing/reactnativenavigation.ts b/packages/core/src/js/tracing/reactnativenavigation.ts index 0b0f696c68..2ce7ef7f5d 100644 --- a/packages/core/src/js/tracing/reactnativenavigation.ts +++ b/packages/core/src/js/tracing/reactnativenavigation.ts @@ -129,7 +129,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..49ad27df88 100644 --- a/packages/core/src/js/tracing/reactnativeprofiler.tsx +++ b/packages/core/src/js/tracing/reactnativeprofiler.tsx @@ -47,7 +47,7 @@ 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') { diff --git a/packages/core/src/js/tracing/reactnavigation.ts b/packages/core/src/js/tracing/reactnavigation.ts index 58afcf1f4a..feebb3b9f7 100644 --- a/packages/core/src/js/tracing/reactnavigation.ts +++ b/packages/core/src/js/tracing/reactnavigation.ts @@ -220,6 +220,7 @@ export const reactNavigationIntegration = ({ const navigationActionType = useDispatchedActionData ? event?.data.action.type : undefined; if ( useDispatchedActionData && + navigationActionType && [ // Process common actions 'PRELOAD', @@ -241,7 +242,7 @@ export const reactNavigationIntegration = ({ } 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, diff --git a/packages/core/src/js/tracing/span.ts b/packages/core/src/js/tracing/span.ts index d7909d8d6b..4e3a70ff66 100644 --- a/packages/core/src/js/tracing/span.ts +++ b/packages/core/src/js/tracing/span.ts @@ -1,6 +1,6 @@ import type { Client, Scope, Span, SpanJSON, StartSpanOptions } from '@sentry/core'; import { - generatePropagationContext, + generateTraceId, getActiveSpan, getClient, getCurrentScope, @@ -104,7 +104,7 @@ export const startIdleSpan = ( return new SentryNonRecordingSpan(); } - getCurrentScope().setPropagationContext(generatePropagationContext()); + getCurrentScope().setPropagationContext({ traceId: generateTraceId(), sampleRand: Math.random() }); const span = coreStartIdleSpan(startSpanOption, { finalTimeout, idleTimeout }); cancelInBackground(client, span); @@ -127,7 +127,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/transports/encodePolyfill.ts b/packages/core/src/js/transports/encodePolyfill.ts index 6e84209ed0..e9244d562a 100644 --- a/packages/core/src/js/transports/encodePolyfill.ts +++ b/packages/core/src/js/transports/encodePolyfill.ts @@ -1,12 +1,12 @@ -import { RN_GLOBAL_OBJ } from '../utils/worldwide'; +import { getMainCarrier, SDK_VERSION } from '@sentry/core'; + import { utf8ToBytes } from '../vendor'; export const useEncodePolyfill = (): void => { - if (!RN_GLOBAL_OBJ.__SENTRY__) { - (RN_GLOBAL_OBJ.__SENTRY__ as Partial<(typeof RN_GLOBAL_OBJ)['__SENTRY__']>) = {}; - } - - RN_GLOBAL_OBJ.__SENTRY__.encodePolyfill = encodePolyfill; + // Based on https://github.com/getsentry/sentry-javascript/blob/f0fc41f6166857cd97a695f5cc9a18caf6a0bf43/packages/core/src/carrier.ts#L49 + const carrier = getMainCarrier(); + const __SENTRY__ = (carrier.__SENTRY__ = carrier.__SENTRY__ || {}); + (__SENTRY__[SDK_VERSION] = __SENTRY__[SDK_VERSION] || {}).encodePolyfill = encodePolyfill; }; export const encodePolyfill = (text: string): Uint8Array => { 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/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 214532b031..ad19462a18 100644 --- a/packages/core/src/js/utils/environment.ts +++ b/packages/core/src/js/utils/environment.ts @@ -36,7 +36,7 @@ export function isExpo(): boolean { /** Check if JS runs in Expo Go */ export function isExpoGo(): boolean { const expoConstants = getExpoConstants(); - return (expoConstants && expoConstants.appOwnership) === 'expo'; + return expoConstants?.appOwnership === 'expo'; } /** Check Expo Go version if available */ @@ -75,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__ */ @@ -91,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/expomodules.ts b/packages/core/src/js/utils/expomodules.ts index 349c454d9d..73d4ea7f1e 100644 --- a/packages/core/src/js/utils/expomodules.ts +++ b/packages/core/src/js/utils/expomodules.ts @@ -5,14 +5,12 @@ import { RN_GLOBAL_OBJ } from './worldwide'; * Returns the Expo Constants module if present */ export function getExpoConstants(): ExpoConstants | undefined { - return ( - (RN_GLOBAL_OBJ.expo && RN_GLOBAL_OBJ.expo.modules && RN_GLOBAL_OBJ.expo.modules.ExponentConstants) || undefined - ); + return RN_GLOBAL_OBJ.expo?.modules?.ExponentConstants || undefined; } /** * Returns the Expo Device module if present */ export function getExpoDevice(): ExpoDevice | undefined { - return (RN_GLOBAL_OBJ.expo && RN_GLOBAL_OBJ.expo.modules && RN_GLOBAL_OBJ.expo.modules.ExpoDevice) || undefined; + return RN_GLOBAL_OBJ.expo?.modules?.ExpoDevice || undefined; } 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/wrapper.ts b/packages/core/src/js/wrapper.ts index 598ea7c9db..289af29923 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -37,7 +37,7 @@ import { base64StringFromByteArray, utf8ToBytes } from './vendor'; */ export function getRNSentryModule(): Spec | undefined { return isTurboModuleEnabled() - ? ReactNativeLibraries.TurboModuleRegistry && ReactNativeLibraries.TurboModuleRegistry.get('RNSentry') + ? ReactNativeLibraries.TurboModuleRegistry?.get('RNSentry') : NativeModules.RNSentry; } @@ -729,7 +729,7 @@ export const NATIVE: SentryNativeWrapper = { return RNSentry.popTimeToDisplayFor(key); } catch (error) { logger.error('Error:', error); - return null; + return Promise.resolve(null); } }, diff --git a/packages/core/test/client.test.ts b/packages/core/test/client.test.ts index 8cb4217356..8e00c06bf9 100644 --- a/packages/core/test/client.test.ts +++ b/packages/core/test/client.test.ts @@ -2,8 +2,21 @@ 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'; @@ -625,6 +638,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 +851,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/feedback.test.ts b/packages/core/test/feedback.test.ts index 6b1831934d..f25bc4eec1 100644 --- a/packages/core/test/feedback.test.ts +++ b/packages/core/test/feedback.test.ts @@ -240,17 +240,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 +264,7 @@ describe('captureFeedback', () => { expect(mockTransport).toHaveBeenCalledWith([ { event_id: eventId, - sent_at: expect.any(String), + sent_at: expect.toBeDateString(), }, [ [ @@ -274,7 +274,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 +298,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 +323,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 +338,7 @@ describe('captureFeedback', () => { contexts: { trace: { trace_id: traceId, - span_id: spanId, + span_id: expect.any(String), }, feedback: { message: 'test', @@ -360,7 +361,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/mocks/client.ts b/packages/core/test/mocks/client.ts index 8a0df75ebc..8db8b51dec 100644 --- a/packages/core/test/mocks/client.ts +++ b/packages/core/test/mocks/client.ts @@ -88,7 +88,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/profiling/integration.test.ts b/packages/core/test/profiling/integration.test.ts index 43a7d3cc75..d83f3e50ec 100644 --- a/packages/core/test/profiling/integration.test.ts +++ b/packages/core/test/profiling/integration.test.ts @@ -380,7 +380,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 +466,5 @@ function addIntegrationAndForceSetupOnce(integration: Integration): void { } client.addIntegration(integration); - integration.setupOnce && integration.setupOnce(); + integration.setupOnce?.(); } diff --git a/packages/core/test/sdk.test.ts b/packages/core/test/sdk.test.ts index 268dc742f2..ac99d81872 100644 --- a/packages/core/test/sdk.test.ts +++ b/packages/core/test/sdk.test.ts @@ -620,6 +620,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({}); diff --git a/packages/core/test/tools/fixtures/mockBabelTransformer.js b/packages/core/test/tools/fixtures/mockBabelTransformer.js index 17628495a5..0e0aad9bd5 100644 --- a/packages/core/test/tools/fixtures/mockBabelTransformer.js +++ b/packages/core/test/tools/fixtures/mockBabelTransformer.js @@ -1,3 +1,6 @@ +var globals = require('@jest/globals'); +var jest = globals.jest; + module.exports = { transform: jest.fn(), getCacheKey: jest.fn(), diff --git a/packages/core/test/tracing/addTracingExtensions.test.ts b/packages/core/test/tracing/addTracingExtensions.test.ts index 4d4c5384c3..bb074e36ee 100644 --- a/packages/core/test/tracing/addTracingExtensions.test.ts +++ b/packages/core/test/tracing/addTracingExtensions.test.ts @@ -1,3 +1,4 @@ +import type { Span } from '@sentry/core'; import { getCurrentScope, spanToJSON, startSpanManual } from '@sentry/core'; import { reactNativeTracingIntegration } from '../../src/js'; @@ -55,9 +56,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 +74,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/integrations/appStart.test.ts b/packages/core/test/tracing/integrations/appStart.test.ts index b31c35db66..926f0edca3 100644 --- a/packages/core/test/tracing/integrations/appStart.test.ts +++ b/packages/core/test/tracing/integrations/appStart.test.ts @@ -161,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', ); @@ -169,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, @@ -199,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', ); @@ -207,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, @@ -238,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, @@ -482,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, @@ -522,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, @@ -562,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, @@ -603,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, @@ -849,6 +849,7 @@ function getMinimalTransactionEvent({ description: 'Test', span_id: '123', trace_id: '456', + data: {}, }, ], }; @@ -883,7 +884,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), @@ -903,6 +904,7 @@ function expectEventWithAttachedColdAppStart({ description: 'Test', span_id: '123', trace_id: '456', + data: {}, }, ]), }); @@ -939,7 +941,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), @@ -959,6 +961,7 @@ function expectEventWithAttachedWarmAppStart({ description: 'Test', span_id: '123', trace_id: '456', + data: {}, }, ]), }); @@ -996,7 +999,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), @@ -1047,7 +1050,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/userInteraction.test.ts b/packages/core/test/tracing/integrations/userInteraction.test.ts index a026e79c0e..1f86614c36 100644 --- a/packages/core/test/tracing/integrations/userInteraction.test.ts +++ b/packages/core/test/tracing/integrations/userInteraction.test.ts @@ -255,15 +255,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/reactnavigation.ttid.test.tsx b/packages/core/test/tracing/reactnavigation.ttid.test.tsx index 68d0f3bb91..ba01506feb 100644 --- a/packages/core/test/tracing/reactnavigation.ttid.test.tsx +++ b/packages/core/test/tracing/reactnavigation.ttid.test.tsx @@ -255,7 +255,7 @@ describe('React Navigation - TTID', () => { type: 'transaction', spans: expect.arrayContaining([ expect.objectContaining>({ - description: 'Cold App Start', + description: 'Cold Start', }), expect.objectContaining>({ data: { @@ -296,7 +296,7 @@ describe('React Navigation - TTID', () => { type: 'transaction', spans: expect.arrayContaining([ expect.objectContaining>({ - description: 'Cold App Start', + description: 'Cold Start', }), expect.objectContaining>({ data: { @@ -327,7 +327,7 @@ describe('React Navigation - TTID', () => { type: 'transaction', spans: expect.arrayContaining([ expect.objectContaining>({ - description: 'Cold App Start', + description: 'Cold Start', }), ]), measurements: expect.objectContaining['measurements']>({ @@ -678,7 +678,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/timetodisplaynative.web.test.tsx b/packages/core/test/tracing/timetodisplaynative.web.test.tsx index 507c33a745..c60dad0a75 100644 --- a/packages/core/test/tracing/timetodisplaynative.web.test.tsx +++ b/packages/core/test/tracing/timetodisplaynative.web.test.tsx @@ -1,6 +1,9 @@ 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; }); 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/tsconfig.build.json b/packages/core/tsconfig.build.json index e5da411ec9..90d9d08e63 100644 --- a/packages/core/tsconfig.build.json +++ b/packages/core/tsconfig.build.json @@ -25,9 +25,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 7f757b53ce..8645e59737 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -12,7 +12,6 @@ "plugin/**/*.ts" ], "exclude": ["dist"], - "allowSyntheticDefaultImports": true, "compilerOptions": { "rootDir": ".", "jsx": "react", diff --git a/samples/react-native-macos/package.json b/samples/react-native-macos/package.json index 302c961d7a..bad4d716f1 100644 --- a/samples/react-native-macos/package.json +++ b/samples/react-native-macos/package.json @@ -16,8 +16,8 @@ "@react-navigation/bottom-tabs": "^6.5.12", "@react-navigation/native": "^6.1.9", "@react-navigation/stack": "^6.3.20", - "@sentry/core": "8.54.0", - "@sentry/react": "8.54.0", + "@sentry/core": "9.12.0", + "@sentry/react": "9.12.0", "@sentry/react-native": "6.11.0-beta.0", "delay": "^6.0.0", "react": "18.2.0", diff --git a/samples/react-native/package.json b/samples/react-native/package.json index af759d0657..6fb1576ae4 100644 --- a/samples/react-native/package.json +++ b/samples/react-native/package.json @@ -27,7 +27,7 @@ "@react-navigation/native": "^7.0.14", "@react-navigation/native-stack": "^7.2.0", "@react-navigation/stack": "^7.1.1", - "@sentry/core": "8.54.0", + "@sentry/core": "9.12.0", "@sentry/react-native": "6.11.0-beta.0", "@shopify/flash-list": "^1.7.3", "axios": "^1.8.3", @@ -74,7 +74,7 @@ "prettier": "2.8.8", "react-test-renderer": "18.3.1", "sentry-react-native-samples-utils": "workspace:^", - "ts-jest": "^29.2.5", + "ts-jest": "^29.3.1", "typescript": "5.0.4" }, "engines": { diff --git a/yarn.lock b/yarn.lock index ff6a9db566..1ea089d174 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8183,68 +8183,67 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/browser-utils@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry-internal/browser-utils@npm:8.54.0" +"@sentry-internal/browser-utils@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry-internal/browser-utils@npm:9.12.0" dependencies: - "@sentry/core": 8.54.0 - checksum: a6dc51bcf0b6029ebb49dd3478bd216da5aac118512ae247a3fa4c61b809a6caa27ee1531dc49cdab18f340f6a0cb337cc160c217e6af60060cb50aa2ab15abd + "@sentry/core": 9.12.0 + checksum: b44b14bffdb7ffe0ea8efd8a8930563af8458c16343f07ed179569cdcb01bc56f99f821ef4782c9fa52d8c500bf4a409cb9ddc3c785bfe887033e7f86449b15f languageName: node linkType: hard -"@sentry-internal/eslint-config-sdk@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry-internal/eslint-config-sdk@npm:8.54.0" +"@sentry-internal/eslint-config-sdk@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry-internal/eslint-config-sdk@npm:9.12.0" dependencies: - "@sentry-internal/eslint-plugin-sdk": 8.54.0 - "@sentry-internal/typescript": 8.54.0 + "@sentry-internal/eslint-plugin-sdk": 9.12.0 + "@sentry-internal/typescript": 9.12.0 "@typescript-eslint/eslint-plugin": ^5.48.0 "@typescript-eslint/parser": ^5.48.0 eslint-config-prettier: ^6.11.0 eslint-plugin-deprecation: ^1.5.0 eslint-plugin-import: ^2.22.0 - eslint-plugin-jest: ^27.2.2 eslint-plugin-jsdoc: ^30.0.3 eslint-plugin-simple-import-sort: ^5.0.3 peerDependencies: eslint: ">=5" - checksum: 4b4ed9e455517ab9ca90f0d9ab8f9a27342dd2635cd3464bda6b06430ab44028c421f4c7a214a034c20514e950380cd7f37f825a0a60d5b6c07d5e7040cbc1b3 + checksum: 1d4151bc3a4e566579438c93798d8eb91cf004bad19ccd559981ac2abf56b7a62e3e099101fe558119052ffe3f632ace4e91649def9cb74cc98616a0479013e3 languageName: node linkType: hard -"@sentry-internal/eslint-plugin-sdk@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry-internal/eslint-plugin-sdk@npm:8.54.0" - checksum: b9f3d9512df82d1e9cbef100b2f19626a1ede9b25eee6ff1f0e163f74f087a9de6dc6705c58c01cedd4b1782b2db31f8c158ee8a05552db8b49315c3917b9d92 +"@sentry-internal/eslint-plugin-sdk@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry-internal/eslint-plugin-sdk@npm:9.12.0" + checksum: 2ac6e8aeb1b22689232c8cd77fd19761e6ec7a6a3f7ae0f8940b28bcedf2c364f6bdb44b0e4f604fe471438c33f722c2d5e4c9020b391be85a03e4ea518be4d9 languageName: node linkType: hard -"@sentry-internal/feedback@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry-internal/feedback@npm:8.54.0" +"@sentry-internal/feedback@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry-internal/feedback@npm:9.12.0" dependencies: - "@sentry/core": 8.54.0 - checksum: e99ca3997609deba59af450b6298dc5125cc7633a59751632cf3b4a7cf12f097cc3ffa9424c483489a0d232a6c577479fe5496355e6a5c2b3ada0c7aa72e7251 + "@sentry/core": 9.12.0 + checksum: 6245100c50b48c178bc2b54999ec6c05614b3de3612902d67ac7cafc1e32fbc2735922461ce6afa82d7a5b1d389c98e65b0a822be739345b05e4ab2676f3ac00 languageName: node linkType: hard -"@sentry-internal/replay-canvas@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry-internal/replay-canvas@npm:8.54.0" +"@sentry-internal/replay-canvas@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry-internal/replay-canvas@npm:9.12.0" dependencies: - "@sentry-internal/replay": 8.54.0 - "@sentry/core": 8.54.0 - checksum: ed11de4c3503833d1e3503e521d0945eb393e16c6e5a2da3b5eae8632540a96530e465e8b6947879440ef19aba5f0d13ce36c4400955fcb310de2882c8610887 + "@sentry-internal/replay": 9.12.0 + "@sentry/core": 9.12.0 + checksum: 5ff030866e8a88273b320cafd3ce89f2816c58dc57e9f2f4900aa4364d59b6a71655bc618ec32d9f03631d68ce2cc11539567bf18c4564ffd32363c88282bc8e languageName: node linkType: hard -"@sentry-internal/replay@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry-internal/replay@npm:8.54.0" +"@sentry-internal/replay@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry-internal/replay@npm:9.12.0" dependencies: - "@sentry-internal/browser-utils": 8.54.0 - "@sentry/core": 8.54.0 - checksum: 2d46529612cb279ef53e8fb0f96d9da7b0526e9c3990af27620cb1f10a3f66b81966121d904da08b4ad07c0ae162230dfa39a9572127033b62ca2aae9b9008d8 + "@sentry-internal/browser-utils": 9.12.0 + "@sentry/core": 9.12.0 + checksum: 47074f5f316860b0909d892132367104b38ad4c75ec6545b3ab484d3af0ec0c873ca2679582769ebf76c80b43ac26347dc33a9cf1e3b79ddb2043eef5dcb3c69 languageName: node linkType: hard @@ -8259,12 +8258,12 @@ __metadata: languageName: node linkType: hard -"@sentry-internal/typescript@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry-internal/typescript@npm:8.54.0" +"@sentry-internal/typescript@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry-internal/typescript@npm:9.12.0" peerDependencies: - typescript: 4.9.5 - checksum: 83cfd303f34dbc5e70fc18e0af788ae37cdc0635258c082924e022b44177bac5c9c589448b368fbeeafb282a22623b5a6ea0e5ef3140c3f54fce1ac222e1920b + typescript: ~5.0.0 + checksum: dfcbb2a0df26196d0ba87d53389784f7291cb01f85a1fe10ad51033c9e01a1f3c5397c08ee3c3fce475f6290e37291ab5d8aca563d2030cc9072232975aeff89 languageName: node linkType: hard @@ -8275,16 +8274,16 @@ __metadata: languageName: node linkType: hard -"@sentry/browser@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry/browser@npm:8.54.0" +"@sentry/browser@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry/browser@npm:9.12.0" dependencies: - "@sentry-internal/browser-utils": 8.54.0 - "@sentry-internal/feedback": 8.54.0 - "@sentry-internal/replay": 8.54.0 - "@sentry-internal/replay-canvas": 8.54.0 - "@sentry/core": 8.54.0 - checksum: b125b194877cbdefc76884f6b1b7995d0949f7e7e5c4283c69f01071656df0e571bd0ac1cf1bb47f89f268370dd8b69126db44e2949ee5176a043e615c5bc7e0 + "@sentry-internal/browser-utils": 9.12.0 + "@sentry-internal/feedback": 9.12.0 + "@sentry-internal/replay": 9.12.0 + "@sentry-internal/replay-canvas": 9.12.0 + "@sentry/core": 9.12.0 + checksum: 4acd1e65dd3a4ce6ca2150a1945ace0ab50e482aa0dd39ed1a3fc1fd3be9d7318af2781f947faf594aaee9d32c58650743527de56d0aeb46b8c5aa1867c37e9e languageName: node linkType: hard @@ -8394,10 +8393,10 @@ __metadata: languageName: node linkType: hard -"@sentry/core@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry/core@npm:8.54.0" - checksum: ad7165a0bc56918cf8b8e5e15c4ceed11358970d8bf4c587a0fb05d8bd2759ee1d965da2a51e705e0e0aaf3a84150ecbf3bd647bfa0dd32bc723b28b91d46a66 +"@sentry/core@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry/core@npm:9.12.0" + checksum: 6bf8b5f7fe91897a598c608a17a6b2024d59c2055e5714629aba280ba7004dae4171481b455d61514367d264b3e2a5b2e82fd0cc264993020cc8fd93ddac03dc languageName: node linkType: hard @@ -8434,16 +8433,15 @@ __metadata: "@expo/metro-config": 0.19.5 "@mswjs/interceptors": ^0.25.15 "@react-native/babel-preset": 0.77.1 - "@sentry-internal/eslint-config-sdk": 8.54.0 - "@sentry-internal/eslint-plugin-sdk": 8.54.0 - "@sentry-internal/typescript": 8.54.0 + "@sentry-internal/eslint-config-sdk": 9.12.0 + "@sentry-internal/eslint-plugin-sdk": 9.12.0 + "@sentry-internal/typescript": 9.12.0 "@sentry/babel-plugin-component-annotate": 3.3.1 - "@sentry/browser": 8.54.0 + "@sentry/browser": 9.12.0 "@sentry/cli": 2.43.0 - "@sentry/core": 8.54.0 - "@sentry/react": 8.54.0 - "@sentry/types": 8.54.0 - "@sentry/utils": 8.54.0 + "@sentry/core": 9.12.0 + "@sentry/react": 9.12.0 + "@sentry/types": 9.12.0 "@sentry/wizard": 4.7.0 "@testing-library/react-native": ^12.7.2 "@types/jest": ^29.5.13 @@ -8473,7 +8471,7 @@ __metadata: react-native: 0.77.1 react-test-renderer: ^18.3.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 @@ -8490,16 +8488,16 @@ __metadata: languageName: unknown linkType: soft -"@sentry/react@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry/react@npm:8.54.0" +"@sentry/react@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry/react@npm:9.12.0" dependencies: - "@sentry/browser": 8.54.0 - "@sentry/core": 8.54.0 + "@sentry/browser": 9.12.0 + "@sentry/core": 9.12.0 hoist-non-react-statics: ^3.3.2 peerDependencies: react: ^16.14.0 || 17.x || 18.x || 19.x - checksum: 6f6d12f4db77f137b7b4361bfe298f9eba19b865e56701f7ba336ffedada683bd4baa962747f8b96a91d3399b1317206689e8a7565fb06b930b71440cd8a00a1 + checksum: 67f1d406a6a2232de72bfe3fe7cf33a95c925b442b9b2d56d2dcab4b53d40e34a19c6fbfb4bd794858dceb5db0f60ec7b26631bf924c119abb083b8ca789e394 languageName: node linkType: hard @@ -8510,12 +8508,12 @@ __metadata: languageName: node linkType: hard -"@sentry/types@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry/types@npm:8.54.0" +"@sentry/types@npm:9.12.0": + version: 9.12.0 + resolution: "@sentry/types@npm:9.12.0" dependencies: - "@sentry/core": 8.54.0 - checksum: 948095843efa280ee1a1dad5ac6dc20688395c25b344860d22505de14a10b1f873bcbeaaf32ba8506955a789a9c0fb0df4e80b93dfee21b1847423e954d0e616 + "@sentry/core": 9.12.0 + checksum: 3cbb403828014e5aea7b92ebb9e3dc7c14d4e6bea0f3e0a54b26f33ddd5e68b457c5a217c9db055857202475a380d0a2733cbe24c2727faa2be48a4e12395512 languageName: node linkType: hard @@ -8528,15 +8526,6 @@ __metadata: languageName: node linkType: hard -"@sentry/utils@npm:8.54.0": - version: 8.54.0 - resolution: "@sentry/utils@npm:8.54.0" - dependencies: - "@sentry/core": 8.54.0 - checksum: 063c31528f4d3d0f17be18050c84fad12a8a62fde97ca8e0e10c0cc01323e644d8d1cbba4b95b20de5d62eaee23ba43ae51b479057da4fd59dbccd49ac97277a - languageName: node - linkType: hard - "@sentry/wizard@npm:4.7.0": version: 4.7.0 resolution: "@sentry/wizard@npm:4.7.0" @@ -14475,7 +14464,7 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-jest@npm:^27.2.2, eslint-plugin-jest@npm:^27.9.0": +"eslint-plugin-jest@npm:^27.9.0": version: 27.9.0 resolution: "eslint-plugin-jest@npm:27.9.0" dependencies: @@ -25002,7 +24991,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:7.6.3, semver@npm:7.x, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:^7.6.3, semver@npm:~7.6.3": +"semver@npm:7.6.3, semver@npm:7.x, semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.2.1, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0, semver@npm:~7.6.3": version: 7.6.3 resolution: "semver@npm:7.6.3" bin: @@ -25020,6 +25009,15 @@ __metadata: languageName: node linkType: hard +"semver@npm:^7.7.1": + version: 7.7.1 + resolution: "semver@npm:7.7.1" + bin: + semver: bin/semver.js + checksum: 586b825d36874007c9382d9e1ad8f93888d8670040add24a28e06a910aeebd673a2eb9e3bf169c6679d9245e66efb9057e0852e70d9daa6c27372aab1dda7104 + languageName: node + linkType: hard + "send@npm:0.18.0": version: 0.18.0 resolution: "send@npm:0.18.0" @@ -25089,7 +25087,7 @@ __metadata: dependencies: "@babel/preset-env": ^7.25.3 "@babel/preset-typescript": ^7.18.6 - "@sentry/core": 8.54.0 + "@sentry/core": 9.12.0 "@sentry/react-native": 6.11.0-beta.0 "@types/node": ^20.9.3 "@types/react": ^18.2.64 @@ -25156,8 +25154,8 @@ __metadata: "@react-navigation/bottom-tabs": ^6.5.12 "@react-navigation/native": ^6.1.9 "@react-navigation/stack": ^6.3.20 - "@sentry/core": 8.54.0 - "@sentry/react": 8.54.0 + "@sentry/core": 9.12.0 + "@sentry/react": 9.12.0 "@sentry/react-native": 6.11.0-beta.0 "@types/react": ^18.2.65 "@types/react-native-vector-icons": ^6.4.18 @@ -25204,7 +25202,7 @@ __metadata: "@react-navigation/native-stack": ^7.2.0 "@react-navigation/stack": ^7.1.1 "@sentry/babel-plugin-component-annotate": 3.3.1 - "@sentry/core": 8.54.0 + "@sentry/core": 9.12.0 "@sentry/react-native": 6.11.0-beta.0 "@shopify/flash-list": ^1.7.3 "@types/jest": ^29.5.14 @@ -25238,7 +25236,7 @@ __metadata: react-test-renderer: 18.3.1 redux: ^4.2.1 sentry-react-native-samples-utils: "workspace:^" - ts-jest: ^29.2.5 + ts-jest: ^29.3.1 typescript: 5.0.4 languageName: unknown linkType: soft @@ -26802,19 +26800,20 @@ __metadata: languageName: node linkType: hard -"ts-jest@npm:^29.1.1, ts-jest@npm:^29.2.5": - version: 29.2.5 - resolution: "ts-jest@npm:29.2.5" +"ts-jest@npm:^29.3.1": + version: 29.3.1 + resolution: "ts-jest@npm:29.3.1" dependencies: - bs-logger: "npm:^0.2.6" - ejs: "npm:^3.1.10" - fast-json-stable-stringify: "npm:^2.1.0" - jest-util: "npm:^29.0.0" - json5: "npm:^2.2.3" - lodash.memoize: "npm:^4.1.2" - make-error: "npm:^1.3.6" - semver: "npm:^7.6.3" - yargs-parser: "npm:^21.1.1" + bs-logger: ^0.2.6 + ejs: ^3.1.10 + fast-json-stable-stringify: ^2.1.0 + jest-util: ^29.0.0 + json5: ^2.2.3 + lodash.memoize: ^4.1.2 + make-error: ^1.3.6 + semver: ^7.7.1 + type-fest: ^4.38.0 + yargs-parser: ^21.1.1 peerDependencies: "@babel/core": ">=7.0.0-beta.0 <8" "@jest/transform": ^29.0.0 @@ -26835,7 +26834,7 @@ __metadata: optional: true bin: ts-jest: cli.js - checksum: d60d1e1d80936f6002b1bb27f7e062408bc733141b9d666565503f023c340a3196d506c836a4316c5793af81a5f910ab49bb9c13f66e2dc66de4e0f03851dbca + checksum: 7320bbfab2e2cc7d5308fadcfe486d24ffc0018f92bf05946cd3ac75888977ec52887b2beaa99ec5ee3d67b8636b56a1688db447fd2794eeefc498862e850f77 languageName: node linkType: hard @@ -27080,6 +27079,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^4.38.0": + version: 4.39.1 + resolution: "type-fest@npm:4.39.1" + checksum: 71ce0e25822d5d984c8882e3243c42c741f089c003d89222dc361a133698d8ac021c18a9139ba3baf0fee2f523383422b8c5aaa390b1b3a98140f23dd0f44795 + languageName: node + linkType: hard + "type-fest@npm:^4.4.0": version: 4.26.0 resolution: "type-fest@npm:4.26.0"