From dd760b531c6a57e11e7a7b0d7b2e237134f494e0 Mon Sep 17 00:00:00 2001 From: Mateusz Rajski Date: Thu, 9 Oct 2025 14:01:08 +0200 Subject: [PATCH] Revert "Revert "[No QA] Add more context to `ActivityIndicator` logs"" This reverts commit 5e01fd8a8c468236a01f7ffa8dc10be6cbf2b886. --- src/components/ActivityIndicator.tsx | 14 ++- src/components/FullscreenLoadingIndicator.tsx | 7 +- .../AppState/RequestsQueuesState/index.ts | 71 ++++++++++++ .../AppState/RequestsQueuesState/types.ts | 66 +++++++++++ src/libs/AppState/index.ts | 103 ++++++++++++++++++ src/libs/AppState/types.ts | 46 ++++++++ src/libs/Network/SequentialQueue.ts | 5 + 7 files changed, 305 insertions(+), 7 deletions(-) create mode 100644 src/libs/AppState/RequestsQueuesState/index.ts create mode 100644 src/libs/AppState/RequestsQueuesState/types.ts create mode 100644 src/libs/AppState/index.ts create mode 100644 src/libs/AppState/types.ts diff --git a/src/components/ActivityIndicator.tsx b/src/components/ActivityIndicator.tsx index 1f0c2bea1924..122191ac69bc 100644 --- a/src/components/ActivityIndicator.tsx +++ b/src/components/ActivityIndicator.tsx @@ -3,7 +3,8 @@ import type {ActivityIndicatorProps as RNActivityIndicatorProps} from 'react-nat // eslint-disable-next-line no-restricted-imports import {ActivityIndicator as RNActivityIndicator} from 'react-native'; import useTheme from '@hooks/useTheme'; -import Log from '@libs/Log'; +import logAppStateOnLongLoading from '@libs/AppState'; +import type {ExtraLoadingContext} from '@libs/AppState'; import CONST from '@src/CONST'; type ActivityIndicatorProps = RNActivityIndicatorProps & { @@ -12,22 +13,23 @@ type ActivityIndicatorProps = RNActivityIndicatorProps & { /** Timeout for the activity indicator after which we fire a log about abnormally long loading */ timeout?: number; + + /** Extra loading context to be passed to the logAppStateOnLongLoading function */ + extraLoadingContext?: ExtraLoadingContext; }; -function ActivityIndicator({timeout = CONST.TIMING.ACTIVITY_INDICATOR_TIMEOUT, ...rest}: ActivityIndicatorProps) { +function ActivityIndicator({timeout = CONST.TIMING.ACTIVITY_INDICATOR_TIMEOUT, extraLoadingContext, ...rest}: ActivityIndicatorProps) { const theme = useTheme(); useEffect(() => { const timeoutId = setTimeout(() => { - Log.warn('ActivityIndicator has been shown for longer than expected', { - timeoutMs: timeout, - }); + logAppStateOnLongLoading(extraLoadingContext, timeout); }, timeout); return () => { clearTimeout(timeoutId); }; - }, [timeout]); + }, [extraLoadingContext, timeout]); return ( ); diff --git a/src/libs/AppState/RequestsQueuesState/index.ts b/src/libs/AppState/RequestsQueuesState/index.ts new file mode 100644 index 000000000000..9537325af647 --- /dev/null +++ b/src/libs/AppState/RequestsQueuesState/index.ts @@ -0,0 +1,71 @@ +import {getAll as getAllPersistedRequests, getOngoingRequest} from '@libs/actions/PersistedRequests'; +import {isClientTheLeader} from '@libs/ActiveClientManager'; +import {getAll as getMainQueueRequests} from '@libs/Network/MainQueue'; +import {isPaused as isSequentialQueuePaused, isRunning as isSequentialQueueRunning} from '@libs/Network/SequentialQueue'; +import type {LeaderInfo, MainQueueInfo, PersistedRequestsInfo, RequestQueuesInfo, SequentialQueueInfo} from './types'; + +/** + * Captures current MainQueue state. + */ +function captureMainQueueState(): MainQueueInfo { + const queuedRequests = getMainQueueRequests(); + + return { + pendingRequestsCount: queuedRequests.length, + queuedCommands: queuedRequests.map((request) => request.command).filter(Boolean), + }; +} + +/** + * Captures current PersistedRequests state. + */ +function capturePersistedRequestsState(): PersistedRequestsInfo { + const persistedRequests = getAllPersistedRequests(); + const ongoingRequest = getOngoingRequest(); + + return { + queuedRequestsCount: persistedRequests.length, + queuedCommands: persistedRequests.map((request) => request.command).filter(Boolean), + ongoingRequestInfo: ongoingRequest + ? { + command: ongoingRequest.command, + persistWhenOngoing: ongoingRequest.persistWhenOngoing, + isRollback: ongoingRequest.isRollback, + } + : undefined, + }; +} + +/** + * Captures current SequentialQueue state. + */ +function captureSequentialQueueState(): SequentialQueueInfo { + return { + isRunning: isSequentialQueueRunning(), + isPaused: isSequentialQueuePaused(), + }; +} + +/** + * Captures leader state (whether this client is the leader). + */ +function captureLeaderInfo(): LeaderInfo { + return { + isClientLeader: isClientTheLeader(), + }; +} + +/** + * Captures current requests queues state. + */ +function captureRequestsQueueState(): RequestQueuesInfo { + return { + mainQueue: captureMainQueueState(), + sequentialQueue: captureSequentialQueueState(), + persistedRequests: capturePersistedRequestsState(), + leaderInfo: captureLeaderInfo(), + }; +} + +export default captureRequestsQueueState; +export type {RequestQueuesInfo} from './types'; diff --git a/src/libs/AppState/RequestsQueuesState/types.ts b/src/libs/AppState/RequestsQueuesState/types.ts new file mode 100644 index 000000000000..aecfb914a415 --- /dev/null +++ b/src/libs/AppState/RequestsQueuesState/types.ts @@ -0,0 +1,66 @@ +import type {Request} from '@src/types/onyx'; + +/** + * Main queue state + */ +type MainQueueInfo = { + /** Number of requests waiting to be launched */ + pendingRequestsCount: number; + + /** Types of requests in queue for debugging */ + queuedCommands?: string[]; +}; + +type OngoingRequestInfo = Pick; + +/** + * Persisted requests state + */ +type PersistedRequestsInfo = { + /** Number of requests waiting in persistent storage */ + queuedRequestsCount: number; + + /** Commands of queued requests for debugging */ + queuedCommands: string[]; + + /** Currently ongoing request state */ + ongoingRequestInfo?: OngoingRequestInfo; +}; + +/** + * Sequential queue state + */ +type SequentialQueueInfo = { + /** Whether queue processing engine is actively running */ + isRunning: boolean; + + /** Whether processing is paused (e.g. due to data gaps/conflicts) */ + isPaused: boolean; +}; + +/** + * Request control and failure state. Global state that affects both queue types. + */ +type LeaderInfo = { + /** Whether this client is the leader */ + isClientLeader?: boolean; +}; + +/** + * Request queues and processing state. + */ +type RequestQueuesInfo = { + /** Main queue state */ + mainQueue: MainQueueInfo; + + /** Sequential queue state */ + sequentialQueue: SequentialQueueInfo; + + /** Persisted requests state */ + persistedRequests: PersistedRequestsInfo; + + /** Leader state affecting requests */ + leaderInfo: LeaderInfo; +}; + +export type {MainQueueInfo, PersistedRequestsInfo, SequentialQueueInfo, LeaderInfo, RequestQueuesInfo}; diff --git a/src/libs/AppState/index.ts b/src/libs/AppState/index.ts new file mode 100644 index 000000000000..38e3d3f574d3 --- /dev/null +++ b/src/libs/AppState/index.ts @@ -0,0 +1,103 @@ +import {getPathFromState} from '@react-navigation/native'; +import type {OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import Log from '@libs/Log'; +import {linkingConfig} from '@libs/Navigation/linkingConfig'; +import {navigationRef} from '@libs/Navigation/Navigation'; +import {isAuthenticating as isAuthenticatingNetworkStore} from '@libs/Network/NetworkStore'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Network, Session} from '@src/types/onyx'; +import captureRequestsQueueState from './RequestsQueuesState'; +import type {ExtraLoadingContext, GlobalStateSnapshot, NavigationStateInfo, NetworkStateInfo, SessionStateInfo} from './types'; + +let currentSession: OnyxEntry; +let currentNetwork: OnyxEntry; + +// We have opted for connectWithoutView here as this is strictly non-UI and only for logging. +Onyx.connectWithoutView({ + key: ONYXKEYS.SESSION, + callback: (value) => { + currentSession = value; + }, +}); + +// We have opted for connectWithoutView here as this is strictly non-UI and only for logging. +Onyx.connectWithoutView({ + key: ONYXKEYS.NETWORK, + callback: (value) => { + currentNetwork = value; + }, +}); + +/** + * Captures current navigation state. + */ +function captureNavigationState(): NavigationStateInfo { + try { + const currentRoute = navigationRef.current?.getCurrentRoute(); + if (!currentRoute?.name) { + return {currentPath: undefined}; + } + + const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config); + return { + currentPath: routeFromState || undefined, + }; + } catch (error) { + return {currentPath: undefined}; + } +} + +/** + * Captures current session authentication state. + */ +function captureSessionState(): SessionStateInfo { + // Check multiple authentication states to get complete picture + const isSessionLoading = !!currentSession?.loading; + const isAuthenticatingWithShortLivedToken = !!currentSession?.isAuthenticatingWithShortLivedToken; + const isAuthenticatingFromNetworkStore = isAuthenticatingNetworkStore(); + + return { + isSessionLoading, + isAuthenticatingWithShortLivedToken, + isAuthenticatingFromNetworkStore, + }; +} + +/** + * Captures current network connectivity state. + */ +function captureNetworkState(): NetworkStateInfo { + return { + networkStatus: (currentNetwork?.networkStatus ?? CONST.NETWORK.NETWORK_STATUS.UNKNOWN) as ValueOf, + timeSkew: currentNetwork?.timeSkew, + shouldForceOffline: currentNetwork?.shouldForceOffline, + shouldSimulatePoorConnection: currentNetwork?.shouldSimulatePoorConnection, + shouldFailAllRequests: currentNetwork?.shouldFailAllRequests, + }; +} + +/** + * Captures current global state of the app including navigation, session, network, and request queues. + */ +function captureAppState(): GlobalStateSnapshot { + return { + navigation: captureNavigationState(), + session: captureSessionState(), + network: captureNetworkState(), + requestQueues: captureRequestsQueueState(), + }; +} + +function logAppStateOnLongLoading(extraLoadingContext?: ExtraLoadingContext, timeout?: number): void { + Log.warn('ActivityIndicator has been shown for longer than expected', { + timeoutMs: timeout, + extraLoadingContext, + appState: captureAppState(), + }); +} + +export type {ExtraLoadingContext}; +export default logAppStateOnLongLoading; diff --git a/src/libs/AppState/types.ts b/src/libs/AppState/types.ts new file mode 100644 index 000000000000..28dcd6d368ee --- /dev/null +++ b/src/libs/AppState/types.ts @@ -0,0 +1,46 @@ +import type Network from '@src/types/onyx/Network'; +import type {RequestQueuesInfo} from './RequestsQueuesState'; + +/** + * Main global state snapshot interface used for logging context when loading states exceed timeout. + */ +type GlobalStateSnapshot = { + navigation: NavigationStateInfo; + session: SessionStateInfo; + network: NetworkStateInfo; + requestQueues: RequestQueuesInfo; +}; + +/** + * Navigation and routing information + */ +type NavigationStateInfo = { + /** Current path from navigation state */ + currentPath?: string; +}; + +/** + * Session and authentication state. + */ +type SessionStateInfo = { + /** Whether session is currently loading */ + isSessionLoading: boolean; + + /** Whether authenticating with short-lived token */ + isAuthenticatingWithShortLivedToken: boolean; + + /** Whether authenticating from network store */ + isAuthenticatingFromNetworkStore: boolean; +}; + +/** + * Network connectivity and status. + */ +type NetworkStateInfo = Pick; + +/** + * Extra loading context for additional debugging information. + */ +type ExtraLoadingContext = Record; + +export type {GlobalStateSnapshot, NavigationStateInfo, SessionStateInfo, NetworkStateInfo, ExtraLoadingContext}; diff --git a/src/libs/Network/SequentialQueue.ts b/src/libs/Network/SequentialQueue.ts index 06da3191fff6..efcba7a1e3be 100644 --- a/src/libs/Network/SequentialQueue.ts +++ b/src/libs/Network/SequentialQueue.ts @@ -306,6 +306,10 @@ function isPaused(): boolean { return isQueuePaused; } +function getShouldFailAllRequests(): boolean { + return shouldFailAllRequests; +} + // Flush the queue when the connection resumes onReconnection(flush); @@ -392,6 +396,7 @@ function resetQueue(): void { export { flush, getCurrentRequest, + getShouldFailAllRequests, isPaused, isRunning, pause,