diff --git a/oxlint-suppressions.json b/oxlint-suppressions.json index ad5d9e20e2e..099cabc495e 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": { @@ -2019,11 +2022,14 @@ "typescript/no-unsafe-assignment": { "count": 3 }, + "typescript/no-unsafe-call": { + "count": 1 + }, "typescript/no-unsafe-member-access": { "count": 1 }, "typescript/no-unsafe-return": { - "count": 2 + "count": 3 } }, "packages/bridge-status-controller/src/bridge-status-controller.intent-manager.test.ts": { @@ -2073,13 +2079,13 @@ "count": 12 }, "typescript/no-unsafe-call": { - "count": 5 + "count": 1 }, "typescript/no-unsafe-member-access": { "count": 13 }, "typescript/no-unsafe-return": { - "count": 9 + "count": 5 } }, "packages/bridge-status-controller/src/bridge-status-controller.ts": { diff --git a/packages/bridge-controller/CHANGELOG.md b/packages/bridge-controller/CHANGELOG.md index c5cff605cb5..0b655025e3a 100644 --- a/packages/bridge-controller/CHANGELOG.md +++ b/packages/bridge-controller/CHANGELOG.md @@ -7,8 +7,18 @@ 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 ([#10893](https://github.com/MetaMask/core/pull/10893)) + - `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 +- 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 ([#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-controller/src/bridge-controller.sse.test.ts b/packages/bridge-controller/src/bridge-controller.sse.test.ts index 2c412553bf3..d0a8f0cbfe3 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', () => { @@ -400,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 () { @@ -507,6 +544,57 @@ 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', + }, + }, + expect.any(Function), + ], + [ + { + 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', + }, + }, + expect.any(Function), + ], + [ + { + 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', + }, + }, + expect.any(Function), + ], + ]); }, ); }); diff --git a/packages/bridge-controller/src/bridge-controller.ts b/packages/bridge-controller/src/bridge-controller.ts index bc99f28e797..c6415c52528 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'; @@ -22,7 +21,11 @@ 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 type { SwapTraceFunction } from './index.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'; @@ -277,7 +281,7 @@ export class BridgeController extends StaticIntervalPollingController, ) => void; - readonly #trace: TraceCallback; + readonly #trace: SwapTraceFunction; readonly #config: { customBridgeApiBaseUrl?: string; @@ -316,7 +320,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 +347,7 @@ export class BridgeController extends StaticIntervalPollingController fn?.()) as TraceCallback); + this.#trace = traceFn; this.#getUseAssetsControllerForRates = getUseAssetsControllerForRates ?? (() => false); @@ -858,7 +862,7 @@ export class BridgeController extends StaticIntervalPollingController(); - const traceWithoutImpact = async (request: TraceRequest): Promise => { + const traceWithoutImpact = ( + request: Omit, 'tags'>, + ) => { try { - await this.#trace(request, () => undefined); + 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. } @@ -884,7 +898,6 @@ export class BridgeController extends StaticIntervalPollingController = { + [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..cddeda11541 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 as SwapFeatureTag, +} 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..2c341d63e34 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,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 } 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 { @@ -43,6 +46,41 @@ 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 + */ +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): unknown; +}; + 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 11c7d708d11..2cb0b256595 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 ([#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 ([#10893](https://github.com/MetaMask/core/pull/10893)) - Update batch-strategy and batch-sell-strategy history persistence ([#10814](https://github.com/MetaMask/core/pull/10814)) - add a history item before submitting transactions. This enables the `TransactionController:transactionStatusUpdated` listener to match the txMeta with history and determine whether to publish the Failed event - include latest `TransactionStatus` when rekey-ing or adding to history @@ -17,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - rekey to the trade's `actionId` if the approval succeeds - rekey to the trade's `id` on confirmation - Bump `@metamask/profile-sync-controller` from `^34.0.3` to `^34.1.0` ([#10905](https://github.com/MetaMask/core/pull/10905)) +- 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)) +- Bump `@metamask/network-controller` from `^37.0.1` to `^37.1.0` ([#10864](https://github.com/MetaMask/core/pull/10864)) ### Fixed @@ -24,10 +34,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Pre-submit history: evm keyed by actionId, batch + batch-sell keyed by batchId - After tx exists: listener publishes Failed; submitTx returns the txMeta - Before tx exists: submitTx catch publishes Failed -- 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)) -- Bump `@metamask/network-controller` from `^37.0.1` to `^37.1.0` ([#10864](https://github.com/MetaMask/core/pull/10864)) ## [76.4.0] 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 79689053427..807ca54e44b 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 @@ -764,6 +764,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -775,6 +778,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1086,6 +1092,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1097,6 +1106,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should delay after submitti "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1721,6 +1733,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -1732,6 +1747,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -2043,6 +2061,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -2054,6 +2075,9 @@ exports[`BridgeStatusController submitTx: EVM bridge should not call handleMobil "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -3566,6 +3590,9 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi "stxEnabled": false, }, "name": "Bridge Transaction Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -3577,6 +3604,9 @@ exports[`BridgeStatusController submitTx: EVM bridge waits for approval tx confi "stxEnabled": false, }, "name": "Bridge Transaction Approval Completed", + "tags": { + "feature": "swap", + }, }, [Function], ], @@ -4753,7 +4783,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", @@ -4772,7 +4802,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", @@ -4898,7 +4928,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 5829da66e49..94097b04250 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 @@ -60,6 +60,7 @@ type RootMessenger = Messenger< >; const addTransactionBatchFn = jest.fn(); +let mockTraceFn: jest.Mock; function getRootMessenger(): RootMessenger { return new Messenger({ namespace: MOCK_ANY_NAMESPACE }); @@ -126,6 +127,8 @@ async function withController( clientId: BridgeClientId.EXTENSION, fetchFn: jest.fn(), addTransactionBatchFn, + traceFn: mockTraceFn, + clientProduct: 'metamask', ...options, }); const startPollingForBridgeTxStatusSpy = jest.fn(); @@ -193,6 +196,7 @@ const mockTransferTx: BatchSellTradesResponse['transactions'][number] = { describe('BridgeStatusController', () => { beforeEach(() => { jest.clearAllMocks(); + mockTraceFn = jest.fn(async (_request, fn) => await fn?.()); }); describe('submitBatchSell', () => { 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 a2ef530f3fd..6c62ae7535e 100644 --- a/packages/bridge-status-controller/src/bridge-status-controller.test.ts +++ b/packages/bridge-status-controller/src/bridge-status-controller.test.ts @@ -760,6 +760,8 @@ type WithControllerOptions = { mockMessengerCall?: jest.Mock; }; +let mockTraceFn: jest.Mock; + async function withController( ...args: | [WithControllerCallback] @@ -778,6 +780,7 @@ async function withController( clientProduct: 'test-client-product', fetchFn: jest.fn(), addTransactionBatchFn, + traceFn: mockTraceFn, ...options, }); const startPollingForBridgeTxStatusSpy = jest.fn(); @@ -814,6 +817,7 @@ const executePollingWithPendingStatus = async () => { fetchFn: jest.fn(), addTransactionBatchFn: jest.fn(), config: {}, + traceFn: mockTraceFn, }); const startPollingSpy = jest.spyOn(bridgeStatusController, 'startPolling'); @@ -1237,6 +1241,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', () => { @@ -1954,6 +1959,47 @@ describe('BridgeStatusController', () => { ); }, ); + + 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([ @@ -3746,9 +3792,6 @@ describe('BridgeStatusController', () => { const handleLineaDelaySpy = jest .spyOn(transactionUtils, 'handleApprovalDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); setupEventTrackingMocks(mockMessengerCall); setupApprovalMocks(mockMessengerCall); @@ -3793,9 +3836,6 @@ describe('BridgeStatusController', () => { const handleBaseDelaySpy = jest .spyOn(transactionUtils, 'handleApprovalDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); setupEventTrackingMocks(mockMessengerCall); setupApprovalMocks(mockMessengerCall); @@ -3846,9 +3886,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({ @@ -3907,9 +3944,6 @@ describe('BridgeStatusController', () => { const handleMobileHardwareWalletDelaySpy = jest .spyOn(transactionUtils, 'handleMobileHardwareWalletDelay') .mockResolvedValueOnce(); - const mockTraceFn = jest - .fn() - .mockImplementation((_p, callback) => callback()); setupEventTrackingMocks(mockMessengerCall); setupApprovalMocks(mockMessengerCall); @@ -3955,9 +3989,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({ @@ -4660,6 +4691,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(); @@ -5933,6 +5997,7 @@ describe('BridgeStatusController', () => { clientId: BridgeClientId.EXTENSION, clientProduct: 'test-client-product', fetchFn: mockFetchFn, + traceFn: mockTraceFn, addTransactionBatchFn: jest.fn(), state: { txHistory: { @@ -7403,6 +7468,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 f02980b9ec8..740c53d70e3 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, @@ -50,6 +50,7 @@ import { MAX_ATTEMPTS, REFRESH_INTERVAL_MS, } from './constants.js'; +import type { TraceName } from './constants.js'; import { QUOTE_STATUS_BACKFILL_WINDOW_MS, QUOTE_STATUS_UPDATE_ENTRY_TTL, @@ -183,7 +184,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; constructor({ messenger, @@ -208,7 +209,7 @@ export class BridgeStatusController extends StaticIntervalPollingController; onQuoteStatusManagerError?: ( error: QuoteStatusUpdateError | QuoteStatusGetError, ) => void; @@ -232,7 +233,7 @@ export class BridgeStatusController extends StaticIntervalPollingController fn?.()) as TraceCallback); + this.#trace = traceFn; this.#intentManager = new IntentManager({ messenger: this.messenger, customBridgeApiBaseUrl: this.#config.customBridgeApiBaseUrl, @@ -376,7 +377,7 @@ 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, diff --git a/tsc-suppressions.json b/tsc-suppressions.json index 557aaf033a6..e80d8ed34ec 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 @@ -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": {