Skip to content
Merged
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
13 changes: 13 additions & 0 deletions packages/bridge-controller/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- Add Batch Sell analytics event types ([#9272](https://github.com/MetaMask/core/pull/9272))
- `Batch Sell Token Page Viewed`
- `Batch Sell Token Page Continue Clicked`
- `Batch Sell Quote Page Viewed`
- `Batch Sell Quote Page Review Clicked`
- `Batch Sell Review Modal Submitted`

### Changed

- Bump `@metamask/multichain-network-controller` from `^3.1.4` to `^3.2.0` ([#9264](https://github.com/MetaMask/core/pull/9264))

### Fixed

- Fix Batch Sell-only `sentAmount.usd` metadata calculations to use each quote's source token exchange rate ([#9272](https://github.com/MetaMask/core/pull/9272))

## [77.0.0]

### Added
Expand Down
158 changes: 158 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,152 @@ describe('BridgeController', function () {
});
});

it('should track Batch Sell token page events with client chain ids', async () => {
await withController(async ({ rootMessenger }) => {
const chainIdSource = formatChainIdToCaip(ChainId.POLYGON);
const chainIdDestination = formatChainIdToCaip(ChainId.BASE);
const sourceTokenAddresses = [
'eip155:137/erc20:0x1111111111111111111111111111111111111111',
'eip155:137/erc20:0x2222222222222222222222222222222222222222',
] satisfies CaipAssetType[];
const sourceTokenSymbols = ['LINK', 'UNI'];

rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellTokenPageViewed,
{
chain_id_source: chainIdSource,
chain_id_destination: chainIdDestination,
location: BatchSellMetricsLocation.TradeMenu,
},
);
rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellTokenPageContinueClicked,
{
chain_id_source: chainIdSource,
chain_id_destination: chainIdDestination,
location: BatchSellMetricsLocation.AssetPicker,
source_token_symbols: sourceTokenSymbols,
source_token_addresses: sourceTokenAddresses,
},
);

expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
1,
BatchSellMetricsEventName.BatchSellTokenPageViewed,
{
chain_id_source: chainIdSource,
chain_id_destination: chainIdDestination,
location: BatchSellMetricsLocation.TradeMenu,
},
);
expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
2,
BatchSellMetricsEventName.BatchSellTokenPageContinueClicked,
{
chain_id_source: chainIdSource,
chain_id_destination: chainIdDestination,
location: BatchSellMetricsLocation.AssetPicker,
source_token_count: sourceTokenAddresses.length,
source_token_symbols: sourceTokenSymbols,
source_token_addresses: sourceTokenAddresses,
},
);
});
});

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,
destChainId: 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 chainIdSource = formatChainIdToCaip(ChainId.POLYGON);
const chainIdDestination = formatChainIdToCaip(ChainId.BASE);
const sourceTokenAddresses = [
'eip155:137/erc20:0x1111111111111111111111111111111111111111',
'eip155:137/erc20:0x2222222222222222222222222222222222222222',
] satisfies CaipAssetType[];
const destinationTokenAddress =
'eip155:8453/erc20:0x3333333333333333333333333333333333333333' satisfies CaipAssetType;
const sharedProperties = {
location: BatchSellMetricsLocation.Deeplink,
source_token_symbols: ['WETH', 'OP'],
source_token_addresses: sourceTokenAddresses,
destination_token_symbol: 'USDC',
destination_token_address: destinationTokenAddress,
usd_amount_source_tokens: [10, 20],
usd_amount_source_total: 30,
source_token_slippages: [0.5, 1],
};
const properties = {
chain_id_source: chainIdSource,
chain_id_destination: chainIdDestination,
...sharedProperties,
};
const expectedProperties = {
source_token_count: sourceTokenAddresses.length,
...properties,
};

rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellQuotePageViewed,
properties,
);
rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellQuotePageReviewClicked,
properties,
);
rootMessenger.call(
'BridgeController:trackUnifiedSwapBridgeEvent',
BatchSellMetricsEventName.BatchSellReviewModalSubmitted,
{
...properties,
usd_quoted_gas: 1,
usd_quoted_return: 29,
},
);

expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
1,
BatchSellMetricsEventName.BatchSellQuotePageViewed,
expectedProperties,
);
expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
2,
BatchSellMetricsEventName.BatchSellQuotePageReviewClicked,
expectedProperties,
);
expect(trackMetaMetricsFn).toHaveBeenNthCalledWith(
3,
BatchSellMetricsEventName.BatchSellReviewModalSubmitted,
{
...expectedProperties,
usd_quoted_gas: 1,
usd_quoted_return: 29,
},
);
});
});

it('should track the FiatCryptoToggleClicked event', async () => {
await withController(async ({ rootMessenger, controller }) => {
jest.spyOn(console, 'warn').mockImplementationOnce(jest.fn());
Expand Down
92 changes: 77 additions & 15 deletions packages/bridge-controller/src/bridge-controller.ts
Original file line number Diff line number Diff line change
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,8 @@ 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 +242,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 @@ -278,10 +283,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
config?: {
customBridgeApiBaseUrl?: string;
};
trackMetaMetricsFn: <
EventName extends
(typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName],
>(
trackMetaMetricsFn: <EventName extends BridgeControllerMetricsEventName>(
eventName: EventName,
properties: CrossChainSwapsEventProperties<EventName>,
) => void;
Expand Down Expand Up @@ -716,7 +718,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 +727,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,16 +1167,15 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
};

readonly #getEventProperties = <
EventName extends
(typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName],
EventName extends BridgeControllerMetricsEventName,
>(
eventName: EventName,
propertiesFromClient: Pick<
RequiredEventContextFromClient,
EventName
>[EventName],
quoteRequestIndex: number = 0,
): CrossChainSwapsEventProperties<EventName> => {
) => {
const clientProps = propertiesFromClient as Record<string, unknown>;
const baseProperties = {
...propertiesFromClient,
Expand All @@ -1184,6 +1185,15 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
const inputPrimaryDenominationProperties = {
input_primary_denomination: this.state.inputPrimaryDenomination,
};
const batchSellClientChainProperties = propertiesFromClient as Pick<
RequiredEventContextFromClient[BatchSellMetricsEventName.BatchSellTokenPageViewed],
'chain_id_source' | 'chain_id_destination'
>;
const batchSellBaseProperties = {
chain_id_source: batchSellClientChainProperties.chain_id_source,
chain_id_destination: batchSellClientChainProperties.chain_id_destination,
location: clientProps?.location ?? this.#location,
};
const quoteRequest = this.state.quoteRequest[quoteRequestIndex];
switch (eventName) {
case UnifiedSwapBridgeEventName.ButtonClicked:
Expand All @@ -1204,6 +1214,56 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
...inputPrimaryDenominationProperties,
...baseProperties,
};
case BatchSellMetricsEventName.BatchSellTokenPageViewed:
return batchSellBaseProperties;
case BatchSellMetricsEventName.BatchSellTokenPageContinueClicked: {
const propsFromClient =
propertiesFromClient as RequiredEventContextFromClient[BatchSellMetricsEventName.BatchSellTokenPageContinueClicked];
return {
...batchSellBaseProperties,
source_token_count: propsFromClient.source_token_addresses.length,
source_token_symbols: propsFromClient.source_token_symbols,
source_token_addresses: propsFromClient.source_token_addresses,
};
}
case BatchSellMetricsEventName.BatchSellQuotePageViewed:
case BatchSellMetricsEventName.BatchSellQuotePageReviewClicked: {
const propsFromClient =
propertiesFromClient as RequiredEventContextFromClient[BatchSellMetricsEventName.BatchSellQuotePageViewed];
return {
...batchSellBaseProperties,
source_token_count: propsFromClient.source_token_addresses.length,
source_token_symbols: propsFromClient.source_token_symbols,
source_token_addresses: propsFromClient.source_token_addresses,
destination_token_symbol: propsFromClient.destination_token_symbol,
destination_token_address: propsFromClient.destination_token_address,
usd_amount_source_tokens: propsFromClient.usd_amount_source_tokens,
usd_amount_source_total: propsFromClient.usd_amount_source_total,
source_token_slippages: propsFromClient.source_token_slippages,
};
}
case BatchSellMetricsEventName.BatchSellReviewModalSubmitted: {
const reviewModalProperties =
propertiesFromClient as RequiredEventContextFromClient[BatchSellMetricsEventName.BatchSellReviewModalSubmitted];
return {
...batchSellBaseProperties,
source_token_count:
reviewModalProperties.source_token_addresses.length,
source_token_symbols: reviewModalProperties.source_token_symbols,
source_token_addresses: reviewModalProperties.source_token_addresses,
destination_token_symbol:
reviewModalProperties.destination_token_symbol,
destination_token_address:
reviewModalProperties.destination_token_address,
usd_amount_source_tokens:
reviewModalProperties.usd_amount_source_tokens,
usd_amount_source_total:
reviewModalProperties.usd_amount_source_total,
source_token_slippages: reviewModalProperties.source_token_slippages,
usd_quoted_gas: reviewModalProperties.usd_quoted_gas,
usd_quoted_return: reviewModalProperties.usd_quoted_return,
};
}
case UnifiedSwapBridgeEventName.FiatCryptoToggleClicked:
return {
...getRequestParams(
Expand Down Expand Up @@ -1349,8 +1409,7 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
* });
*/
trackUnifiedSwapBridgeEvent = <
EventName extends
(typeof UnifiedSwapBridgeEventName)[keyof typeof UnifiedSwapBridgeEventName],
EventName extends BridgeControllerMetricsEventName,
>(
eventName: EventName,
propertiesFromClient: Pick<
Expand All @@ -1366,7 +1425,10 @@ export class BridgeController extends StaticIntervalPollingController<BridgePoll
quoteRequestIndex,
);

this.#trackMetaMetricsFn(eventName, combinedPropertiesForEvent);
this.#trackMetaMetricsFn(
eventName,
combinedPropertiesForEvent as CrossChainSwapsEventProperties<EventName>,
);
} catch (error) {
console.error(
`Error tracking cross-chain swaps MetaMetrics event ${eventName}`,
Expand Down
Loading
Loading