Skip to content

Commit 892b4f0

Browse files
committed
fix(perps): honour route source and count each quote/abandon session
- position_close screen view now emits the route-provided source (reduce exposure -> position_screen, order book -> order_book) instead of always perp_asset_screen, falling back to the asset screen for direct entries. - The trade-quote dedupe key now includes the amount and pay-token identity so a retried failed quote with the same blocking alert (which carries no payTotals) counts as a distinct attempt instead of being deduped away. - The abandon-order tracker resets the caller's committed flag on each focus so a commit from a prior session on a reused screen no longer suppresses abandon tracking for the next one.
1 parent 213e3f9 commit 892b4f0

6 files changed

Lines changed: 86 additions & 7 deletions

File tree

app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.test.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3374,10 +3374,11 @@ describe('PerpsClosePositionView', () => {
33743374
}),
33753375
);
33763376

3377-
it('reports button_clicked=reduce_exposure when opened via the reduce-exposure entry', () => {
3377+
it('reports button_clicked=reduce_exposure and source=position_screen when opened via the reduce-exposure entry', () => {
33783378
useRouteMock.mockReturnValue({
33793379
params: {
33803380
position: defaultPerpsPositionMock,
3381+
source: PERPS_EVENT_VALUE.SOURCE.POSITION_SCREEN,
33813382
buttonClicked: PERPS_EVENT_VALUE.BUTTON_CLICKED.REDUCE_EXPOSURE,
33823383
buttonLocation: PERPS_EVENT_VALUE.BUTTON_LOCATION.SCREEN,
33833384
},
@@ -3386,17 +3387,19 @@ describe('PerpsClosePositionView', () => {
33863387
renderWithProvider(<PerpsClosePositionView />);
33873388

33883389
expectScreenViewed({
3390+
[PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.POSITION_SCREEN,
33893391
[PERPS_EVENT_PROPERTY.BUTTON_CLICKED]:
33903392
PERPS_EVENT_VALUE.BUTTON_CLICKED.REDUCE_EXPOSURE,
33913393
[PERPS_EVENT_PROPERTY.BUTTON_LOCATION]:
33923394
PERPS_EVENT_VALUE.BUTTON_LOCATION.SCREEN,
33933395
});
33943396
});
33953397

3396-
it('reports button_clicked=close when opened via a plain close entry', () => {
3398+
it('reports button_clicked=close and source=order_book when opened via the order-book close entry', () => {
33973399
useRouteMock.mockReturnValue({
33983400
params: {
33993401
position: defaultPerpsPositionMock,
3402+
source: PERPS_EVENT_VALUE.SOURCE.ORDER_BOOK,
34003403
buttonClicked: PERPS_EVENT_VALUE.BUTTON_CLICKED.CLOSE,
34013404
buttonLocation: PERPS_EVENT_VALUE.BUTTON_LOCATION.ORDER_BOOK,
34023405
},
@@ -3405,21 +3408,24 @@ describe('PerpsClosePositionView', () => {
34053408
renderWithProvider(<PerpsClosePositionView />);
34063409

34073410
expectScreenViewed({
3411+
[PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.ORDER_BOOK,
34083412
[PERPS_EVENT_PROPERTY.BUTTON_CLICKED]:
34093413
PERPS_EVENT_VALUE.BUTTON_CLICKED.CLOSE,
34103414
[PERPS_EVENT_PROPERTY.BUTTON_LOCATION]:
34113415
PERPS_EVENT_VALUE.BUTTON_LOCATION.ORDER_BOOK,
34123416
});
34133417
});
34143418

3415-
it('defaults button_clicked=close when no entry action is provided', () => {
3419+
it('defaults button_clicked=close and source=perp_asset_screen when no entry action is provided', () => {
34163420
useRouteMock.mockReturnValue({
34173421
params: { position: defaultPerpsPositionMock },
34183422
});
34193423

34203424
renderWithProvider(<PerpsClosePositionView />);
34213425

34223426
expectScreenViewed({
3427+
[PERPS_EVENT_PROPERTY.SOURCE]:
3428+
PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN,
34233429
[PERPS_EVENT_PROPERTY.BUTTON_CLICKED]:
34243430
PERPS_EVENT_VALUE.BUTTON_CLICKED.CLOSE,
34253431
});

app/components/UI/Perps/Views/PerpsClosePositionView/PerpsClosePositionView.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -351,7 +351,11 @@ const PerpsClosePositionView: React.FC = () => {
351351
[PERPS_EVENT_PROPERTY.POSITION_SIZE]: absSize,
352352
[PERPS_EVENT_PROPERTY.UNREALIZED_PNL_DOLLAR]: pnl,
353353
[PERPS_EVENT_PROPERTY.UNREALIZED_PNL_PERCENT]: unrealizedPnlPercent,
354-
[PERPS_EVENT_PROPERTY.SOURCE]: PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN,
354+
// Honour the route-provided source threaded by each entry CTA
355+
// (reduce-exposure → position_screen, order-book → order_book); fall back
356+
// to the asset screen for direct entries that pass no source.
357+
[PERPS_EVENT_PROPERTY.SOURCE]:
358+
routeSource ?? PERPS_EVENT_VALUE.SOURCE.PERP_ASSET_SCREEN,
355359
[PERPS_EVENT_PROPERTY.RECEIVED_AMOUNT]: receiveAmount,
356360
// The entry CTA (close vs reduce_exposure) is passed via the navigation
357361
// route param — closePercentage defaults to 100 at open, so isPartialClose

app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.test.tsx

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4536,6 +4536,47 @@ describe('PerpsOrderView', () => {
45364536
}),
45374537
);
45384538
});
4539+
4540+
it('emits a distinct PERPS_TRADE_QUOTE_RECEIVED per failed attempt when the amount changes (same blocking alert)', () => {
4541+
const { useNoPayTokenQuotesAlert: mockNoQuotes } = jest.requireMock(
4542+
'../../../../Views/confirmations/hooks/alerts/useNoPayTokenQuotesAlert',
4543+
) as { useNoPayTokenQuotesAlert: jest.Mock };
4544+
// Same blocking no-quotes alert for both attempts; a failed quote carries
4545+
// no payTotals, so only the amount distinguishes the two attempts.
4546+
mockNoQuotes.mockReturnValue([
4547+
{ key: 'no_quotes', message: 'No quotes available', isBlocking: true },
4548+
]);
4549+
4550+
try {
4551+
const { rerender } = render(<PerpsOrderView />, {
4552+
wrapper: TestWrapper,
4553+
});
4554+
4555+
// Second attempt: user edits the amount, same failing alert.
4556+
(usePerpsOrderContext as jest.Mock).mockReturnValue({
4557+
...defaultMockHooks.usePerpsOrderContext,
4558+
orderForm: {
4559+
...defaultMockHooks.usePerpsOrderContext.orderForm,
4560+
amount: '22',
4561+
},
4562+
});
4563+
act(() => {
4564+
rerender(<PerpsOrderView />);
4565+
});
4566+
4567+
const quotes = eventsOf(MetaMetricsEvents.PERPS_TRADE_QUOTE_RECEIVED);
4568+
expect(quotes).toHaveLength(2);
4569+
quotes.forEach((q) =>
4570+
expect(q.props).toEqual(
4571+
expect.objectContaining({
4572+
[PERPS_EVENT_PROPERTY.STATUS]: PERPS_EVENT_VALUE.STATUS.FAILED,
4573+
}),
4574+
),
4575+
);
4576+
} finally {
4577+
mockNoQuotes.mockReturnValue([]);
4578+
}
4579+
});
45394580
});
45404581

45414582
describe('abandon order tracking', () => {

app/components/UI/Perps/Views/PerpsOrderView/PerpsOrderView.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -842,8 +842,16 @@ const PerpsOrderViewContentBase: React.FC<PerpsOrderViewContentProps> = ({
842842
return;
843843
}
844844

845+
// Include the amount and pay-token identity so a retry after changing them
846+
// is a distinct attempt, not deduped against the previous one — a failed
847+
// quote carries no payTotals, so without these a repeated failure with the
848+
// same blocking alert would be silently undercounted.
845849
const quoteKey = JSON.stringify({
846850
asset: orderForm.asset,
851+
amount: orderForm.amount,
852+
payToken: payToken
853+
? `${payToken.symbol ?? ''}:${payToken.chainId ?? ''}`
854+
: null,
847855
payTotals,
848856
errorKey: blockingNoQuoteAlert?.key,
849857
errorMessage: blockingNoQuoteAlert?.message,
@@ -885,6 +893,8 @@ const PerpsOrderViewContentBase: React.FC<PerpsOrderViewContentProps> = ({
885893
payTotals,
886894
noQuotesAlerts,
887895
orderForm.asset,
896+
orderForm.amount,
897+
payToken,
888898
track,
889899
]);
890900

app/components/UI/Perps/hooks/usePerpsAbandonOrderTracking.test.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ describe('usePerpsAbandonOrderTracking', () => {
5050
[PERPS_EVENT_PROPERTY.ASSET]: 'BTC',
5151
[PERPS_EVENT_PROPERTY.ACTION]: 'abandon_order',
5252
});
53-
return renderHook(() =>
53+
const result = renderHook(() =>
5454
usePerpsAbandonOrderTracking({ getAbandonProperties, hasCommittedRef }),
5555
);
56+
return { ...result, hasCommittedRef };
5657
};
5758

5859
beforeEach(() => {
@@ -98,13 +99,25 @@ describe('usePerpsAbandonOrderTracking', () => {
9899
});
99100

100101
it('does NOT emit after the flow is committed (placed / confirmed)', () => {
101-
renderTracking(true);
102+
const { hasCommittedRef } = renderTracking();
103+
// Commit happens during the focused session.
104+
hasCommittedRef.current = true;
102105
fire(nav, 'beforeRemove');
103106
fire(nav, 'blur');
104107

105108
expect(mockTrack).not.toHaveBeenCalled();
106109
});
107110

111+
it('clears a committed flag from a prior session on focus so the next session still tracks abandon', () => {
112+
// A stale commit carried in at focus time (reused, non-remounted screen)
113+
// must not suppress the fresh session — focus resets it.
114+
const { hasCommittedRef } = renderTracking(true);
115+
expect(hasCommittedRef.current).toBe(false);
116+
117+
fire(nav, 'beforeRemove');
118+
expect(mockTrack).toHaveBeenCalledTimes(1);
119+
});
120+
108121
it('emits at most once across overlapping beforeRemove and blur', () => {
109122
renderTracking();
110123
fire(nav, 'beforeRemove');

app/components/UI/Perps/hooks/usePerpsAbandonOrderTracking.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,12 @@ export function usePerpsAbandonOrderTracking({
7272
focusStartRef.current = Date.now();
7373
focusDepthRef.current = getNavigationStackDepth(navigation);
7474
emittedRef.current = false;
75-
}, [navigation]),
75+
// A new focus is a fresh order session: clear the committed flag so a
76+
// commit from a previous session (on a reused, non-remounted screen) does
77+
// not suppress abandon tracking for this one. Safe when the screen does
78+
// remount too — the ref simply starts false either way.
79+
hasCommittedRef.current = false;
80+
}, [navigation, hasCommittedRef]),
7681
);
7782

7883
useEffect(() => {

0 commit comments

Comments
 (0)