@metamask-previews/bridge-controller 79.2.0-preview-e81fe8853 → 79.2.0-preview-f7d67b054

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,84 @@ 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 = (providerData) => {
561
+ const provider = (0, properties_js_1.formatProviderLabel)(providerData);
562
+ if (isBatchSellRequest || tracedProviders.has(provider)) {
563
+ return;
564
+ }
565
+ tracedProviders.add(provider);
566
+ // Provider telemetry must not delay quote processing.
567
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
568
+ traceWithoutImpact({
569
+ name: traces_js_1.TraceName.QuoteProviderFirstResult,
570
+ startTime: quoteTraceStartTime,
536
571
  data: {
572
+ provider,
573
+ feature_id: context.feature_id,
574
+ request_id: quoteTraceRequestId,
575
+ swap_type: (0, properties_js_1.getSwapType)(firstQuoteRequest.srcChainId, firstQuoteRequest.destChainId),
537
576
  srcChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.srcChainId),
538
577
  destChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.destChainId),
578
+ result: 'success',
539
579
  },
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);
580
+ });
581
+ };
582
+ try {
583
+ const selectedAccount = __classPrivateFieldGet(this, _BridgeController_instances, "m", _BridgeController_getMultichainSelectedAccount).call(this, firstQuoteRequest.walletAddress);
584
+ // This call is not awaited to prevent blocking quote fetching if the snap takes too long to respond
585
+ // eslint-disable-next-line @typescript-eslint/no-floating-promises
586
+ __classPrivateFieldGet(this, _BridgeController_setMinimumBalanceForRentExemptionInLamports, "f").call(this, firstQuoteRequest.srcChainId, selectedAccount?.metadata?.snap?.id);
587
+ // Use SSE if enabled and return early
588
+ if (shouldStream || isBatchSellRequest) {
589
+ const quoteCount = await __classPrivateFieldGet(this, _BridgeController_handleQuoteStreaming, "f").call(this, {
590
+ quoteRequests,
591
+ featureId: context.feature_id,
592
+ jwt,
593
+ selectedAccount,
594
+ signal: quoteAbortSignal,
595
+ traceProviderFirstResult,
596
+ });
597
+ if (quoteAbortSignal.aborted) {
598
+ traceResult = 'cancelled';
548
599
  return;
549
600
  }
601
+ traceResult = quoteCount > 0 ? 'success' : 'no_quotes';
602
+ }
603
+ else {
550
604
  // Otherwise use regular fetch
551
- const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, __classPrivateFieldGet(this, _BridgeController_abortController, "f")?.signal);
605
+ const quotes = await this.fetchQuotes(firstQuoteRequest, context.feature_id, quoteAbortSignal);
606
+ for (const quote of quotes) {
607
+ traceProviderFirstResult(quote.quote);
608
+ }
552
609
  this.update((state) => {
553
610
  // Set the initial load time if this is the first fetch
554
611
  if (state.quotesRefreshCount ===
555
612
  bridge_js_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotesRefreshCount &&
556
613
  __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f")) {
557
- state.quotesInitialLoadTime =
558
- Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
614
+ state.quotesInitialLoadTime = Date.now() - __classPrivateFieldGet(this, _BridgeController_quotesFirstFetched, "f");
559
615
  }
560
616
  state.quotes = quotes.map(quote_response_v1_to_v2_js_1.toQuoteResponseV2);
561
617
  state.quotesLoadingStatus = types_js_1.RequestStatus.FETCHED;
562
618
  });
563
- });
619
+ traceResult = quotes.length > 0 ? 'success' : 'no_quotes';
620
+ }
564
621
  }
565
622
  catch (error) {
566
623
  // Reset the quotes list if the fetch fails to avoid showing stale quotes
@@ -568,14 +625,8 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
568
625
  state.quotes = bridge_js_1.DEFAULT_BRIDGE_CONTROLLER_STATE.quotes;
569
626
  });
570
627
  // 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)) {
628
+ if (isExpectedQuoteAbort(error, quoteAbortSignal)) {
629
+ traceResult = 'cancelled';
579
630
  // Exit the function early to prevent other state updates
580
631
  return;
581
632
  }
@@ -600,6 +651,21 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
600
651
  this.trackUnifiedSwapBridgeEvent(constants_js_1.UnifiedSwapBridgeEventName.QuotesError, context);
601
652
  console.log(`Failed to ${shouldStream ? 'stream' : 'fetch'} bridge quotes`, error);
602
653
  }
654
+ finally {
655
+ await traceWithoutImpact({
656
+ name: traceName,
657
+ startTime: quoteTraceStartTime,
658
+ data: {
659
+ srcChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.srcChainId),
660
+ destChainId: (0, caip_formatters_js_1.formatChainIdToCaip)(firstQuoteRequest.destChainId),
661
+ ...(!isBatchSellRequest && {
662
+ request_id: quoteTraceRequestId,
663
+ feature_id: context.feature_id,
664
+ result: traceResult,
665
+ }),
666
+ },
667
+ });
668
+ }
603
669
  // Update refresh count after fetching, validation and fee calculation have completed
604
670
  this.update((state) => {
605
671
  state.quotesRefreshCount += 1;
@@ -614,7 +680,7 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
614
680
  this.stopAllPolling();
615
681
  }
616
682
  });
617
- _BridgeController_handleQuoteStreaming.set(this, async (quoteRequests, featureId, jwt, selectedAccount) => {
683
+ _BridgeController_handleQuoteStreaming.set(this, async ({ quoteRequests, featureId, jwt, selectedAccount, signal, traceProviderFirstResult, }) => {
618
684
  /**
619
685
  * Tracks the number of valid quotes received from the current stream, which is used
620
686
  * to determine when to clear the quotes list and set the initial load time
@@ -625,13 +691,14 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
625
691
  * before setting quotesLoadingStatus to FETCHED
626
692
  */
627
693
  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, {
694
+ 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
695
  onQuoteValidationFailure: (validationFailures) => __classPrivateFieldGet(this, _BridgeController_trackQuoteValidationFailures, "f").call(this, validationFailures, featureId),
630
696
  onValidQuoteReceived: async (quote) => {
631
697
  const feeAppendPromise = (async () => {
632
698
  const quotesWithFees = await (0, quote_fees_js_1.appendFeesToQuotes)(quote.chainId, [quote], this.messenger, __classPrivateFieldGet(this, _BridgeController_getLayer1GasFee, "f"), selectedAccount);
633
699
  if (quotesWithFees.length > 0) {
634
700
  validQuotesCounter += 1;
701
+ traceProviderFirstResult(quote.quote);
635
702
  }
636
703
  this.update((state) => {
637
704
  // Clear previous quotes and quotes load time when first quote in the current
@@ -691,6 +758,7 @@ class BridgeController extends (0, polling_controller_1.StaticIntervalPollingCon
691
758
  });
692
759
  },
693
760
  }, __classPrivateFieldGet(this, _BridgeController_clientVersion, "f"));
761
+ return validQuotesCounter;
694
762
  });
695
763
  _BridgeController_setMinimumBalanceForRentExemptionInLamports.set(this, async (srcChainId, snapId) => {
696
764
  if (!(0, bridge_js_2.isSolanaChainId)(srcChainId) || !snapId) {