From 2d4da6b8f19609f4fbc866fa683a51218b3736bf Mon Sep 17 00:00:00 2001
From: Hubert Sosinski <hubert.sosinski@callstack.com>
Date: Thu, 30 Oct 2025 14:47:50 +0100
Subject: [PATCH 1/7] Send fullstory session id to Sentry

---
 src/Expensify.tsx                  |  9 +++++++++
 src/libs/Fullstory/index.native.ts | 13 +++++++++----
 src/libs/Fullstory/index.ts        | 29 +++++++++++++++++------------
 src/libs/Fullstory/types.ts        |  5 +++++
 4 files changed, 40 insertions(+), 16 deletions(-)

diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 5d392d350201..41c5f59ab384 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -1,4 +1,5 @@
 import HybridAppModule from '@expensify/react-native-hybrid-app';
+import * as Sentry from '@sentry/react-native';
 import {Audio} from 'expo-av';
 import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
 import type {NativeEventSubscription} from 'react-native';
@@ -184,6 +185,14 @@ function Expensify() {
     useEffect(() => {
         // Initialize Fullstory lib
         FS.init(userMetadata);
+        FS.onReady().then(async () => {
+            // After the successful connection we want to link Fullstory to Sentry via session id
+            const sessionId = await FS.getSessionId();
+            if (!sessionId) {
+                return;
+            }
+            Sentry.setContext('Fullstory', {sessionId});
+        });
     }, [userMetadata]);
 
     // Log the platform and config to debug .env issues
diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts
index 21fa6bb44aac..4a915f06d584 100644
--- a/src/libs/Fullstory/index.native.ts
+++ b/src/libs/Fullstory/index.native.ts
@@ -34,10 +34,11 @@ const FS: Fullstory = {
             // after the init function since this function is also called on updates for
             // UserMetadata onyx key.
             getEnvironment().then((envName: string) => {
-                const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
-                if ((CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) || Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME) {
-                    return;
-                }
+                // todo: Uncomment before merge. For testing purposes only.
+                // const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
+                // if ((CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) || Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME) {
+                //     return;
+                // }
                 FullStory.restart();
                 FullStory.consent(true);
                 FS.identify(userMetadata, envName);
@@ -48,6 +49,10 @@ const FS: Fullstory = {
     },
 
     anonymize: () => FullStory.anonymize(),
+
+    getSessionId: () => {
+        return FullStory.getCurrentSession();
+    },
 };
 
 export default FS;
diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts
index ac2d941fb610..463d0ed5d691 100644
--- a/src/libs/Fullstory/index.ts
+++ b/src/libs/Fullstory/index.ts
@@ -55,18 +55,19 @@ const FS: Fullstory = {
         }
         try {
             getEnvironment().then((envName: string) => {
-                const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
-                if (
-                    (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) ||
-                    Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME ||
-                    Session.isSupportAuthToken()
-                ) {
-                    // On web, if we started FS at some point in a browser, it will run forever. So let's shut it down if we don't want it to run.
-                    if (isInitialized()) {
-                        FullStory(CONST.FULLSTORY.OPERATION.SHUTDOWN);
-                    }
-                    return;
-                }
+                // todo: Uncomment before merge. For testing purposes only.
+                // const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
+                // if (
+                //     (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) ||
+                //     Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME ||
+                //     Session.isSupportAuthToken()
+                // ) {
+                //     // On web, if we started FS at some point in a browser, it will run forever. So let's shut it down if we don't want it to run.
+                //     if (isInitialized()) {
+                //         FullStory(CONST.FULLSTORY.OPERATION.SHUTDOWN);
+                //     }
+                //     return;
+                // }
 
                 // If Fullstory was already initialized, we might have shutdown the session. So let's
                 // restart it before identifying the user.
@@ -87,6 +88,10 @@ const FS: Fullstory = {
     },
 
     anonymize: () => FullStory(CONST.FULLSTORY.OPERATION.SET_IDENTITY, {anonymous: true}),
+
+    getSessionId: () => {
+        return FullStory('getSessionAsync', {format: 'id'});
+    },
 };
 
 export default FS;
diff --git a/src/libs/Fullstory/types.ts b/src/libs/Fullstory/types.ts
index 14d06b270f46..4c1901c8a43b 100644
--- a/src/libs/Fullstory/types.ts
+++ b/src/libs/Fullstory/types.ts
@@ -66,6 +66,11 @@ type Fullstory = {
      * Sets the identity as anonymous using the Fullstory library.
      */
     anonymize: () => void;
+
+    /**
+     * Returns the current Fullstory session ID.
+     */
+    getSessionId: () => Promise<string | undefined>;
 };
 
 /**

From 3cab3f10fa9dde26ada4fd01df0509078b38b250 Mon Sep 17 00:00:00 2001
From: Hubert Sosinski <hubert.sosinski@callstack.com>
Date: Thu, 30 Oct 2025 15:13:04 +0100
Subject: [PATCH 2/7] CONST namespace for telemetry

---
 src/CONST/index.ts | 3 +++
 src/Expensify.tsx  | 2 +-
 2 files changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index cf0c6368090a..b173002440fe 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1610,6 +1610,9 @@ const CONST = {
         SHOW_HOVER_PREVIEW_ANIMATION_DURATION: 250,
         ACTIVITY_INDICATOR_TIMEOUT: 10000,
     },
+    TELEMETRY: {
+        CONTEXT_FULLSTORY: 'Fullstory',
+    },
     PRIORITY_MODE: {
         GSD: 'gsd',
         DEFAULT: 'default',
diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 41c5f59ab384..74cf0706fc57 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -191,7 +191,7 @@ function Expensify() {
             if (!sessionId) {
                 return;
             }
-            Sentry.setContext('Fullstory', {sessionId});
+            Sentry.setContext(CONST.TELEMETRY.CONTEXT_FULLSTORY, {sessionId});
         });
     }, [userMetadata]);
 

From 14593a8d41d671ab24b2a3de9709ebb292e912d2 Mon Sep 17 00:00:00 2001
From: Hubert Sosinski <hubert.sosinski@callstack.com>
Date: Fri, 31 Oct 2025 10:20:12 +0100
Subject: [PATCH 3/7] Change the way connection to Sentry is done. Outside
 render loop

---
 src/Expensify.tsx                 | 10 +---------
 src/libs/TelemetrySynchronizer.ts | 19 +++++++++++++++++++
 2 files changed, 20 insertions(+), 9 deletions(-)
 create mode 100644 src/libs/TelemetrySynchronizer.ts

diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 74cf0706fc57..9344816eeb96 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -1,5 +1,4 @@
 import HybridAppModule from '@expensify/react-native-hybrid-app';
-import * as Sentry from '@sentry/react-native';
 import {Audio} from 'expo-av';
 import React, {useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState} from 'react';
 import type {NativeEventSubscription} from 'react-native';
@@ -43,6 +42,7 @@ import './libs/Notification/PushNotification/subscribeToPushNotifications';
 import './libs/registerPaginationConfig';
 import setCrashlyticsUserId from './libs/setCrashlyticsUserId';
 import StartupTimer from './libs/StartupTimer';
+import './libs/TelemetrySynchronizer';
 // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
 import './libs/UnreadIndicatorUpdater';
 import Visibility from './libs/Visibility';
@@ -185,14 +185,6 @@ function Expensify() {
     useEffect(() => {
         // Initialize Fullstory lib
         FS.init(userMetadata);
-        FS.onReady().then(async () => {
-            // After the successful connection we want to link Fullstory to Sentry via session id
-            const sessionId = await FS.getSessionId();
-            if (!sessionId) {
-                return;
-            }
-            Sentry.setContext(CONST.TELEMETRY.CONTEXT_FULLSTORY, {sessionId});
-        });
     }, [userMetadata]);
 
     // Log the platform and config to debug .env issues
diff --git a/src/libs/TelemetrySynchronizer.ts b/src/libs/TelemetrySynchronizer.ts
new file mode 100644
index 000000000000..8a8c35d35250
--- /dev/null
+++ b/src/libs/TelemetrySynchronizer.ts
@@ -0,0 +1,19 @@
+/**
+ * This file contains the logic for sending additional data to Sentry.
+ *
+ * It uses Onyx.connectWithoutView as nothing here is related to the UI. We only send data to external provider and want to keep this outside of the render loop.
+ */
+import * as Sentry from '@sentry/react-native';
+import FS from '@libs/Fullstory';
+import CONST from '@src/CONST';
+
+/**
+ * Connect to FullStory to retrieve session id from it. We want to link FullStory with Sentry for easier debugging.
+ */
+FS.onReady().then(async () => {
+    const sessionId = await FS.getSessionId();
+    if (!sessionId) {
+        return;
+    }
+    Sentry.setContext(CONST.TELEMETRY.CONTEXT_FULLSTORY, {sessionId});
+});

From ca9163b69843dee1fb5baf9b040637e39dd41f4d Mon Sep 17 00:00:00 2001
From: Hubert Sosinski <hubert.sosinski@callstack.com>
Date: Fri, 31 Oct 2025 10:49:28 +0100
Subject: [PATCH 4/7] import fix

---
 src/libs/TelemetrySynchronizer.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/libs/TelemetrySynchronizer.ts b/src/libs/TelemetrySynchronizer.ts
index 8a8c35d35250..bfb91239b8ac 100644
--- a/src/libs/TelemetrySynchronizer.ts
+++ b/src/libs/TelemetrySynchronizer.ts
@@ -4,8 +4,9 @@
  * It uses Onyx.connectWithoutView as nothing here is related to the UI. We only send data to external provider and want to keep this outside of the render loop.
  */
 import * as Sentry from '@sentry/react-native';
-import FS from '@libs/Fullstory';
 import CONST from '@src/CONST';
+import FS from './Fullstory';
+
 
 /**
  * Connect to FullStory to retrieve session id from it. We want to link FullStory with Sentry for easier debugging.

From f166dc355ea044dd16b7021b3ca01fdde01c395c Mon Sep 17 00:00:00 2001
From: Hubert Sosinski <hubert.sosinski@callstack.com>
Date: Fri, 31 Oct 2025 10:51:26 +0100
Subject: [PATCH 5/7] restore Fullstory checks

---
 src/libs/Fullstory/index.native.ts |  9 ++++-----
 src/libs/Fullstory/index.ts        | 25 ++++++++++++-------------
 src/libs/TelemetrySynchronizer.ts  |  1 -
 3 files changed, 16 insertions(+), 19 deletions(-)

diff --git a/src/libs/Fullstory/index.native.ts b/src/libs/Fullstory/index.native.ts
index 4a915f06d584..a88d82056f32 100644
--- a/src/libs/Fullstory/index.native.ts
+++ b/src/libs/Fullstory/index.native.ts
@@ -34,11 +34,10 @@ const FS: Fullstory = {
             // after the init function since this function is also called on updates for
             // UserMetadata onyx key.
             getEnvironment().then((envName: string) => {
-                // todo: Uncomment before merge. For testing purposes only.
-                // const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
-                // if ((CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) || Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME) {
-                //     return;
-                // }
+                const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
+                if ((CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) || Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME) {
+                    return;
+                }
                 FullStory.restart();
                 FullStory.consent(true);
                 FS.identify(userMetadata, envName);
diff --git a/src/libs/Fullstory/index.ts b/src/libs/Fullstory/index.ts
index 463d0ed5d691..0f0cf0bea05a 100644
--- a/src/libs/Fullstory/index.ts
+++ b/src/libs/Fullstory/index.ts
@@ -55,19 +55,18 @@ const FS: Fullstory = {
         }
         try {
             getEnvironment().then((envName: string) => {
-                // todo: Uncomment before merge. For testing purposes only.
-                // const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
-                // if (
-                //     (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) ||
-                //     Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME ||
-                //     Session.isSupportAuthToken()
-                // ) {
-                //     // On web, if we started FS at some point in a browser, it will run forever. So let's shut it down if we don't want it to run.
-                //     if (isInitialized()) {
-                //         FullStory(CONST.FULLSTORY.OPERATION.SHUTDOWN);
-                //     }
-                //     return;
-                // }
+                const isTestEmail = userMetadata.email !== undefined && userMetadata.email.startsWith('fullstory') && userMetadata.email.endsWith(CONST.EMAIL.QA_DOMAIN);
+                if (
+                    (CONST.ENVIRONMENT.PRODUCTION !== envName && !isTestEmail) ||
+                    Str.extractEmailDomain(userMetadata.email ?? '') === CONST.EXPENSIFY_PARTNER_NAME ||
+                    Session.isSupportAuthToken()
+                ) {
+                    // On web, if we started FS at some point in a browser, it will run forever. So let's shut it down if we don't want it to run.
+                    if (isInitialized()) {
+                        FullStory(CONST.FULLSTORY.OPERATION.SHUTDOWN);
+                    }
+                    return;
+                }
 
                 // If Fullstory was already initialized, we might have shutdown the session. So let's
                 // restart it before identifying the user.
diff --git a/src/libs/TelemetrySynchronizer.ts b/src/libs/TelemetrySynchronizer.ts
index bfb91239b8ac..43a1cfdaea8c 100644
--- a/src/libs/TelemetrySynchronizer.ts
+++ b/src/libs/TelemetrySynchronizer.ts
@@ -7,7 +7,6 @@ import * as Sentry from '@sentry/react-native';
 import CONST from '@src/CONST';
 import FS from './Fullstory';
 
-
 /**
  * Connect to FullStory to retrieve session id from it. We want to link FullStory with Sentry for easier debugging.
  */

From 2845a6369b29370ddcaf5868b59dc3d196d5a3f9 Mon Sep 17 00:00:00 2001
From: Hubert Sosinski <hubert.sosinski@callstack.com>
Date: Fri, 31 Oct 2025 11:34:05 +0100
Subject: [PATCH 6/7] Fix fullstory mock for tests

---
 jest/setupMockFullstoryLib.ts | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/jest/setupMockFullstoryLib.ts b/jest/setupMockFullstoryLib.ts
index 6fd0495e5ab0..82226627dfbb 100644
--- a/jest/setupMockFullstoryLib.ts
+++ b/jest/setupMockFullstoryLib.ts
@@ -17,11 +17,12 @@ export default function mockFSLibrary() {
             Page: FSPage,
             getChatFSClass: jest.fn(),
             init: jest.fn(),
-            onReady: jest.fn(),
+            onReady: jest.fn().mockResolvedValue(undefined),
             consent: jest.fn(),
             identify: jest.fn(),
             consentAndIdentify: jest.fn(),
             anonymize: jest.fn(),
+            getSessionId: jest.fn(),
         };
     });
 }

From b433a34fce226f69d6f669f0b25506e7804d2c43 Mon Sep 17 00:00:00 2001
From: Hubert Sosinski <hubert.sosinski@callstack.com>
Date: Fri, 31 Oct 2025 12:15:40 +0100
Subject: [PATCH 7/7] add comment on the import

---
 src/Expensify.tsx | 1 +
 1 file changed, 1 insertion(+)

diff --git a/src/Expensify.tsx b/src/Expensify.tsx
index 9344816eeb96..85c6e51515ee 100644
--- a/src/Expensify.tsx
+++ b/src/Expensify.tsx
@@ -42,6 +42,7 @@ import './libs/Notification/PushNotification/subscribeToPushNotifications';
 import './libs/registerPaginationConfig';
 import setCrashlyticsUserId from './libs/setCrashlyticsUserId';
 import StartupTimer from './libs/StartupTimer';
+// This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
 import './libs/TelemetrySynchronizer';
 // This lib needs to be imported, but it has nothing to export since all it contains is an Onyx connection
 import './libs/UnreadIndicatorUpdater';
