Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {useIsReportLoadPending} from '@hooks/useInFlightRequests';
import useIsReportActionsLoaded from '@hooks/useIsReportActionsLoaded';
import useLoadReportActions from '@hooks/useLoadReportActions';
import useLocalize from '@hooks/useLocalize';
import useMarkOpenReportEndOnSkeleton from '@hooks/useMarkOpenReportEndOnSkeleton';
import useNetworkWithOfflineStatus from '@hooks/useNetworkWithOfflineStatus';
import useNewTransactions from '@hooks/useNewTransactions';
import useOnyx from '@hooks/useOnyx';
Expand Down Expand Up @@ -213,9 +212,6 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
return filteredActions.slice().reverse();
}, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction, visibleReportActionsData, reportID]);

const shouldShowOpenReportLoadingSkeleton = isInitialReportLoadPending && visibleReportActions.length === 0;
useMarkOpenReportEndOnSkeleton(report, shouldShowOpenReportLoadingSkeleton);

const lastAction = visibleReportActions.at(-1);

const {scrollOffsetRef} = useActionListContext();
Expand Down Expand Up @@ -726,14 +722,14 @@ function MoneyRequestReportActionsList({onLayout}: MoneyRequestReportListProps)
* Runs when the FlatList finishes laying out
*/
const recordTimeToMeasureItemLayout = useCallback(() => {
if (didLayout.current || !report) {
if (didLayout.current || !reportIDFromRoute) {
return;
}

didLayout.current = true;

markOpenReportEnd(report, {warm: !shouldShowOpenReportLoadingSkeleton});
}, [report, shouldShowOpenReportLoadingSkeleton]);
markOpenReportEnd(reportIDFromRoute, report, {warm: true});
}, [reportIDFromRoute, report]);

const isReportEmpty = isEmpty(visibleReportActions) && isEmpty(transactions) && !isInitialReportLoadPending;
const showEmptyState = isReportEmpty;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ type MoneyRequestReportViewProps = {
/** The report */
report: OnyxEntry<OnyxTypes.Report>;

/** Report ID from the route, known before the report itself loads */
reportIDFromRoute: string | undefined;

/** Loading state for report */
reportLoadingState: OnyxEntry<OnyxTypes.ReportLoadingState>;

Expand Down Expand Up @@ -110,7 +113,7 @@ function InitialLoadingSkeleton({styles, onLayout}: {styles: ThemeStyles; onLayo
);
}

function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReportFooter, backToRoute, onLayout}: MoneyRequestReportViewProps) {
function MoneyRequestReportView({report, reportIDFromRoute, reportLoadingState, shouldDisplayReportFooter, backToRoute, onLayout}: MoneyRequestReportViewProps) {
const styles = useThemeStyles();
const {isOffline} = useNetwork();

Expand Down Expand Up @@ -205,21 +208,24 @@ function MoneyRequestReportView({report, reportLoadingState, shouldDisplayReport
};
}, [reportID]);

useMarkOpenReportEndOnSkeleton(report, shouldShowOpenReportLoadingSkeleton);
const shouldShowEmptyActionsSkeleton = reportActions.length === 0;
const shouldShowAppLoadSkeleton = !!report && isAppLoadPending;
// Route id, not report?.reportID: these skeletons render before the report lands in Onyx.
useMarkOpenReportEndOnSkeleton(reportIDFromRoute, shouldShowOpenReportLoadingSkeleton || shouldShowEmptyActionsSkeleton || shouldShowAppLoadSkeleton);

if (shouldShowOpenReportLoadingSkeleton) {
return <InitialLoadingSkeleton styles={styles} />;
}

if (reportActions.length === 0) {
if (shouldShowEmptyActionsSkeleton) {
return <ReportActionsSkeletonView shouldAnimate={false} />;
}

if (!report) {
return;
}

if (isAppLoadPending) {
if (shouldShowAppLoadSkeleton) {
return (
<View style={styles.flex1}>
<ReportHeaderSkeletonView />
Expand Down
24 changes: 13 additions & 11 deletions src/hooks/useMarkOpenReportEndOnSkeleton.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';

import type * as OnyxTypes from '@src/types/onyx';

import type {OnyxEntry} from 'react-native-onyx';
import ONYXKEYS from '@src/ONYXKEYS';

import {useEffect} from 'react';

import useOnyx from './useOnyx';

/**
* Closes the open-report performance span as a cold open (`warm: false`) the moment a cold-load skeleton
* appears. Shared by every report surface that gates its own skeleton (the chat-list guard, the route
* orchestrator, and the money-request views): the open-report span must still close even though the list
* body — which would otherwise own this mark once mounted — isn't mounted while the skeleton shows.
* Closes the open-report span as a cold open (`warm: false`) while a skeleton shows. Call it from a component
* mounted exactly while the skeleton is on screen — the list body that would otherwise close the span isn't

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-16 (docs)

Comments should read as plain sentences without em dashes. This JSDoc description line uses an em dash to join two clauses.

Replace the em dash with a period and start a new sentence:

 * Closes the open-report span as a cold open (`warm: false`) while a skeleton shows. Call it from a component
 * mounted exactly while the skeleton is on screen. The list body that would otherwise close the span isn't
 * mounted yet.

Reviewed at: f281e4a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

* mounted yet.
*/
function useMarkOpenReportEndOnSkeleton(report: OnyxEntry<OnyxTypes.Report>, isSkeletonVisible: boolean) {
function useMarkOpenReportEndOnSkeleton(reportID: string | undefined, isSkeletonVisible = true) {
const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`);

useEffect(() => {
if (!isSkeletonVisible || !report) {
if (!isSkeletonVisible || !reportID) {
return;
}
markOpenReportEnd(report, {warm: false});
}, [report, isSkeletonVisible]);
markOpenReportEnd(reportID, report, {warm: false});
}, [reportID, report, isSkeletonVisible]);
}

export default useMarkOpenReportEndOnSkeleton;
12 changes: 6 additions & 6 deletions src/libs/telemetry/markOpenReportEnd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import CONST from '@src/CONST';
import type * as OnyxTypes from '@src/types/onyx';

import type {SpanAttributes} from '@sentry/core';
import type {OnyxEntry} from 'react-native-onyx';

import {endSpanWithAttributes} from './activeSpans';

Expand All @@ -12,11 +13,10 @@ type MarkOpenReportEndOptions = {
};

/**
* Mark all 'open_report*' telemetry spans as finished.
* Mark all 'open_report*' telemetry spans as finished. Keyed by `reportID` so it still ends when the report
* hasn't loaded; the report-shape attributes are then left off.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-16 (docs)

Comments should use two separate sentences instead of a semicolon. This JSDoc line joins two independent statements with a semicolon.

Split into two sentences:

 * Mark all 'open_report*' telemetry spans as finished. Keyed by `reportID` so it still ends when the report
 * hasn't loaded. The report-shape attributes are then left off.

Reviewed at: f281e4a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

*/
function markOpenReportEnd(report: OnyxTypes.Report, options: MarkOpenReportEndOptions = {}) {
const {reportID, type, chatType} = report;

function markOpenReportEnd(reportID: string, report: OnyxEntry<OnyxTypes.Report>, options: MarkOpenReportEndOptions = {}) {
const isTransactionThread = isReportTransactionThread(report);
const isOneTransactionThread = isOneTransactionReport(report);

Expand All @@ -25,8 +25,8 @@ function markOpenReportEnd(report: OnyxTypes.Report, options: MarkOpenReportEndO
const attributes: SpanAttributes = {
[CONST.TELEMETRY.ATTRIBUTE_IS_TRANSACTION_THREAD]: isTransactionThread,
[CONST.TELEMETRY.ATTRIBUTE_IS_ONE_TRANSACTION_REPORT]: isOneTransactionThread,
[CONST.TELEMETRY.ATTRIBUTE_REPORT_TYPE]: type,
[CONST.TELEMETRY.ATTRIBUTE_CHAT_TYPE]: chatType,
[CONST.TELEMETRY.ATTRIBUTE_REPORT_TYPE]: report?.type,
[CONST.TELEMETRY.ATTRIBUTE_CHAT_TYPE]: report?.chatType,
};

if (options.warm !== undefined) {
Expand Down
1 change: 1 addition & 0 deletions src/pages/Search/SearchMoneyRequestReportPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,7 @@ function SearchMoneyRequestReportPage({route}: SearchMoneyRequestPageProps) {
<DragAndDropProvider isDisabled={isEditingDisabled}>
<MoneyRequestReportView
report={report}
reportIDFromRoute={reportIDFromRoute}
reportLoadingState={reportLoadingState}
shouldDisplayReportFooter={isCurrentReportLoadedFromOnyx}
key={report?.reportID}
Expand Down
8 changes: 3 additions & 5 deletions src/pages/inbox/ReportActions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import MoneyRequestReportActionsList from '@components/MoneyRequestReportView/Mo
import NavigationDeferredMount from '@components/NavigationDeferredMount';

import {useIsAppLoadPending, useIsReportLoadPending} from '@hooks/useInFlightRequests';
import useMarkOpenReportEndOnSkeleton from '@hooks/useMarkOpenReportEndOnSkeleton';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import usePaginatedReportActions from '@hooks/usePaginatedReportActions';
Expand Down Expand Up @@ -62,16 +61,13 @@ function ReportActions() {
// The app-load skeleton is hoisted out of the body so the body's data hooks/effects never run
// during app boot. It only applies on the chat path (after the skeleton and money-request
// branches below) — matching the previous behavior, where this skeleton lived inside the
// chat-only ReportActionsView. Because the body won't mount for this branch, it can't close the
// open-report span itself, so we close it here for the branch we gate.
// chat-only ReportActionsView.
//
// Concierge is excluded so the body still mounts under the app-load skeleton, seeding sessionStartTime
// before content appeared.
const isConciergeMainDM = isConciergeChatReport(report, conciergeReportID);
const shouldShowAppLoadSkeleton = isAppLoadPending && !isOffline && !!report && !shouldWaitForTransactions && !shouldDisplayMoneyRequestActionsList && !isConciergeMainDM;

useMarkOpenReportEndOnSkeleton(report, shouldShowAppLoadSkeleton);

if (!report || shouldWaitForTransactions) {
return (
<ReportActionsLoadingSkeleton
Expand Down Expand Up @@ -122,9 +118,11 @@ function ReportActionsWithInboxTabDeferredMount({reportID, shouldDefer}: ReportA
<NavigationDeferredMount
waitForUpcomingTransition={false}
placeholder={
// Deferral, not a data wait — closing the span here would time the defer and tag a cached report cold.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-16 (docs)

Comments should read as plain sentences without em dashes. This comment uses an em dash to join two clauses.

Replace the em dash with a period and start a new sentence:

// Deferral, not a data wait. Closing the span here would time the defer and tag a cached report cold.

Reviewed at: f281e4a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

<ReportActionsLoadingSkeleton
reportID={reportID}
skeletonName={CONST.TELEMETRY.CANCELED_BY_SKELETON.INBOX_TAB_DEFER}
shouldMarkOpenReportEnd={false}
/>
}
>
Expand Down
4 changes: 1 addition & 3 deletions src/pages/inbox/report/ReportActionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -430,9 +430,7 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListContent

didLayout.current = true;

if (report) {
markOpenReportEnd(report, {warm: true});
}
markOpenReportEnd(reportID, report, {warm: true});
};

// The guard only mounts this content when the report is loaded, so this is effectively unreachable.
Expand Down
1 change: 0 additions & 1 deletion src/pages/inbox/report/ReportActionsListContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,6 @@ function computeReportActionsSkeletonState(readinessSignals: ReportActionsReadin
return {
shouldShowLoadingSkeleton,
shouldShowDerivedTimingSkeleton,
shouldShowInitialSkeleton,
};
}

Expand Down
12 changes: 9 additions & 3 deletions src/pages/inbox/report/ReportActionsLoadingSkeleton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';

import useCancelSendMessageSpanOnSkeleton from '@hooks/useCancelSendMessageSpanOnSkeleton';
import type {SkeletonName} from '@hooks/useCancelSendMessageSpanOnSkeleton';
import useMarkOpenReportEndOnSkeleton from '@hooks/useMarkOpenReportEndOnSkeleton';

import React from 'react';

Expand All @@ -14,14 +15,19 @@ type ReportActionsLoadingSkeletonProps = {

/** Whether the skeleton rows animate */
shouldAnimate?: boolean;

/** Whether this skeleton closes the open-report span as a cold open. Off when it isn't waiting on report data */
shouldMarkOpenReportEnd?: boolean;
};

/**
* Report-actions loading skeleton. Mounted only while the skeleton shows, so it hosts the hook that cancels
* the otherwise never-ending send-message span (tagged with `skeletonName`).
* Report-actions loading skeleton. Hosts the skeleton-phase span marks — cancelling the never-ending

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

❌ CONSISTENCY-16 (docs)

Comments should read as plain sentences without em dashes. This JSDoc description uses two em dashes to bracket a clause.

Rewrite without em dashes, for example:

 * Report-actions loading skeleton. Hosts the skeleton-phase span marks: cancelling the never-ending
 * send-message span, and closing the open-report span as cold. Mounted here means visible, while a
 * parent's copy of the skeleton condition can drift from what actually renders.

Reviewed at: f281e4a | Please rate this suggestion with 👍 or 👎 to help us improve! Reactions are used to monitor reviewer efficiency.

* send-message span, and closing the open-report span as cold — because mounted here means visible, while a
* parent's copy of the skeleton condition can drift from what actually renders.
*/
function ReportActionsLoadingSkeleton({reportID, skeletonName, shouldAnimate = true}: ReportActionsLoadingSkeletonProps) {
function ReportActionsLoadingSkeleton({reportID, skeletonName, shouldAnimate = true, shouldMarkOpenReportEnd = true}: ReportActionsLoadingSkeletonProps) {
useCancelSendMessageSpanOnSkeleton(reportID, skeletonName);
useMarkOpenReportEndOnSkeleton(reportID, shouldMarkOpenReportEnd);
return <ReportActionsSkeletonView shouldAnimate={shouldAnimate} />;
}

Expand Down
6 changes: 1 addition & 5 deletions src/pages/inbox/report/ReportActionsSkeletonGuard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import useBackfillWhenNoVisibleActions from '@hooks/useBackfillWhenNoVisibleActions';
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
import {useIsReportLoadPending} from '@hooks/useInFlightRequests';
import useMarkOpenReportEndOnSkeleton from '@hooks/useMarkOpenReportEndOnSkeleton';
import usePendingConciergeResponse from '@hooks/usePendingConciergeResponse';
import useReportActionsListModel from '@hooks/useReportActionsListModel';
import useStartConciergeSession from '@hooks/useStartConciergeSession';
Expand Down Expand Up @@ -34,10 +33,9 @@ type ReportActionsSkeletonGuardProps = {
function ReportActionsSkeletonGuard({reportID, children}: ReportActionsSkeletonGuardProps) {
const isReportLoadPending = useIsReportLoadPending(reportID);
const {readinessSignals, state, actions} = useReportActionsListModel(reportID, isReportLoadPending);
const {shouldShowLoadingSkeleton, shouldShowDerivedTimingSkeleton, shouldShowInitialSkeleton} = computeReportActionsSkeletonState(readinessSignals);
const {shouldShowLoadingSkeleton, shouldShowDerivedTimingSkeleton} = computeReportActionsSkeletonState(readinessSignals);

const {
report,
isConciergeMainDM,
oldestUnreadReportAction,
hasOnceLoadedReportActions,
Expand All @@ -63,8 +61,6 @@ function ReportActionsSkeletonGuard({reportID, children}: ReportActionsSkeletonG
hasCachedReportActions,
});

useMarkOpenReportEndOnSkeleton(report, shouldShowInitialSkeleton);

useBackfillWhenNoVisibleActions({
reportID,
isMissingReportActions,
Expand Down
1 change: 1 addition & 0 deletions tests/ui/MoneyRequestReportViewTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const renderMoneyRequestReportView = (onLayout: (event: LayoutChangeEvent) => vo
render(
<MoneyRequestReportView
report={mockReport}
reportIDFromRoute={mockReport.reportID}
reportLoadingState={mockReportLoadingState}
shouldDisplayReportFooter={false}
backToRoute={undefined}
Expand Down
9 changes: 4 additions & 5 deletions tests/ui/ReportActionsListTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,7 @@ describe('ReportActionsList (body)', () => {
renderReportActionsList();

expect(screen.getByTestId('ReportActionsSkeletonView')).toBeTruthy();
expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(mockReport, {warm: false});
expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(mockReport.reportID, mockReport, {warm: false});
expect(mockUseIsReportLoadPending).toHaveBeenCalledWith(mockReport.reportID);
});

Expand Down Expand Up @@ -575,10 +575,9 @@ describe('ReportActionsList (body)', () => {

renderReportActionsList();

// The guard owns this mark now (it used to live in the body); it must still fire while the
// initial skeleton shows, otherwise the open-report span regresses.
// Must fire while the skeleton shows or the open-report span regresses.
expect(screen.getByTestId('ReportActionsSkeletonView')).toBeTruthy();
expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(mockReport, {warm: false});
expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(mockReport.reportID, mockReport, {warm: false});
});

it('does not fire the warm:false mark once content is visible', () => {
Expand All @@ -589,7 +588,7 @@ describe('ReportActionsList (body)', () => {
renderReportActionsList();

expect(screen.queryByTestId('ReportActionsSkeletonView')).toBeNull();
expect(mockMarkOpenReportEnd).not.toHaveBeenCalledWith(mockReport, {warm: false});
expect(mockMarkOpenReportEnd).not.toHaveBeenCalledWith(mockReport.reportID, mockReport, {warm: false});
});
});

Expand Down
2 changes: 1 addition & 1 deletion tests/ui/ReportActionsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ describe('ReportActions (orchestrator)', () => {

expect(screen.getByTestId('ReportActionsSkeletonView')).toBeTruthy();
expect(mockReportActionsListBody).not.toHaveBeenCalled();
expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(mockReport, {warm: false});
expect(mockMarkOpenReportEnd).toHaveBeenCalledWith(REPORT_ID, mockReport, {warm: false});
});

it('mounts the body (not the orchestrator app-load skeleton) for a Concierge report during app load', () => {
Expand Down
20 changes: 10 additions & 10 deletions tests/unit/ReportActionsListContextTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,45 +58,45 @@ function createReadinessSignals(overrides: Partial<ReportActionsReadinessSignals
}

describe('computeReportActionsSkeletonState', () => {
it('shows the initial skeleton for a pending OpenReport matching this report', () => {
it('shows the loading skeleton for a pending OpenReport matching this report', () => {
const state = computeReportActionsSkeletonState(
createReadinessSignals({
isReportLoadPending: true,
}),
);

expect(state.shouldShowInitialSkeleton).toBe(true);
expect(state.shouldShowLoadingSkeleton).toBe(true);
});

it('does not show the initial skeleton when no OpenReport matching this report is pending', () => {
it('does not show the loading skeleton when no OpenReport matching this report is pending', () => {
const state = computeReportActionsSkeletonState(
createReadinessSignals({
isReportLoadPending: false,
}),
);

expect(state.shouldShowInitialSkeleton).toBe(false);
expect(state.shouldShowLoadingSkeleton).toBe(false);
});

it('does not show the initial skeleton for a pending OpenReport while offline', () => {
it('does not show the loading skeleton for a pending OpenReport while offline', () => {
const state = computeReportActionsSkeletonState(
createReadinessSignals({
isOffline: true,
isReportLoadPending: true,
}),
);

expect(state.shouldShowInitialSkeleton).toBe(false);
expect(state.shouldShowLoadingSkeleton).toBe(false);
});

it('releases the initial skeleton after a terminal OpenReport failure', () => {
it('releases the loading skeleton after a terminal OpenReport failure', () => {
const state = computeReportActionsSkeletonState(
createReadinessSignals({
isReportLoadPending: false,
}),
);

expect(state.shouldShowInitialSkeleton).toBe(false);
expect(state.shouldShowLoadingSkeleton).toBe(false);
});

it('releases the unread initial load when no report load is pending', () => {
Expand Down Expand Up @@ -196,7 +196,7 @@ describe('computeReportActionsSkeletonState', () => {
}),
);

expect(state.shouldShowInitialSkeleton).toBe(false);
expect(state.shouldShowLoadingSkeleton).toBe(false);
});

it('preserves the linked message skeleton when the report load is pending', () => {
Expand All @@ -210,6 +210,6 @@ describe('computeReportActionsSkeletonState', () => {
}),
);

expect(state.shouldShowInitialSkeleton).toBe(true);
expect(state.shouldShowLoadingSkeleton).toBe(true);
});
});
Loading