From 14278a4e6c75a57962cc42d497345e81c6f382dc Mon Sep 17 00:00:00 2001 From: tyleroooo Date: Thu, 3 Oct 2024 14:33:13 -0400 Subject: [PATCH] feat: vault monitoring (#1106) --- src/constants/analytics.ts | 19 ++++++ src/hooks/vaultsHooks.ts | 10 +++ .../portfolio/AccountOverviewSection.tsx | 3 + src/pages/portfolio/Positions.tsx | 3 + src/pages/vaults/VaultDepositWithdrawForm.tsx | 61 +++++++++++++++++-- src/pages/vaults/VaultPage.tsx | 1 + 6 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/constants/analytics.ts b/src/constants/analytics.ts index 9510070aa..51688374a 100644 --- a/src/constants/analytics.ts +++ b/src/constants/analytics.ts @@ -328,6 +328,25 @@ export const AnalyticsEvents = unionize( assetSymbol?: string; assetName?: string; }>(), + + // vaults + ClickViewVaultFromPositionCard: ofType<{}>(), + ClickViewVaultFromOverview: ofType<{}>(), + + EnterValidVaultAmountForm: ofType<{}>(), + VaultFormPreviewStep: ofType<{ operation: 'DEPOSIT' | 'WITHDRAW'; amount: number }>(), + AttemptVaultOperation: ofType<{ + operation: 'DEPOSIT' | 'WITHDRAW'; + amount: number; + slippage: number | null | undefined; + }>(), + VaultOperationPreAborted: ofType<{ operation: 'DEPOSIT' | 'WITHDRAW'; amount: number }>(), + SuccessfulVaultOperation: ofType<{ + operation: 'DEPOSIT' | 'WITHDRAW'; + amount: number; + amountDiff: number | null | undefined; + }>(), + VaultOperationProtocolError: ofType<{ operation: 'DEPOSIT' | 'WITHDRAW' }>(), }, { tag: 'type' as const, value: 'payload' as const } ); diff --git a/src/hooks/vaultsHooks.ts b/src/hooks/vaultsHooks.ts index d979463ac..f630ef646 100644 --- a/src/hooks/vaultsHooks.ts +++ b/src/hooks/vaultsHooks.ts @@ -15,6 +15,7 @@ import { VaultFormAction, VaultFormData, } from '@/constants/abacus'; +import { AnalyticsEvents } from '@/constants/analytics'; import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; import { timeUnits } from '@/constants/time'; @@ -22,6 +23,7 @@ import { selectSubaccountStateForVaults } from '@/state/accountCalculators'; import { getVaultForm, selectVaultFormStateExceptAmount } from '@/state/vaultSelectors'; import abacusStateManager from '@/lib/abacus'; +import { track } from '@/lib/analytics/analytics'; import { assertNever } from '@/lib/assertNever'; import { MustBigNumber } from '@/lib/numbers'; import { safeStringifyForAbacusParsing } from '@/lib/stringifyHelpers'; @@ -230,12 +232,20 @@ const VAULT_FORM_AMOUNT_DEBOUNCE_MS = 500; const useVaultFormAmountDebounced = () => { const amount = useAppSelector((state) => state.vaults.vaultForm.amount); const debouncedAmount = useDebounce(amount, VAULT_FORM_AMOUNT_DEBOUNCE_MS); + + useEffect(() => { + if (MustBigNumber(debouncedAmount).gt(0)) { + track(AnalyticsEvents.EnterValidVaultAmountForm()); + } + }, [debouncedAmount]); + // if the user goes back to the beginning, use that value for calculations // this fixes an issue where the validation logic would show an error for a second after submission // when we reset the value if (amount === '') { return amount; } + return debouncedAmount; }; diff --git a/src/pages/portfolio/AccountOverviewSection.tsx b/src/pages/portfolio/AccountOverviewSection.tsx index 03c1bd7ed..e2d515fde 100644 --- a/src/pages/portfolio/AccountOverviewSection.tsx +++ b/src/pages/portfolio/AccountOverviewSection.tsx @@ -4,6 +4,7 @@ import { shallowEqual } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import { AnalyticsEvents } from '@/constants/analytics'; import { ButtonAction } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { AppRoute } from '@/constants/routes'; @@ -21,6 +22,7 @@ import { WithLabel } from '@/components/WithLabel'; import { getSubaccount } from '@/state/accountSelectors'; import { useAppSelector } from '@/state/appTypes'; +import { track } from '@/lib/analytics/analytics'; import { runIf } from '@/lib/do'; import { isTruthy } from '@/lib/isTruthy'; import { testFlags } from '@/lib/testFlags'; @@ -54,6 +56,7 @@ export const AccountOverviewSection = () => { const totalValue = runIf(equity?.current, (e) => e + (vaultBalance ?? 0)); const handleViewVault = useCallback(() => { + track(AnalyticsEvents.ClickViewVaultFromOverview()); navigate(`${AppRoute.Vault}`, { state: { from: AppRoute.Portfolio }, }); diff --git a/src/pages/portfolio/Positions.tsx b/src/pages/portfolio/Positions.tsx index e8c37b4ff..475e59586 100644 --- a/src/pages/portfolio/Positions.tsx +++ b/src/pages/portfolio/Positions.tsx @@ -3,6 +3,7 @@ import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import styled from 'styled-components'; +import { AnalyticsEvents } from '@/constants/analytics'; import { STRING_KEYS } from '@/constants/localization'; import { AppRoute, PortfolioRoute } from '@/constants/routes'; @@ -19,6 +20,7 @@ import { PositionsTable, PositionsTableColumnKey } from '@/views/tables/Position import { calculateShouldRenderActionsInPositionsTable } from '@/state/accountCalculators'; +import { track } from '@/lib/analytics/analytics'; import { isTruthy } from '@/lib/isTruthy'; import { MaybeUnopenedIsolatedPositionsPanel } from '../trade/UnopenedIsolatedPositions'; @@ -41,6 +43,7 @@ export const Positions = () => { }, [navigate]); const handleViewVault = useCallback(() => { + track(AnalyticsEvents.ClickViewVaultFromPositionCard()); navigate(`${AppRoute.Vault}`, { state: { from: AppRoute.Portfolio }, }); diff --git a/src/pages/vaults/VaultDepositWithdrawForm.tsx b/src/pages/vaults/VaultDepositWithdrawForm.tsx index 848992006..412ed5afd 100644 --- a/src/pages/vaults/VaultDepositWithdrawForm.tsx +++ b/src/pages/vaults/VaultDepositWithdrawForm.tsx @@ -6,6 +6,7 @@ import styled, { css } from 'styled-components'; import tw from 'twin.macro'; import { AlertType } from '@/constants/alerts'; +import { AnalyticsEvents } from '@/constants/analytics'; import { ButtonAction, ButtonShape, ButtonSize, ButtonType } from '@/constants/buttons'; import { STRING_KEYS } from '@/constants/localization'; import { QUANTUM_MULTIPLIER } from '@/constants/numbers'; @@ -52,6 +53,7 @@ import { setVaultFormSlippageAck, } from '@/state/vaults'; +import { track } from '@/lib/analytics/analytics'; import { dd } from '@/lib/analytics/datadog'; import { assertNever } from '@/lib/assertNever'; import { runFn } from '@/lib/do'; @@ -169,6 +171,13 @@ export const VaultDepositWithdrawForm = ({ if (isSubmitting) { return; } + track( + AnalyticsEvents.AttemptVaultOperation({ + amount: MustBigNumber(amount).toNumber(), + operation, + slippage: validationResponse.summaryData.estimatedSlippage, + }) + ); setIsSubmitting(true); try { const { submissionData } = validationResponse; @@ -180,6 +189,12 @@ export const VaultDepositWithdrawForm = ({ title: stringGetter({ key: STRING_KEYS.MEGAVAULT_CANT_SUBMIT }), body: stringGetter({ key: STRING_KEYS.MEGAVAULT_CANT_SUBMIT_BODY }), }); + track( + AnalyticsEvents.VaultOperationPreAborted({ + amount: MustBigNumber(amount).toNumber(), + operation, + }) + ); dd.error('Megavault deposit blocked, invalid validation response amount', { deposit: submissionData?.deposit, }); @@ -192,6 +207,13 @@ export const VaultDepositWithdrawForm = ({ await depositToMegavault(cachedAmount); + track( + AnalyticsEvents.SuccessfulVaultOperation({ + amount: MustBigNumber(amount).toNumber(), + operation, + amountDiff: undefined, + }) + ); notify({ title: stringGetter({ key: STRING_KEYS.MEGAVAULT_DEPOSIT_SUCCESSFUL }), slotTitleLeft: <$SmallIcon iconName={IconName.CheckCircle} />, @@ -218,6 +240,12 @@ export const VaultDepositWithdrawForm = ({ title: stringGetter({ key: STRING_KEYS.MEGAVAULT_CANT_SUBMIT }), body: stringGetter({ key: STRING_KEYS.MEGAVAULT_CANT_SUBMIT_BODY }), }); + track( + AnalyticsEvents.VaultOperationPreAborted({ + amount: MustBigNumber(amount).toNumber(), + operation, + }) + ); dd.error('Megavault withdraw blocked, invalid validation response values', { withdraw: submissionData?.withdraw, }); @@ -228,16 +256,25 @@ export const VaultDepositWithdrawForm = ({ return; } + const preEstimate = validationResponse.summaryData.estimatedAmountReceived; const result = await withdrawFromMegavault( submissionData?.withdraw?.shares, submissionData?.withdraw?.minAmount ); - const events = (result as IndexedTx).events; + const events = (result as IndexedTx)?.events; const actualAmount = events - .find((e) => e.type === 'withdraw_from_megavault') + ?.find((e) => e.type === 'withdraw_from_megavault') ?.attributes.find((a) => a.key === 'redeemed_quote_quantums')?.value; - + const realAmountReceived = MustBigNumber(actualAmount).div(QUANTUM_MULTIPLIER).toNumber(); + + track( + AnalyticsEvents.SuccessfulVaultOperation({ + amount: realAmountReceived, + operation, + amountDiff: Math.abs((preEstimate ?? 0) - (realAmountReceived ?? 0)), + }) + ); notify({ slotTitleLeft: <$SmallIcon iconName={IconName.CheckCircle} />, title: stringGetter({ key: STRING_KEYS.MEGAVAULT_WITHDRAWAL_SUCCESSFUL }), @@ -251,7 +288,7 @@ export const VaultDepositWithdrawForm = ({ ), }, @@ -281,7 +318,11 @@ export const VaultDepositWithdrawForm = ({ : STRING_KEYS.MEGAVAULT_WITHDRAWAL_FAILED_BODY, }), }); - + track( + AnalyticsEvents.VaultOperationProtocolError({ + operation, + }) + ); dd.error('Megavault transaction failed', { ...validationResponse.submissionData }, e); // eslint-disable-next-line no-console console.error('Error submitting megavault transaction', e); @@ -573,7 +614,15 @@ export const VaultDepositWithdrawForm = ({