Repository: getsentry/sentry-react-native (TypeScript)

Issue:
Enabling Sentry.ReactNativeTracing results in Cannot read properties of undefined (reading 'remove') when sending error.
**OS:**
- [x]  Windows
- [x]  Linux

**Platform:**
- [ ]  iOS
- [x]  Android

**SDK:**
- [x]  `@sentry/react-native` (>= 1.0.0)

**SDK version:** 5.19.1

**`react-native` version:** 0.73.6

**Are you using Expo?**
- [x]  Yes

**Are you using sentry.io or on-premise?**
- [x]  sentry.io (SaaS)

**If you are using sentry.io, please post a link to your issue so we can take a look:**
[The issue is logged but breaks the dev server.](https://lifeguides.sentry.io/issues/5085465590/?environment=development&project=4506758892093440&query=is%3Aunresolved&referrer=issue-stream&statsPeriod=14d&stream_index=0)

[Link to issue]

**Configuration:**

(`@sentry/react-native`)
```ts
import * as Sentry from '@sentry/react-native';
import { Slot, useNavigationContainerRef } from 'expo-router';

const routingInstrumentation = new Sentry.ReactNavigationInstrumentation();

Sentry.init({
  dsn: 'My-DSN',
  debug: true,
  tracesSampleRate: 1.0,
  integrations: [
    new Sentry.ReactNativeTracing({
        enableNativeFramesTracking: Constants.appOwnership !== 'expo',
      enableUserInteractionTracing: true,
      routingInstrumentation,
    }),
  ],
});

export function RootLayout() {
  useEffect(() => {
    if (navRef) {
      routingInstrumentation.registerNavigationContainer(navRef);
    }
  }, [navRef]);

   return <Slot />
}



export default Sentry.wrap(RootLayout);
```


---
**I have the following issue:**

When I willfully throw a test error via Sentry, with the above configuration, the dev server is halted with an error emitting from sentry itself. Here is the trace:

```sh
TypeError: Cannot read properties of undefined (reading 'remove')
    at callback (/home/the-simian/<app>/node_modules/@sentry/react-native/dist/js/tracing/transaction.js:28:22)
    at IdleTransaction.end (/home/the-simian/<app>/node_modules/@sentry/core/cjs/tracing/idletransaction.js:160:9)
    at IdleTransaction.finish (/home/the-simian/<app>/node_modules/@sentry/core/cjs/tracing/span.js:475:17)
    at ReactNavigationInstrumentation.apply [as _discardLatestTransaction] (/home/the-simian/<app>/node_modules/@sentry/react-native/dist/js/tracing/reactnavigation.js:174:37)
    at Timeout.sentryWrapped [as _onTimeout] (/home/the-simian/<app>/node_modules/@sentry/browser/cjs/helpers.js:93:17)
    at listOnTimeout (node:internal/timers:573:17)
    at processTimers (node:internal/timers:514:7)

Node.js v21.6.2
```



**Steps to reproduce:**
- Step 1: Configure Sentry as seen above.
- Step 2: Add a 'Test' Button and Press it like:

```ts
      <Pressable
        onPress={() => {
          Sentry.captureException(
            new Error(
              `[Test Error] | 💥💥 Kaboom!!! 💥💥 at ${new Date().toISOString()}`,
            ),
          );
        }}
      >
        <View>
          <Text>
            Kaboom!
          </Text>
        </View>
      </Pressable>
```

**Actual result:**

See trace

**Expected result:**

No Error



Discussion:
I have the same issue.
Hi,
thank you for the message @the-simian and @daehyeonmun2021.

I was able to reproduce this with `react-native-web`. Can you confirm it happens on the web or do you experience it also on Android or iOS with or without Expo Go?
This happens in Android on Expo Go as well as the Dev Client Build; both break.
I've not tested the web build, @krystofwoldrich 
I tested the web build, and I get the same error. The trace is slightly different, here is the web build:

```sh
/home/the-simian/<app>/apps/lifeguides-mobile/node_modules/@sentry/react-native/dist/js/tracing/transaction.js:28
        subscription.remove();
                     ^
TypeError: Cannot read properties of undefined (reading 'remove')
    at callback (/home/the-simian/<app>/apps/lifeguides-mobile/node_modules/@sentry/react-native/dist/js/tracing/transaction.js:28:22)
    at IdleTransaction.end (/home/the-simian/<app>/apps/lifeguides-mobile/node_modules/@sentry/core/cjs/tracing/idletransaction.js:160:9)
    at IdleTransaction.finish (/home/the-simian/<app>/apps/lifeguides-mobile/node_modules/@sentry/core/cjs/tracing/span.js:475:17)
    at ReactNavigationInstrumentation.apply [as _discardLatestTransaction] (/home/the-simian/<app>/apps/lifeguides-mobile/node_modules/@sentry/react-native/dist/js/tracing/reactnavigation.js:174:37)
    at Timeout.sentryWrapped [as _onTimeout] (/home/the-simian/<app>/apps/lifeguides-mobile/node_modules/@sentry/browser/cjs/helpers.js:93:17)
    at listOnTimeout (node:internal/timers:573:17)
    at processTimers (node:internal/timers:514:7)
```    

this points to the tracing/transaction.js file.
Was this issue solved or just closed? 



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e8fe8f27d8..996893e939 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
 ### Fixes
 
 - Do not enable NativeFramesTracking when native is not available ([#3705](https://github.com/getsentry/sentry-react-native/pull/3705))
+- Cancel spans in background doesn't crash in environments without AppState ([#3727](https://github.com/getsentry/sentry-react-native/pull/3727))
 
 ### Dependencies
 
diff --git a/src/js/tracing/transaction.ts b/src/js/tracing/transaction.ts
index 2686fe9603..a980f86d95 100644
--- a/src/js/tracing/transaction.ts
+++ b/src/js/tracing/transaction.ts
@@ -1,6 +1,6 @@
 import { type BeforeFinishCallback, type IdleTransaction } from '@sentry/core';
 import { logger } from '@sentry/utils';
-import type { AppStateStatus } from 'react-native';
+import type { AppStateStatus, NativeEventSubscription } from 'react-native';
 import { AppState } from 'react-native';
 
 /**
@@ -22,15 +22,25 @@ export const onlySampleIfChildSpans: BeforeFinishCallback = (transaction: IdleTr
  * Hooks on AppState change to cancel the transaction if the app goes background.
  */
 export const cancelInBackground = (transaction: IdleTransaction): void => {
-  const subscription = AppState.addEventListener('change', (newState: AppStateStatus) => {
-    if (newState === 'background') {
-      logger.debug(`Setting ${transaction.op} transaction to cancelled because the app is in the background.`);
-      transaction.setStatus('cancelled');
-      transaction.finish();
-    }
-  });
-  transaction.registerBeforeFinishCallback(() => {
-    logger.debug(`Removing AppState listener for ${transaction.op} transaction.`);
-    subscription.remove();
-  });
+  if (!AppState || !AppState.isAvailable) {
+    logger.warn('AppState is not available, spans will not be canceled in background.');
+    return;
+  }
+
+  // RN Web can return undefined, https://github.com/necolas/react-native-web/blob/8cf720f0e57c74a254bfa7bed0313e33a4b29c11/packages/react-native-web/src/exports/AppState/index.js#L55
+  const subscription: NativeEventSubscription | undefined = AppState.addEventListener(
+    'change',
+    (newState: AppStateStatus) => {
+      if (newState === 'background') {
+        logger.debug(`Setting ${transaction.op} transaction to cancelled because the app is in the background.`);
+        transaction.setStatus('cancelled');
+        transaction.finish();
+      }
+    },
+  );
+  subscription &&
+    transaction.registerBeforeFinishCallback(() => {
+      logger.debug(`Removing AppState listener for ${transaction.op} transaction.`);
+      subscription && subscription.remove && subscription.remove();
+    });
 };


Test patch:
diff --git a/test/tracing/reactnativetracing.test.ts b/test/tracing/reactnativetracing.test.ts
index ce87565a22..183f963660 100644
--- a/test/tracing/reactnativetracing.test.ts
+++ b/test/tracing/reactnativetracing.test.ts
@@ -127,6 +127,13 @@ describe('ReactNativeTracing', () => {
   beforeEach(() => {
     jest.useFakeTimers();
     NATIVE.enableNative = true;
+    mockedAppState.isAvailable = true;
+    mockedAppState.addEventListener = (_, listener) => {
+      mockedAppState.listener = listener;
+      return {
+        remove: mockedAppState.removeSubscription,
+      };
+    };
   });
 
   afterEach(() => {
@@ -357,6 +364,26 @@ describe('ReactNativeTracing', () => {
         expect(mockedAppState.removeSubscription).toBeCalledTimes(1);
       });
 
+      it('Does not crash when AppState is not available', async () => {
+        mockedAppState.isAvailable = false;
+        mockedAppState.addEventListener = (() => {
+          return undefined;
+        }) as unknown as (typeof mockedAppState)['addEventListener']; // RN Web can return undefined
+        const integration = new ReactNativeTracing();
+
+        mockAppStartResponse({ cold: false });
+
+        const mockHub = getMockHub();
+        integration.setupOnce(addGlobalEventProcessor, () => mockHub);
+
+        await jest.advanceTimersByTimeAsync(500);
+        const transaction = mockHub.getScope()?.getTransaction();
+
+        jest.runAllTimers();
+
+        expect(transaction?.endTimestamp).toBeDefined();
+      });
+
       it('Does not add app start measurement if more than 60s', async () => {
         const integration = new ReactNativeTracing();