From 91088519cdfec73eee0367769a57f9fe94df0bbf Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 19 Oct 2026 13:17:40 -0700 Subject: [PATCH 1/9] feat: tag swap/bridge Sentry traces --- packages/bridge-controller/CHANGELOG.md | 12 +++ .../src/bridge-controller.sse.test.ts | 51 +++++++++++ .../src/bridge-controller.ts | 26 ++++-- .../bridge-controller/src/constants/traces.ts | 64 ++++++++++++-- packages/bridge-controller/src/index.ts | 8 ++ packages/bridge-controller/src/types.ts | 39 +++++++++ .../bridge-status-controller/CHANGELOG.md | 6 ++ .../bridge-status-controller.test.ts.snap | 36 +++++++- ...ridge-status-controller.batch-sell.test.ts | 19 ++-- .../src/bridge-status-controller.test.ts | 86 +++++++++++++++---- .../src/bridge-status-controller.ts | 20 ++--- .../src/strategy/types.ts | 5 +- .../src/utils/trace.ts | 19 +++- 13 files changed, 336 insertions(+), 55 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index ab8fdd4b055..eaa1e8a53d8 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,8 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Export trace-related constants and types to ensure client requests include required fields + - `TraceName` now includes both client-emitted and controller trace names + - `TraceFeatureTag` and `BRIDGE_FEATURE_ID_TO_FEATURE_TAG` for supported features + - `TraceOperation` for client-side operations + - `SwapRelatedTraceName` filters Swap, Bridge and Batch Sell traces from client-defined `TraceName` + - `SwapTraceRequest` defines required request fields for swap trace calls + - `SwapTraceFunction` is the type-safe shared trace interface that works for mobile, extension and controller consumers +- Populate the `tags` field with the active `feature` when tracing swap/bridge operations + ### Changed +- **BREAKING:** Require `traceFn` when initializing the BridgeController - Bump `@metamask/keyring-api` from `^24.1.0` to `^24.3.0` ([#10792](https://github.com/MetaMask/core/pull/10792), [#10844](https://github.com/MetaMask/core/pull/10844)) - Bump `@metamask/transaction-controller` from `^72.2.0` to `^72.2.1` ([#10817](https://github.com/MetaMask/core/pull/10817)) - Bump `@metamask/accounts-controller` from `^40.0.0` to `^40.1.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 2c412553bf3..7f904ddca1d 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -143,6 +143,7 @@ type WithControllerOptions = { options?: Partial[0]>; }; +let mockTraceFn: jest.Mock; async function withController( ...args: | [WithControllerCallback] @@ -221,6 +222,7 @@ async function withController( fetchFn: mockFetchFn, trackMetaMetricsFn, clientVersion: '13.8.0', + traceFn: mockTraceFn, ...options, }); @@ -257,6 +259,7 @@ describe('BridgeController SSE', function () { jest.clearAllMocks(); jest.clearAllTimers(); jest.resetAllMocks(); + mockTraceFn = jest.fn(async (_request, fn) => await fn?.()); }); describe('quote tracing', () => { @@ -507,6 +510,54 @@ describe('BridgeController SSE', function () { expect(getLayer1GasFeeMock).toHaveBeenCalledTimes(2); // eslint-disable-next-line jest/no-restricted-matchers expect(trackMetaMetricsFn.mock.calls).toMatchSnapshot(); + expect(mockTraceFn.mock.calls).toMatchObject([ + [ + { + data: { + request_id: 'test-uuid-1234', + result: 'success', + srcChainId: 'eip155:1', + swap_type: 'crosschain', + }, + name: 'Quote Provider First Result', + tags: { + feature: 'swap', + }, + }, + ], + [ + { + data: { + destChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + feature_id: 'unified_swap_bridge', + provider: 'socket_celercircle', + request_id: 'test-uuid-1234', + result: 'success', + srcChainId: 'eip155:1', + swap_type: 'crosschain', + }, + name: 'Quote Provider First Result', + tags: { + feature: 'swap', + }, + }, + ], + [ + { + data: { + destChainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', + feature_id: 'unified_swap_bridge', + request_id: 'test-uuid-1234', + result: 'success', + srcChainId: 'eip155:1', + }, + name: 'Bridge Quotes Fetched', + tags: { + feature: 'swap', + }, + }, + ], + ]); }, ); }); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index bc99f28e797..832ece178bc 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -22,7 +22,10 @@ import { } from './constants/bridge.js'; import { CHAIN_IDS } from './constants/chains.js'; import { SWAPS_CONTRACT_ADDRESSES } from './constants/swaps.js'; -import { TraceName } from './constants/traces.js'; +import { + BRIDGE_FEATURE_ID_TO_FEATURE_TAG, + TraceName, +} from './constants/traces.js'; import { RequestStatus } from './types.js'; import type { L1GasFees, @@ -33,6 +36,7 @@ import type { BridgeControllerMessenger, FetchFunction, InputPrimaryDenomination, + SwapTraceRequest, } from './types.js'; import { getAssetIdsForToken, toExchangeRates } from './utils/assets.js'; import { hasSufficientBalance } from './utils/balance.js'; @@ -98,6 +102,7 @@ import { } from './validators/quote-request.js'; import type { QuoteResponseV1 } from './validators/quote-response-v1.js'; import type { QuoteResponse } from './validators/quote-response.js'; +import type { SwapTraceFunction } from './index.js'; const metadata: StateMetadata = { quoteRequest: { @@ -277,7 +282,7 @@ export class BridgeController extends StaticIntervalPollingController, ) => void; - readonly #trace: TraceCallback; + readonly #trace: SwapTraceFunction; readonly #config: { customBridgeApiBaseUrl?: string; @@ -316,7 +321,7 @@ export class BridgeController extends StaticIntervalPollingController, ) => void; - traceFn?: TraceCallback; + traceFn: SwapTraceFunction; /** * When provided, called to determine whether to use AssetsController for exchange rates. * When true, rates are read from AssetsController:getExchangeRatesForBridge instead of @@ -343,7 +348,7 @@ export class BridgeController extends StaticIntervalPollingController fn?.()) as TraceCallback); + this.#trace = traceFn; this.#getUseAssetsControllerForRates = getUseAssetsControllerForRates ?? (() => false); @@ -858,7 +863,7 @@ export class BridgeController extends StaticIntervalPollingController(); - const traceWithoutImpact = async (request: TraceRequest): Promise => { + const traceWithoutImpact = async ( + request: Omit, 'tags'>, + ): Promise => { try { - await this.#trace(request, () => undefined); + await this.#trace({ + ...request, + tags: { + feature: BRIDGE_FEATURE_ID_TO_FEATURE_TAG[context.feature_id], + }, + }); } catch { // Telemetry failures must not affect quote fetching or state updates. } diff --git a/packages/bridge-controller/src/constants/traces.ts b/packages/bridge-controller/src/constants/traces.ts index 9a61707ce5a..12a799591b0 100644 --- a/packages/bridge-controller/src/constants/traces.ts +++ b/packages/bridge-controller/src/constants/traces.ts @@ -1,6 +1,58 @@ -export enum TraceName { - BatchSellQuotesFetched = 'Batch Sell Quotes Fetched', - BridgeQuotesFetched = 'Bridge Quotes Fetched', - QuoteProviderFirstResult = 'Quote Provider First Result', - SwapQuotesFetched = 'Swap Quotes Fetched', -} +import { FeatureId } from '../validators/feature-flags.js'; + +/** + * Sentry trace names published by the BridgeController + */ +export const TraceName = { + BatchSellQuotesFetched: 'Batch Sell Quotes Fetched', + BridgeQuotesFetched: 'Bridge Quotes Fetched', + QuoteProviderFirstResult: 'Quote Provider First Result', + SwapQuotesFetched: 'Swap Quotes Fetched', + SwapQuoteFetch: 'Swap Quote Fetch', + SwapTokenSearch: 'Swap Token Search', + SwapPopularTokensFetch: 'Swap Popular Tokens Fetch', + SwapViewLoaded: 'Swap View Loaded', + BridgeBalancesUpdated: 'Bridge Balances Updated', +} as const; +export type TraceName = (typeof TraceName)[keyof typeof TraceName]; + +/** + * Sentry tags used to identify swap/bridge features + */ +export const TraceFeatureTag = { + SWAP: 'swap', + BATCH_SELL: 'batch_sell', + QUICK_BUY: 'quick_buy', + PERPS: 'perps', + DAPP_SWAP: 'dapp_swap', + LIMIT_ORDER: 'limit_order', + RECURRING_ORDER: 'recurring_order', +} as const; +export type TraceFeatureTag = + (typeof TraceFeatureTag)[keyof typeof TraceFeatureTag]; + +export const TraceOperation = { + BridgeScreenPerformance: 'bridge.screen.performance', + BridgeDataFetch: 'bridge.data_fetch', +} as const; +export type TraceOperation = + (typeof TraceOperation)[keyof typeof TraceOperation]; + +/** + * Map of FeatureId to Sentry Trace Feature Tag + */ +export const BRIDGE_FEATURE_ID_TO_FEATURE_TAG: Record< + FeatureId, + TraceFeatureTag +> = { + [FeatureId.UNIFIED_SWAP_BRIDGE]: TraceFeatureTag.SWAP, + [FeatureId.BATCH_SELL]: TraceFeatureTag.BATCH_SELL, + [FeatureId.PERPS]: TraceFeatureTag.PERPS, + [FeatureId.QUICK_BUY_FOLLOW_TRADING]: TraceFeatureTag.QUICK_BUY, + [FeatureId.QUICK_BUY_EXPLORE]: TraceFeatureTag.QUICK_BUY, + [FeatureId.QUICK_BUY_TOKEN_DETAILS]: TraceFeatureTag.QUICK_BUY, + [FeatureId.DAPP_SWAP]: TraceFeatureTag.DAPP_SWAP, + [FeatureId.LIMIT_ORDER]: TraceFeatureTag.LIMIT_ORDER, + [FeatureId.RECURRING_ORDER]: TraceFeatureTag.RECURRING_ORDER, + [FeatureId.UNKNOWN]: TraceFeatureTag.SWAP, +}; diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 4206e27da54..48381c4ed0e 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -76,6 +76,9 @@ export type { BridgeControllerGetStateAction, BridgeControllerStateChangeEvent, DeepPartial, + SwapTraceRequest, + SwapTraceFunction, + SwapRelatedTraceName, } from './types.js'; export { @@ -271,6 +274,11 @@ export { selectTokenWarnings, } from './selectors.js'; +export { + BRIDGE_FEATURE_ID_TO_FEATURE_TAG, + TraceFeatureTag, +} from './constants/traces.js'; + export { DEFAULT_FEATURE_FLAG_CONFIG } from './constants/bridge.js'; export { getBridgeFeatureFlags } from './utils/feature-flags.js'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index cb44f9615b0..4a612951359 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -43,6 +43,45 @@ import type { GaslessPropertiesSchema } from './validators/quote.js'; import type { StepSchema } from './validators/step.js'; import type { TokenFeatureSchema } from './validators/token-feature.js'; +import type { TraceRequest } from '@metamask/controller-utils'; +import { TraceFeatureTag, type TraceOperation } from './constants/traces.js'; +[]; + +/** + * Filters the client-supplied trace names to only include swap/bridge/batch sell related names + */ +export type SwapRelatedTraceName = { + [TName in TNameEnum]: `${TName}` extends + | `${string}Swap${string}` + | `${string}Bridge${string}` + | `${string}Batch Sell${string}` + ? TName + : never; +}[TNameEnum]; + +/** + * Trace request type for swap/bridge traces + */ +export type SwapTraceRequest = Omit< + TraceRequest, + 'name' | 'tags' | 'parentContext' +> & { + name: TTraceName; + op?: TraceOperation; + tags: { feature: TraceFeatureTag }; +}; + +/** + * Generic trace function type that supports both mobile and extension interfaces + */ +export type SwapTraceFunction = { + ( + request: SwapTraceRequest, + fn: (args?: unknown) => TResult, + ): TResult; + (request: SwapTraceRequest): void; +}; + export type FetchFunction = ( input: RequestInfo | URL | string, init?: RequestInit, diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index 16e4488b13a..acd3735d975 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -7,8 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Update `TraceName` to include both client-emitted and controller trace names +- Populate the `tags` field with the active `feature` when tracing swap/bridge operations + ### Changed +- **BREAKING:** Require `traceFn` when initializing the BridgeStatusController - Bump `@metamask/accounts-controller` from `^40.0.0` to `^40.1.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) - Bump `@metamask/keyring-controller` from `^28.1.0` to `^28.2.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) - Bump `@metamask/base-controller` from `^10.0.0` to `^10.1.0` ([#10864](https://github.com/MetaMask/core/pull/10864)) diff --git a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap index a5c895ec8a1..de78d2ccfa8 100644 --- a/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap +++ b/packages/bridge-status-controller/src/__snapshots__/bridge-status-controller.test.ts.snap @@ -763,6 +763,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -774,6 +777,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1084,6 +1090,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1095,6 +1104,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1715,6 +1727,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1726,6 +1741,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -2036,6 +2054,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -2047,6 +2068,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -3506,6 +3530,9 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -3517,6 +3544,9 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -4324,7 +4354,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an ] `; -exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 1`] = ` +exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 2`] = ` { "chainId": "0xa4b1", "hash": "0xevmTxHash", @@ -4343,7 +4373,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an } `; -exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 2`] = ` +exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 3`] = ` { "account": "0xaccount1", "actionId": "1234567891.456", @@ -4469,7 +4499,7 @@ exports[`BridgeStatusController submitTx: EVM swap should successfully submit an } `; -exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 3`] = ` +exports[`BridgeStatusController submitTx: EVM swap should successfully submit an EVM swap transaction with no approval 4`] = ` [ [ "BridgeController:stopPollingForQuotes", diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index 71022d73302..6731f12f99a 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -125,6 +125,8 @@ async function withController( clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), addTransactionBatchFn, + traceFn: mockTraceFn, + clientProduct: 'metamask', ...options, }); const startPollingForBridgeTxStatusSpy = jest.fn(); @@ -189,9 +191,12 @@ const mockTransferTx: BatchSellTradesResponse['transactions'][number] = { type: BatchSellTransactionType.TRANSFER, }; +let mockTraceFn: jest.Mock; + describe('BridgeStatusController', () => { beforeEach(() => { jest.clearAllMocks(); + mockTraceFn = jest.fn(async (_request, fn) => await fn?.()); }); describe('submitBatchSell', () => { @@ -213,14 +218,12 @@ describe('BridgeStatusController', () => { it.each([true, false])( 'isDelegatedAccount=%s', async (isDelegatedAccount) => { - if ( - !( - !gasIncluded7702 && - !gasIncluded && - !stxEnabled && - !isDelegatedAccount - ) - ) { + if (!( + !gasIncluded7702 && + !gasIncluded && + !stxEnabled && + !isDelegatedAccount + )) { // return; } const is7702 = !shouldDisable7702( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.test.ts index 8b2d8c31690..8b180a5fa37 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -755,6 +755,8 @@ type WithControllerOptions = { mockMessengerCall?: jest.Mock; }; +let mockTraceFn: jest.Mock; + async function withController( ...args: | [WithControllerCallback] @@ -773,6 +775,7 @@ async function withController( clientProduct: 'test-client-product', fetchFn: jest.fn(), addTransactionBatchFn, + traceFn: mockTraceFn, ...options, }); const startPollingForBridgeTxStatusSpy = jest.fn(); @@ -809,6 +812,7 @@ const executePollingWithPendingStatus = async () => { fetchFn: jest.fn(), addTransactionBatchFn: jest.fn(), config: {}, + traceFn: mockTraceFn, }); const startPollingSpy = jest.spyOn(bridgeStatusController, 'startPolling'); @@ -1232,6 +1236,7 @@ describe('BridgeStatusController', () => { jest.clearAllMocks(); jest.clearAllTimers(); jest.spyOn(historyUtils, 'isHistoryItemTooOld').mockReturnValue(false); + mockTraceFn = jest.fn(async (_request, fn) => await fn?.()); }); describe('startPolling - error handling', () => { @@ -3687,9 +3692,6 @@ describe('BridgeStatusController', () => { const handleLineaDelaySpy = jest .spyOn(transactionUtils, 'handleApprovalDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); setupEventTrackingMocks(mockMessengerCall); setupApprovalMocks(mockMessengerCall); @@ -3734,9 +3736,6 @@ describe('BridgeStatusController', () => { const handleBaseDelaySpy = jest .spyOn(transactionUtils, 'handleApprovalDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); setupEventTrackingMocks(mockMessengerCall); setupApprovalMocks(mockMessengerCall); @@ -3787,9 +3786,6 @@ describe('BridgeStatusController', () => { const handleMobileHardwareWalletDelaySpy = jest .spyOn(transactionUtils, 'handleMobileHardwareWalletDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); // Mock for hardware wallet check mockMessengerCall.mockReturnValueOnce({ @@ -3848,9 +3844,6 @@ describe('BridgeStatusController', () => { const handleMobileHardwareWalletDelaySpy = jest .spyOn(transactionUtils, 'handleMobileHardwareWalletDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); setupEventTrackingMocks(mockMessengerCall); setupApprovalMocks(mockMessengerCall); @@ -3896,9 +3889,6 @@ describe('BridgeStatusController', () => { const handleMobileHardwareWalletDelaySpy = jest .spyOn(transactionUtils, 'handleMobileHardwareWalletDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); // Mock for non-hardware wallet check mockMessengerCall.mockReturnValueOnce({ @@ -4505,6 +4495,39 @@ describe('BridgeStatusController', () => { false, ); + expect(mockTraceFn.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "data": { + "feature_id": "unified_swap_bridge", + "srcChainId": "eip155:42161", + "stxEnabled": false, + }, + "name": "Swap Transaction Completed", + "tags": { + "feature": "swap", + }, + }, + [Function], + ], + [ + { + "data": { + "feature_id": "unified_swap_bridge", + "srcChainId": "eip155:42161", + "stxEnabled": false, + }, + "name": "Swap Transaction Approval Completed", + "tags": { + "feature": "swap", + }, + }, + [Function], + ], + ] + `); + expect(result).toMatchSnapshot(); expect(startPollingForBridgeTxStatusSpy).toHaveBeenCalledTimes(0); expect(controller.state.txHistory[result.id]).toMatchSnapshot(); @@ -5607,6 +5630,7 @@ describe('BridgeStatusController', () => { clientId: BridgeClientId.EXTENSION, clientProduct: 'test-client-product', fetchFn: mockFetchFn, + traceFn: mockTraceFn, addTransactionBatchFn: jest.fn(), state: { txHistory: { @@ -7022,6 +7046,38 @@ describe('BridgeStatusController', () => { }, ); + expect(mockTraceFn.mock.calls).toMatchInlineSnapshot(` + [ + [ + { + "data": { + "feature_id": "unified_swap_bridge", + "srcChainId": "eip155:42161", + "stxEnabled": false, + }, + "name": "Swap Transaction Completed", + "tags": { + "feature": "swap", + }, + }, + [Function], + ], + [ + { + "data": { + "feature_id": "unified_swap_bridge", + "srcChainId": "eip155:42161", + "stxEnabled": false, + }, + "name": "Swap Transaction Approval Completed", + "tags": { + "feature": "swap", + }, + }, + [Function], + ], + ] + `); expect( controller.state.txHistory[EVM_TX_META_ID] .reportedSubmittedTxHash, diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index aea9171603f..502e1093162 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -17,6 +17,7 @@ import { import type { FailureTelemetryData, QuoteMetadataMigrationPhase, + SwapTraceFunction, } from '@metamask/bridge-controller'; import { isNonEvmChainId, @@ -29,7 +30,6 @@ import { PollingStatus, formatChainIdToHex, } from '@metamask/bridge-controller'; -import type { TraceCallback } from '@metamask/controller-utils'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; import { TransactionStatus, @@ -49,6 +49,7 @@ import { DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, MAX_ATTEMPTS, REFRESH_INTERVAL_MS, + type TraceName, } from './constants.js'; import { QUOTE_STATUS_BACKFILL_WINDOW_MS, @@ -182,7 +183,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; constructor({ messenger, @@ -207,7 +208,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; onQuoteStatusManagerError?: ( error: QuoteStatusUpdateError | QuoteStatusGetError, ) => void; @@ -231,7 +232,7 @@ export class BridgeStatusController extends StaticIntervalPollingController fn?.()) as TraceCallback); + this.#trace = traceFn; this.#intentManager = new IntentManager({ messenger: this.messenger, customBridgeApiBaseUrl: this.#config.customBridgeApiBaseUrl, @@ -380,7 +381,6 @@ export class BridgeStatusController extends StaticIntervalPollingController undefined, ); }; @@ -1762,12 +1762,10 @@ export class BridgeStatusController extends StaticIntervalPollingController; // Used for intent transactions fetchFn: FetchFunction; clientId: BridgeClientId; diff --git a/packages/bridge-status-controller/src/utils/trace.ts b/packages/bridge-status-controller/src/utils/trace.ts index 95800264e92..c97b93d9ad6 100644 --- a/packages/bridge-status-controller/src/utils/trace.ts +++ b/packages/bridge-status-controller/src/utils/trace.ts @@ -6,6 +6,7 @@ import { getSwapType, isCrossChain, QuoteResponseV1, + BRIDGE_FEATURE_ID_TO_FEATURE_TAG, } from '@metamask/bridge-controller'; import { TraceName } from '../constants.js'; @@ -18,6 +19,7 @@ export const getTraceParams = ( quoteResponse: QuoteResponseV1, isStxEnabled: boolean, ) => { + const featureId = quoteResponse.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE; return { name: isCrossChain( quoteResponse.quote.srcChainId, @@ -28,7 +30,10 @@ export const getTraceParams = ( data: { srcChainId: formatChainIdToCaip(quoteResponse.quote.srcChainId), stxEnabled: isStxEnabled, - feature_id: quoteResponse.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE, + feature_id: featureId, + }, + tags: { + feature: BRIDGE_FEATURE_ID_TO_FEATURE_TAG[featureId], }, }; }; @@ -37,6 +42,7 @@ export const getApprovalTraceParams = ( quoteResponse: QuoteResponseV1, isStxEnabled: boolean, ) => { + const featureId = quoteResponse.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE; return { name: isCrossChain( quoteResponse.quote.srcChainId, @@ -47,7 +53,10 @@ export const getApprovalTraceParams = ( data: { srcChainId: formatChainIdToCaip(quoteResponse.quote.srcChainId), stxEnabled: isStxEnabled, - feature_id: quoteResponse.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE, + feature_id: featureId, + }, + tags: { + feature: BRIDGE_FEATURE_ID_TO_FEATURE_TAG[featureId], }, }; }; @@ -61,14 +70,18 @@ export const getSwapOperationCompletedTraceParams = ( const quoteId = historyItem.quoteId ?? historyItem.quote.requestId; const sourceTransactionHash = historyItem.status.srcChain.txHash; const destinationTransactionHash = historyItem.status.destChain?.txHash; + const featureId = historyItem.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE; return { name: TraceName.SwapOperationCompleted, startTime: historyItem.startTime, + tags: { + feature: BRIDGE_FEATURE_ID_TO_FEATURE_TAG[featureId], + }, data: { srcChainId: formatChainIdToCaip(historyItem.quote.srcChainId), destChainId: formatChainIdToCaip(historyItem.quote.destChainId), - feature_id: historyItem.featureId ?? FeatureId.UNIFIED_SWAP_BRIDGE, + feature_id: featureId, provider: formatProviderLabel(historyItem.quote), swap_type: getSwapType( historyItem.quote.srcChainId, From d7b8f1f31d1fada1660a4075ba491b4b45a31298 Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 19 Oct 2026 13:20:06 -0700 Subject: [PATCH 2/9] changelog --- packages/bridge-controller/CHANGELOG.md | 6 +++--- packages/bridge-status-controller/CHANGELOG.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index eaa1e8a53d8..2bb568dd6a2 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -9,18 +9,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Export trace-related constants and types to ensure client requests include required fields +- Export trace-related constants and types to ensure client requests include required fields ([#10893](https://github.com/MetaMask/core/pull/10893)) - `TraceName` now includes both client-emitted and controller trace names - `TraceFeatureTag` and `BRIDGE_FEATURE_ID_TO_FEATURE_TAG` for supported features - `TraceOperation` for client-side operations - `SwapRelatedTraceName` filters Swap, Bridge and Batch Sell traces from client-defined `TraceName` - `SwapTraceRequest` defines required request fields for swap trace calls - `SwapTraceFunction` is the type-safe shared trace interface that works for mobile, extension and controller consumers -- Populate the `tags` field with the active `feature` when tracing swap/bridge operations +- Populate the `tags` field with the active `feature` when tracing swap/bridge operations ([#10893](https://github.com/MetaMask/core/pull/10893)) ### Changed -- **BREAKING:** Require `traceFn` when initializing the BridgeController +- **BREAKING:** Require `traceFn` when initializing the BridgeController ([#10893](https://github.com/MetaMask/core/pull/10893)) - Bump `@metamask/keyring-api` from `^24.1.0` to `^24.3.0` ([#10792](https://github.com/MetaMask/core/pull/10792), [#10844](https://github.com/MetaMask/core/pull/10844)) - Bump `@metamask/transaction-controller` from `^72.2.0` to `^72.2.1` ([#10817](https://github.com/MetaMask/core/pull/10817)) - Bump `@metamask/accounts-controller` from `^40.0.0` to `^40.1.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) diff --git a/packages/bridge-status-controller/CHANGELOG.md b/packages/bridge-status-controller/CHANGELOG.md index acd3735d975..3ed6a6d202d 100644 --- a/packages/bridge-status-controller/CHANGELOG.md +++ b/packages/bridge-status-controller/CHANGELOG.md @@ -9,12 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Update `TraceName` to include both client-emitted and controller trace names -- Populate the `tags` field with the active `feature` when tracing swap/bridge operations +- Update `TraceName` to include both client-emitted and controller trace names ([#10893](https://github.com/MetaMask/core/pull/10893)) +- Populate the `tags` field with the active `feature` when tracing swap/bridge operations ([#10893](https://github.com/MetaMask/core/pull/10893)) ### Changed -- **BREAKING:** Require `traceFn` when initializing the BridgeStatusController +- **BREAKING:** Require `traceFn` when initializing the BridgeStatusController ([#10893](https://github.com/MetaMask/core/pull/10893)) - Bump `@metamask/accounts-controller` from `^40.0.0` to `^40.1.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) - Bump `@metamask/keyring-controller` from `^28.1.0` to `^28.2.0` ([#10851](https://github.com/MetaMask/core/pull/10851)) - Bump `@metamask/base-controller` from `^10.0.0` to `^10.1.0` ([#10864](https://github.com/MetaMask/core/pull/10864)) From 4ef3a5414c6baa036a9af5937edda0322ee74e58 Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 19 Oct 2026 13:29:09 -0700 Subject: [PATCH 3/9] fix: lint --- packages/bridge-controller/CHANGELOG.md | 4 +--- .../bridge-controller/src/bridge-controller.ts | 2 +- packages/bridge-controller/src/index.ts | 2 +- packages/bridge-controller/src/types.ts | 5 ++--- .../bridge-status-controller.batch-sell.test.ts | 14 ++++++++------ .../src/bridge-status-controller.ts | 10 ++++++---- .../bridge-status-controller/src/strategy/types.ts | 2 +- 7 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index 2bb568dd6a2..e84584cc6bd 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -10,9 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Export trace-related constants and types to ensure client requests include required fields ([#10893](https://github.com/MetaMask/core/pull/10893)) - - `TraceName` now includes both client-emitted and controller trace names - - `TraceFeatureTag` and `BRIDGE_FEATURE_ID_TO_FEATURE_TAG` for supported features - - `TraceOperation` for client-side operations + - `SwapFeatureTag` and `BRIDGE_FEATURE_ID_TO_FEATURE_TAG` for supported features - `SwapRelatedTraceName` filters Swap, Bridge and Batch Sell traces from client-defined `TraceName` - `SwapTraceRequest` defines required request fields for swap trace calls - `SwapTraceFunction` is the type-safe shared trace interface that works for mobile, extension and controller consumers diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 832ece178bc..b64bfe4570d 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -26,6 +26,7 @@ import { BRIDGE_FEATURE_ID_TO_FEATURE_TAG, TraceName, } from './constants/traces.js'; +import type { SwapTraceFunction } from './index.js'; import { RequestStatus } from './types.js'; import type { L1GasFees, @@ -102,7 +103,6 @@ import { } from './validators/quote-request.js'; import type { QuoteResponseV1 } from './validators/quote-response-v1.js'; import type { QuoteResponse } from './validators/quote-response.js'; -import type { SwapTraceFunction } from './index.js'; const metadata: StateMetadata = { quoteRequest: { diff --git a/packages/bridge-controller/src/index.ts b/packages/bridge-controller/src/index.ts index 48381c4ed0e..cddeda11541 100644 --- a/packages/bridge-controller/src/index.ts +++ b/packages/bridge-controller/src/index.ts @@ -276,7 +276,7 @@ export { export { BRIDGE_FEATURE_ID_TO_FEATURE_TAG, - TraceFeatureTag, + TraceFeatureTag as SwapFeatureTag, } from './constants/traces.js'; export { DEFAULT_FEATURE_FLAG_CONFIG } from './constants/bridge.js'; diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index 4a612951359..beb8574fd5e 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -6,6 +6,7 @@ import type { ControllerGetStateAction, ControllerStateChangeEvent, } from '@metamask/base-controller'; +import type { TraceRequest } from '@metamask/controller-utils'; import type { Messenger } from '@metamask/messenger'; import type { NetworkControllerFindNetworkClientIdByChainIdAction, @@ -26,6 +27,7 @@ import type { import type { BridgeControllerMethodActions } from './bridge-controller-method-action-types.js'; import type { BridgeController } from './bridge-controller.js'; import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge.js'; +import { TraceFeatureTag, type TraceOperation } from './constants/traces.js'; import type { SimulatedGasFeeLimitsSchema } from './validators/batch-sell.js'; import type { BatchSellTradesResponseSchema } from './validators/batch-sell.js'; import type { @@ -42,9 +44,6 @@ import type { FeeDataSchema } from './validators/quote.js'; import type { GaslessPropertiesSchema } from './validators/quote.js'; import type { StepSchema } from './validators/step.js'; import type { TokenFeatureSchema } from './validators/token-feature.js'; - -import type { TraceRequest } from '@metamask/controller-utils'; -import { TraceFeatureTag, type TraceOperation } from './constants/traces.js'; []; /** diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index 6731f12f99a..1f155693c2f 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -218,12 +218,14 @@ describe('BridgeStatusController', () => { it.each([true, false])( 'isDelegatedAccount=%s', async (isDelegatedAccount) => { - if (!( - !gasIncluded7702 && - !gasIncluded && - !stxEnabled && - !isDelegatedAccount - )) { + if ( + !( + !gasIncluded7702 && + !gasIncluded && + !stxEnabled && + !isDelegatedAccount + ) + ) { // return; } const is7702 = !shouldDisable7702( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 502e1093162..a842dcc9332 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1762,10 +1762,12 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Mon, 19 Oct 2026 14:01:35 -0700 Subject: [PATCH 4/9] fix: lint --- oxlint-suppressions.json | 20 ++++--- .../src/bridge-controller.ts | 1 - .../bridge-controller/src/constants/traces.ts | 53 +++++++++---------- packages/bridge-controller/src/types.ts | 9 ++-- ...ridge-status-controller.batch-sell.test.ts | 17 +++--- .../src/bridge-status-controller.ts | 12 ++--- 6 files changed, 49 insertions(+), 63 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 5876c844d39..6d4dac2298d 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -1782,11 +1782,14 @@ "typescript/no-unsafe-assignment": { "count": 22 }, + "typescript/no-unsafe-call": { + "count": 1 + }, "typescript/no-unsafe-member-access": { "count": 2 }, "typescript/no-unsafe-return": { - "count": 3 + "count": 4 } }, "packages/bridge-controller/src/bridge-controller.test.ts": { @@ -2023,13 +2026,13 @@ "count": 9 }, "typescript/no-unsafe-call": { - "count": 2 + "count": 3 }, "typescript/no-unsafe-member-access": { "count": 11 }, "typescript/no-unsafe-return": { - "count": 4 + "count": 5 } }, "packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts": { @@ -2079,13 +2082,13 @@ "count": 12 }, "typescript/no-unsafe-call": { - "count": 5 + "count": 1 }, "typescript/no-unsafe-member-access": { "count": 13 }, "typescript/no-unsafe-return": { - "count": 7 + "count": 3 } }, "packages/bridge-status-controller/src/bridge-status-controller.ts": { @@ -3648,11 +3651,6 @@ "count": 1 } }, - "packages/mfa-recovery-controller/jest.config.cjs": { - "import/unambiguous": { - "count": 1 - } - }, "packages/money-account-api-data-service/jest.config.cjs": { "import/unambiguous": { "count": 1 @@ -7659,4 +7657,4 @@ "count": 1 } } -} +} \ No newline at end of file diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index b64bfe4570d..2b470da782a 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -3,7 +3,6 @@ import { BigNumber } from '@ethersproject/bignumber'; import { Contract } from '@ethersproject/contracts'; import { Web3Provider } from '@ethersproject/providers'; import type { StateMetadata } from '@metamask/base-controller'; -import type { TraceCallback, TraceRequest } from '@metamask/controller-utils'; import type { InternalAccount } from '@metamask/keyring-internal-api'; import { abiERC20 } from '@metamask/metamask-eth-abis'; import { StaticIntervalPollingController } from '@metamask/polling-controller'; diff --git a/packages/bridge-controller/src/constants/traces.ts b/packages/bridge-controller/src/constants/traces.ts index 12a799591b0..7b03992aea4 100644 --- a/packages/bridge-controller/src/constants/traces.ts +++ b/packages/bridge-controller/src/constants/traces.ts @@ -3,40 +3,35 @@ import { FeatureId } from '../validators/feature-flags.js'; /** * Sentry trace names published by the BridgeController */ -export const TraceName = { - BatchSellQuotesFetched: 'Batch Sell Quotes Fetched', - BridgeQuotesFetched: 'Bridge Quotes Fetched', - QuoteProviderFirstResult: 'Quote Provider First Result', - SwapQuotesFetched: 'Swap Quotes Fetched', - SwapQuoteFetch: 'Swap Quote Fetch', - SwapTokenSearch: 'Swap Token Search', - SwapPopularTokensFetch: 'Swap Popular Tokens Fetch', - SwapViewLoaded: 'Swap View Loaded', - BridgeBalancesUpdated: 'Bridge Balances Updated', -} as const; -export type TraceName = (typeof TraceName)[keyof typeof TraceName]; +export enum TraceName { + BatchSellQuotesFetched = 'Batch Sell Quotes Fetched', + BridgeQuotesFetched = 'Bridge Quotes Fetched', + QuoteProviderFirstResult = 'Quote Provider First Result', + SwapQuotesFetched = 'Swap Quotes Fetched', + SwapQuoteFetch = 'Swap Quote Fetch', + SwapTokenSearch = 'Swap Token Search', + SwapPopularTokensFetch = 'Swap Popular Tokens Fetch', + SwapViewLoaded = 'Swap View Loaded', + BridgeBalancesUpdated = 'Bridge Balances Updated', +} /** * Sentry tags used to identify swap/bridge features */ -export const TraceFeatureTag = { - SWAP: 'swap', - BATCH_SELL: 'batch_sell', - QUICK_BUY: 'quick_buy', - PERPS: 'perps', - DAPP_SWAP: 'dapp_swap', - LIMIT_ORDER: 'limit_order', - RECURRING_ORDER: 'recurring_order', -} as const; -export type TraceFeatureTag = - (typeof TraceFeatureTag)[keyof typeof TraceFeatureTag]; +export enum TraceFeatureTag { + SWAP = 'swap', + BATCH_SELL = 'batch_sell', + QUICK_BUY = 'quick_buy', + PERPS = 'perps', + DAPP_SWAP = 'dapp_swap', + LIMIT_ORDER = 'limit_order', + RECURRING_ORDER = 'recurring_order', +} -export const TraceOperation = { - BridgeScreenPerformance: 'bridge.screen.performance', - BridgeDataFetch: 'bridge.data_fetch', -} as const; -export type TraceOperation = - (typeof TraceOperation)[keyof typeof TraceOperation]; +export enum TraceOperation { + BridgeScreenPerformance = 'bridge.screen.performance', + BridgeDataFetch = 'bridge.data_fetch', +} /** * Map of FeatureId to Sentry Trace Feature Tag diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index beb8574fd5e..fd9a829cb8f 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -27,7 +27,8 @@ import type { import type { BridgeControllerMethodActions } from './bridge-controller-method-action-types.js'; import type { BridgeController } from './bridge-controller.js'; import type { BRIDGE_CONTROLLER_NAME } from './constants/bridge.js'; -import { TraceFeatureTag, type TraceOperation } from './constants/traces.js'; +import { TraceFeatureTag } from './constants/traces.js'; +import type { TraceOperation } from './constants/traces.js'; import type { SimulatedGasFeeLimitsSchema } from './validators/batch-sell.js'; import type { BatchSellTradesResponseSchema } from './validators/batch-sell.js'; import type { @@ -44,7 +45,6 @@ import type { FeeDataSchema } from './validators/quote.js'; import type { GaslessPropertiesSchema } from './validators/quote.js'; import type { StepSchema } from './validators/step.js'; import type { TokenFeatureSchema } from './validators/token-feature.js'; -[]; /** * Filters the client-supplied trace names to only include swap/bridge/batch sell related names @@ -78,7 +78,7 @@ export type SwapTraceFunction = { request: SwapTraceRequest, fn: (args?: unknown) => TResult, ): TResult; - (request: SwapTraceRequest): void; + (request: SwapTraceRequest): unknown; }; export type FetchFunction = ( @@ -390,8 +390,7 @@ export type BridgeControllerStateChangeEvent = ControllerStateChangeEvent< >; export type BridgeControllerActions = - | BridgeControllerGetStateAction - | BridgeControllerMethodActions; + BridgeControllerGetStateAction | BridgeControllerMethodActions; export type BridgeControllerEvents = BridgeControllerStateChangeEvent; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index 1f155693c2f..de74ca4fdaa 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -59,6 +59,7 @@ type RootMessenger = Messenger< >; const addTransactionBatchFn = jest.fn(); +let mockTraceFn: jest.Mock; function getRootMessenger(): RootMessenger { return new Messenger({ namespace: MOCK_ANY_NAMESPACE }); @@ -191,8 +192,6 @@ const mockTransferTx: BatchSellTradesResponse['transactions'][number] = { type: BatchSellTransactionType.TRANSFER, }; -let mockTraceFn: jest.Mock; - describe('BridgeStatusController', () => { beforeEach(() => { jest.clearAllMocks(); @@ -218,14 +217,12 @@ describe('BridgeStatusController', () => { it.each([true, false])( 'isDelegatedAccount=%s', async (isDelegatedAccount) => { - if ( - !( - !gasIncluded7702 && - !gasIncluded && - !stxEnabled && - !isDelegatedAccount - ) - ) { + if (!( + !gasIncluded7702 && + !gasIncluded && + !stxEnabled && + !isDelegatedAccount + )) { // return; } const is7702 = !shouldDisable7702( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index a842dcc9332..85014ebe756 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -49,8 +49,8 @@ import { DEFAULT_BRIDGE_STATUS_CONTROLLER_STATE, MAX_ATTEMPTS, REFRESH_INTERVAL_MS, - type TraceName, } from './constants.js'; +import type { TraceName } from './constants.js'; import { QUOTE_STATUS_BACKFILL_WINDOW_MS, QUOTE_STATUS_UPDATE_ENTRY_TTL, @@ -1762,12 +1762,10 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Mon, 19 Oct 2026 14:09:18 -0700 Subject: [PATCH 5/9] fix: test --- .../src/bridge-controller.sse.test.ts | 3 +++ packages/bridge-controller/src/bridge-controller.ts | 13 ++++++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 7f904ddca1d..25ebe3753d8 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -524,6 +524,7 @@ describe('BridgeController SSE', function () { feature: 'swap', }, }, + expect.any(Function), ], [ { @@ -541,6 +542,7 @@ describe('BridgeController SSE', function () { feature: 'swap', }, }, + expect.any(Function), ], [ { @@ -556,6 +558,7 @@ describe('BridgeController SSE', function () { feature: 'swap', }, }, + expect.any(Function), ], ]); }, diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 2b470da782a..7724e22dd3b 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -876,12 +876,15 @@ export class BridgeController extends StaticIntervalPollingController, 'tags'>, ): Promise => { try { - await this.#trace({ - ...request, - tags: { - feature: BRIDGE_FEATURE_ID_TO_FEATURE_TAG[context.feature_id], + await this.#trace( + { + ...request, + tags: { + feature: BRIDGE_FEATURE_ID_TO_FEATURE_TAG[context.feature_id], + }, }, - }); + () => undefined, + ); } catch { // Telemetry failures must not affect quote fetching or state updates. } From ef7c17995211264a4903d96a7cb2718a635404b5 Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 19 Oct 2026 14:20:59 -0700 Subject: [PATCH 6/9] fix: lint --- oxlint-suppressions.json | 2 +- .../bridge-controller/src/bridge-controller.ts | 2 +- packages/bridge-controller/src/types.ts | 3 ++- .../bridge-status-controller.batch-sell.test.ts | 14 ++++++++------ .../src/bridge-status-controller.ts | 3 ++- tsc-suppressions.json | 4 ++-- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 6d4dac2298d..6b34873fb75 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -7657,4 +7657,4 @@ "count": 1 } } -} \ No newline at end of file +} diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 7724e22dd3b..998a0c50c55 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -876,7 +876,7 @@ export class BridgeController extends StaticIntervalPollingController, 'tags'>, ): Promise => { try { - await this.#trace( + this.#trace( { ...request, tags: { diff --git a/packages/bridge-controller/src/types.ts b/packages/bridge-controller/src/types.ts index fd9a829cb8f..2c341d63e34 100644 --- a/packages/bridge-controller/src/types.ts +++ b/packages/bridge-controller/src/types.ts @@ -390,7 +390,8 @@ export type BridgeControllerStateChangeEvent = ControllerStateChangeEvent< >; export type BridgeControllerActions = - BridgeControllerGetStateAction | BridgeControllerMethodActions; + | BridgeControllerGetStateAction + | BridgeControllerMethodActions; export type BridgeControllerEvents = BridgeControllerStateChangeEvent; diff --git a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts index de74ca4fdaa..9bd95066ac4 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts @@ -217,12 +217,14 @@ describe('BridgeStatusController', () => { it.each([true, false])( 'isDelegatedAccount=%s', async (isDelegatedAccount) => { - if (!( - !gasIncluded7702 && - !gasIncluded && - !stxEnabled && - !isDelegatedAccount - )) { + if ( + !( + !gasIncluded7702 && + !gasIncluded && + !stxEnabled && + !isDelegatedAccount + ) + ) { // return; } const is7702 = !shouldDisable7702( diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index 85014ebe756..d4cce9038a2 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -374,13 +374,14 @@ export class BridgeStatusController extends StaticIntervalPollingController undefined, ); }; diff --git a/tsc-suppressions.json b/tsc-suppressions.json index be0430f54c8..489b7497b0f 100644 --- a/tsc-suppressions.json +++ b/tsc-suppressions.json @@ -312,7 +312,7 @@ "count": 1 }, "TS2345": { - "count": 1 + "count": 2 }, "TS2769": { "count": 1 @@ -337,7 +337,7 @@ "count": 1 }, "TS2345": { - "count": 1 + "count": 2 }, "TS6305": { "count": 1 From e0107cfa7485f8829d4cdadfea7c545d83e7fc5d Mon Sep 17 00:00:00 2001 From: micaelae Date: Mon, 19 Oct 2026 14:49:26 -0700 Subject: [PATCH 7/9] fix: lint --- .../src/bridge-controller.sse.test.ts | 34 +++++++++++++++ .../src/bridge-controller.ts | 7 ++-- .../src/bridge-status-controller.test.ts | 41 +++++++++++++++++++ .../src/bridge-status-controller.ts | 10 +++-- tsc-suppressions.json | 5 --- 5 files changed, 84 insertions(+), 13 deletions(-) diff --git a/packages/bridge-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 25ebe3753d8..d0a8f0cbfe3 100644 --- a/packages/bridge-controller/src/bridge-controller.sse.test.ts +++ b/packages/bridge-controller/src/bridge-controller.sse.test.ts @@ -403,6 +403,40 @@ describe('BridgeController SSE', function () { }), ); }); + + it('swallows throws from a synchronous traceFn', async () => { + const traceFn = jest.fn(() => { + throw new Error('trace failed'); + }); + + await withController( + { + options: { + traceFn, + }, + }, + async ({ controller, rootMessenger }) => { + mockFetchFn.mockImplementationOnce(async () => + mockSseEventSource([mockBridgeQuotesErc20Erc20V1[0]]), + ); + await rootMessenger.call( + 'BridgeController:updateBridgeQuoteRequestParams', + quoteRequest, + metricsContext, + ); + jest.advanceTimersByTime(1000); + await advanceToNthTimerThenFlush(); + jest.advanceTimersByTime(11000); + await flushPromises(); + + expect(traceFn).toHaveBeenCalled(); + expect(controller.state.quotesLoadingStatus).toBe( + RequestStatus.FETCHED, + ); + expect(controller.state.quoteFetchError).toBeNull(); + }, + ); + }); }); it('should trigger quote polling if request is valid', async function () { diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index 998a0c50c55..c6415c52528 100644 --- a/packages/bridge-controller/src/bridge-controller.ts +++ b/packages/bridge-controller/src/bridge-controller.ts @@ -872,9 +872,9 @@ export class BridgeController extends StaticIntervalPollingController(); - const traceWithoutImpact = async ( + const traceWithoutImpact = ( request: Omit, 'tags'>, - ): Promise => { + ) => { try { this.#trace( { @@ -898,7 +898,6 @@ export class BridgeController extends StaticIntervalPollingController { ); }, ); + + it('lets caller catch throws from a synchronous traceFn', async () => { + const traceFn = jest.fn(() => { + throw new Error('trace failed'); + }); + + await withController( + { + options: { + state: { + txHistory: MockTxHistory.getPendingSwap(), + }, + traceFn, + }, + }, + async ({ controller, rootMessenger }) => { + registerDefaultActionHandlers(rootMessenger); + rootMessenger.publish( + 'TransactionController:transactionStatusUpdated', + { + transactionMeta: { + chainId: CHAIN_IDS.ARBITRUM, + hash: '0xsourceTxHash', + networkClientId: 'eth-id', + time: Date.now(), + txParams: {} as unknown as TransactionParams, + type: TransactionType.swap, + status: TransactionStatus.confirmed, + id: 'swapTxMetaId1', + }, + }, + ); + await flushPromises(); + + expect(traceFn).toHaveBeenCalled(); + expect( + controller.state.txHistory.swapTxMetaId1?.status.status, + ).toBe(StatusTypes.COMPLETE); + }, + ); + }); }); it.each([ diff --git a/packages/bridge-status-controller/src/bridge-status-controller.ts b/packages/bridge-status-controller/src/bridge-status-controller.ts index d4cce9038a2..b248ffc1e9d 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.ts @@ -1763,10 +1763,12 @@ export class BridgeStatusController extends StaticIntervalPollingController Date: Tue, 20 Oct 2026 08:59:14 -0700 Subject: [PATCH 8/9] fix: lint --- oxlint-suppressions.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index 9d0b24881aa..099cabc495e 100644 --- a/oxlint-suppressions.json +++ b/oxlint-suppressions.json @@ -2020,16 +2020,16 @@ }, "packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts": { "typescript/no-unsafe-assignment": { - "count": 9 + "count": 3 }, "typescript/no-unsafe-call": { - "count": 3 + "count": 1 }, "typescript/no-unsafe-member-access": { "count": 1 }, "typescript/no-unsafe-return": { - "count": 5 + "count": 3 } }, "packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts": { @@ -2085,7 +2085,7 @@ "count": 13 }, "typescript/no-unsafe-return": { - "count": 3 + "count": 5 } }, "packages/bridge-status-controller/src/bridge-status-controller.ts": { From 71590e0613f2ca9d86ba2f30679ef491d1bc89ca Mon Sep 17 00:00:00 2001 From: micaelae Date: Tue, 20 Oct 2026 09:09:32 -0700 Subject: [PATCH 9/9] fix: lint --- tsc-suppressions.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/tsc-suppressions.json b/tsc-suppressions.json index 8516096abaa..e80d8ed34ec 100644 --- a/tsc-suppressions.json +++ b/tsc-suppressions.json @@ -374,9 +374,6 @@ "packages/bridge-status-controller/src/bridge-status-controller.batch-sell.test.ts": { "TS2322": { "count": 5 - }, - "TS2345": { - "count": 1 } }, "packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts": {