From e169aa10070a3ffcdaa24eb62b3ecbea079912b5 Mon Sep 17 00:00:00 2001 From: woody <125113430+woodenfurniture@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:51:58 +1100 Subject: [PATCH] feat: add mixpanel event tracking for limit orders (#8343) --- ...ncelLimtOrder.tsx => CancelLimitOrder.tsx} | 61 ++++++++++++++---- .../components/LimitOrderConfirm.tsx | 26 +++++++- .../LimitOrder/components/LimitOrderList.tsx | 2 +- .../components/LimitOrder/helpers.ts | 64 ++++++++++++++++++- src/lib/mixpanel/types.ts | 2 + src/state/apis/limit-orders/limitOrderApi.ts | 1 + 6 files changed, 140 insertions(+), 16 deletions(-) rename src/components/MultiHopTrade/components/LimitOrder/components/{CancelLimtOrder.tsx => CancelLimitOrder.tsx} (82%) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/CancelLimtOrder.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/CancelLimitOrder.tsx similarity index 82% rename from src/components/MultiHopTrade/components/LimitOrder/components/CancelLimtOrder.tsx rename to src/components/MultiHopTrade/components/LimitOrder/components/CancelLimitOrder.tsx index db947b49a7a..8896b6eb838 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/CancelLimtOrder.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/CancelLimitOrder.tsx @@ -27,11 +27,14 @@ import { RawText, Text } from 'components/Text' import { TransactionTypeIcon } from 'components/TransactionHistory/TransactionTypeIcon' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' +import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' +import { MixPanelEvent } from 'lib/mixpanel/types' import { useCancelLimitOrderMutation } from 'state/apis/limit-orders/limitOrderApi' import { selectAssetById, selectFeeAssetById } from 'state/slices/selectors' import { useSelectorWithArgs } from 'state/store' import { SwapperIcon } from '../../TradeInput/components/SwapperIcon/SwapperIcon' +import { getMixpanelLimitOrderEventData } from '../helpers' import type { OrderToCancel } from '../types' const cardBorderRadius = { base: '2xl' } @@ -45,8 +48,13 @@ export const CancelLimitOrder = ({ orderToCancel, resetOrderToCancel }: CancelLi const wallet = useWallet().state.wallet const { showErrorToast } = useErrorHandler() const queryClient = useQueryClient() + const mixpanel = getMixPanel() - const [cancelLimitOrders, { error, isLoading, reset }] = useCancelLimitOrderMutation() + const [cancelLimitOrder, { error, isLoading, reset }] = useCancelLimitOrderMutation() + + const sellAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.sellAssetId ?? '') + const buyAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.buyAssetId ?? '') + const feeAsset = useSelectorWithArgs(selectFeeAssetById, orderToCancel?.sellAssetId ?? '') useEffect(() => { if (!error) return @@ -54,6 +62,16 @@ export const CancelLimitOrder = ({ orderToCancel, resetOrderToCancel }: CancelLi showErrorToast(error, 'limitOrder.cancel.cancellationFailed') }, [error, showErrorToast]) + const buyAmountCryptoPrecision = useMemo(() => { + if (!orderToCancel || !buyAsset) return '0' + return fromBaseUnit(orderToCancel.order.buyAmount, buyAsset.precision) + }, [buyAsset, orderToCancel]) + + const sellAmountCryptoPrecision = useMemo(() => { + if (!orderToCancel || !sellAsset) return '0' + return fromBaseUnit(orderToCancel.order.sellAmount, sellAsset.precision) + }, [orderToCancel, sellAsset]) + const handleClose = useCallback(() => { reset() resetOrderToCancel() @@ -64,7 +82,10 @@ export const CancelLimitOrder = ({ orderToCancel, resetOrderToCancel }: CancelLi return } - await cancelLimitOrders({ wallet, ...orderToCancel }) + const result = await cancelLimitOrder({ wallet, ...orderToCancel }) + + // Exit if the request failed. + if ((result as { error: unknown }).error) return // refetch the orders list for this account queryClient.invalidateQueries({ @@ -72,21 +93,35 @@ export const CancelLimitOrder = ({ orderToCancel, resetOrderToCancel }: CancelLi }) resetOrderToCancel() - }, [orderToCancel, wallet, cancelLimitOrders, queryClient, resetOrderToCancel]) - const sellAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.sellAssetId ?? '') - const buyAsset = useSelectorWithArgs(selectAssetById, orderToCancel?.buyAssetId ?? '') - const feeAsset = useSelectorWithArgs(selectFeeAssetById, orderToCancel?.sellAssetId ?? '') + // Track event in mixpanel + const eventData = getMixpanelLimitOrderEventData({ + sellAsset, + buyAsset, + sellAmountCryptoPrecision, + buyAmountCryptoPrecision, + }) + if (mixpanel && eventData) { + mixpanel.track(MixPanelEvent.LimitOrderCanceled, eventData) + } + }, [ + orderToCancel, + wallet, + cancelLimitOrder, + queryClient, + resetOrderToCancel, + sellAsset, + buyAsset, + sellAmountCryptoPrecision, + buyAmountCryptoPrecision, + mixpanel, + ]) const limitPrice = useMemo(() => { - if (!orderToCancel || !sellAsset || !buyAsset) return - const buyAmountCryptoPrecision = fromBaseUnit(orderToCancel.order.buyAmount, buyAsset.precision) - const sellAmountCryptoPrecision = fromBaseUnit( - orderToCancel.order.sellAmount, - sellAsset.precision, - ) + if (bnOrZero(sellAmountCryptoPrecision).isZero() || bnOrZero(buyAmountCryptoPrecision).isZero()) + return return bn(buyAmountCryptoPrecision).div(sellAmountCryptoPrecision).toFixed() - }, [buyAsset, orderToCancel, sellAsset]) + }, [buyAmountCryptoPrecision, sellAmountCryptoPrecision]) const expiryText = useMemo(() => { if (!orderToCancel) return diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfirm.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfirm.tsx index cbed828ff4f..a003284fbb1 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfirm.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderConfirm.tsx @@ -24,6 +24,8 @@ import { TransactionDate } from 'components/TransactionHistoryRows/TransactionDa import { useActions } from 'hooks/useActions' import { useErrorHandler } from 'hooks/useErrorToast/useErrorToast' import { useWallet } from 'hooks/useWallet/useWallet' +import { getMixPanel } from 'lib/mixpanel/mixPanelSingleton' +import { MixPanelEvent } from 'lib/mixpanel/types' import { usePlaceLimitOrderMutation } from 'state/apis/limit-orders/limitOrderApi' import { limitOrderSlice } from 'state/slices/limitOrderSlice/limitOrderSlice' import { @@ -43,6 +45,7 @@ import { useAppSelector } from 'state/store' import { SwapperIcon } from '../../TradeInput/components/SwapperIcon/SwapperIcon' import { WithBackButton } from '../../WithBackButton' +import { getMixpanelLimitOrderEventData } from '../helpers' import { LimitOrderRoutePaths } from '../types' const cardBorderRadius = { base: '2xl' } @@ -54,6 +57,7 @@ export const LimitOrderConfirm = () => { const { confirmSubmit, setLimitOrderInitialized } = useActions(limitOrderSlice.actions) const { showErrorToast } = useErrorHandler() const queryClient = useQueryClient() + const mixpanel = getMixPanel() const activeQuote = useAppSelector(selectActiveQuote) const sellAsset = useAppSelector(selectActiveQuoteSellAsset) @@ -99,18 +103,38 @@ export const LimitOrderConfirm = () => { // TEMP: Bypass allowance approvals and jump straight to placing the order setLimitOrderInitialized(quoteId) confirmSubmit(quoteId) - await placeLimitOrder({ quoteId, wallet }) + const result = await placeLimitOrder({ quoteId, wallet }) + + // Exit if the request failed. + if ((result as { error: unknown }).error) return + // refetch the orders list for this account queryClient.invalidateQueries({ queryKey: ['getLimitOrdersForAccount', accountId], refetchType: 'all', }) + + // Track event in mixpanel + const eventData = getMixpanelLimitOrderEventData({ + sellAsset, + buyAsset, + sellAmountCryptoPrecision, + buyAmountCryptoPrecision, + }) + if (mixpanel && eventData) { + mixpanel.track(MixPanelEvent.LimitOrderPlaced, eventData) + } }, [ activeQuote?.params.accountId, activeQuote?.response.id, + buyAmountCryptoPrecision, + buyAsset, confirmSubmit, + mixpanel, placeLimitOrder, queryClient, + sellAmountCryptoPrecision, + sellAsset, setLimitOrderInitialized, wallet, ]) diff --git a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx index c8e746d9d7a..6ce0e572c67 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx +++ b/src/components/MultiHopTrade/components/LimitOrder/components/LimitOrderList.tsx @@ -26,7 +26,7 @@ import { Text } from 'components/Text' import { WithBackButton } from '../../WithBackButton' import { useGetLimitOrdersQuery } from '../hooks/useGetLimitOrdersForAccountQuery' import type { OrderToCancel } from '../types' -import { CancelLimitOrder } from './CancelLimtOrder' +import { CancelLimitOrder } from './CancelLimitOrder' import { LimitOrderCard } from './LimitOrderCard' const textSelectedProps = { diff --git a/src/components/MultiHopTrade/components/LimitOrder/helpers.ts b/src/components/MultiHopTrade/components/LimitOrder/helpers.ts index 92a9269133e..d4eb1c3d5eb 100644 --- a/src/components/MultiHopTrade/components/LimitOrder/helpers.ts +++ b/src/components/MultiHopTrade/components/LimitOrder/helpers.ts @@ -1,8 +1,19 @@ import type { SerializedError } from '@reduxjs/toolkit' -import type { CowSwapError } from '@shapeshiftoss/types' +import type { Asset, CowSwapError } from '@shapeshiftoss/types' import { OrderError } from '@shapeshiftoss/types' +import { bn } from '@shapeshiftoss/utils' import type { InterpolationOptions } from 'node-polyglot' +import { getMaybeCompositeAssetSymbol } from 'lib/mixpanel/helpers' import { assertUnreachable } from 'lib/utils' +import { selectCalculatedFees } from 'state/apis/snapshot/selectors' +import type { ReduxState } from 'state/reducer' +import { + selectAssets, + selectFeeAssetById, + selectMarketDataUsd, + selectUserCurrencyToUsdRate, +} from 'state/slices/selectors' +import { store } from 'state/store' export const isCowSwapError = ( maybeCowSwapError: CowSwapError | SerializedError | undefined, @@ -66,3 +77,54 @@ export const getCowSwapErrorTranslation = ( assertUnreachable(errorType) } } + +export const getMixpanelLimitOrderEventData = ({ + sellAsset, + buyAsset, + sellAmountCryptoPrecision, + buyAmountCryptoPrecision, +}: { + sellAsset: Asset | undefined + buyAsset: Asset | undefined + sellAmountCryptoPrecision: string + buyAmountCryptoPrecision: string +}) => { + // mixpanel paranoia seeing impossibly high values + if (!sellAsset?.precision) return + if (!buyAsset?.precision) return + + const state = store.getState() as ReduxState + + const buyAssetFeeAsset = selectFeeAssetById(state, buyAsset.assetId) + const sellAssetFeeAsset = selectFeeAssetById(state, sellAsset.assetId) + const userCurrencyToUsdRate = selectUserCurrencyToUsdRate(state) + const marketDataUsd = selectMarketDataUsd(state) + const assets = selectAssets(state) + + const sellAmountBeforeFeesUsd = bn(sellAmountCryptoPrecision) + .times(marketDataUsd[sellAsset.assetId]?.price ?? 0) + .toString() + const sellAmountBeforeFeesUserCurrency = bn(sellAmountBeforeFeesUsd) + .times(userCurrencyToUsdRate) + .toString() + + const feeParams = { feeModel: 'SWAPPER' as const, inputAmountUsd: sellAmountBeforeFeesUsd } + const { feeUsd: shapeshiftFeeUsd } = selectCalculatedFees(state, feeParams) + const shapeShiftFeeUserCurrency = shapeshiftFeeUsd.times(userCurrencyToUsdRate).toString() + + const compositeBuyAsset = getMaybeCompositeAssetSymbol(buyAsset.assetId, assets) + const compositeSellAsset = getMaybeCompositeAssetSymbol(sellAsset.assetId, assets) + + return { + buyAsset: compositeBuyAsset, + sellAsset: compositeSellAsset, + buyAssetChain: buyAssetFeeAsset?.networkName, + sellAssetChain: sellAssetFeeAsset?.networkName, + amountUsd: sellAmountBeforeFeesUsd, + amountUserCurrency: sellAmountBeforeFeesUserCurrency, + shapeShiftFeeUserCurrency, + shapeshiftFeeUsd: shapeshiftFeeUsd.toString(), + [compositeBuyAsset]: buyAmountCryptoPrecision, + [compositeSellAsset]: sellAmountCryptoPrecision, + } +} diff --git a/src/lib/mixpanel/types.ts b/src/lib/mixpanel/types.ts index a0e5025b730..fe05fd28170 100644 --- a/src/lib/mixpanel/types.ts +++ b/src/lib/mixpanel/types.ts @@ -66,6 +66,8 @@ export enum MixPanelEvent { LpIncompleteWithdrawConfirm = 'LP Incomplete Withdraw Confirm', CustomAssetAdded = 'Custom Asset Added', ToggleWatchAsset = 'Toggle Watch Asset', + LimitOrderPlaced = 'Limit Order Placed', + LimitOrderCanceled = 'Limit Order Canceled', } export type TrackOpportunityProps = { diff --git a/src/state/apis/limit-orders/limitOrderApi.ts b/src/state/apis/limit-orders/limitOrderApi.ts index 343abfa63a0..050ba82931f 100644 --- a/src/state/apis/limit-orders/limitOrderApi.ts +++ b/src/state/apis/limit-orders/limitOrderApi.ts @@ -162,6 +162,7 @@ export const limitOrderApi = createApi({ `${baseUrl}/${network}/api/v1/orders/`, limitOrder, ) + const orderId = result.data return { data: orderId } } catch (e) {