Skip to content
Closed
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
2 changes: 1 addition & 1 deletion src/libs/telemetry/ReceiptObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const RECEIPT_LOG_PREFIX = '[Receipt]';
type ReceiptSnapshotTrigger = 'signOut' | 'background' | 'foreground';

/** How a receipt entered the app. */
type ReceiptCaptureSource = 'camera' | 'gallery' | 'file' | 'replace';
type ReceiptCaptureSource = 'camera' | 'gallery' | 'file' | 'replace' | 'share';

/**
* Maps the picker capture path to a source. On native the picker is the OS gallery. On web the same callback fires
Expand Down
15 changes: 15 additions & 0 deletions src/pages/Share/SubmitDetailsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
import {setMoneyRequestReceipt} from '@libs/actions/IOU/Receipt';
import {requestMoney, trackExpense} from '@libs/actions/IOU/TrackExpense';
import type {GPSPoint as GpsPoint} from '@libs/actions/IOU/types/TrackExpenseTransactionParams';
import {WRITE_COMMANDS} from '@libs/API/types';
import DateUtils from '@libs/DateUtils';
import {getFileName, readFileAsync} from '@libs/fileDownload/FileUtils';
import getCurrentPosition from '@libs/getCurrentPosition';
Expand All @@ -46,6 +47,7 @@ import {hasOnlyPersonalPolicies as hasOnlyPersonalPoliciesUtil, isGroupPolicy} f
import {shouldValidateFile} from '@libs/ReceiptUtils';
import {isMoneyRequestReport, isSelfDM} from '@libs/ReportUtils';
import {cancelSpan, endSpan} from '@libs/telemetry/activeSpans';
import {logReceiptCaptured, logReceiptSubmitted, mintAndStampReceiptTraceId} from '@libs/telemetry/ReceiptObservability';
import {getDefaultTaxCode, getIsFromGlobalCreate, getTaxValue} from '@libs/TransactionUtils';

import DraftWorkspaceOpener from '@pages/iou/request/step/confirmation/DraftWorkspaceOpener';
Expand Down Expand Up @@ -258,6 +260,15 @@ function SubmitDetailsPage({
// `transaction.transactionID` is the draft placeholder; mirror the action's `existingTransactionID ?? rand64()` chain so cleanup nav targets the created expense.
const optimisticTransactionID = existingTransactionID ?? rand64();

// This path skips createTransaction, so log the submit milestone here to map the draft id to the final one.
logReceiptSubmitted({
receiptTraceId: receipt.receiptTraceId,
draftTransactionID: transaction.transactionID,
transactionID: optimisticTransactionID,
command: isSelfDM(report) ? WRITE_COMMANDS.TRACK_EXPENSE : WRITE_COMMANDS.REQUEST_MONEY,
iouType,
});

if (isSelfDM(report)) {
trackExpense({
report: report ?? {reportID: reportOrAccountID},
Expand Down Expand Up @@ -361,6 +372,10 @@ function SubmitDetailsPage({
const onSuccess = (file: File, locationPermissionGranted?: boolean) => {
const receipt: Receipt = file;
receipt.state = file && CONST.IOU.RECEIPT_STATE.SCAN_READY;
// The share flow builds the receipt here by hand and skips buildReceiptFiles, so this is the only place to stamp
// the trace id and log the capture.
const receiptTraceId = mintAndStampReceiptTraceId(receipt);
logReceiptCaptured({file: receipt, captureSource: 'share', receiptTraceId});
if (!locationPermissionGranted) {
finishRequestAndNavigate(receipt);
return;
Expand Down
56 changes: 56 additions & 0 deletions tests/ui/SubmitDetailsPageTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
/* eslint-disable @typescript-eslint/no-unsafe-return */
import {act, fireEvent, render, screen} from '@testing-library/react-native';

import Log from '@libs/Log';

import SubmitDetailsPage from '@pages/Share/SubmitDetailsPage';

import CONST from '@src/CONST';
Expand Down Expand Up @@ -177,4 +179,58 @@ describe('SubmitDetailsPage', () => {
expect(requestMoneyArg?.optimisticTransactionID).not.toBe(CONST.IOU.OPTIMISTIC_TRANSACTION_ID);
expect(cleanupArg?.transactionID).toBe(requestMoneyArg?.optimisticTransactionID);
});

it('stamps the shared receipt with a trace id and logs the capture and submit milestones with source share', async () => {
// Capture each [Receipt] log line into a typed list so assertions never have to cast mock.calls.
const receiptLogs: Array<{message: string; params: Record<string, unknown>}> = [];
const logInfoSpy = jest.spyOn(Log, 'info').mockImplementation((message, _sendNow, params) => {
if (message.includes('[Receipt]') && !!params && typeof params === 'object' && !Array.isArray(params) && !(params instanceof Error)) {
receiptLogs.push({message, params});
}
});

await act(async () => {
await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${SHARED_REPORT_ID}`, createTestReport());
await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${CONST.IOU.OPTIMISTIC_TRANSACTION_ID}`, createDraftTransaction());
await Onyx.merge(ONYXKEYS.SHARE_TEMP_FILE, {content: 'file://shared.jpg', mimeType: 'image/jpeg'});
await Onyx.merge(ONYXKEYS.NVP_LAST_LOCATION_PERMISSION_PROMPT, new Date().toISOString());
await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {
[PARTICIPANT_ACCOUNT_ID]: {accountID: PARTICIPANT_ACCOUNT_ID, login: 'participant@example.com', displayName: 'Participant'},
});
});

render(
<SubmitDetailsPage
// @ts-expect-error minimal route for test
route={{key: 'submit-test', name: 'Share_Submit_Details', params: {reportOrAccountID: SHARED_REPORT_ID}}}
navigation={{} as never}
/>,
);

await waitForBatchedUpdatesWithAct();

fireEvent.press(screen.getByTestId('mock-confirm-button'));
await waitForBatchedUpdatesWithAct();

// The trace id minted at capture travels into the request receipt, so the enqueued and snapshot logs can join back to the capture log.
const requestMoneyArg = jest.mocked(TrackExpense.requestMoney).mock.calls.at(0)?.[0];
const receiptTraceId = requestMoneyArg?.transactionParams?.receipt?.receiptTraceId;
expect(typeof receiptTraceId).toBe('string');
expect(receiptTraceId).toBeTruthy();

// The capture milestone fires for the share entry point.
const captured = receiptLogs.find((line) => line.params.event === 'captured');
expect(captured?.params).toMatchObject({event: 'captured', captureSource: 'share', receiptTraceId});

// The submit milestone maps the fixed draft id to the final transaction id.
const submitted = receiptLogs.find((line) => line.params.event === 'submitted');
expect(submitted?.params).toMatchObject({
event: 'submitted',
receiptTraceId,
draftTransactionID: CONST.IOU.OPTIMISTIC_TRANSACTION_ID,
transactionID: requestMoneyArg?.optimisticTransactionID,
});

logInfoSpy.mockRestore();
});
});
Loading