Skip to content

Commit 2f40572

Browse files
authored
Merge pull request Expensify#82287 from callstack-internal/sentry-create-expense-flow-improvements
[NOQA] Sentry: Add spans for expense creation flow
2 parents a94a603 + 330ae99 commit 2f40572

9 files changed

Lines changed: 265 additions & 1 deletion

File tree

src/CONST/index.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1726,6 +1726,8 @@ const CONST = {
17261726
SIDEBAR_LOADED: 'sidebar_loaded',
17271727
LOAD_SEARCH_OPTIONS: 'load_search_options',
17281728
SEND_MESSAGE: 'send_message',
1729+
SUBMIT_EXPENSE: 'submit_expense',
1730+
NAVIGATE_AFTER_EXPENSE_CREATE: 'navigate_after_expense_create',
17291731
OPEN_CREATE_EXPENSE: 'open_create_expense',
17301732
OPEN_CREATE_EXPENSE_CONTACT: 'open_create_expense_contact',
17311733
OPEN_CREATE_EXPENSE_APPROVE: 'open_create_expense_approve',
@@ -1805,6 +1807,9 @@ const CONST = {
18051807
SPAN_OD_ND_TRANSITION_LOGGED_OUT: 'ManualOdNdTransitionLoggedOut',
18061808
SPAN_OPEN_SEARCH_ROUTER: 'ManualOpenSearchRouter',
18071809
SPAN_OPEN_CREATE_EXPENSE: 'ManualOpenCreateExpense',
1810+
SPAN_SUBMIT_EXPENSE: 'ManualCreateExpenseSubmit',
1811+
SPAN_NAVIGATE_AFTER_EXPENSE_CREATE: 'ManualCreateExpenseNavigation',
1812+
SPAN_EXPENSE_SERVER_RESPONSE: 'ManualCreateExpenseServerResponse',
18081813
SPAN_SEND_MESSAGE: 'ManualSendMessage',
18091814
SPAN_NOT_FOUND_PAGE: 'ManualNotFoundPage',
18101815
SPAN_SKELETON: 'ManualSkeleton',
@@ -1845,6 +1850,22 @@ const CONST = {
18451850
ATTRIBUTE_MIN_DURATION: 'min_duration',
18461851
ATTRIBUTE_FINISHED_MANUALLY: 'finished_manually',
18471852
ATTRIBUTE_SKELETON_PREFIX: 'skeleton.',
1853+
ATTRIBUTE_SCENARIO: 'scenario',
1854+
ATTRIBUTE_HAS_RECEIPT: 'has_receipt',
1855+
ATTRIBUTE_IS_FROM_GLOBAL_CREATE: 'is_from_global_create',
1856+
ATTRIBUTE_COMMAND: 'command',
1857+
ATTRIBUTE_JSON_CODE: 'json_code',
1858+
SUBMIT_EXPENSE_SCENARIO: {
1859+
REQUEST_MONEY_MANUAL: 'request_money_manual',
1860+
REQUEST_MONEY_SCAN: 'request_money_scan',
1861+
DISTANCE: 'distance',
1862+
TRACK_EXPENSE: 'track_expense',
1863+
SPLIT: 'split',
1864+
SPLIT_RECEIPT: 'split_receipt',
1865+
SPLIT_GLOBAL: 'split_global',
1866+
INVOICE: 'invoice',
1867+
PER_DIEM: 'per_diem',
1868+
},
18481869
// Event names
18491870
EVENT_SKELETON_ATTRIBUTES_UPDATE: 'skeleton_attributes_updated',
18501871
CONFIG: {

src/components/Search/index.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import {
6666
shouldShowYear as shouldShowYearUtil,
6767
} from '@libs/SearchUIUtils';
6868
import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans';
69+
import markNavigateAfterExpenseCreateEnd from '@libs/telemetry/markNavigateAfterExpenseCreateEnd';
6970
import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isOnHold, isTransactionPendingDelete} from '@libs/TransactionUtils';
7071
import Navigation, {navigationRef} from '@navigation/Navigation';
7172
import type {SearchFullscreenNavigatorParamList} from '@navigation/types';
@@ -1144,6 +1145,7 @@ function Search({
11441145
const onLayout = useCallback(() => {
11451146
endSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB);
11461147
endSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB_RENDER);
1148+
markNavigateAfterExpenseCreateEnd();
11471149
handleSelectionListScroll(sortedData, searchListRef.current);
11481150
}, [handleSelectionListScroll, sortedData]);
11491151

@@ -1191,12 +1193,14 @@ function Search({
11911193
if (searchResults === undefined) {
11921194
Log.alert('[Search] Undefined search type');
11931195
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB);
1196+
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE);
11941197
return <FullPageOfflineBlockingView>{null}</FullPageOfflineBlockingView>;
11951198
}
11961199

11971200
if (hasErrors) {
11981201
const isInvalidQuery = searchRequestResponseStatusCode === CONST.JSON_CODE.INVALID_SEARCH_QUERY;
11991202
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB);
1203+
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE);
12001204
return (
12011205
<View style={[shouldUseNarrowLayout ? styles.searchListContentContainerStyles : styles.mt3, styles.flex1]}>
12021206
<FullPageErrorView
@@ -1213,6 +1217,7 @@ function Search({
12131217
const visibleDataLength = filteredData.filter((item) => item.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || isOffline).length;
12141218
if (shouldShowEmptyState(isDataLoaded, visibleDataLength, searchDataType)) {
12151219
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_REPORTS_TAB);
1220+
cancelSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE);
12161221
return (
12171222
<View style={[shouldUseNarrowLayout ? styles.searchListContentContainerStyles : styles.mt3, styles.flex1]}>
12181223
<EmptySearchView

src/libs/API/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import type {AnyRequestMatcher, EnablePolicyFeatureCommand} from '@libs/actions/
66
import Log from '@libs/Log';
77
import {handleDeletedAccount, HandleUnusedOptimisticID, Logging, Pagination, Reauthentication, RecheckConnection, SaveResponseInOnyx, SupportalPermission} from '@libs/Middleware';
88
import FraudMonitoring from '@libs/Middleware/FraudMonitoring';
9+
import SentryServerTiming from '@libs/Middleware/SentryServerTiming';
910
import {isOffline} from '@libs/Network/NetworkStore';
1011
import {push as pushToSequentialQueue, waitForIdle as waitForSequentialQueueIdle} from '@libs/Network/SequentialQueue';
1112
import Pusher from '@libs/Pusher';
@@ -42,6 +43,9 @@ addMiddleware(HandleUnusedOptimisticID);
4243

4344
addMiddleware(Pagination);
4445

46+
// SentryServerTiming - Tracks server round-trip time for configured command groups via Sentry spans.
47+
addMiddleware(SentryServerTiming);
48+
4549
// SaveResponseInOnyx - Merges either the successData or failureData (or finallyData, if included in place of the former two values) into Onyx depending on if the call was successful or not. This needs to be the LAST middleware we use, don't add any
4650
// middlewares after this, because the SequentialQueue depends on the result of this middleware to pause the queue (if needed) to bring the app to an up-to-date state.
4751
addMiddleware(SaveResponseInOnyx);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import {WRITE_COMMANDS} from '@libs/API/types';
2+
import {cancelSpan, endSpan, getSpan, startSpan} from '@libs/telemetry/activeSpans';
3+
import CONST from '@src/CONST';
4+
import type Middleware from './types';
5+
6+
/**
7+
* Configuration for a tracked command group.
8+
* Maps a set of write commands to a Sentry span operation name.
9+
*/
10+
type TrackedCommandGroup = {
11+
/** Set of write commands that should be instrumented */
12+
commands: Set<string>;
13+
/** Sentry span operation name */
14+
spanOp: string;
15+
/** Human-readable span name */
16+
spanName: string;
17+
};
18+
19+
/**
20+
* Registry of command groups to instrument with server round-trip timing.
21+
* Add new entries here to track additional flows without creating new middlewares.
22+
*/
23+
const TRACKED_COMMAND_GROUPS: TrackedCommandGroup[] = [
24+
{
25+
commands: new Set<string>([
26+
WRITE_COMMANDS.REQUEST_MONEY,
27+
WRITE_COMMANDS.CREATE_PER_DIEM_REQUEST,
28+
WRITE_COMMANDS.SPLIT_BILL,
29+
WRITE_COMMANDS.SPLIT_BILL_AND_OPEN_REPORT,
30+
WRITE_COMMANDS.START_SPLIT_BILL,
31+
WRITE_COMMANDS.CREATE_DISTANCE_REQUEST,
32+
WRITE_COMMANDS.TRACK_EXPENSE,
33+
WRITE_COMMANDS.SEND_INVOICE,
34+
]),
35+
spanOp: CONST.TELEMETRY.SPAN_EXPENSE_SERVER_RESPONSE,
36+
spanName: 'expense-server-response',
37+
},
38+
];
39+
40+
/**
41+
* Finds the tracked command group for a given command, if any.
42+
*/
43+
function findTrackedGroup(command: string): TrackedCommandGroup | undefined {
44+
return TRACKED_COMMAND_GROUPS.find((group) => group.commands.has(command));
45+
}
46+
47+
/**
48+
* Middleware that tracks server round-trip time for configured command groups via Sentry spans.
49+
* For non-tracked commands, this is a no-op pass-through.
50+
*
51+
* To add tracking for a new flow, add an entry to TRACKED_COMMAND_GROUPS above.
52+
*/
53+
const SentryServerTiming: Middleware = (response, request) => {
54+
const group = findTrackedGroup(request.command);
55+
if (!group) {
56+
return response;
57+
}
58+
59+
const spanId = `${group.spanOp}_${request.requestID}`;
60+
startSpan(spanId, {
61+
name: group.spanName,
62+
op: group.spanOp,
63+
attributes: {
64+
[CONST.TELEMETRY.ATTRIBUTE_COMMAND]: request.command,
65+
},
66+
});
67+
68+
return response
69+
.then((data) => {
70+
const span = getSpan(spanId);
71+
span?.setAttributes({
72+
[CONST.TELEMETRY.ATTRIBUTE_JSON_CODE]: data?.jsonCode,
73+
});
74+
endSpan(spanId);
75+
return data;
76+
})
77+
.catch((error) => {
78+
cancelSpan(spanId);
79+
throw error;
80+
});
81+
};
82+
83+
export default SentryServerTiming;

src/libs/actions/IOU/index.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1144,6 +1144,13 @@ function handleNavigateAfterExpenseCreate({
11441144
if (!shouldHandleNavigation) {
11451145
return;
11461146
}
1147+
1148+
Performance.markStart(CONST.TIMING.NAVIGATE_AFTER_EXPENSE_CREATE);
1149+
startSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE, {
1150+
name: 'navigate-after-expense-create',
1151+
op: CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE,
1152+
});
1153+
11471154
const queryString = buildCannedSearchQuery({type});
11481155
Navigation.isNavigationReady().then(() => {
11491156
if (getIsNarrowLayout()) {
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import type {IOUType} from '@src/CONST';
2+
import CONST from '@src/CONST';
3+
4+
type GetSubmitExpenseScenarioParams = {
5+
iouType: IOUType;
6+
isDistanceRequest: boolean;
7+
isMovingTransactionFromTrackExpense: boolean;
8+
isUnreported: boolean;
9+
isCategorizingTrackExpense: boolean;
10+
isSharingTrackExpense: boolean;
11+
isPerDiemRequest: boolean;
12+
isFromGlobalCreate: boolean;
13+
hasReceiptFiles: boolean;
14+
};
15+
16+
/**
17+
* Determines the telemetry scenario string for a submit expense action.
18+
*/
19+
function getSubmitExpenseScenario({
20+
iouType,
21+
isDistanceRequest,
22+
isMovingTransactionFromTrackExpense,
23+
isUnreported,
24+
isCategorizingTrackExpense,
25+
isSharingTrackExpense,
26+
isPerDiemRequest,
27+
isFromGlobalCreate,
28+
hasReceiptFiles,
29+
}: GetSubmitExpenseScenarioParams): string {
30+
const {SUBMIT_EXPENSE_SCENARIO} = CONST.TELEMETRY;
31+
32+
if (iouType !== CONST.IOU.TYPE.TRACK && isDistanceRequest && !isMovingTransactionFromTrackExpense && !isUnreported) {
33+
return SUBMIT_EXPENSE_SCENARIO.DISTANCE;
34+
}
35+
if (iouType === CONST.IOU.TYPE.SPLIT) {
36+
if (hasReceiptFiles) {
37+
return SUBMIT_EXPENSE_SCENARIO.SPLIT_RECEIPT;
38+
}
39+
if (isFromGlobalCreate) {
40+
return SUBMIT_EXPENSE_SCENARIO.SPLIT_GLOBAL;
41+
}
42+
return SUBMIT_EXPENSE_SCENARIO.SPLIT;
43+
}
44+
if (iouType === CONST.IOU.TYPE.INVOICE) {
45+
return SUBMIT_EXPENSE_SCENARIO.INVOICE;
46+
}
47+
if (iouType === CONST.IOU.TYPE.TRACK || isCategorizingTrackExpense || isSharingTrackExpense) {
48+
return SUBMIT_EXPENSE_SCENARIO.TRACK_EXPENSE;
49+
}
50+
if (isPerDiemRequest) {
51+
return SUBMIT_EXPENSE_SCENARIO.PER_DIEM;
52+
}
53+
if (hasReceiptFiles) {
54+
return SUBMIT_EXPENSE_SCENARIO.REQUEST_MONEY_SCAN;
55+
}
56+
57+
return SUBMIT_EXPENSE_SCENARIO.REQUEST_MONEY_MANUAL;
58+
}
59+
60+
export default getSubmitExpenseScenario;
61+
export type {GetSubmitExpenseScenarioParams};
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import Performance from '@libs/Performance';
2+
import CONST from '@src/CONST';
3+
import {endSpan, getSpan} from './activeSpans';
4+
5+
/**
6+
* Mark the post-submit navigation telemetry span as finished.
7+
*/
8+
function markNavigateAfterExpenseCreateEnd() {
9+
if (!getSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE)) {
10+
return;
11+
}
12+
endSpan(CONST.TELEMETRY.SPAN_NAVIGATE_AFTER_EXPENSE_CREATE);
13+
Performance.markEnd(CONST.TIMING.NAVIGATE_AFTER_EXPENSE_CREATE);
14+
}
15+
16+
export default markNavigateAfterExpenseCreateEnd;
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import Performance from '@libs/Performance';
2+
import CONST from '@src/CONST';
3+
import {endSpan, getSpan} from './activeSpans';
4+
5+
/**
6+
* Mark the submit expense telemetry span as finished.
7+
* Called after all API.write() calls for the expense have been dispatched.
8+
*/
9+
function markSubmitExpenseEnd() {
10+
if (!getSpan(CONST.TELEMETRY.SPAN_SUBMIT_EXPENSE)) {
11+
return;
12+
}
13+
endSpan(CONST.TELEMETRY.SPAN_SUBMIT_EXPENSE);
14+
Performance.markEnd(CONST.TIMING.SUBMIT_EXPENSE);
15+
}
16+
17+
export default markSubmitExpenseEnd;

0 commit comments

Comments
 (0)