From eb6f266f36d96a10438872d97c5d04c5ac47a616 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Wed, 20 May 2026 13:56:18 +0200 Subject: [PATCH 1/2] feat(core): Expose `pauseAppHangTracking` and `resumeAppHangTracking` APIs Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 1 + .../io/sentry/react/RNSentryModuleImpl.java | 8 +++ .../java/io/sentry/react/RNSentryModule.java | 10 ++++ .../java/io/sentry/react/RNSentryModule.java | 10 ++++ packages/core/etc/sentry-react-native.api.md | 6 +++ packages/core/ios/RNSentry.mm | 4 ++ packages/core/ios/SentrySDKWrapper.h | 4 ++ packages/core/ios/SentrySDKWrapper.m | 10 ++++ packages/core/src/js/NativeRNSentry.ts | 2 + packages/core/src/js/index.ts | 13 ++++- packages/core/src/js/sdk.tsx | 28 ++++++++++ packages/core/src/js/wrapper.ts | 24 +++++++++ packages/core/test/mockWrapper.ts | 2 + packages/core/test/wrapper.test.ts | 54 +++++++++++++++++++ 14 files changed, 175 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 090a346215..7e40168be2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Add `textComponentNames` option to `annotateReactComponents` for custom text components ([#6169](https://github.com/getsentry/sentry-react-native/pull/6169)) - Expose `addConsoleInstrumentationFilter` from `@sentry/core` ([#6180](https://github.com/getsentry/sentry-react-native/pull/6180)) - Expose experimental `captureSurfaceViews` option for Android Session Replay ([#6175](https://github.com/getsentry/sentry-react-native/pull/6175)) +- Expose `pauseAppHangTracking` and `resumeAppHangTracking` APIs on iOS ([#6192](https://github.com/getsentry/sentry-react-native/pull/6192)) ### Fixes 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 f2f492bd8d..7fb53b8ef5 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 @@ -279,6 +279,14 @@ public void disableShakeDetection() { stopShakeDetection(); } + public void pauseAppHangTracking() { + // No-op: App hang tracking is iOS-only + } + + public void resumeAppHangTracking() { + // No-op: App hang tracking is iOS-only + } + public void fetchModules(Promise promise) { final AssetManager assets = this.getReactApplicationContext().getResources().getAssets(); try (InputStream stream = new BufferedInputStream(assets.open(modulesPath))) { diff --git a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java index 82d2622996..973480ef64 100644 --- a/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/newarch/java/io/sentry/react/RNSentryModule.java @@ -234,6 +234,16 @@ public void disableShakeDetection() { this.impl.disableShakeDetection(); } + @Override + public void pauseAppHangTracking() { + this.impl.pauseAppHangTracking(); + } + + @Override + public void resumeAppHangTracking() { + this.impl.resumeAppHangTracking(); + } + @Override public void invalidate() { this.impl.invalidate(); diff --git a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java index 099623f056..9d1bac1028 100644 --- a/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java +++ b/packages/core/android/src/oldarch/java/io/sentry/react/RNSentryModule.java @@ -234,6 +234,16 @@ public void disableShakeDetection() { this.impl.disableShakeDetection(); } + @ReactMethod + public void pauseAppHangTracking() { + this.impl.pauseAppHangTracking(); + } + + @ReactMethod + public void resumeAppHangTracking() { + this.impl.resumeAppHangTracking(); + } + @Override public void invalidate() { this.impl.invalidate(); diff --git a/packages/core/etc/sentry-react-native.api.md b/packages/core/etc/sentry-react-native.api.md index bacfa15508..d33a00911c 100644 --- a/packages/core/etc/sentry-react-native.api.md +++ b/packages/core/etc/sentry-react-native.api.md @@ -490,6 +490,9 @@ export { OpenAiClient } export { OpenAiOptions } +// @public +export function pauseAppHangTracking(): void; + // @public export const primitiveTagIntegration: () => Integration; @@ -554,6 +557,9 @@ export const reactNavigationIntegration: (input?: Partial; enableShakeDetection(): void; disableShakeDetection(): void; + pauseAppHangTracking(): void; + resumeAppHangTracking(): void; } export type NativeStackFrame = { diff --git a/packages/core/src/js/index.ts b/packages/core/src/js/index.ts index 2463ca3c2c..3bd8270ea6 100644 --- a/packages/core/src/js/index.ts +++ b/packages/core/src/js/index.ts @@ -98,7 +98,18 @@ export { SDK_NAME, SDK_VERSION } from './version'; export type { ReactNativeOptions, NativeLogEntry } from './options'; export { ReactNativeClient } from './client'; -export { init, wrap, nativeCrash, flush, close, withScope, crashedLastRun, appLoaded } from './sdk'; +export { + init, + wrap, + nativeCrash, + flush, + close, + withScope, + crashedLastRun, + appLoaded, + pauseAppHangTracking, + resumeAppHangTracking, +} from './sdk'; export { TouchEventBoundary, withTouchEventBoundary } from './touchevents'; export { GlobalErrorBoundary, withGlobalErrorBoundary } from './GlobalErrorBoundary'; export type { GlobalErrorBoundaryProps } from './GlobalErrorBoundary'; diff --git a/packages/core/src/js/sdk.tsx b/packages/core/src/js/sdk.tsx index 6fffe6d66f..84cee9a088 100644 --- a/packages/core/src/js/sdk.tsx +++ b/packages/core/src/js/sdk.tsx @@ -312,3 +312,31 @@ export function withScope(callback: (scope: Scope) => T): T | undefined { export async function crashedLastRun(): Promise { return NATIVE.crashedLastRun(); } + +/** + * Pauses app hang tracking on iOS. + * + * App hang detection will ignore detected app hangs until + * `resumeAppHangTracking` is called. + * + * Use this when showing system dialogs (e.g., permission prompts) + * that block the main thread but are not real hangs. + * + * No-op on Android and when native is not available. + * + * @platform ios + */ +export function pauseAppHangTracking(): void { + NATIVE.pauseAppHangTracking(); +} + +/** + * Resumes app hang tracking on iOS after it was paused. + * + * No-op on Android and when native is not available. + * + * @platform ios + */ +export function resumeAppHangTracking(): void { + NATIVE.resumeAppHangTracking(); +} diff --git a/packages/core/src/js/wrapper.ts b/packages/core/src/js/wrapper.ts index c9d5236610..6b337bf20b 100644 --- a/packages/core/src/js/wrapper.ts +++ b/packages/core/src/js/wrapper.ts @@ -98,6 +98,8 @@ interface SentryNativeWrapper { disableNativeFramesTracking(): void; enableNativeFramesTracking(): void; + pauseAppHangTracking(): void; + resumeAppHangTracking(): void; addBreadcrumb(breadcrumb: Breadcrumb): void; // oxlint-disable-next-line typescript-eslint(no-explicit-any) @@ -669,6 +671,28 @@ export const NATIVE: SentryNativeWrapper = { RNSentry.enableNativeFramesTracking(); }, + pauseAppHangTracking(): void { + if (!this.enableNative) { + return; + } + if (!this._isModuleLoaded(RNSentry)) { + return; + } + + RNSentry.pauseAppHangTracking(); + }, + + resumeAppHangTracking(): void { + if (!this.enableNative) { + return; + } + if (!this._isModuleLoaded(RNSentry)) { + return; + } + + RNSentry.resumeAppHangTracking(); + }, + isNativeAvailable(): boolean { if (!RNSentry) { RNSentry = getRNSentryModule(); diff --git a/packages/core/test/mockWrapper.ts b/packages/core/test/mockWrapper.ts index 86612bafa8..d1814169e5 100644 --- a/packages/core/test/mockWrapper.ts +++ b/packages/core/test/mockWrapper.ts @@ -36,6 +36,8 @@ const NATIVE: MockInterface = { disableNativeFramesTracking: jest.fn(), enableNativeFramesTracking: jest.fn(), + pauseAppHangTracking: jest.fn(), + resumeAppHangTracking: jest.fn(), addBreadcrumb: jest.fn(), setContext: jest.fn(), diff --git a/packages/core/test/wrapper.test.ts b/packages/core/test/wrapper.test.ts index e7a3f164cd..6f5699089f 100644 --- a/packages/core/test/wrapper.test.ts +++ b/packages/core/test/wrapper.test.ts @@ -59,6 +59,8 @@ jest.mock('react-native', () => { _getLastPayload: () => ({ initPayload }), startProfiling: jest.fn(), stopProfiling: jest.fn(), + pauseAppHangTracking: jest.fn(), + resumeAppHangTracking: jest.fn(), }; return { @@ -1191,6 +1193,58 @@ describe('Tests Native Wrapper', () => { }); }); + describe('pauseAppHangTracking', () => { + test('calls native pauseAppHangTracking', async () => { + await NATIVE.initNativeSdk({ + dsn: VALID_DSN, + enableNative: true, + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + NATIVE.pauseAppHangTracking(); + expect(RNSentry.pauseAppHangTracking).toHaveBeenCalled(); + }); + + test('does not call native when enableNative is false', async () => { + await NATIVE.initNativeSdk({ + dsn: VALID_DSN, + enableNative: false, + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + NATIVE.pauseAppHangTracking(); + expect(RNSentry.pauseAppHangTracking).not.toHaveBeenCalled(); + }); + }); + + describe('resumeAppHangTracking', () => { + test('calls native resumeAppHangTracking', async () => { + await NATIVE.initNativeSdk({ + dsn: VALID_DSN, + enableNative: true, + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + NATIVE.resumeAppHangTracking(); + expect(RNSentry.resumeAppHangTracking).toHaveBeenCalled(); + }); + + test('does not call native when enableNative is false', async () => { + await NATIVE.initNativeSdk({ + dsn: VALID_DSN, + enableNative: false, + devServerUrl: undefined, + defaultSidecarUrl: undefined, + mobileReplayOptions: undefined, + }); + NATIVE.resumeAppHangTracking(); + expect(RNSentry.resumeAppHangTracking).not.toHaveBeenCalled(); + }); + }); + describe('primitiveProcessor and _setPrimitiveProcessor', () => { describe('primitiveProcessor', () => { it('default primitiveProcessor returns value as string', () => { From a7e380142a3ff28c564b86d9791756ec1bd28935 Mon Sep 17 00:00:00 2001 From: Antonis Lilis Date: Thu, 21 May 2026 11:42:26 +0200 Subject: [PATCH 2/2] Fix changelog --- CHANGELOG.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19384ee805..5b688ba123 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,12 @@ > make sure you follow our [migration guide](https://docs.sentry.io/platforms/react-native/migration/) first. +## Unreleased + +### Features + +- Expose `pauseAppHangTracking` and `resumeAppHangTracking` APIs on iOS ([#6192](https://github.com/getsentry/sentry-react-native/pull/6192)) + ## 8.12.0 ### Features @@ -20,7 +26,6 @@ - Add first-class `expoRouterIntegration()` with auto-registration ([#6189](https://github.com/getsentry/sentry-react-native/pull/6189)) - Expose `addConsoleInstrumentationFilter` from `@sentry/core` ([#6180](https://github.com/getsentry/sentry-react-native/pull/6180)) - Expose experimental `captureSurfaceViews` option for Android Session Replay ([#6175](https://github.com/getsentry/sentry-react-native/pull/6175)) -- Expose `pauseAppHangTracking` and `resumeAppHangTracking` APIs on iOS ([#6192](https://github.com/getsentry/sentry-react-native/pull/6192)) - Add OTA SDK version to native `sdk.packages` when JS bundle version differs from built-in version ([#6191](https://github.com/getsentry/sentry-react-native/pull/6191)) ### Fixes