Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 8 additions & 6 deletions src/components/ActivityIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 & {
Expand All @@ -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 (
<RNActivityIndicator
Expand Down
7 changes: 6 additions & 1 deletion src/components/FullscreenLoadingIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React from 'react';
import type {ActivityIndicatorProps, StyleProp, ViewStyle} from 'react-native';
import {StyleSheet, View} from 'react-native';
import useThemeStyles from '@hooks/useThemeStyles';
import type {ExtraLoadingContext} from '@libs/AppState';
import CONST from '@src/CONST';
import ActivityIndicator from './ActivityIndicator';

Expand All @@ -16,15 +17,19 @@ type FullScreenLoadingIndicatorProps = {

/** The ID of the test to be used for testing */
testID?: string;

/** Extra loading context to be passed to the logAppStateOnLongLoading function */
extraLoadingContext?: ExtraLoadingContext;
};

function FullScreenLoadingIndicator({style, iconSize = CONST.ACTIVITY_INDICATOR_SIZE.LARGE, testID = ''}: FullScreenLoadingIndicatorProps) {
function FullScreenLoadingIndicator({style, iconSize = CONST.ACTIVITY_INDICATOR_SIZE.LARGE, testID = '', extraLoadingContext}: FullScreenLoadingIndicatorProps) {
const styles = useThemeStyles();
return (
<View style={[StyleSheet.absoluteFillObject, styles.fullScreenLoading, style]}>
<ActivityIndicator
size={iconSize}
testID={testID}
extraLoadingContext={extraLoadingContext}
/>
</View>
);
Expand Down
71 changes: 71 additions & 0 deletions src/libs/AppState/RequestsQueuesState/index.ts
Original file line number Diff line number Diff line change
@@ -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';
66 changes: 66 additions & 0 deletions src/libs/AppState/RequestsQueuesState/types.ts
Original file line number Diff line number Diff line change
@@ -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<Request, 'command' | 'persistWhenOngoing' | 'isRollback'>;

/**
* 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};
103 changes: 103 additions & 0 deletions src/libs/AppState/index.ts
Original file line number Diff line number Diff line change
@@ -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<Session>;
let currentNetwork: OnyxEntry<Network>;

// 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<typeof CONST.NETWORK.NETWORK_STATUS>,
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;
46 changes: 46 additions & 0 deletions src/libs/AppState/types.ts
Original file line number Diff line number Diff line change
@@ -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<Network, 'networkStatus' | 'timeSkew' | 'shouldForceOffline' | 'shouldSimulatePoorConnection' | 'shouldFailAllRequests'>;

/**
* Extra loading context for additional debugging information.
*/
type ExtraLoadingContext = Record<string, unknown>;

export type {GlobalStateSnapshot, NavigationStateInfo, SessionStateInfo, NetworkStateInfo, ExtraLoadingContext};
5 changes: 5 additions & 0 deletions src/libs/Network/SequentialQueue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,10 @@ function isPaused(): boolean {
return isQueuePaused;
}

function getShouldFailAllRequests(): boolean {
return shouldFailAllRequests;
}

// Flush the queue when the connection resumes
onReconnection(flush);

Expand Down Expand Up @@ -392,6 +396,7 @@ function resetQueue(): void {
export {
flush,
getCurrentRequest,
getShouldFailAllRequests,
isPaused,
isRunning,
pause,
Expand Down
Loading