Skip to content

Commit 707eecf

Browse files
authored
fix(bridge): preserve Unified SwapBridge funnel metrics (#9986)
## Summary - Add `has_sufficient_funds` to Unified SwapBridge Quotes Received metrics for [SWAPS-4977](https://consensyssoftware.atlassian.net/browse/SWAPS-4977). - Preserve explicit slippage intent and normalized slippage limits across quote, submission, and history metrics for [SWAPS-4979](https://consensyssoftware.atlassian.net/browse/SWAPS-4979). - Support client fallbacks for incomplete quote USD and token-symbol data, while preserving Quick Buy and Batch Sell legacy behavior. ## Test plan - [x] `yarn workspace @metamask/bridge-controller test --coverage=false --runInBand --testPathPatterns=src/utils/metrics/properties.test.ts` - [x] `yarn workspace @metamask/bridge-status-controller test --coverage=false --runInBand --testPathPatterns=src/utils/metrics.test.ts` - [x] Bridge status-controller tests and Batch Sell tests - [x] Core typecheck and targeted ESLint - [x] Prettier and `git diff --check` [SWAPS-4977]: https://consensyssoftware.atlassian.net/browse/SWAPS-4977?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ [SWAPS-4979]: https://consensyssoftware.atlassian.net/browse/SWAPS-4979?atlOrigin=eyJpIjoiNWRkNTljNzYxNjVmNDY3MDlhMDU5Y2ZhYzA5YTRkZjUiLCJwIjoiZ2l0aHViLWNvbS1KU1cifQ <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Analytics and metrics payload changes only; no transaction or auth logic. Snapshot/test updates reflect corrected slippage and custom_slippage semantics. > > **Overview** > Fixes **Unified SwapBridge** analytics so quote-through-completion events carry consistent balance and slippage fields. > > **Quotes Received** now includes **`has_sufficient_funds`** (from the quote request’s insufficient-balance flag). **`slippage_limit`** is always a number (**`0`** means auto when no explicit limit), including on request metadata and in **`getQuotesReceivedProperties`**, which also accepts optional client fallbacks for slippage, **`custom_slippage`**, USD amount, and token symbols when quote data is incomplete. > > **Bridge status** threads **`quotesReceivedContext`** into submit/pre-confirmation metrics, persists **`customSlippage`** on history, and prefers that (with Batch Sell / Quick Buy legacy inference) when rebuilding **`custom_slippage`** and **`slippage_limit`** after submission instead of inferring only from quote slippage. > > <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit b915d67. Bugbot is set up for automated code reviews on this repo. Configure [here](https://www.cursor.com/dashboard/bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent 0289347 commit 707eecf

16 files changed

Lines changed: 295 additions & 28 deletions

packages/bridge-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111

1212
- Bump `@metamask/remote-feature-flag-controller` from `^6.0.0` to `^6.1.0` ([#9980](https://github.com/MetaMask/core/pull/9980))
1313

14+
### Fixed
15+
16+
- Include sufficient-funds and normalized slippage properties in Unified SwapBridge quote metrics ([#9986](https://github.com/MetaMask/core/pull/9986))
17+
1418
## [80.0.0]
1519

1620
### Added

packages/bridge-controller/src/__snapshots__/bridge-controller.test.ts.snap

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent bridge-status-controller c
136136
"quotes_count": 0,
137137
"quotes_list": [],
138138
"security_warnings": [],
139-
"slippage_limit": undefined,
139+
"slippage_limit": 0,
140140
"source_transaction": "PENDING",
141141
"stx_enabled": false,
142142
"swap_type": "crosschain",
@@ -262,7 +262,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t
262262
"price_impact": 6,
263263
"quotes_count": 0,
264264
"quotes_list": [],
265-
"slippage_limit": undefined,
265+
"slippage_limit": 0,
266266
"stx_enabled": false,
267267
"swap_type": "crosschain",
268268
"token_address_destination": null,
@@ -296,7 +296,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t
296296
"price_impact": 6,
297297
"quotes_count": 0,
298298
"quotes_list": [],
299-
"slippage_limit": undefined,
299+
"slippage_limit": 0,
300300
"sort_order": "cost_ascending",
301301
"stx_enabled": false,
302302
"swap_type": "crosschain",
@@ -408,7 +408,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t
408408
"input_primary_denomination": "token_amount",
409409
"is_hardware_wallet": false,
410410
"location": "Unknown",
411-
"slippage_limit": undefined,
411+
"slippage_limit": 0,
412412
"swap_type": "crosschain",
413413
"token_address_destination": null,
414414
"token_address_source": "eip155:1/slip44:60",
@@ -443,7 +443,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t
443443
"quoted_time_minutes": 10,
444444
"quotes_count": 0,
445445
"quotes_list": [],
446-
"slippage_limit": undefined,
446+
"slippage_limit": 0,
447447
"swap_type": "crosschain",
448448
"token_address_destination": null,
449449
"token_address_source": "eip155:1/slip44:60",
@@ -480,6 +480,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t
480480
"gas_included": false,
481481
"gas_included_7702": false,
482482
"has_gas_included_quote": false,
483+
"has_sufficient_funds": true,
483484
"initial_load_time_all_quotes": 0,
484485
"input_primary_denomination": "token_amount",
485486
"is_hardware_wallet": false,
@@ -490,7 +491,7 @@ exports[`BridgeController trackUnifiedSwapBridgeEvent client-side calls should t
490491
"quotes_count": 0,
491492
"quotes_list": [],
492493
"refresh_count": 0,
493-
"slippage_limit": undefined,
494+
"slippage_limit": 0,
494495
"swap_type": "crosschain",
495496
"token_address_destination": null,
496497
"token_address_source": "eip155:1/slip44:60",
@@ -588,6 +589,7 @@ exports[`BridgeController updateBridgeQuoteRequestParams should only poll once i
588589
"gas_included": false,
589590
"gas_included_7702": false,
590591
"has_gas_included_quote": false,
592+
"has_sufficient_funds": false,
591593
"initial_load_time_all_quotes": 11000,
592594
"input_primary_denomination": "token_amount",
593595
"is_hardware_wallet": false,

packages/bridge-controller/src/bridge-controller.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1212,7 +1212,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
12121212
);
12131213

12141214
return {
1215-
slippage_limit: quoteRequest.slippage,
1215+
slippage_limit: quoteRequest.slippage ?? 0,
12161216
swap_type: getSwapTypeFromQuote(quoteRequest),
12171217
custom_slippage: isCustomSlippage(quoteRequest.slippage),
12181218
account_hardware_type: accountHardwareType,
@@ -1361,6 +1361,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
13611361
...this.#getRequestMetadata(),
13621362
...this.#getQuoteFetchData(),
13631363
refresh_count: this.state.quotesRefreshCount,
1364+
has_sufficient_funds: !quoteRequest.insufficientBal,
13641365
...inputPrimaryDenominationProperties,
13651366
...baseProperties,
13661367
};

packages/bridge-controller/src/utils/metrics/properties.test.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ describe('properties', () => {
436436
"price_impact": 0,
437437
"provider": "bridge1_bridge1",
438438
"quoted_time_minutes": 1,
439+
"slippage_limit": 0,
439440
"token_symbol_destination": "USDC",
440441
"token_symbol_source": "ETH",
441442
"usd_amount_source": 0,
@@ -445,13 +446,76 @@ describe('properties', () => {
445446
"warnings": [],
446447
}
447448
`);
449+
450+
const quoteWithSlippage = {
451+
...mockQuoteResponseV2,
452+
quote: {
453+
...mockQuoteResponseV2.quote,
454+
slippage: 0.5,
455+
},
456+
};
457+
458+
expect(
459+
getQuotesReceivedProperties(
460+
quoteWithSlippage,
461+
[],
462+
true,
463+
undefined,
464+
undefined,
465+
undefined,
466+
{ slippage_limit: 3.5 },
467+
).slippage_limit,
468+
).toBe(3.5);
469+
expect(
470+
getQuotesReceivedProperties(
471+
quoteWithSlippage,
472+
[],
473+
true,
474+
undefined,
475+
undefined,
476+
undefined,
477+
{ custom_slippage: true, slippage_limit: undefined },
478+
).slippage_limit,
479+
).toBe(0.5);
480+
expect(
481+
getQuotesReceivedProperties(quoteWithSlippage).slippage_limit,
482+
).toBe(0.5);
448483
});
449484

450485
it('should return empty source and null destination token symbols when activeQuote is null', () => {
451486
const result = getQuotesReceivedProperties(null);
452487

453488
expect(result.token_symbol_source).toBe('');
454489
expect(result.token_symbol_destination).toBeNull();
490+
expect(result.slippage_limit).toBe(0);
491+
});
492+
493+
it('should use client fallbacks and explicit slippage context', () => {
494+
const result = getQuotesReceivedProperties(
495+
null,
496+
[],
497+
true,
498+
undefined,
499+
undefined,
500+
undefined,
501+
{
502+
custom_slippage: true,
503+
slippage_limit: 3.5,
504+
usd_amount_source: 100,
505+
token_symbol_source: 'ETH',
506+
token_symbol_destination: 'USDC',
507+
},
508+
);
509+
510+
expect(result).toStrictEqual(
511+
expect.objectContaining({
512+
custom_slippage: true,
513+
slippage_limit: 3.5,
514+
token_symbol_source: 'ETH',
515+
token_symbol_destination: 'USDC',
516+
usd_amount_source: 100,
517+
}),
518+
);
455519
});
456520

457521
it('should derive token symbols from the active quote asset metadata', () => {

packages/bridge-controller/src/utils/metrics/properties.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,8 +166,28 @@ export const getQuotesReceivedProperties = (
166166
recommendedQuote?: null | QuoteResponse,
167167
usdBalanceSource?: number,
168168
hasSufficientGasForQuote?: boolean | null,
169+
options: {
170+
// eslint-disable-next-line @typescript-eslint/naming-convention -- analytics property
171+
custom_slippage?: boolean;
172+
// eslint-disable-next-line @typescript-eslint/naming-convention -- analytics property
173+
slippage_limit?: number;
174+
// eslint-disable-next-line @typescript-eslint/naming-convention -- analytics property
175+
usd_amount_source?: number;
176+
// eslint-disable-next-line @typescript-eslint/naming-convention -- analytics property
177+
token_symbol_source?: string;
178+
// eslint-disable-next-line @typescript-eslint/naming-convention -- analytics property
179+
token_symbol_destination?: string | null;
180+
} = {},
169181
) => {
170182
const provider = activeQuote ? formatProviderLabel(activeQuote.quote) : '_';
183+
const quoteUsdAmountSource = activeQuote?.quote?.src?.usd;
184+
const quoteTokenSymbolSource = activeQuote?.quote.src.asset.symbol;
185+
const quoteTokenSymbolDestination = activeQuote?.quote.dest.asset.symbol;
186+
const usdAmountSource = Number(
187+
quoteUsdAmountSource ?? options.usd_amount_source ?? 0,
188+
);
189+
const slippageLimit =
190+
options.slippage_limit ?? activeQuote?.quote?.slippage ?? 0;
171191
return {
172192
can_submit: isSubmittable,
173193
gas_included: Boolean(activeQuote?.quote?.gasIncluded),
@@ -178,20 +198,26 @@ export const getQuotesReceivedProperties = (
178198
usd_quoted_gas: Number(activeQuote?.quote?.feeData?.network?.[0]?.usd ?? 0),
179199
usd_quoted_return: Number(activeQuote?.quote?.dest?.usd ?? 0),
180200
usd_balance_source: usdBalanceSource ?? 0,
181-
usd_amount_source: Number(activeQuote?.quote?.src?.usd ?? 0),
201+
usd_amount_source: usdAmountSource,
202+
slippage_limit: slippageLimit,
182203
best_quote_provider: recommendedQuote
183204
? formatProviderLabel(recommendedQuote.quote)
184205
: provider,
185206
provider,
186-
token_symbol_source: activeQuote?.quote.src.asset.symbol ?? '',
187-
token_symbol_destination: activeQuote?.quote.dest.asset.symbol ?? null,
207+
token_symbol_source:
208+
quoteTokenSymbolSource ?? options.token_symbol_source ?? '',
209+
token_symbol_destination:
210+
quoteTokenSymbolDestination ?? options.token_symbol_destination ?? null,
188211
warnings,
189212
price_impact: Number(
190213
activeQuote?.quote.priceData?.priceImpact?.amount ?? 0,
191214
),
192215
...(hasSufficientGasForQuote !== undefined && {
193216
has_sufficient_gas_for_quote: hasSufficientGasForQuote,
194217
}),
218+
...(options.custom_slippage !== undefined && {
219+
custom_slippage: options.custom_slippage,
220+
}),
195221
feature_id: activeQuote?.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE,
196222
};
197223
};

packages/bridge-controller/src/utils/metrics/types.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export type AccountHardwareType =
4646
| null;
4747

4848
export type RequestMetadata = {
49-
slippage_limit?: number; // undefined === auto
49+
slippage_limit: number; // 0 === auto when no numeric limit is available
5050
custom_slippage: boolean;
5151
usd_amount_source: number; // Use quoteResponse when available
5252
stx_enabled: boolean;
@@ -221,6 +221,7 @@ type RequiredEventContextFromClientBase = {
221221
token_symbol_source: RequestParams['token_symbol_source'];
222222
token_symbol_destination: RequestParams['token_symbol_destination'];
223223
token_security_type_destination: RequestParams['token_security_type_destination'];
224+
custom_slippage?: RequestMetadata['custom_slippage'];
224225
} & InputPrimaryDenominationData;
225226
[UnifiedSwapBridgeEventName.QuotesReceived]: TradeData &
226227
Pick<RequestParams, 'token_symbol_source' | 'token_symbol_destination'> &
@@ -232,6 +233,8 @@ type RequiredEventContextFromClientBase = {
232233
usd_balance_source?: number;
233234
has_sufficient_gas_for_quote?: boolean | null;
234235
usd_amount_source: number;
236+
custom_slippage?: RequestMetadata['custom_slippage'];
237+
slippage_limit?: RequestMetadata['slippage_limit'];
235238
};
236239
[UnifiedSwapBridgeEventName.QuotesError]: Pick<
237240
RequestMetadata,
@@ -401,6 +404,7 @@ export type EventPropertiesFromControllerState = {
401404
QuoteFetchData &
402405
TradeData & {
403406
refresh_count: number; // starts from 0
407+
has_sufficient_funds: boolean;
404408
} & InputPrimaryDenominationData;
405409
[UnifiedSwapBridgeEventName.QuotesError]: RequestParams &
406410
RequestMetadata & {

packages/bridge-status-controller/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Fixed
11+
12+
- Preserve explicit slippage intent and normalized slippage limits in post-submission Unified SwapBridge metrics ([#9986](https://github.com/MetaMask/core/pull/9986))
13+
1014
## [75.3.0]
1115

1216
### Changed

0 commit comments

Comments
 (0)