Skip to content
Merged
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
12 changes: 9 additions & 3 deletions src/hooks/useInboxTabSpanLifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@ import {useEffect, useRef} from 'react';
* Manages the ManualNavigateToInboxTab span lifecycle for the inbox sidebar.
*
* Three signals are handled:
* - onLayout fires on first mount: ends the span (normal path).
* - useFocusEffect fires on re-focus when react-freeze has cached the layout: ends the span (warm path).
* - onLayout ends the span: cold on the first layout, warm on any later one. It can fire more than once on the
* same instance, so it cannot assume a fresh mount.
* - useFocusEffect ends the span as warm on re-focus. On native this is the only signal, because react-freeze
* keeps the cached layout and onLayout does not fire again.
* The blur cleanup cancels any orphaned span when the user navigates away before layout completes.
* - useEffect unmount cleanup cancels the span only if layout never completed AND the active span
* is the same one that was present when this instance mounted (avoids canceling a span started
Expand All @@ -22,9 +24,13 @@ function useInboxTabSpanLifecycle(): () => void {
const hasHadFirstLayout = useRef(false);
const spanOnMount = useRef(getSpan(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB));

// onLayout can fire more than once on the same instance, so the first layout is the cold one and any later
// layout is a warm re-visit. Both end paths read this ref, so the reported value does not depend on which
// of them fires first.
const onLayout = () => {
const isRepeatLayout = hasHadFirstLayout.current;
hasHadFirstLayout.current = true;
endSpanWithAttributes(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, {[CONST.TELEMETRY.ATTRIBUTE_IS_WARM]: false});
endSpanWithAttributes(CONST.TELEMETRY.SPAN_NAVIGATE_TO_INBOX_TAB, {[CONST.TELEMETRY.ATTRIBUTE_IS_WARM]: isRepeatLayout});
spanOnMount.current = undefined;
};

Expand Down
36 changes: 36 additions & 0 deletions tests/unit/hooks/useInboxTabSpanLifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,42 @@ describe('useInboxTabSpanLifecycle', () => {
expect(mockCancelSpan).not.toHaveBeenCalled();
});

it('reports a repeat layout as warm', () => {
const {result} = renderHook(() => useInboxTabSpanLifecycle());

act(() => {
result.current();
});
mockEndSpanWithAttributes.mockClear();

act(() => {
result.current();
});

expect(mockEndSpanWithAttributes).toHaveBeenCalledTimes(1);
expect(mockEndSpanWithAttributes).toHaveBeenCalledWith(SPAN, {[CONST.TELEMETRY.ATTRIBUTE_IS_WARM]: true});
});

it('reports the same value from either end path', () => {
const {result} = renderHook(() => useInboxTabSpanLifecycle());

act(() => {
result.current();
});
mockEndSpanWithAttributes.mockClear();

act(() => {
result.current();
});
act(() => {
getFocusCallback()();
});

expect(mockEndSpanWithAttributes).toHaveBeenCalledTimes(2);
expect(mockEndSpanWithAttributes).toHaveBeenNthCalledWith(1, SPAN, {[CONST.TELEMETRY.ATTRIBUTE_IS_WARM]: true});
expect(mockEndSpanWithAttributes).toHaveBeenNthCalledWith(2, SPAN, {[CONST.TELEMETRY.ATTRIBUTE_IS_WARM]: true});
});

it('does not cancel on unmount after layout has completed', () => {
const {result, unmount} = renderHook(() => useInboxTabSpanLifecycle());

Expand Down
Loading