Skip to content

Commit e176a21

Browse files
authored
Merge pull request Expensify#87963 from software-mansion-labs/korytko/perf/refactor-orchestrator-telemetry
[Performance] Refactor: Extract SubmitExpenseOrchestrator + centralize telemetry
2 parents bafbc6b + 662b31e commit e176a21

17 files changed

Lines changed: 614 additions & 303 deletions

src/CONST/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2022,6 +2022,7 @@ const CONST = {
20222022
ATTRIBUTE_IS_FROM_GLOBAL_CREATE: 'is_from_global_create',
20232023
/** Sentry span attribute: follow-up action taken after submit (e.g. dismiss_modal_and_open_report, navigate_to_search). */
20242024
ATTRIBUTE_SUBMIT_FOLLOW_UP_ACTION: 'submit_follow_up_action',
2025+
ATTRIBUTE_FAST_PATH_HANDLER: 'fast_path_handler',
20252026
ATTRIBUTE_COMMAND: 'command',
20262027
ATTRIBUTE_JSON_CODE: 'json_code',
20272028
ATTRIBUTE_COLD_START: 'cold_start',
@@ -2035,6 +2036,19 @@ const CONST = {
20352036
NAVIGATE_TO_SEARCH: 'navigate_to_search',
20362037
DISMISS_MODAL_ONLY: 'dismiss_modal_only',
20372038
},
2039+
FAST_PATH_HANDLER: {
2040+
SEARCH_PRE_INSERT: 'search_pre_insert',
2041+
REPORT_PRE_INSERT: 'report_pre_insert',
2042+
DISMISS_MODAL: 'dismiss_modal',
2043+
REPORT_IN_RHP_DISMISS: 'report_in_rhp_dismiss',
2044+
SEARCH_DISMISS: 'search_dismiss',
2045+
DEFAULT: 'default',
2046+
},
2047+
SUBMIT_OPTIMIZATION: {
2048+
PRE_INSERT: 'pre_insert',
2049+
DISMISS_FIRST: 'dismiss_first',
2050+
DEFERRED_WRITE: 'deferred_write',
2051+
},
20382052
/** Trigger for useSubmitToDestinationVisible: end span on focus vs on layout. */
20392053
SUBMIT_TO_DESTINATION_VISIBLE_TRIGGER: {
20402054
FOCUS: 'focus',
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {useCallback, useRef} from 'react';
2+
import {endSubmitFollowUpActionSpan, getPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
3+
import CONST from '@src/CONST';
4+
5+
type Options = {
6+
requireLayout?: boolean;
7+
};
8+
9+
/**
10+
* Shared callback for ending submit-expense navigation spans on Search pages.
11+
*
12+
* - `requireLayout: true` (narrow): dual-gate requiring both focus + layout signals
13+
* before ending, except for DISMISS_MODAL_ONLY where focus alone suffices (the page
14+
* is already mounted and laid out so onLayout won't re-fire).
15+
* - `requireLayout: false` (wide): ends immediately on any signal (pre-insert is
16+
* narrow-only so the dual-gate isn't needed).
17+
*/
18+
function useEndSubmitNavigationSpans({requireLayout = true}: Options = {}): (wasListEmpty: boolean, source: 'focus' | 'layout') => void {
19+
const hadFocusRef = useRef(false);
20+
const hadLayoutRef = useRef(false);
21+
22+
return useCallback(
23+
(wasListEmpty: boolean, source: 'focus' | 'layout') => {
24+
if (requireLayout) {
25+
if (source === 'focus') {
26+
hadFocusRef.current = true;
27+
} else {
28+
hadLayoutRef.current = true;
29+
}
30+
31+
const pendingForGate = getPendingSubmitFollowUpAction();
32+
const isDismissOnly = pendingForGate?.followUpAction === CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY;
33+
const hasBothSignals = hadFocusRef.current && hadLayoutRef.current;
34+
const hasFocusOnly = hadFocusRef.current && isDismissOnly;
35+
36+
if (!hasBothSignals && !hasFocusOnly) {
37+
return;
38+
}
39+
40+
hadFocusRef.current = false;
41+
hadLayoutRef.current = false;
42+
}
43+
44+
// Re-read after the gate check - the value is synchronous module state so it
45+
// can't change between the two reads in the same tick, but reading once here
46+
// keeps the logic self-contained for the actual end-span decision.
47+
const pending = getPendingSubmitFollowUpAction();
48+
if (pending && pending.followUpAction !== CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT) {
49+
endSubmitFollowUpActionSpan(pending.followUpAction, undefined, {
50+
[CONST.TELEMETRY.ATTRIBUTE_IS_WARM]: true,
51+
[CONST.TELEMETRY.ATTRIBUTE_WAS_LIST_EMPTY]: wasListEmpty,
52+
});
53+
}
54+
},
55+
[requireLayout],
56+
);
57+
}
58+
59+
export default useEndSubmitNavigationSpans;

src/libs/Navigation/Navigation.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1050,6 +1050,10 @@ function getIsFullscreenPreInsertedUnderRHP() {
10501050
return isFullscreenPreInsertedUnderRHP;
10511051
}
10521052

1053+
function getPreInsertedFullscreenRouteName() {
1054+
return preInsertedFullscreenRouteName;
1055+
}
1056+
10531057
function clearFullscreenPreInsertedFlag() {
10541058
isFullscreenPreInsertedUnderRHP = false;
10551059
preInsertedFullscreenRouteName = undefined;
@@ -1166,6 +1170,7 @@ export default {
11661170
revealRouteBeforeDismissingModal,
11671171
preInsertFullscreenUnderRHP,
11681172
getIsFullscreenPreInsertedUnderRHP,
1173+
getPreInsertedFullscreenRouteName,
11691174
clearFullscreenPreInsertedFlag,
11701175
removePreInsertedFullscreenIfNeeded,
11711176
getTopmostSearchReportID,

src/libs/Navigation/helpers/dismissModalAndOpenReportInInboxTab.ts

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import {InteractionManager} from 'react-native';
22
import getIsNarrowLayout from '@libs/getIsNarrowLayout';
33
import Navigation, {navigationRef} from '@libs/Navigation/Navigation';
4-
import {getSpan} from '@libs/telemetry/activeSpans';
5-
import {endSubmitFollowUpActionSpan, setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
4+
import {endSubmitFollowUpActionSpan, isTracking as isSubmitTracking, setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
65
import CONST from '@src/CONST';
76
import ROUTES from '@src/ROUTES';
87
import isReportOpenInRHP from './isReportOpenInRHP';
@@ -16,15 +15,15 @@ import setNavigationActionToMicrotaskQueue from './setNavigationActionToMicrotas
1615
*/
1716
function dismissModalAndOpenReportInInboxTab(reportID: string | undefined, isInvoice: boolean | undefined, hasMultipleTransactions: boolean) {
1817
const rootState = navigationRef.getRootState();
19-
const hasSubmitToDestinationVisibleSpan = !!getSpan(CONST.TELEMETRY.SPAN_SUBMIT_TO_DESTINATION_VISIBLE);
18+
const hasActiveTracking = isSubmitTracking();
2019

2120
if (!isInvoice && isReportOpenInRHP(rootState)) {
2221
const rhpKey = rootState.routes.at(-1)?.state?.key;
2322
if (rhpKey) {
2423
const isSuperWideRHP = isReportOpenInSuperWideRHP(rootState);
2524

26-
// submit_follow_up_action: only set when the span was started.
27-
if (hasSubmitToDestinationVisibleSpan) {
25+
// submit_follow_up_action: only set when tracking is active.
26+
if (hasActiveTracking) {
2827
if (isSuperWideRHP) {
2928
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY, reportID);
3029
} else if (hasMultipleTransactions && reportID) {
@@ -58,19 +57,19 @@ function dismissModalAndOpenReportInInboxTab(reportID: string | undefined, isInv
5857
}
5958
}
6059
if (isSearchTopmostFullScreenRoute() || !reportID) {
61-
if (hasSubmitToDestinationVisibleSpan) {
60+
if (hasActiveTracking) {
6261
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY);
6362
}
6463
Navigation.dismissModal();
65-
if (hasSubmitToDestinationVisibleSpan) {
64+
if (hasActiveTracking) {
6665
// eslint-disable-next-line @typescript-eslint/no-deprecated -- we need to wait for the modal to be dismissed before marking the span
6766
InteractionManager.runAfterInteractions(() => {
6867
endSubmitFollowUpActionSpan(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_ONLY);
6968
});
7069
}
7170
return;
7271
}
73-
if (hasSubmitToDestinationVisibleSpan) {
72+
if (hasActiveTracking) {
7473
Navigation.dismissModalWithReport({reportID}, undefined, {
7574
onBeforeNavigate: (willOpenReport) => {
7675
setPendingSubmitFollowUpAction(

src/libs/actions/IOU/SendMoney.ts

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import {
1919
getParsedComment,
2020
} from '@libs/ReportUtils';
2121
import playSound, {SOUNDS} from '@libs/Sound';
22-
import {startSpan} from '@libs/telemetry/activeSpans';
22+
import {startTracking} from '@libs/telemetry/submitFollowUpAction';
2323
import {buildOptimisticTransaction} from '@libs/TransactionUtils';
2424
import {notifyNewAction} from '@userActions/Report';
2525
import CONST from '@src/CONST';
@@ -510,17 +510,16 @@ function sendMoneyElsewhere({report, quickAction, amount, currency, comment, cur
510510
optimisticChatReportID,
511511
currentUserAccountID,
512512
});
513-
startSpan(CONST.TELEMETRY.SPAN_SUBMIT_TO_DESTINATION_VISIBLE, {
514-
name: 'submit-to-destination-visible',
515-
op: CONST.TELEMETRY.SPAN_SUBMIT_TO_DESTINATION_VISIBLE,
516-
attributes: {
517-
[CONST.TELEMETRY.ATTRIBUTE_SCENARIO]: CONST.TELEMETRY.SUBMIT_EXPENSE_SCENARIO.SEND_MONEY,
518-
[CONST.TELEMETRY.ATTRIBUTE_HAS_RECEIPT]: !!receipt,
519-
[CONST.TELEMETRY.ATTRIBUTE_IS_FROM_GLOBAL_CREATE]: isEmptyObject(report) || !report?.reportID,
520-
[CONST.TELEMETRY.ATTRIBUTE_IOU_TYPE]: CONST.IOU.TYPE.PAY,
521-
[CONST.TELEMETRY.ATTRIBUTE_IOU_REQUEST_TYPE]: 'pay',
513+
startTracking(
514+
{
515+
scenario: CONST.TELEMETRY.SUBMIT_EXPENSE_SCENARIO.SEND_MONEY,
516+
iouType: CONST.IOU.TYPE.PAY,
517+
requestType: 'pay',
518+
isFromGlobalCreate: isEmptyObject(report) || !report?.reportID,
519+
hasReceipt: !!receipt,
522520
},
523-
});
521+
{skipSubmitExpenseSpan: true},
522+
);
524523
playSound(SOUNDS.DONE);
525524
API.write(WRITE_COMMANDS.SEND_MONEY_ELSEWHERE, params, {optimisticData, successData, failureData});
526525

@@ -543,17 +542,16 @@ function sendMoneyWithWallet({report, quickAction, amount, currency, comment, cu
543542
optimisticChatReportID,
544543
currentUserAccountID,
545544
});
546-
startSpan(CONST.TELEMETRY.SPAN_SUBMIT_TO_DESTINATION_VISIBLE, {
547-
name: 'submit-to-destination-visible',
548-
op: CONST.TELEMETRY.SPAN_SUBMIT_TO_DESTINATION_VISIBLE,
549-
attributes: {
550-
[CONST.TELEMETRY.ATTRIBUTE_SCENARIO]: CONST.TELEMETRY.SUBMIT_EXPENSE_SCENARIO.SEND_MONEY,
551-
[CONST.TELEMETRY.ATTRIBUTE_HAS_RECEIPT]: !!receipt,
552-
[CONST.TELEMETRY.ATTRIBUTE_IS_FROM_GLOBAL_CREATE]: isEmptyObject(report) || !report?.reportID,
553-
[CONST.TELEMETRY.ATTRIBUTE_IOU_TYPE]: CONST.IOU.TYPE.PAY,
554-
[CONST.TELEMETRY.ATTRIBUTE_IOU_REQUEST_TYPE]: 'pay',
545+
startTracking(
546+
{
547+
scenario: CONST.TELEMETRY.SUBMIT_EXPENSE_SCENARIO.SEND_MONEY,
548+
iouType: CONST.IOU.TYPE.PAY,
549+
requestType: 'pay',
550+
isFromGlobalCreate: isEmptyObject(report) || !report?.reportID,
551+
hasReceipt: !!receipt,
555552
},
556-
});
553+
{skipSubmitExpenseSpan: true},
554+
);
557555
playSound(SOUNDS.DONE);
558556
API.write(WRITE_COMMANDS.SEND_MONEY_WITH_WALLET, params, {optimisticData, successData, failureData});
559557

src/libs/actions/IOU/Split.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,7 @@ import {
5959
updateReportPreview,
6060
} from '@libs/ReportUtils';
6161
import playSound, {SOUNDS} from '@libs/Sound';
62-
import {getSpan} from '@libs/telemetry/activeSpans';
63-
import {setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
62+
import {isTracking, setPendingSubmitFollowUpAction} from '@libs/telemetry/submitFollowUpAction';
6463
import {
6564
buildOptimisticTransaction,
6665
getAmount,
@@ -2291,7 +2290,7 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac
22912290

22922291
const targetReportID = params.expenseReport?.reportID ?? String(CONST.DEFAULT_NUMBER_ID);
22932292

2294-
if (getSpan(CONST.TELEMETRY.SPAN_SUBMIT_TO_DESTINATION_VISIBLE)) {
2293+
if (isTracking()) {
22952294
setPendingSubmitFollowUpAction(CONST.TELEMETRY.SUBMIT_FOLLOW_UP_ACTION.DISMISS_MODAL_AND_OPEN_REPORT, targetReportID);
22962295
}
22972296
Navigation.dismissModalWithReport({reportID: targetReportID});

0 commit comments

Comments
 (0)