From 2d4da6b8f19609f4fbc866fa683a51218b3736bf Mon Sep 17 00:00:00 2001 From: Hubert Sosinski 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; }; /** From 3cab3f10fa9dde26ada4fd01df0509078b38b250 Mon Sep 17 00:00:00 2001 From: Hubert Sosinski 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 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 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 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 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 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';