Repository: getsentry/sentry-react-native
Pull request #4826: fix(promise): Use engine-specific promise rejection tracking
Author: login: antonis
Created: 2025-05-13T12:31:52Z | Merged: 2025-05-20T12:26:20Z

## :loudspeaker: Type of change
<!--- Put an `x` in the boxes that apply -->
- [x] Bugfix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactoring


## :scroll: Description
<!--- Describe your changes in detail -->
- Uses Hermes's native tracker when available
- Leverages Sentry Browser SDK for React Native Web
- Falls back to existing implementation for JSC

## :bulb: Motivation and Context
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Fixes https://github.com/getsentry/sentry-react-native/issues/3957

## :green_heart: How did you test it?
CI, Manual ([mobile](https://sentry-sdks.sentry.io/issues/6018946733/events/latest/?project=5428561&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D&referrer=latest-event&stream_index=0), [web](https://sentry-sdks.sentry.io/issues/6604676621/events/latest/?project=5428561&query=is%3Aunresolved%20issue.priority%3A%5Bhigh%2C%20medium%5D&referrer=latest-event&stream_index=0))

## :pencil: Checklist
<!--- Put an `x` in the boxes that apply -->
- [x] I added tests to verify changes
- [x] No new PII added or SDK only sends newly added PII if `sendDefaultPII` is enabled
- [ ] I updated the docs if needed.
- [x] I updated the wizard if needed.
- [x] All tests passing
- [x] No breaking changes

## :crystal_ball: Next steps



Diff:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6d8a9bcef..0708a142d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@
 ### Fixes
 
 - crashedLastRun now returns the correct value ([#4829](https://github.com/getsentry/sentry-react-native/pull/4829))
+- Use engine-specific promise rejection tracking ([#4826](https://github.com/getsentry/sentry-react-native/pull/4826))
 
 ## 6.14.0
 
diff --git a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts
index 4025dc13d5..df56da47cb 100644
--- a/packages/core/src/js/integrations/reactnativeerrorhandlers.ts
+++ b/packages/core/src/js/integrations/reactnativeerrorhandlers.ts
@@ -1,6 +1,14 @@
 import type { EventHint, Integration, SeverityLevel } from '@sentry/core';
-import { addExceptionMechanism, captureException, getClient, getCurrentScope, logger } from '@sentry/core';
-
+import {
+  addExceptionMechanism,
+  addGlobalUnhandledRejectionInstrumentationHandler,
+  captureException,
+  getClient,
+  getCurrentScope,
+  logger,
+} from '@sentry/core';
+
+import { isHermesEnabled, isWeb } from '../utils/environment';
 import { createSyntheticError, isErrorLike } from '../utils/error';
 import { RN_GLOBAL_OBJ } from '../utils/worldwide';
 import { checkPromiseAndWarn, polyfillPromise, requireRejectionTracking } from './reactnativeerrorhandlersutils';
@@ -44,49 +52,83 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (
+      isHermesEnabled() &&
+      RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker &&
+      RN_GLOBAL_OBJ?.HermesInternal?.hasPromise?.()
+    ) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: promiseRejectionTrackingOptions.onUnhandled,
+        onHandled: promiseRejectionTrackingOptions.onHandled,
+      });
 
-  attachUnhandledRejectionHandler();
-  checkPromiseAndWarn();
+      logger.log('Unhandled promise rejections will be caught by Sentry.');
+    } else if (isWeb()) {
+      logger.log('Using Browser JS promise rejection tracking for React Native Web');
+
+      // Use Sentry's built-in global unhandled rejection handler
+      addGlobalUnhandledRejectionInstrumentationHandler((error: unknown) => {
+        captureException(error, {
+          originalException: error,
+          syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+          mechanism: { handled: false, type: 'onunhandledrejection' },
+        });
+      });
+    } else if (patchGlobalPromise) {
+      // For JSC and other environments, use the existing approach
+      polyfillPromise();
+      attachUnhandledRejectionHandler();
+      checkPromiseAndWarn();
+    } else {
+      // For JSC and other environments, patching was disabled by user configuration
+      logger.log('Unhandled promise rejections will not be caught by Sentry.');
+    }
+  } catch (e) {
+    logger.warn(
+      'Failed to set up promise rejection tracking. ' +
+        'Unhandled promise rejections will not be caught by Sentry.' +
+        'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.',
+    );
+  }
 }
 
-function attachUnhandledRejectionHandler(): void {
-  const tracking = requireRejectionTracking();
+const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = {
+  onUnhandled: (id, error: unknown, rejection = {}) => {
+    if (__DEV__) {
+      logger.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${rejection}`);
+    }
 
-  const promiseRejectionTrackingOptions: PromiseRejectionTrackingOptions = {
-    onUnhandled: (id, rejection = {}) => {
-      // eslint-disable-next-line no-console
-      console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${rejection}`);
-    },
-    onHandled: id => {
-      // eslint-disable-next-line no-console
-      console.warn(
+    // Marking the rejection as handled to avoid breaking crash rate calculations.
+    // See: https://github.com/getsentry/sentry-react-native/issues/4141
+    captureException(error, {
+      data: { id },
+      originalException: error,
+      syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+      mechanism: { handled: true, type: 'onunhandledrejection' },
+    });
+  },
+  onHandled: id => {
+    if (__DEV__) {
+      logger.warn(
         `Promise Rejection Handled (id: ${id})\n` +
           'This means you can ignore any previous messages of the form ' +
           `"Possible Unhandled Promise Rejection (id: ${id}):"`,
       );
-    },
-  };
+    }
+  },
+};
+
+function attachUnhandledRejectionHandler(): void {
+  const tracking = requireRejectionTracking();
 
   tracking.enable({
     allRejections: true,
-    onUnhandled: (id: string, error: unknown) => {
-      if (__DEV__) {
-        promiseRejectionTrackingOptions.onUnhandled(id, error);
-      }
-
-      captureException(error, {
-        data: { id },
-        originalException: error,
-        syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
-        mechanism: { handled: true, type: 'onunhandledrejection' },
-      });
-    },
-    onHandled: (id: string) => {
-      promiseRejectionTrackingOptions.onHandled(id);
-    },
+    onUnhandled: promiseRejectionTrackingOptions.onUnhandled,
+    onHandled: promiseRejectionTrackingOptions.onHandled,
   });
 }
 
diff --git a/packages/core/src/js/utils/worldwide.ts b/packages/core/src/js/utils/worldwide.ts
index 03327bac36..04c854984a 100644
--- a/packages/core/src/js/utils/worldwide.ts
+++ b/packages/core/src/js/utils/worldwide.ts
@@ -3,6 +3,11 @@ import { GLOBAL_OBJ } from '@sentry/core';
 import type { ErrorUtils } from 'react-native/types';
 
 import type { ExpoGlobalObject } from './expoglobalobject';
+export interface HermesPromiseRejectionTrackingOptions {
+  allRejections: boolean;
+  onUnhandled: (id: string, error: unknown) => void;
+  onHandled: (id: string) => void;
+}
 
 /** Internal Global object interface with common and Sentry specific properties */
 export interface ReactNativeInternalGlobal extends InternalGlobal {
@@ -10,6 +15,8 @@ export interface ReactNativeInternalGlobal extends InternalGlobal {
   __sentry_rn_v5_registered?: boolean;
   HermesInternal?: {
     getRuntimeProperties?: () => Record<string, string | undefined>;
+    enablePromiseRejectionTracker?: (options: HermesPromiseRejectionTrackingOptions) => void;
+    hasPromise?: () => boolean;
   };
   Promise: unknown;
   __turboModuleProxy: unknown;
diff --git a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts
index aff3b6210f..076008fc61 100644
--- a/packages/core/test/integrations/reactnativeerrorhandlers.test.ts
+++ b/packages/core/test/integrations/reactnativeerrorhandlers.test.ts
@@ -1,16 +1,53 @@
 jest.mock('../../src/js/integrations/reactnativeerrorhandlersutils');
+jest.mock('../../src/js/utils/environment');
 
-import type { ExtendedError, Mechanism, SeverityLevel } from '@sentry/core';
-import { setCurrentClient } from '@sentry/core';
+import type { SeverityLevel } from '@sentry/core';
+import { addGlobalUnhandledRejectionInstrumentationHandler, captureException, setCurrentClient } from '@sentry/core';
 
 import { reactNativeErrorHandlersIntegration } from '../../src/js/integrations/reactnativeerrorhandlers';
-import { requireRejectionTracking } from '../../src/js/integrations/reactnativeerrorhandlersutils';
+import {
+  checkPromiseAndWarn,
+  polyfillPromise,
+  requireRejectionTracking,
+} from '../../src/js/integrations/reactnativeerrorhandlersutils';
+import { isHermesEnabled, isWeb } from '../../src/js/utils/environment';
+import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide';
 import { getDefaultTestClientOptions, TestClient } from '../mocks/client';
 
+let errorHandlerCallback: ((error: Error, isFatal?: boolean) => Promise<void>) | null = null;
+
+jest.mock('../../src/js/utils/worldwide', () => {
+  const actual = jest.requireActual('../../src/js/utils/worldwide');
+  return {
+    ...actual,
+    RN_GLOBAL_OBJ: {
+      ...actual.RN_GLOBAL_OBJ,
+      ErrorUtils: {
+        setGlobalHandler: jest.fn(callback => {
+          errorHandlerCallback = callback;
+        }),
+        getGlobalHandler: jest.fn(() => jest.fn()),
+        reportError: jest.fn(),
+      },
+    },
+  };
+});
+
+jest.mock('@sentry/core', () => {
+  const actual = jest.requireActual('@sentry/core');
+  return {
+    ...actual,
+    captureException: jest.fn(),
+    addGlobalUnhandledRejectionInstrumentationHandler: jest.fn(),
+  };
+});
+
 describe('ReactNativeErrorHandlers', () => {
   let client: TestClient;
   let mockDisable: jest.Mock;
   let mockEnable: jest.Mock;
+  let originalHermesInternal: any;
+  let mockEnablePromiseRejectionTracker: jest.Mock;
 
   beforeEach(() => {
     mockDisable = jest.fn();
@@ -19,35 +56,48 @@ describe('ReactNativeErrorHandlers', () => {
       disable: mockDisable,
       enable: mockEnable,
     });
-    ErrorUtils.getGlobalHandler = () => jest.fn();
+    (polyfillPromise as jest.Mock).mockImplementation(() => {});
+    (checkPromiseAndWarn as jest.Mock).mockImplementation(() => {});
+
+    errorHandlerCallback = null;
+
+    (isWeb as jest.Mock).mockReturnValue(false);
+    (isHermesEnabled as jest.Mock).mockReturnValue(false);
+
+    originalHermesInternal = RN_GLOBAL_OBJ.HermesInternal;
 
     client = new TestClient(getDefaultTestClientOptions());
     setCurrentClient(client);
     client.init();
+
+    mockEnablePromiseRejectionTracker = jest.fn();
+    RN_GLOBAL_OBJ.HermesInternal = {
+      enablePromiseRejectionTracker: mockEnablePromiseRejectionTracker,
+      hasPromise: jest.fn(() => true),
+    };
+
+    jest.clearAllMocks();
   });
 
   afterEach(() => {
     jest.clearAllMocks();
+    RN_GLOBAL_OBJ.HermesInternal = originalHermesInternal;
   });
 
   describe('onError', () => {
-    let errorHandlerCallback: (error: Error, isFatal: boolean) => Promise<void>;
-
-    beforeEach(() => {
-      errorHandlerCallback = () => Promise.resolve();
-
-      ErrorUtils.setGlobalHandler = jest.fn(_callback => {
-        errorHandlerCallback = _callback as typeof errorHandlerCallback;
-      });
-
+    test('Sets up the global error handler', () => {
       const integration = reactNativeErrorHandlersIntegration();
-
       integration.setupOnce();
 
-      expect(ErrorUtils.setGlobalHandler).toHaveBeenCalledWith(errorHandlerCallback);
+      expect(RN_GLOBAL_OBJ.ErrorUtils.setGlobalHandler).toHaveBeenCalled();
     });
 
     test('Sets handled:false on a fatal error', async () => {
+      const integration = reactNativeErrorHandlersIntegration();
+      integration.setupOnce();
+
+      expect(errorHandlerCallback).not.toBeNull();
+
       await errorHandlerCallback(new Error('Test Error'), true);
       await client.flush();
 
@@ -59,6 +109,11 @@ describe('ReactNativeErrorHandlers', () => {
     });
 
     test('Does not set handled:false on a non-fatal error', async () => {
+      const integration = reactNativeErrorHandlersIntegration();
+      integration.setupOnce();
+
+      expect(errorHandlerCallback).not.toBeNull();
+
       await errorHandlerCallback(new Error('Test Error'), false);
       await client.flush();
 
@@ -70,6 +125,11 @@ describe('ReactNativeErrorHandlers', () => {
     });
 
     test('Includes original exception in hint', async () => {
+      const integration = reactNativeErrorHandlersIntegration();
+      integration.setupOnce();
+
+      expect(errorHandlerCallback).not.toBeNull();
+
       await errorHandlerCallback(new Error('Test Error'), false);
       await client.flush();
 
@@ -80,17 +140,14 @@ describe('ReactNativeErrorHandlers', () => {
   });
 
   describe('onUnhandledRejection', () => {
-    test('unhandled rejected promise is captured with synthetical error', async () => {
+    test('unhandled rejected promise is captured with JSC approach', async () => {
+      (isWeb as jest.Mock).mockReturnValue(false);
+      (isHermesEnabled as jest.Mock).mockReturnValue(false);
+
       const integration = reactNativeErrorHandlersIntegration();
       integration.setupOnce();
 
-      const [actualTrackingOptions] = mockEnable.mock.calls[0] || [];
-      actualTrackingOptions?.onUnhandled?.(1, 'Test Error');
-
-      await client.flush();
-      const actualSyntheticError = client.hint?.syntheticException;
-
-      expect(mockDisable).not.toHaveBeenCalled();
+      expect(polyfillPromise).toHaveBeenCalled();
       expect(mockEnable).toHaveBeenCalledWith(
         expect.objectContaining({
           allRejections: true,
@@ -98,53 +155,180 @@ describe('ReactNativeErrorHandlers', () => {
           onHandled: expect.any(Function),
         }),
       );
-      expect(mockEnable).toHaveBeenCalledTimes(1);
-      expect((actualSyntheticError as ExtendedError).framesToPop).toBe(3);
-    });
-
-    test('error like unhandled rejected promise is captured without synthetical error', async () => {
-      const integration = reactNativeErrorHandlersIntegration();
-      integration.setupOnce();
 
-      const [actualTrackingOptions] = mockEnable.mock.calls[0] || [];
-      actualTrackingOptions?.onUnhandled?.(1, new Error('Test Error'));
+      const [options] = mockEnable.mock.calls[0];
+      const onUnhandledHandler = options.onUnhandled;
 
-      await client.flush();
-      const actualSyntheticError = client.hint?.syntheticException;
+      onUnhandledHandler('test-id', 'Test Error');
 
-      expect(mockDisable).not.toHaveBeenCalled();
-      expect(mockEnable).toHaveBeenCalledWith(
+      expect(captureException).toHaveBeenCalledWith(
+        'Test Error',
         expect.objectContaining({
-          allRejections: true,
-          onUnhandled: expect.any(Function),
-          onHandled: expect.any(Function),
+          data: { id: 'test-id' },
+          originalException: 'Test Error',
+          mechanism: {
+            handled: true,
+            type: 'onunhandledrejection',
+          },
         }),
       );
-      expect(mockEnable).toHaveBeenCalledTimes(1);
-      expect(actualSyntheticError).toBeUndefined();
     });
 
-    test('unhandled rejected sets error mechanism', async () => {
+    test('error like unhandled rejected promise is captured without synthetical error', async () => {
+      (isWeb as jest.Mock).mockReturnValue(false);
+      (isHermesEnabled as jest.Mock).mockReturnValue(false);
+
       const integration = reactNativeErrorHandlersIntegration();
       integration.setupOnce();
 
-      const [actualTrackingOptions] = mockEnable.mock.calls[0] || [];
-      actualTrackingOptions?.onUnhandled?.(1, 'Test Error');
+      const [options] = mockEnable.mock.calls[0];
+      const onUnhandledHandler = options.onUnhandled;
 
-      await client.flush();
-      const actualSyntheticError = client.hint?.syntheticException;
-      const errorMechanism = client.event?.exception?.values[0]?.mechanism;
-      expect(mockDisable).not.toHaveBeenCalled();
-      expect(mockEnable).toHaveBeenCalledWith(
+      const error = new Error('Test Error');
+      onUnhandledHandler('test-id', error);
+
+      expect(captureException).toHaveBeenCalledWith(
+        error,
         expect.objectContaining({
-          allRejections: true,
-          onUnhandled: expect.any(Function),
-          onHandled: expect.any(Function),
+          data: { id: 'test-id' },
+          originalException: error,
+          syntheticException: undefined,
+          mechanism: {
+            handled: true,
+            type: 'onunhandledrejection',
+          },
         }),
       );
-      expect(mockEnable).toHaveBeenCalledTimes(1);
-      expect((actualSyntheticError as ExtendedError).framesToPop).toBe(3);
-      expect(errorMechanism).toEqual({ handled: true, type: 'onunhandledrejection' } as Mechanism);
+    });
+
+    describe('Hermes engine', () => {
+      beforeEach(() => {
+        (isHermesEnabled as jest.Mock).mockReturnValue(true);
+      });
+
+      test('uses native Hermes promise rejection tracking', () => {
+        const integration = reactNativeErrorHandlersIntegration();
+        integration.setupOnce();
+
+        expect(mockEnablePromiseRejectionTracker).toHaveBeenCalledTimes(1);
+        expect(mockEnablePromiseRejectionTracker).toHaveBeenCalledWith(
+          expect.objectContaining({
+            allRejections: true,
+            onUnhandled: expect.any(Function),
+            onHandled: expect.any(Function),
+          }),
+        );
+
+        expect(polyfillPromise).not.toHaveBeenCalled();
+      });
+
+      test('captures unhandled rejection with Hermes tracker', async () => {
+        const integration = reactNativeErrorHandlersIntegration();
+        integration.setupOnce();
+
+        const [options] = mockEnablePromiseRejectionTracker.mock.calls[0];
+        const onUnhandledHandler = options.onUnhandled;
+
+        const testError = new Error('Hermes Test Error');
+        onUnhandledHandler('hermes-test-error', testError);
+
+        expect(captureException).toHaveBeenCalledWith(
+          testError,
+          expect.objectContaining({
+            data: { id: 'hermes-test-error' },
+            originalException: testError,
+            mechanism: {
+              handled: true,
+              type: 'onunhandledrejection',
+            },
+          }),
+        );
+      });
+    });
+
+    describe('React Native Web', () => {
+      beforeEach(() => {
+        (isWeb as jest.Mock).mockReturnValue(true);
+        (isHermesEnabled as jest.Mock).mockReturnValue(false);
+      });
+
+      test('uses addGlobalUnhandledRejectionInstrumentationHandler for React Native Web', () => {
+        const integration = reactNativeErrorHandlersIntegration();
+        integration.setupOnce();
+
+        expect(addGlobalUnhandledRejectionInstrumentationHandler).toHaveBeenCalledTimes(1);
+        expect(addGlobalUnhandledRejectionInstrumentationHandler).toHaveBeenCalledWith(expect.any(Function));
+
+        // Verify JSC fallback was not used
+        expect(polyfillPromise).not.toHaveBeenCalled();
+        expect(requireRejectionTracking).not.toHaveBeenCalled();
+      });
+
+      test('captures unhandled rejection with the callback', () => {
+        const integration = reactNativeErrorHandlersIntegration();
+        integration.setupOnce();
+
+        const [callback] = (addGlobalUnhandledRejectionInstrumentationHandler as jest.Mock).mock.calls[0];
+
+        const mockError = new Error('Web Test Error');
+        callback(mockError);
+
+        expect(captureException).toHaveBeenCalledWith(
+          mockError,
+          expect.objectContaining({
+            originalException: mockError,
+            mechanism: {
+              handled: false,
+              type: 'onunhandledrejection',
+            },
+          }),
+        );
+      });
+
+      test('handles non-error rejection with synthetic error', () => {
+        const integration = reactNativeErrorHandlersIntegration();
+        integration.setupOnce();
+
+        const [callback] = (addGlobalUnhandledRejectionInstrumentationHandler as jest.Mock).mock.calls[0];
+
+        const nonErrorObject = { message: 'Custom rejection object' };
+        callback(nonErrorObject);
+
+        expect(captureException).toHaveBeenCalledWith(
+          nonErrorObject,
+          expect.objectContaining({
+            originalException: nonErrorObject,
+            syntheticException: expect.anything(),
+            mechanism: {
+              handled: false,
+              type: 'onunhandledrejection',
+            },
+          }),
+        );
+      });
+    });
+
+    describe('JSC and other environments', () => {
+      beforeEach(() => {
+        (isHermesEnabled as jest.Mock).mockReturnValue(false);
+        (isWeb as jest.Mock).mockReturnValue(false);
+      });
+
+      test('uses existing polyfill for JSC environments', () => {
+        const integration = reactNativeErrorHandlersIntegration();
+        integration.setupOnce();
+
+        expect(polyfillPromise).toHaveBeenCalledTimes(1);
+        expect(requireRejectionTracking).toHaveBeenCalledTimes(1);
+      });
+
+      test('respects patchGlobalPromise option', () => {
+        const integration = reactNativeErrorHandlersIntegration({ patchGlobalPromise: false });
+        integration.setupOnce();
+
+        expect(polyfillPromise).not.toHaveBeenCalled();
+        expect(requireRejectionTracking).not.toHaveBeenCalled();
+      });
     });
   });
 });


Reviews:
user: login: lucas-zimerman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T16:54:39Z
user: login: lucas-zimerman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T16:56:03Z
user: login: lucas-zimerman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T16:56:20Z
user: login: lucas-zimerman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T16:58:49Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-14T16:59:48Z
user: login: lucas-zimerman
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-14T17:05:15Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T07:42:56Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T07:42:58Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T07:43:01Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-15T07:43:04Z
user: login: lucas-zimerman
body: LGTM!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T13:22:53Z
user: login: krystofwoldrich
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T12:36:54Z
user: login: krystofwoldrich
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T12:51:00Z
user: login: krystofwoldrich
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T12:51:52Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:12:00Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T15:12:03Z
user: login: krystofwoldrich
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T10:45:24Z
user: login: antonis
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T10:55:33Z
user: login: krystofwoldrich
body: LGTM 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-20T11:58:20Z

Review comments:
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: lucas-zimerman
body: why handled true here?
created_at: 2025-05-14T16:54:38Z
author_association: COLLABORATOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: lucas-zimerman
body: I would use logger.warn here so that users can opt out from seeing this log in production.
created_at: 2025-05-14T16:56:02Z
author_association: COLLABORATOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
+          });
+        },
+        onHandled: (id: string) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: lucas-zimerman
body: same here
created_at: 2025-05-14T16:56:19Z
author_association: COLLABORATOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
+          });
+        },
+        onHandled: (id: string) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(
+              `Promise Rejection Handled (id: ${id})\n` +
+                'This means you can ignore any previous messages of the form ' +
+                `"Possible Unhandled Promise Rejection (id: ${id})"`,
+            );
+          }
+        },
+      });
+
+      logger.log('Unhandled promise rejections will be caught by Sentry.');
+    } else if (isWeb()) {
+      logger.log('Using Browser JS promise rejection tracking for React Native Web');
+
+      // Use Sentry's built-in global unhandled rejection handler
+      addGlobalUnhandledRejectionInstrumentationHandler((error: unknown) => {
+        captureException(error, {
+          originalException: error,
+          syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+          mechanism: { handled: false, type: 'onunhandledrejection' },
+        });
+      });
 
-  attachUnhandledRejectionHandler();
-  checkPromiseAndWarn();
+      logger.warn(
+        'Unhandled promise rejections will not be caught by Sentry. ' +
+          'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.',
+      );
+    } else if (patchGlobalPromise) {
+      // For JSC and other environments, use the existing approach
+      polyfillPromise();
+      attachUnhandledRejectionHandler();
+      checkPromiseAndWarn();
+    } else {
+      // Patching was disabled by user configuration
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: lucas-zimerman
body: how users can disable this configuration on web? it will always enter on the else if on line 87
created_at: 2025-05-14T16:58:48Z
author_association: COLLABORATOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Good question @lucas-zimerman 👍 
I kept [the same `captureException` call from the existing `attachUnhandledRejectionHandler` below](https://github.com/getsentry/sentry-react-native/blob/11f67eb3cf750fae2a7182d0948450b6fb288791/packages/core/src/js/integrations/reactnativeerrorhandlers.ts#L84).
created_at: 2025-05-14T16:59:48Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: lucas-zimerman
body: For reference https://github.com/getsentry/sentry-react-native/issues/4141
And https://github.com/getsentry/sentry-react-native/pull/4457#discussion_r1922179466
```
We have to mark the rejection handled: true to avoid breaking crash rate calculations.

Rejections do not crash the app in RN at the moment.
```
Might be worth adding a small comment in the comment pointing to https://github.com/getsentry/sentry-react-native/issues/4141
created_at: 2025-05-14T17:05:15Z
author_association: COLLABORATOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
+          });
+        },
+        onHandled: (id: string) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(
+              `Promise Rejection Handled (id: ${id})\n` +
+                'This means you can ignore any previous messages of the form ' +
+                `"Possible Unhandled Promise Rejection (id: ${id})"`,
+            );
+          }
+        },
+      });
+
+      logger.log('Unhandled promise rejections will be caught by Sentry.');
+    } else if (isWeb()) {
+      logger.log('Using Browser JS promise rejection tracking for React Native Web');
+
+      // Use Sentry's built-in global unhandled rejection handler
+      addGlobalUnhandledRejectionInstrumentationHandler((error: unknown) => {
+        captureException(error, {
+          originalException: error,
+          syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+          mechanism: { handled: false, type: 'onunhandledrejection' },
+        });
+      });
 
-  attachUnhandledRejectionHandler();
-  checkPromiseAndWarn();
+      logger.warn(
+        'Unhandled promise rejections will not be caught by Sentry. ' +
+          'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.',
+      );
+    } else if (patchGlobalPromise) {
+      // For JSC and other environments, use the existing approach
+      polyfillPromise();
+      attachUnhandledRejectionHandler();
+      checkPromiseAndWarn();
+    } else {
+      // Patching was disabled by user configuration
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Good point. On the web we always use the [browser rejection tracking](https://github.com/getsentry/sentry-react-native/issues/3957#issuecomment-2262487656). I updated the comment and log to be more clear with 184ece469e3aabd800c5cd7b558aee08ed106e19. Let me know it that makes sense 🙏 
created_at: 2025-05-15T07:42:56Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
+          });
+        },
+        onHandled: (id: string) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Updated with 64f85dab7fad4c15abc37db44b4da3cd27a76e68
created_at: 2025-05-15T07:42:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Updated with 4e2149c7029e18e0a130a6341b589b8688b0d97f
created_at: 2025-05-15T07:43:01Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            // eslint-disable-next-line no-console
+            console.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Thank you for the context @lucas-zimerman 🙇 
I didn't know that. I've added comments with 92ae70abd4b415f60c72dd8b60a234e5c208e4de
created_at: 2025-05-15T07:43:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: krystofwoldrich
body: I think we should also check the `hasPromise` same as RN does it to ensure we are adding the tracker to the correct Promise.

https://github.com/facebook/react-native/blob/c89c5d7e3d981e5e11ae926bdd83567cbe61201a/packages/react-native/Libraries/Core/polyfillPromise.js#L25
created_at: 2025-05-19T12:36:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            logger.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          // Marking the rejection as handled to avoid breaking crash rate calculations.
+          // See: https://github.com/getsentry/sentry-react-native/issues/4141
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
+          });
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: krystofwoldrich
body: Since the handling logic is the same for Hermes Promise and JSC Promise Polyfill I think we should share the impl to avoid discrepancies. 
created_at: 2025-05-19T12:50:59Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            logger.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          // Marking the rejection as handled to avoid breaking crash rate calculations.
+          // See: https://github.com/getsentry/sentry-react-native/issues/4141
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
+          });
+        },
+        onHandled: (id: string) => {
+          if (__DEV__) {
+            logger.warn(
+              `Promise Rejection Handled (id: ${id})\n` +
+                'This means you can ignore any previous messages of the form ' +
+                `"Possible Unhandled Promise Rejection (id: ${id})"`,
+            );
+          }
+        },
+      });
+
+      logger.log('Unhandled promise rejections will be caught by Sentry.');
+    } else if (isWeb()) {
+      logger.log('Using Browser JS promise rejection tracking for React Native Web');
+
+      // Use Sentry's built-in global unhandled rejection handler
+      addGlobalUnhandledRejectionInstrumentationHandler((error: unknown) => {
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: krystofwoldrich
body: Nice, make sense. Thank you for adding it.
created_at: 2025-05-19T12:51:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Good idea Krystof 👍 
Updated with 9fd5226f326a7bbd131c8dc7db77e4c06566e5ac
created_at: 2025-05-19T15:12:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,12 +52,70 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (isHermesEnabled() && RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: (id: string, error: unknown) => {
+          if (__DEV__) {
+            logger.warn(`Possible Unhandled Promise Rejection (id: ${id}):\n${error}`);
+          }
+
+          // Marking the rejection as handled to avoid breaking crash rate calculations.
+          // See: https://github.com/getsentry/sentry-react-native/issues/4141
+          captureException(error, {
+            data: { id },
+            originalException: error,
+            syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+            mechanism: { handled: true, type: 'onunhandledrejection' },
+          });
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Good point 👍 
Updated with 4ed5c880ff10f40a566a4c16915760244da91367 to share the handling logic
created_at: 2025-05-19T15:12:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,49 +52,88 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (
+      isHermesEnabled() &&
+      RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker &&
+      RN_GLOBAL_OBJ?.HermesInternal?.hasPromise?.()
+    ) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: promiseRejectionTrackingOptions.onUnhandled,
+        onHandled: promiseRejectionTrackingOptions.onHandled,
+      });
 
-  attachUnhandledRejectionHandler();
-  checkPromiseAndWarn();
+      logger.log('Unhandled promise rejections will be caught by Sentry.');
+    } else if (isWeb()) {
+      logger.log('Using Browser JS promise rejection tracking for React Native Web');
+
+      // Use Sentry's built-in global unhandled rejection handler
+      addGlobalUnhandledRejectionInstrumentationHandler((error: unknown) => {
+        captureException(error, {
+          originalException: error,
+          syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+          mechanism: { handled: false, type: 'onunhandledrejection' },
+        });
+      });
+
+      logger.warn(
+        'Unhandled promise rejections will not be caught by Sentry. ' +
+          'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.',
+      );
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: krystofwoldrich
body: I think this is a oversight, we setup the global rejections handler above, so the rejections will be captured.

````suggestion
```` 
created_at: 2025-05-20T10:45:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,49 +52,88 @@ function setup(options: ReactNativeErrorHandlersOptions): void {
  * Setup unhandled promise rejection tracking
  */
 function setupUnhandledRejectionsTracking(patchGlobalPromise: boolean): void {
-  if (patchGlobalPromise) {
-    polyfillPromise();
-  }
+  try {
+    if (
+      isHermesEnabled() &&
+      RN_GLOBAL_OBJ.HermesInternal?.enablePromiseRejectionTracker &&
+      RN_GLOBAL_OBJ?.HermesInternal?.hasPromise?.()
+    ) {
+      logger.log('Using Hermes native promise rejection tracking');
+
+      RN_GLOBAL_OBJ.HermesInternal.enablePromiseRejectionTracker({
+        allRejections: true,
+        onUnhandled: promiseRejectionTrackingOptions.onUnhandled,
+        onHandled: promiseRejectionTrackingOptions.onHandled,
+      });
 
-  attachUnhandledRejectionHandler();
-  checkPromiseAndWarn();
+      logger.log('Unhandled promise rejections will be caught by Sentry.');
+    } else if (isWeb()) {
+      logger.log('Using Browser JS promise rejection tracking for React Native Web');
+
+      // Use Sentry's built-in global unhandled rejection handler
+      addGlobalUnhandledRejectionInstrumentationHandler((error: unknown) => {
+        captureException(error, {
+          originalException: error,
+          syntheticException: isErrorLike(error) ? undefined : createSyntheticError(),
+          mechanism: { handled: false, type: 'onunhandledrejection' },
+        });
+      });
+
+      logger.warn(
+        'Unhandled promise rejections will not be caught by Sentry. ' +
+          'See https://docs.sentry.io/platforms/react-native/troubleshooting/ for more details.',
+      );
path: packages/core/src/js/integrations/reactnativeerrorhandlers.ts
user: login: antonis
body: Good catch @krystofwoldrich 👍 
This is indeed an oversight. No warning is needed there. Removed with a2fbf2bc6ad990d9653bd32ff6c7e03a935cb36b
created_at: 2025-05-20T10:55:33Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-13T12:50:07Z
body: ## Android (legacy) Performance metrics :rocket:
<table>
  <tr>
    <th>&nbsp;</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr>
  <tr>
    <th align="left">Startup time</th>
    <td align="right">431.17 ms</td>
    <td align="right">458.02 ms</td>
    <td align="right"><strong>26.85 ms</strong></td>
  </tr>
  <tr>
    <th align="left">Size</th>
    <td align="right">17.75 MiB</td>
    <td align="right">20.13 MiB</td>
    <td align="right"><strong>2.38 MiB</strong></td>
  </tr>
</table>
<details><summary><h3>Baseline results on branch: main</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">4a6664fa7140687cfa079f2d738b59b18e4ac7b7</th>
  <td align="right">548.79 ms</td>
  <td align="right">585.00 ms</td>
  <td align="right"><strong>36.21 ms</strong></td>
</tr><tr>
  <th align="left">d8e8c67f974f7aaa25e0b0414e42a69dda682c78</th>
  <td align="right">448.79 ms</td>
  <td align="right">438.70 ms</td>
  <td align="right"><strong>-10.09 ms</strong></td>
</tr><tr>
  <th align="left">18ce5e87b6e981e23ef9493207fb152d9c6aa8e7</th>
  <td align="right">450.83 ms</td>
  <td align="right">436.08 ms</td>
  <td align="right"><strong>-14.75 ms</strong></td>
</tr><tr>
  <th align="left">deae0619fe23295c69b2a5fc10b2bbe725d92706</th>
  <td align="right">409.04 ms</td>
  <td align="right">396.06 ms</td>
  <td align="right"><strong>-12.98 ms</strong></td>
</tr><tr>
  <th align="left">f80e57eb7a15d1b57496089e3eca10b9a4349b3c</th>
  <td align="right">496.93 ms</td>
  <td align="right">482.26 ms</td>
  <td align="right"><strong>-14.67 ms</strong></td>
</tr><tr>
  <th align="left">5852d7735754597e0f952d5630410101804d65e9</th>
  <td align="right">415.81 ms</td>
  <td align="right">421.02 ms</td>
  <td align="right"><strong>5.21 ms</strong></td>
</tr><tr>
  <th align="left">30189bee035975c3831f2bf4c7ceea05c9ad05b3</th>
  <td align="right">440.46 ms</td>
  <td align="right">435.60 ms</td>
  <td align="right"><strong>-4.85 ms</strong></td>
</tr><tr>
  <th align="left">457e29fc08e6edbebee646d095513d72728a5756</th>
  <td align="right">398.10 ms</td>
  <td align="right">421.39 ms</td>
  <td align="right"><strong>23.29 ms</strong></td>
</tr><tr>
  <th align="left">4bec795c3a17d92ed7c6d4637767f9776b070626</th>
  <td align="right">437.23 ms</td>
  <td align="right">428.21 ms</td>
  <td align="right"><strong>-9.02 ms</strong></td>
</tr><tr>
  <th align="left">e540498478edf6f59bd7e731809a3ea6409dcc1c</th>
  <td align="right">436.26 ms</td>
  <td align="right">433.00 ms</td>
  <td align="right"><strong>-3.26 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">4a6664fa7140687cfa079f2d738b59b18e4ac7b7</th>
  <td align="right">17.73 MiB</td>
  <td align="right">19.94 MiB</td>
  <td align="right"><strong>2.21 MiB</strong></td>
</tr><tr>
  <th align="left">d8e8c67f974f7aaa25e0b0414e42a69dda682c78</th>
  <td align="right">17.74 MiB</td>
  <td align="right">20.10 MiB</td>
  <td align="right"><strong>2.36 MiB</strong></td>
</tr><tr>
  <th align="left">18ce5e87b6e981e23ef9493207fb152d9c6aa8e7</th>
  <td align="right">17.74 MiB</td>
  <td align="right">20.10 MiB</td>
  <td align="right"><strong>2.36 MiB</strong></td>
</tr><tr>
  <th align="left">deae0619fe23295c69b2a5fc10b2bbe725d92706</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr><tr>
  <th align="left">f80e57eb7a15d1b57496089e3eca10b9a4349b3c</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr><tr>
  <th align="left">5852d7735754597e0f952d5630410101804d65e9</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.11 MiB</td>
  <td align="right"><strong>2.36 MiB</strong></td>
</tr><tr>
  <th align="left">30189bee035975c3831f2bf4c7ceea05c9ad05b3</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.11 MiB</td>
  <td align="right"><strong>2.36 MiB</strong></td>
</tr><tr>
  <th align="left">457e29fc08e6edbebee646d095513d72728a5756</th>
  <td align="right">17.73 MiB</td>
  <td align="right">19.84 MiB</td>
  <td align="right"><strong>2.10 MiB</strong></td>
</tr><tr>
  <th align="left">4bec795c3a17d92ed7c6d4637767f9776b070626</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr><tr>
  <th align="left">e540498478edf6f59bd7e731809a3ea6409dcc1c</th>
  <td align="right">17.73 MiB</td>
  <td align="right">20.11 MiB</td>
  <td align="right"><strong>2.37 MiB</strong></td>
</tr></table>
</details>
<details><summary><h3>Previous results on branch: antonis/3957-hermes-rejection-tracker</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793</th>
  <td align="right">370.16 ms</td>
  <td align="right">356.40 ms</td>
  <td align="right"><strong>-13.76 ms</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41</th>
  <td align="right">413.89 ms</td>
  <td align="right">410.87 ms</td>
  <td align="right"><strong>-3.02 ms</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9</th>
  <td align="right">430.98 ms</td>
  <td align="right">427.08 ms</td>
  <td align="right"><strong>-3.90 ms</strong></td>
</tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f</th>
  <td align="right">394.31 ms</td>
  <td align="right">397.06 ms</td>
  <td align="right"><strong>2.76 ms</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e</th>
  <td align="right">439.14 ms</td>
  <td align="right">426.10 ms</td>
  <td align="right"><strong>-13.04 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e</th>
  <td align="right">17.75 MiB</td>
  <td align="right">20.13 MiB</td>
  <td align="right"><strong>2.38 MiB</strong></td>
</tr></table>
</details>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-13T12:59:18Z
body: ## Android (new) Performance metrics :rocket:
<table>
  <tr>
    <th>&nbsp;</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr>
  <tr>
    <th align="left">Startup time</th>
    <td align="right">405.17 ms</td>
    <td align="right">409.02 ms</td>
    <td align="right"><strong>3.85 ms</strong></td>
  </tr>
  <tr>
    <th align="left">Size</th>
    <td align="right">7.15 MiB</td>
    <td align="right">8.40 MiB</td>
    <td align="right"><strong>1.25 MiB</strong></td>
  </tr>
</table>
<details><summary><h3>Baseline results on branch: main</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">f86dde73b56428b85b322cf3d8b0619f364c55d5+dirty</th>
  <td align="right">363.98 ms</td>
  <td align="right">368.56 ms</td>
  <td align="right"><strong>4.58 ms</strong></td>
</tr><tr>
  <th align="left">60ad48cdd1e6882c9512161f0dfa85fd7026b764+dirty</th>
  <td align="right">373.40 ms</td>
  <td align="right">393.20 ms</td>
  <td align="right"><strong>19.80 ms</strong></td>
</tr><tr>
  <th align="left">2501f3610f669f291e82900a25fca48f90674e74+dirty</th>
  <td align="right">403.27 ms</td>
  <td align="right">427.94 ms</td>
  <td align="right"><strong>24.67 ms</strong></td>
</tr><tr>
  <th align="left">241d6479aa8d9615860362947d05a202057a9199+dirty</th>
  <td align="right">384.77 ms</td>
  <td align="right">390.77 ms</td>
  <td align="right"><strong>6.00 ms</strong></td>
</tr><tr>
  <th align="left">c1d05991608bd087b82e0a7e0d2550d679db7139+dirty</th>
  <td align="right">401.12 ms</td>
  <td align="right">398.98 ms</td>
  <td align="right"><strong>-2.14 ms</strong></td>
</tr><tr>
  <th align="left">3f05680301917556cc58f173d8a674cb5af6682c+dirty</th>
  <td align="right">399.86 ms</td>
  <td align="right">405.20 ms</td>
  <td align="right"><strong>5.34 ms</strong></td>
</tr><tr>
  <th align="left">ed3d77e497f60a66611b5ca8ab6c4ccf699c1256+dirty</th>
  <td align="right">366.04 ms</td>
  <td align="right">411.33 ms</td>
  <td align="right"><strong>45.28 ms</strong></td>
</tr><tr>
  <th align="left">13f280b8e8d43c2387029341d9f5e4b01e8531ad+dirty</th>
  <td align="right">376.13 ms</td>
  <td align="right">406.41 ms</td>
  <td align="right"><strong>30.28 ms</strong></td>
</tr><tr>
  <th align="left">deae0619fe23295c69b2a5fc10b2bbe725d92706+dirty</th>
  <td align="right">410.00 ms</td>
  <td align="right">433.92 ms</td>
  <td align="right"><strong>23.92 ms</strong></td>
</tr><tr>
  <th align="left">f54118bf430b509e9bae81c4c0758cc57c755ed1+dirty</th>
  <td align="right">250.87 ms</td>
  <td align="right">312.22 ms</td>
  <td align="right"><strong>61.35 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">f86dde73b56428b85b322cf3d8b0619f364c55d5+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.39 MiB</td>
  <td align="right"><strong>1.23 MiB</strong></td>
</tr><tr>
  <th align="left">60ad48cdd1e6882c9512161f0dfa85fd7026b764+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.39 MiB</td>
  <td align="right"><strong>1.23 MiB</strong></td>
</tr><tr>
  <th align="left">2501f3610f669f291e82900a25fca48f90674e74+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">241d6479aa8d9615860362947d05a202057a9199+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">c1d05991608bd087b82e0a7e0d2550d679db7139+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">3f05680301917556cc58f173d8a674cb5af6682c+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.38 MiB</td>
  <td align="right"><strong>1.23 MiB</strong></td>
</tr><tr>
  <th align="left">ed3d77e497f60a66611b5ca8ab6c4ccf699c1256+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.35 MiB</td>
  <td align="right"><strong>1.21 MiB</strong></td>
</tr><tr>
  <th align="left">13f280b8e8d43c2387029341d9f5e4b01e8531ad+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.35 MiB</td>
  <td align="right"><strong>1.20 MiB</strong></td>
</tr><tr>
  <th align="left">deae0619fe23295c69b2a5fc10b2bbe725d92706+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">f54118bf430b509e9bae81c4c0758cc57c755ed1+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.37 MiB</td>
  <td align="right"><strong>1.22 MiB</strong></td>
</tr></table>
</details>
<details><summary><h3>Previous results on branch: antonis/3957-hermes-rejection-tracker</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793+dirty</th>
  <td align="right">431.79 ms</td>
  <td align="right">463.82 ms</td>
  <td align="right"><strong>32.03 ms</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e+dirty</th>
  <td align="right">361.06 ms</td>
  <td align="right">365.91 ms</td>
  <td align="right"><strong>4.85 ms</strong></td>
</tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f+dirty</th>
  <td align="right">393.06 ms</td>
  <td align="right">393.60 ms</td>
  <td align="right"><strong>0.54 ms</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9+dirty</th>
  <td align="right">395.54 ms</td>
  <td align="right">393.44 ms</td>
  <td align="right"><strong>-2.10 ms</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41+dirty</th>
  <td align="right">391.02 ms</td>
  <td align="right">383.27 ms</td>
  <td align="right"><strong>-7.75 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41+dirty</th>
  <td align="right">7.15 MiB</td>
  <td align="right">8.40 MiB</td>
  <td align="right"><strong>1.25 MiB</strong></td>
</tr></table>
</details>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-13T13:33:54Z
body: ## iOS (new) Performance metrics :rocket:
<table>
  <tr>
    <th>&nbsp;</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr>
  <tr>
    <th align="left">Startup time</th>
    <td align="right">1234.08 ms</td>
    <td align="right">1241.61 ms</td>
    <td align="right"><strong>7.53 ms</strong></td>
  </tr>
  <tr>
    <th align="left">Size</th>
    <td align="right">3.19 MiB</td>
    <td align="right">4.37 MiB</td>
    <td align="right"><strong>1.18 MiB</strong></td>
  </tr>
</table>
<details><summary><h3>Baseline results on branch: main</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">de59d3a18e282982e656cedc399b67a5ebda7cc8+dirty</th>
  <td align="right">1241.17 ms</td>
  <td align="right">1249.16 ms</td>
  <td align="right"><strong>8.00 ms</strong></td>
</tr><tr>
  <th align="left">63ed251266077130df2d3a329b28bd4718163a8c+dirty</th>
  <td align="right">1223.27 ms</td>
  <td align="right">1222.94 ms</td>
  <td align="right"><strong>-0.33 ms</strong></td>
</tr><tr>
  <th align="left">29e4db94b2962f21a62b67c16770e276e11ca9a4+dirty</th>
  <td align="right">1216.20 ms</td>
  <td align="right">1213.85 ms</td>
  <td align="right"><strong>-2.36 ms</strong></td>
</tr><tr>
  <th align="left">148f9248007774c638943e03dc044cf71dec0c3d+dirty</th>
  <td align="right">1220.72 ms</td>
  <td align="right">1221.30 ms</td>
  <td align="right"><strong>0.58 ms</strong></td>
</tr><tr>
  <th align="left">22e31b6ad7bd629bf078367aee82121c08977ded+dirty</th>
  <td align="right">1276.55 ms</td>
  <td align="right">1278.12 ms</td>
  <td align="right"><strong>1.57 ms</strong></td>
</tr><tr>
  <th align="left">eb597b373680e752880646852c50d83d5bc152a7+dirty</th>
  <td align="right">1217.49 ms</td>
  <td align="right">1231.69 ms</td>
  <td align="right"><strong>14.20 ms</strong></td>
</tr><tr>
  <th align="left">ed3d77e497f60a66611b5ca8ab6c4ccf699c1256+dirty</th>
  <td align="right">1246.19 ms</td>
  <td align="right">1244.00 ms</td>
  <td align="right"><strong>-2.19 ms</strong></td>
</tr><tr>
  <th align="left">b6f8ea26ccc93d7a6ce3aacc0faafecdd16b3d3d+dirty</th>
  <td align="right">1254.10 ms</td>
  <td align="right">1236.67 ms</td>
  <td align="right"><strong>-17.43 ms</strong></td>
</tr><tr>
  <th align="left">6f52f970e9c77296759604ca9c929b9ae964a609+dirty</th>
  <td align="right">1228.02 ms</td>
  <td align="right">1234.75 ms</td>
  <td align="right"><strong>6.73 ms</strong></td>
</tr><tr>
  <th align="left">d053743637a2dc193e0e4cab223c6da286bc0d51+dirty</th>
  <td align="right">1223.18 ms</td>
  <td align="right">1214.25 ms</td>
  <td align="right"><strong>-8.93 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">de59d3a18e282982e656cedc399b67a5ebda7cc8+dirty</th>
  <td align="right">2.92 MiB</td>
  <td align="right">3.67 MiB</td>
  <td align="right"><strong>772.59 KiB</strong></td>
</tr><tr>
  <th align="left">63ed251266077130df2d3a329b28bd4718163a8c+dirty</th>
  <td align="right">2.92 MiB</td>
  <td align="right">3.66 MiB</td>
  <td align="right"><strong>757.10 KiB</strong></td>
</tr><tr>
  <th align="left">29e4db94b2962f21a62b67c16770e276e11ca9a4+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.37 MiB</td>
  <td align="right"><strong>1.18 MiB</strong></td>
</tr><tr>
  <th align="left">148f9248007774c638943e03dc044cf71dec0c3d+dirty</th>
  <td align="right">2.92 MiB</td>
  <td align="right">3.60 MiB</td>
  <td align="right"><strong>701.88 KiB</strong></td>
</tr><tr>
  <th align="left">22e31b6ad7bd629bf078367aee82121c08977ded+dirty</th>
  <td align="right">2.92 MiB</td>
  <td align="right">3.43 MiB</td>
  <td align="right"><strong>524.74 KiB</strong></td>
</tr><tr>
  <th align="left">eb597b373680e752880646852c50d83d5bc152a7+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.32 MiB</td>
  <td align="right"><strong>1.13 MiB</strong></td>
</tr><tr>
  <th align="left">ed3d77e497f60a66611b5ca8ab6c4ccf699c1256+dirty</th>
  <td align="right">2.92 MiB</td>
  <td align="right">3.66 MiB</td>
  <td align="right"><strong>758.57 KiB</strong></td>
</tr><tr>
  <th align="left">b6f8ea26ccc93d7a6ce3aacc0faafecdd16b3d3d+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.25 MiB</td>
  <td align="right"><strong>1.06 MiB</strong></td>
</tr><tr>
  <th align="left">6f52f970e9c77296759604ca9c929b9ae964a609+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.34 MiB</td>
  <td align="right"><strong>1.16 MiB</strong></td>
</tr><tr>
  <th align="left">d053743637a2dc193e0e4cab223c6da286bc0d51+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.32 MiB</td>
  <td align="right"><strong>1.13 MiB</strong></td>
</tr></table>
</details>
<details><summary><h3>Previous results on branch: antonis/3957-hermes-rejection-tracker</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f+dirty</th>
  <td align="right">1222.33 ms</td>
  <td align="right">1230.96 ms</td>
  <td align="right"><strong>8.63 ms</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9+dirty</th>
  <td align="right">1215.06 ms</td>
  <td align="right">1227.82 ms</td>
  <td align="right"><strong>12.75 ms</strong></td>
</tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793+dirty</th>
  <td align="right">1231.71 ms</td>
  <td align="right">1231.13 ms</td>
  <td align="right"><strong>-0.59 ms</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e+dirty</th>
  <td align="right">1227.49 ms</td>
  <td align="right">1230.31 ms</td>
  <td align="right"><strong>2.82 ms</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41+dirty</th>
  <td align="right">1215.57 ms</td>
  <td align="right">1211.00 ms</td>
  <td align="right"><strong>-4.57 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.36 MiB</td>
  <td align="right"><strong>1.17 MiB</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.37 MiB</td>
  <td align="right"><strong>1.18 MiB</strong></td>
</tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.37 MiB</td>
  <td align="right"><strong>1.18 MiB</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.36 MiB</td>
  <td align="right"><strong>1.17 MiB</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41+dirty</th>
  <td align="right">3.19 MiB</td>
  <td align="right">4.37 MiB</td>
  <td align="right"><strong>1.18 MiB</strong></td>
</tr></table>
</details>

author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-13T13:37:16Z
body: ## iOS (legacy) Performance metrics :rocket:
<table>
  <tr>
    <th>&nbsp;</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr>
  <tr>
    <th align="left">Startup time</th>
    <td align="right">1209.08 ms</td>
    <td align="right">1216.15 ms</td>
    <td align="right"><strong>7.06 ms</strong></td>
  </tr>
  <tr>
    <th align="left">Size</th>
    <td align="right">2.63 MiB</td>
    <td align="right">3.80 MiB</td>
    <td align="right"><strong>1.17 MiB</strong></td>
  </tr>
</table>
<details><summary><h3>Baseline results on branch: main</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">de59d3a18e282982e656cedc399b67a5ebda7cc8+dirty</th>
  <td align="right">1223.73 ms</td>
  <td align="right">1236.28 ms</td>
  <td align="right"><strong>12.55 ms</strong></td>
</tr><tr>
  <th align="left">63ed251266077130df2d3a329b28bd4718163a8c+dirty</th>
  <td align="right">1232.55 ms</td>
  <td align="right">1238.77 ms</td>
  <td align="right"><strong>6.22 ms</strong></td>
</tr><tr>
  <th align="left">29e4db94b2962f21a62b67c16770e276e11ca9a4+dirty</th>
  <td align="right">1222.41 ms</td>
  <td align="right">1214.49 ms</td>
  <td align="right"><strong>-7.92 ms</strong></td>
</tr><tr>
  <th align="left">148f9248007774c638943e03dc044cf71dec0c3d+dirty</th>
  <td align="right">1214.76 ms</td>
  <td align="right">1215.73 ms</td>
  <td align="right"><strong>0.97 ms</strong></td>
</tr><tr>
  <th align="left">22e31b6ad7bd629bf078367aee82121c08977ded+dirty</th>
  <td align="right">1253.62 ms</td>
  <td align="right">1265.96 ms</td>
  <td align="right"><strong>12.34 ms</strong></td>
</tr><tr>
  <th align="left">eb597b373680e752880646852c50d83d5bc152a7+dirty</th>
  <td align="right">1223.78 ms</td>
  <td align="right">1228.22 ms</td>
  <td align="right"><strong>4.44 ms</strong></td>
</tr><tr>
  <th align="left">ed3d77e497f60a66611b5ca8ab6c4ccf699c1256+dirty</th>
  <td align="right">1214.61 ms</td>
  <td align="right">1205.83 ms</td>
  <td align="right"><strong>-8.78 ms</strong></td>
</tr><tr>
  <th align="left">b6f8ea26ccc93d7a6ce3aacc0faafecdd16b3d3d+dirty</th>
  <td align="right">1236.08 ms</td>
  <td align="right">1228.58 ms</td>
  <td align="right"><strong>-7.50 ms</strong></td>
</tr><tr>
  <th align="left">6f52f970e9c77296759604ca9c929b9ae964a609+dirty</th>
  <td align="right">1211.85 ms</td>
  <td align="right">1221.42 ms</td>
  <td align="right"><strong>9.57 ms</strong></td>
</tr><tr>
  <th align="left">d053743637a2dc193e0e4cab223c6da286bc0d51+dirty</th>
  <td align="right">1213.69 ms</td>
  <td align="right">1218.88 ms</td>
  <td align="right"><strong>5.18 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">de59d3a18e282982e656cedc399b67a5ebda7cc8+dirty</th>
  <td align="right">2.36 MiB</td>
  <td align="right">3.11 MiB</td>
  <td align="right"><strong>760.16 KiB</strong></td>
</tr><tr>
  <th align="left">63ed251266077130df2d3a329b28bd4718163a8c+dirty</th>
  <td align="right">2.36 MiB</td>
  <td align="right">3.10 MiB</td>
  <td align="right"><strong>752.55 KiB</strong></td>
</tr><tr>
  <th align="left">29e4db94b2962f21a62b67c16770e276e11ca9a4+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.80 MiB</td>
  <td align="right"><strong>1.17 MiB</strong></td>
</tr><tr>
  <th align="left">148f9248007774c638943e03dc044cf71dec0c3d+dirty</th>
  <td align="right">2.36 MiB</td>
  <td align="right">3.04 MiB</td>
  <td align="right"><strong>696.25 KiB</strong></td>
</tr><tr>
  <th align="left">22e31b6ad7bd629bf078367aee82121c08977ded+dirty</th>
  <td align="right">2.36 MiB</td>
  <td align="right">2.87 MiB</td>
  <td align="right"><strong>520.67 KiB</strong></td>
</tr><tr>
  <th align="left">eb597b373680e752880646852c50d83d5bc152a7+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.75 MiB</td>
  <td align="right"><strong>1.12 MiB</strong></td>
</tr><tr>
  <th align="left">ed3d77e497f60a66611b5ca8ab6c4ccf699c1256+dirty</th>
  <td align="right">2.36 MiB</td>
  <td align="right">3.10 MiB</td>
  <td align="right"><strong>753.32 KiB</strong></td>
</tr><tr>
  <th align="left">b6f8ea26ccc93d7a6ce3aacc0faafecdd16b3d3d+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.68 MiB</td>
  <td align="right"><strong>1.05 MiB</strong></td>
</tr><tr>
  <th align="left">6f52f970e9c77296759604ca9c929b9ae964a609+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.78 MiB</td>
  <td align="right"><strong>1.14 MiB</strong></td>
</tr><tr>
  <th align="left">d053743637a2dc193e0e4cab223c6da286bc0d51+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.75 MiB</td>
  <td align="right"><strong>1.12 MiB</strong></td>
</tr></table>
</details>
<details><summary><h3>Previous results on branch: antonis/3957-hermes-rejection-tracker</h3></summary>
<h3>Startup times</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f+dirty</th>
  <td align="right">1208.78 ms</td>
  <td align="right">1222.46 ms</td>
  <td align="right"><strong>13.68 ms</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9+dirty</th>
  <td align="right">1216.56 ms</td>
  <td align="right">1227.14 ms</td>
  <td align="right"><strong>10.58 ms</strong></td>
</tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793+dirty</th>
  <td align="right">1235.49 ms</td>
  <td align="right">1237.24 ms</td>
  <td align="right"><strong>1.76 ms</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e+dirty</th>
  <td align="right">1218.98 ms</td>
  <td align="right">1236.04 ms</td>
  <td align="right"><strong>17.06 ms</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41+dirty</th>
  <td align="right">1235.50 ms</td>
  <td align="right">1230.98 ms</td>
  <td align="right"><strong>-4.52 ms</strong></td>
</tr></table>
<h3>App size</h3>
<table>
  <tr>
    <th>Revision</th>
    <th align="right">Plain</th>
    <th align="right">With Sentry</th>
    <th align="right">Diff</th>
  </tr><tr>
  <th align="left">adbfee08c6c8548ca4e9b1bd9f3526ac52727a4f+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.80 MiB</td>
  <td align="right"><strong>1.16 MiB</strong></td>
</tr><tr>
  <th align="left">854278d1b45d88cb2ca09ab56ae043ffbee0a2a9+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.80 MiB</td>
  <td align="right"><strong>1.17 MiB</strong></td>
</tr><tr>
  <th align="left">9d73c56cddda7464bfc66783bf8e6b6be0c74793+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.80 MiB</td>
  <td align="right"><strong>1.17 MiB</strong></td>
</tr><tr>
  <th align="left">9ac2ec3ea79d34136f07376618557692466b933e+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.79 MiB</td>
  <td align="right"><strong>1.16 MiB</strong></td>
</tr><tr>
  <th align="left">56715d998f13b066c87373f1dfaa1a88fcb18d41+dirty</th>
  <td align="right">2.63 MiB</td>
  <td align="right">3.80 MiB</td>
  <td align="right"><strong>1.17 MiB</strong></td>
</tr></table>
</details>

author_association: CONTRIBUTOR
user: login: krystofwoldrich
created_at: 2025-05-19T13:19:56Z
body: Thank you for the improvement. It looks great, I only have a two small comments, on reusability of the handlers code and check of the `hasPromise` method.

author_association: CONTRIBUTOR