Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add Batch Sell token and quote page analytics event types

### Changed

- Bump `@metamask/multichain-network-controller` from `^3.1.4` to `^3.2.0` ([#9264](https://github.com/MetaMask/core/pull/9264))
Expand Down
127 changes: 127 additions & 0 deletions packages/bridge-controller/src/bridge-controller.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type {
MessengerEvents,
MockAnyNamespace,
} from '@metamask/messenger';
import type { CaipAssetType } from '@metamask/utils';
import nock from 'nock';

import { flushPromises } from '../../../tests/helpers';
Expand Down Expand Up @@ -52,6 +53,8 @@ import {
import * as featureFlagUtils from './utils/feature-flags';
import * as fetchUtils from './utils/fetch';
import {
BatchSellMetricsEventName,
BatchSellMetricsLocation,
InputAmountPreset,
MetaMetricsSwapsEventSource,
MetricsActionType,
Expand Down Expand Up @@ -2911,6 +2914,15 @@ describe('BridgeController', function () {
expect(rootMessenger.call('BridgeController:getLocation')).toBe(
MetaMetricsSwapsEventSource.TokenView,
);

rootMessenger.call(
'BridgeController:setLocation',
BatchSellMetricsLocation.AssetPicker,
);

expect(rootMessenger.call('BridgeController:getLocation')).toBe(
BatchSellMetricsLocation.AssetPicker,
);
});
});
});
Expand Down Expand Up @@ -3032,6 +3044,121 @@ describe('BridgeController', function () {
});
});

it('should track Batch Sell token page events with default chain fallback', async () => {
await withController(async ({ rootMessenger }) => {
rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellTokenPageViewed,
{
location: BatchSellMetricsLocation.TradeMenu,
feature_id: FeatureId.BATCH_SELL,
},
);
rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellTokenPageSubmitted,
{
location: BatchSellMetricsLocation.AssetPicker,
feature_id: FeatureId.BATCH_SELL,
},
);

expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
1,
BatchSellMetricsEventName.BatchSellTokenPageViewed,
{
chain_id: formatChainIdToCaip(ChainId.ETH),
location: BatchSellMetricsLocation.TradeMenu,
feature_id: FeatureId.BATCH_SELL,
action_type: MetricsActionType.SWAPBRIDGE_V1,
},
);
expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
2,
BatchSellMetricsEventName.BatchSellTokenPageSubmitted,
{
chain_id: formatChainIdToCaip(ChainId.ETH),
location: BatchSellMetricsLocation.AssetPicker,
feature_id: FeatureId.BATCH_SELL,
action_type: MetricsActionType.SWAPBRIDGE_V1,
},
);
});
});

it('should track Batch Sell quote page events with selected token metadata', async () => {
await withController(async ({ rootMessenger }) => {
await rootMessenger.call(
'BridgeController:updateBridgeQuoteRequestParams',
{
walletAddress: '0x123',
srcChainId: ChainId.OPTIMISM,
},
{
stx_enabled: false,
security_warnings: [],
token_symbol_source: 'ETH',
token_symbol_destination: 'USDC',
usd_amount_source: 100,
token_security_type_destination: null,
feature_id: FeatureId.BATCH_SELL,
},
);
jest.clearAllMocks();

const selectedTokenAddressList = [
'eip155:10/erc20:0x1111111111111111111111111111111111111111',
'eip155:10/erc20:0x2222222222222222222222222222222222222222',
] satisfies CaipAssetType[];
const properties = {
selected_token_address_list: selectedTokenAddressList,
target_token_symbol: 'USDC',
location: BatchSellMetricsLocation.Deeplink,
slider_percentages: [25, 75],
slippage_percentages: [0.5, 1],
feature_id: FeatureId.BATCH_SELL,
};
const expectedProperties = {
chain_id: formatChainIdToCaip(ChainId.OPTIMISM),
selected_tokens_count: selectedTokenAddressList.length,
...properties,
action_type: MetricsActionType.SWAPBRIDGE_V1,
};

rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellQuotePageViewed,
properties,
);
rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellQuotesReviewed,
properties,
);
rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellQuotePageSubmitted,
properties,
);

expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
1,
BatchSellMetricsEventName.BatchSellQuotePageViewed,
expectedProperties,
);
expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
2,
BatchSellMetricsEventName.BatchSellQuotesReviewed,
expectedProperties,
);
expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
3,
BatchSellMetricsEventName.BatchSellQuotePageSubmitted,
expectedProperties,
);
});
});

it('should track the FiatCryptoToggleClicked event', async () => {
await withController(async ({ rootMessenger, controller }) => {
jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn());
Expand Down
49 changes: 37 additions & 12 deletions packages/bridge-controller/src/bridge-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
ExchangeRateSourcesForLookup,
selectIsAssetExchangeRateInState,
} from './selectors';
import { FeatureId, RequestStatus } from './types';
import { ChainId, FeatureId, RequestStatus } from './types';
import type {
L1GasFees,
GenericQuoteRequest,
Expand Down Expand Up @@ -63,10 +63,15 @@ import {
} from './utils/fetch';
import {
AbortReason,
BatchSellMetricsEventName,
MetaMetricsSwapsEventSource,
MetricsActionType,
UnifiedSwapBridgeEventName,
} from './utils/metrics/constants';
import type {
BridgeControllerMetricsEventName,
BridgeControllerMetricsLocation,
} from './utils/metrics/constants';
import {
formatProviderLabel,
getAccountHardwareType,
Expand Down Expand Up @@ -225,7 +230,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
* Set via setLocation() before navigating to the swap/bridge flow.
* Used as default for all subsequent internal events.
*/
#location: MetaMetricsSwapsEventSource = MetaMetricsSwapsEventSource.Unknown;
#location: BridgeControllerMetricsLocation = MetaMetricsSwapsEventSource.Unknown;

readonly #clientId: BridgeClientId;

Expand All @@ -236,8 +241,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
readonly #fetchFn: FetchFunction;

readonly #trackMetaMetricsFn: <
EventName extends
(typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName],
EventName extends BridgeControllerMetricsEventName,
>(
eventName: EventName,
properties: CrossChainSwapsEventProperties<EventName>,
Expand Down Expand Up @@ -279,8 +283,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
customBridgeApiBaseUrl?: string;
};
trackMetaMetricsFn: <
EventName extends
(typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName],
EventName extends BridgeControllerMetricsEventName,
>(
eventName: EventName,
properties: CrossChainSwapsEventProperties<EventName>,
Expand Down Expand Up @@ -716,7 +719,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
*
* @param location - The entry point from which the user initiated the flow
*/
setLocation = (location: MetaMetricsSwapsEventSource) => {
setLocation = (location: BridgeControllerMetricsLocation) => {
this.#location = location;
};

Expand All @@ -725,7 +728,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
*
* @returns The entry point from which the user initiated the flow
*/
getLocation = (): MetaMetricsSwapsEventSource => {
getLocation = (): BridgeControllerMetricsLocation => {
return this.#location;
};

Expand Down Expand Up @@ -1165,8 +1168,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
};

readonly #getEventProperties = <
EventName extends
(typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName],
EventName extends BridgeControllerMetricsEventName,
>(
eventName: EventName,
propertiesFromClient: Pick<
Expand All @@ -1184,6 +1186,11 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
const inputPrimaryDenominationProperties = {
input_primary_denomination: this.state.inputPrimaryDenomination,
};
const batchSellPageProperties = {
chain_id: formatChainIdToCaip(
this.state.quoteRequest[0]?.srcChainId ?? ChainId.ETH,
),
};
const quoteRequest = this.state.quoteRequest[quoteRequestIndex];
switch (eventName) {
case UnifiedSwapBridgeEventName.ButtonClicked:
Expand All @@ -1204,6 +1211,25 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
...inputPrimaryDenominationProperties,
...baseProperties,
};
case BatchSellMetricsEventName.BatchSellTokenPageViewed:
case BatchSellMetricsEventName.BatchSellTokenPageSubmitted:
return {
...batchSellPageProperties,
...baseProperties,
};
case BatchSellMetricsEventName.BatchSellQuotePageViewed:
case BatchSellMetricsEventName.BatchSellQuotesReviewed:
case BatchSellMetricsEventName.BatchSellQuotePageSubmitted: {
const selectedTokenAddressList =
(
propertiesFromClient as RequiredEventContextFromClient[BatchSellMetricsEventName.BatchSellQuotePageViewed]
).selected_token_address_list;
return {
...batchSellPageProperties,
selected_tokens_count: selectedTokenAddressList.length,
...baseProperties,
};
}
case UnifiedSwapBridgeEventName.FiatCryptoToggleClicked:
return {
...getRequestParams(
Expand Down Expand Up @@ -1349,8 +1375,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
* });
*/
trackUnifiedSwapBridgeEvent = <
EventName extends
(typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName],
EventName extends BridgeControllerMetricsEventName,
>(
eventName: EventName,
propertiesFromClient: Pick<
Expand Down
6 changes: 6 additions & 0 deletions packages/bridge-controller/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
export { BridgeController } from './bridge-controller';

export {
BatchSellMetricsEventName,
UnifiedSwapBridgeEventName,
BATCH_SELL_EVENT_CATEGORY,
UNIFIED_SWAP_BRIDGE_EVENT_CATEGORY,
BatchSellMetricsLocation,
InputAmountPreset,
MetaMetricsSwapsEventSource,
PollingStatus,
} from './utils/metrics/constants';

export type { BridgeControllerMetricsEventName } from './utils/metrics/constants';
export type { BridgeControllerMetricsLocation } from './utils/metrics/constants';

export type {
AccountHardwareType,
RequiredEventContextFromClient,
Expand Down
24 changes: 24 additions & 0 deletions packages/bridge-controller/src/utils/metrics/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable @typescript-eslint/naming-convention */
export const UNIFIED_SWAP_BRIDGE_EVENT_CATEGORY = 'Unified SwapBridge';
export const BATCH_SELL_EVENT_CATEGORY = 'Batch Sell';

/**
* These event names map to events defined in the segment-schema: https://github.com/Consensys/segment-schema/tree/main/libraries/events/metamask-cross-chain-swaps
Expand All @@ -26,6 +27,18 @@ export enum UnifiedSwapBridgeEventName {
PollingStatusUpdated = `${UNIFIED_SWAP_BRIDGE_EVENT_CATEGORY} Polling Status Updated`,
}

export enum BatchSellMetricsEventName {
BatchSellTokenPageViewed = `${BATCH_SELL_EVENT_CATEGORY} Token Page Viewed`,
BatchSellTokenPageSubmitted = `${BATCH_SELL_EVENT_CATEGORY} Token Page Submitted`,
BatchSellQuotePageViewed = `${BATCH_SELL_EVENT_CATEGORY} Quote Page Viewed`,
BatchSellQuotesReviewed = `${BATCH_SELL_EVENT_CATEGORY} Quotes Reviewed`,
BatchSellQuotePageSubmitted = `${BATCH_SELL_EVENT_CATEGORY} Quote Page Submitted`,
}

export type BridgeControllerMetricsEventName =
| UnifiedSwapBridgeEventName
| BatchSellMetricsEventName;

export enum PollingStatus {
MaxPollingReached = 'max_polling_reached',
InvalidTransactionHash = 'invalid_transaction_hash',
Expand Down Expand Up @@ -57,6 +70,17 @@ export enum MetaMetricsSwapsEventSource {
Unknown = 'Unknown',
}

export enum BatchSellMetricsLocation {
TradeMenu = 'trade_menu',
Deeplink = 'deeplink',
AssetPicker = 'asset_picker',
Unknown = 'Unknown',
}

export type BridgeControllerMetricsLocation =
| MetaMetricsSwapsEventSource
| BatchSellMetricsLocation;

export enum InputAmountPreset {
PERCENT_25 = '25%',
PERCENT_50 = '50%',
Expand Down
Loading
Loading