Skip to content

Commit d1e248e

Browse files
committed
refactor: extract SubmitExpenseOrchestrator + centralize telemetry
1 parent 90d893a commit d1e248e

17 files changed

Lines changed: 633 additions & 305 deletions

src/CONST/index.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2019,6 +2019,7 @@ const CONST = {
20192019
ATTRIBUTE_IS_FROM_GLOBAL_CREATE: 'is_from_global_create',
20202020
/** Sentry span attribute: follow-up action taken after submit (e.g. dismiss_modal_and_open_report, navigate_to_search). */
20212021
ATTRIBUTE_SUBMIT_FOLLOW_UP_ACTION: 'submit_follow_up_action',
2022+
ATTRIBUTE_FAST_PATH_HANDLER: 'fast_path_handler',
20222023
ATTRIBUTE_COMMAND: 'command',
20232024
ATTRIBUTE_JSON_CODE: 'json_code',
20242025
ATTRIBUTE_COLD_START: 'cold_start',
@@ -2032,6 +2033,19 @@ const CONST = {
20322033
NAVIGATE_TO_SEARCH: 'navigate_to_search',
20332034
DISMISS_MODAL_ONLY: 'dismiss_modal_only',
20342035
},
2036+
FAST_PATH_HANDLER: {
2037+
SEARCH_PRE_INSERT: 'search_pre_insert',
2038+
REPORT_PRE_INSERT: 'report_pre_insert',
2039+
DISMISS_MODAL: 'dismiss_modal',
2040+
REPORT_IN_RHP_DISMISS: 'report_in_rhp_dismiss',
2041+
SEARCH_DISMISS: 'search_dismiss',
2042+
DEFAULT: 'default',
2043+
},
2044+
SUBMIT_OPTIMIZATION: {
2045+
PRE_INSERT: 'pre_insert',
2046+
DISMISS_FIRST: 'dismiss_first',
2047+
DEFERRED_WRITE: 'deferred_write',
2048+
},
20352049
/** Trigger for useSubmitToDestinationVisible: end span on focus vs on layout. */
20362050
SUBMIT_TO_DESTINATION_VISIBLE_TRIGGER: {
20372051
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
@@ -1024,6 +1024,10 @@ function getIsFullscreenPreInsertedUnderRHP() {
10241024
return isFullscreenPreInsertedUnderRHP;
10251025
}
10261026

1027+
function getPreInsertedFullscreenRouteName() {
1028+
return preInsertedFullscreenRouteName;
1029+
}
1030+
10271031
function clearFullscreenPreInsertedFlag() {
10281032
isFullscreenPreInsertedUnderRHP = false;
10291033
preInsertedFullscreenRouteName = undefined;
@@ -1140,6 +1144,7 @@ export default {
11401144
revealRouteBeforeDismissingModal,
11411145
preInsertFullscreenUnderRHP,
11421146
getIsFullscreenPreInsertedUnderRHP,
1147+
getPreInsertedFullscreenRouteName,
11431148
clearFullscreenPreInsertedFlag,
11441149
removePreInsertedFullscreenIfNeeded,
11451150
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,
@@ -2290,7 +2289,7 @@ function updateSplitTransactionsFromSplitExpensesFlow(params: UpdateSplitTransac
22902289

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

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

0 commit comments

Comments
 (0)