@metamask-previews/bridge-controller 79.2.0-preview-a1017a8ee → 79.2.0-preview-c2ef75d18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ### Added
11
+
12
+ - Add Sentry quote-fetch and provider first-result performance traces ([#9899](https://github.com/MetaMask/core/pull/9899))
13
+
10
14
  ### Changed
11
15
 
12
16
  - Bump `@metamask/assets-controller` from `^13.1.2` to `^13.1.4` ([#9873](https://github.com/MetaMask/core/pull/9873), [#9886](https://github.com/MetaMask/core/pull/9886))
@@ -19,6 +19,7 @@ const contracts_1 = require("@ethersproject/contracts");
19
19
  const providers_1 = require("@ethersproject/providers");
20
20
  const metamask_eth_abis_1 = require("@metamask/metamask-eth-abis");
21
21
  const polling_controller_1 = require("@metamask/polling-controller");
22
+ const uuid_1 = require("uuid");
22
23
  const quote_response_v1_to_v2_js_1 = require("./coercers/quote-response-v1-to-v2.cjs");
23
24
  const bridge_js_1 = require("./constants/bridge.cjs");
24
25
  const chains_js_1 = require("./constants/chains.cjs");
@@ -129,6 +130,18 @@ const metadata = {
129
130
  usedInUi: true,
130
131
  },
131
132
  };
133
+ const QUOTE_ABORT_REASONS = new Set(Object.values(constants_js_1.AbortReason));
134
+ const isExpectedQuoteAbort = (error, signal) => {
135
+ if (signal?.aborted) {
136
+ return true;
137
+ }
138
+ if (QUOTE_ABORT_REASONS.has(String(error))) {
139
+ return true;
140
+ }
141
+ const errorText = error instanceof Error ? `${error.name} ${error.message}` : String(error);
142
+ return (errorText.includes('AbortError') ||
143
+ errorText.includes('FetchRequestCanceledException'));
144
+ };
132
145
  const MESSENGER_EXPOSED_METHODS = [
133
146
  'updateBridgeQuoteRequestParams',
134
147
  'fetchQuotes',
@@ -504,6 +517,9 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
504
517
  __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.abort(constants_js_1.AbortReason.NewQuoteRequest);
505
518
  __classPrivateFieldGet(this, _BridgeController_batchSellTradesAbortController, "f")?.abort(constants_js_1.AbortReason.NewQuoteRequest);
506
519
  __classPrivateFieldSet(this, _BridgeController_abortController, new AbortController(), "f");
520
+ const quoteTraceStartTime = Date.now();
521
+ const quoteTraceRequestId = (0, uuid_1.v4)();
522
+ const quoteAbortSignal = __classPrivateFieldGet(this, _BridgeController_abortController, "f").signal;
507
523
  __classPrivateFieldGet(this, _BridgeController_fetchAssetExchangeRates, "f").call(this, quoteRequests).catch((error) => console.warn('Failed to fetch asset exchange rates', error));
508
524
  this.trackUnifiedSwapBridgeEvent(constants_js_1.UnifiedSwapBridgeEventName.QuotesRequested, context);
509
525
  const { sse, maxRefreshCount } = (0, feature_flags_js_1.getBridgeFeatureFlags)(this.messenger);
@@ -524,43 +540,81 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
524
540
  }
525
541
  });
526
542
  const jwt = await __classPrivateFieldGet(this, _BridgeController_getJwt, "f").call(this);
527
- try {
528
- const [firstQuoteRequest] = quoteRequests;
529
- const unifiedSwapTraceName = (0, bridge_js_2.isCrossChain)(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId)
543
+ const [firstQuoteRequest] = quoteRequests;
544
+ let traceResult = 'error';
545
+ let traceName = traces_js_1.TraceName.BatchSellQuotesFetched;
546
+ if (!isBatchSellRequest) {
547
+ traceName = (0, bridge_js_2.isCrossChain)(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId)
530
548
  ? traces_js_1.TraceName.BridgeQuotesFetched
531
549
  : traces_js_1.TraceName.SwapQuotesFetched;
532
- await __classPrivateFieldGet(this, _BridgeController_trace, "f").call(this, {
533
- name: isBatchSellRequest
534
- ? traces_js_1.TraceName.BatchSellQuotesFetched
535
- : unifiedSwapTraceName,
550
+ }
551
+ const tracedProviders = new Set();
552
+ const traceWithoutImpact = async (request) => {
553
+ try {
554
+ await __classPrivateFieldGet(this, _BridgeController_trace, "f").call(this, request, () => undefined);
555
+ }
556
+ catch {
557
+ // Telemetry failures must not affect quote fetching or state updates.
558
+ }
559
+ };
560
+ const traceProviderFirstResult = async (providerData) => {
561
+ const provider = (0, properties_js_1.formatProviderLabel)(providerData);
562
+ if (isBatchSellRequest || tracedProviders.has(provider)) {
563
+ return;
564
+ }
565
+ tracedProviders.add(provider);
566
+ await traceWithoutImpact({
567
+ name: traces_js_1.TraceName.QuoteProviderFirstResult,
568
+ startTime: quoteTraceStartTime,
536
569
  data: {
570
+ provider,
571
+ request_id: quoteTraceRequestId,
572
+ swap_type: (0, properties_js_1.getSwapType)(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId),
537
573
  srcChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.srcChainId),
538
574
  destChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.destChainId),
575
+ result: 'success',
539
576
  },
540
- }, async () => {
541
- const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, firstQuoteRequest.walletAddress);
542
- // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
543
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
544
- __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, firstQuoteRequest.srcChainId, selectedAccount?.metadata?.snap?.id);
545
- // Use SSE if enabled and return early
546
- if (shouldStream || isBatchSellRequest) {
547
- await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, quoteRequests, context.feature_id, jwt, selectedAccount);
577
+ });
578
+ };
579
+ try {
580
+ const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, firstQuoteRequest.walletAddress);
581
+ // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
582
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
583
+ __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, firstQuoteRequest.srcChainId, selectedAccount?.metadata?.snap?.id);
584
+ // Use SSE if enabled and return early
585
+ if (shouldStream || isBatchSellRequest) {
586
+ const quoteCount = await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, {
587
+ quoteRequests,
588
+ featureId: context.feature_id,
589
+ jwt,
590
+ selectedAccount,
591
+ signal: quoteAbortSignal,
592
+ traceProviderFirstResult,
593
+ });
594
+ if (quoteAbortSignal.aborted) {
595
+ traceResult = 'cancelled';
548
596
  return;
549
597
  }
598
+ traceResult = quoteCount > 0 ? 'success' : 'no_quotes';
599
+ }
600
+ else {
550
601
  // Otherwise use regular fetch
551
- const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal);
602
+ const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, quoteAbortSignal);
603
+ for (const quote of quotes) {
604
+ await traceProviderFirstResult(quote.quote);
605
+ }
552
606
  this.update((state) => {
553
607
  // Set the initial load time if this is the first fetch
554
608
  if (state.quotesRefreshCount ===
555
609
  bridge_js_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount &&
556
610
  __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
557
- state.quotesInitialLoadTime =
558
- Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
611
+ state.quotesInitialLoadTime = Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
559
612
  }
560
613
  state.quotes = quotes.map(quote_response_v1_to_v2_js_1.toQuoteResponseV2);
561
614
  state.quotesLoadingStatus = types_js_1.RequestStatus.FETCHED;
562
615
  });
563
- });
616
+ traceResult = quotes.length > 0 ? 'success' : 'no_quotes';
617
+ }
564
618
  }
565
619
  catch (error) {
566
620
  // Reset the quotes list if the fetch fails to avoid showing stale quotes
@@ -568,14 +622,8 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
568
622
  state.quotes = bridge_js_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
569
623
  });
570
624
  // Ignore abort errors
571
- if (error.toString().includes('AbortError') ||
572
- error.toString().includes('FetchRequestCanceledException') ||
573
- [
574
- constants_js_1.AbortReason.ResetState,
575
- constants_js_1.AbortReason.NewQuoteRequest,
576
- constants_js_1.AbortReason.QuoteRequestUpdated,
577
- constants_js_1.AbortReason.TransactionSubmitted,
578
- ].includes(error)) {
625
+ if (isExpectedQuoteAbort(error, quoteAbortSignal)) {
626
+ traceResult = 'cancelled';
579
627
  // Exit the function early to prevent other state updates
580
628
  return;
581
629
  }
@@ -600,6 +648,22 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
600
648
  this.trackUnifiedSwapBridgeEvent(constants_js_1.UnifiedSwapBridgeEventName.QuotesError, context);
601
649
  console.log(`Failed to ${shouldStream ? 'stream' : 'fetch'} bridge quotes`, error);
602
650
  }
651
+ finally {
652
+ await traceWithoutImpact({
653
+ name: traceName,
654
+ startTime: quoteTraceStartTime,
655
+ data: {
656
+ srcChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.srcChainId),
657
+ destChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.destChainId),
658
+ ...(!isBatchSellRequest && {
659
+ request_id: quoteTraceRequestId,
660
+ swap_type: (0, properties_js_1.getSwapType)(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId),
661
+ feature_id: context.feature_id,
662
+ result: traceResult,
663
+ }),
664
+ },
665
+ });
666
+ }
603
667
  // Update refresh count after fetching, validation and fee calculation have completed
604
668
  this.update((state) => {
605
669
  state.quotesRefreshCount += 1;
@@ -614,7 +678,7 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
614
678
  this.stopAllPolling();
615
679
  }
616
680
  });
617
- _BridgeController_handleQuoteStreaming.set(this, async (quoteRequests, featureId, jwt, selectedAccount) => {
681
+ _BridgeController_handleQuoteStreaming.set(this, async ({ quoteRequests, featureId, jwt, selectedAccount, signal, traceProviderFirstResult, }) => {
618
682
  /**
619
683
  * Tracks the number of valid quotes received from the current stream, which is used
620
684
  * to determine when to clear the quotes list and set the initial load time
@@ -625,9 +689,10 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
625
689
  * before setting quotesLoadingStatus to FETCHED
626
690
  */
627
691
  const pendingFeeAppendPromises = new Set();
628
- await (0, fetch_js_1.fetchBridgeQuoteStream)(__classPrivateFieldGet(this, _BridgeController_fetchFn, "f"), quoteRequests, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal, featureId, __classPrivateFieldGet(this, _BridgeController_clientId, "f"), jwt, __classPrivateFieldGet(this, _BridgeController_config, "f").customBridgeApiBaseUrl ?? bridge_js_1.BRIDGE_PROD_API_BASE_URL, {
692
+ await (0, fetch_js_1.fetchBridgeQuoteStream)(__classPrivateFieldGet(this, _BridgeController_fetchFn, "f"), quoteRequests, signal, featureId, __classPrivateFieldGet(this, _BridgeController_clientId, "f"), jwt, __classPrivateFieldGet(this, _BridgeController_config, "f").customBridgeApiBaseUrl ?? bridge_js_1.BRIDGE_PROD_API_BASE_URL, {
629
693
  onQuoteValidationFailure: (validationFailures) => __classPrivateFieldGet(this, _BridgeController_trackQuoteValidationFailures, "f").call(this, validationFailures, featureId),
630
694
  onValidQuoteReceived: async (quote) => {
695
+ await traceProviderFirstResult(quote.quote);
631
696
  const feeAppendPromise = (async () => {
632
697
  const quotesWithFees = await (0, quote_fees_js_1.appendFeesToQuotes)(quote.chainId, [quote], this.messenger, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), selectedAccount);
633
698
  if (quotesWithFees.length > 0) {
@@ -691,6 +756,7 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
691
756
  });
692
757
  },
693
758
  }, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
759
+ return validQuotesCounter;
694
760
  });
695
761
  _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, async (srcChainId, snapId) => {
696
762
  if (!(0, bridge_js_2.isSolanaChainId)(srcChainId) || !snapId) {