diff --git a/src/constants/analytics.ts b/src/constants/analytics.ts index 9558c94..f68ee0b 100644 --- a/src/constants/analytics.ts +++ b/src/constants/analytics.ts @@ -85,6 +85,8 @@ export enum AnalyticsEvent { // Trading TradeOrderTypeSelected = 'TradeOrderTypeSelected', TradePlaceOrder = 'TradePlaceOrder', + TradePlaceOrderConfirmed = 'TradePlaceOrderConfirmed', + TradeCancelOrderConfirmed = 'TradeCancelOrderConfirmed', } export type AnalyticsEventData = @@ -94,6 +96,12 @@ export type AnalyticsEventData = : T extends AnalyticsEvent.NetworkStatus ? { status: typeof AbacusApiStatus['name']; + /** Last time indexer node was queried successfully */ + lastSuccessfulIndexerRpcQuery?: number; + /** Time elapsed since indexer node was queried successfully */ + elapsedTime?: number; + blockHeight?: number; + indexerBlockHeight?: number; } // Navigation @@ -151,6 +159,19 @@ export type AnalyticsEventData = SubaccountPlaceOrderPayload & { isClosePosition: boolean; } - + : T extends AnalyticsEvent.TradePlaceOrderConfirmed ? + { + /** roundtrip time between user placing an order and confirmation from indexer (client → validator → indexer → client) */ + roundtripMs: number; + /** URL/IP of node the order was sent to */ + validator: string; + } + : T extends AnalyticsEvent.TradeCancelOrderConfirmed ? + { + /** roundtrip time between user canceling an order and confirmation from indexer (client → validator → indexer → client) */ + roundtripMs: number; + /** URL/IP of node the order was sent to */ + validator: string; + } : - {}; + never; diff --git a/src/hooks/useAnalytics.ts b/src/hooks/useAnalytics.ts index cec2f20..6dca099 100644 --- a/src/hooks/useAnalytics.ts +++ b/src/hooks/useAnalytics.ts @@ -10,6 +10,7 @@ import { useApiState } from './useApiState'; import { useBreakpoints } from './useBreakpoints'; import { useSelectedNetwork } from './useSelectedNetwork'; import { useAccounts } from './useAccounts'; +import { useDydxClient } from './useDydxClient'; import { getSelectedLocale } from '@/state/localizationSelectors'; import { getOnboardingState, getSubaccountId } from '@/state/accountSelectors'; @@ -22,6 +23,7 @@ import { getInputTradeData } from '@/state/inputsSelectors'; export const useAnalytics = () => { const { walletType, walletConnectionType, evmAddress, dydxAddress, selectedWalletType } = useAccounts(); + const { compositeClient } = useDydxClient(); /** User properties */ @@ -95,10 +97,25 @@ export const useAnalytics = () => { }, []); // AnalyticsEvent.NetworkStatus - const { status } = useApiState(); + const { height, indexerHeight, status } = useApiState(); useEffect(() => { - if (status) track(AnalyticsEvent.NetworkStatus, { status: status.name }); + if (status) { + const websocketEndpoint = compositeClient?.indexerClient?.config.websocketEndpoint; + + const lastSuccessfulIndexerRpcQuery = + (websocketEndpoint && + lastSuccessfulWebsocketRequestByOrigin[new URL(websocketEndpoint).origin]) || + undefined; + + track(AnalyticsEvent.NetworkStatus, { + status: status.name, + lastSuccessfulIndexerRpcQuery, + elapsedTime: lastSuccessfulIndexerRpcQuery && Date.now() - lastSuccessfulIndexerRpcQuery, + blockHeight: height ?? undefined, + indexerBlockHeight: indexerHeight ?? undefined, + }); + } }, [status]); // AnalyticsEvent.NavigatePage @@ -191,3 +208,6 @@ export const useAnalytics = () => { } }, [selectedOrderType]); }; + +export const lastSuccessfulRestRequestByOrigin: Record = {}; +export const lastSuccessfulWebsocketRequestByOrigin: Record = {}; diff --git a/src/hooks/useSubaccount.tsx b/src/hooks/useSubaccount.tsx index 7e375e0..570878b 100644 --- a/src/hooks/useSubaccount.tsx +++ b/src/hooks/useSubaccount.tsx @@ -235,8 +235,10 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo execution: OrderExecution; postOnly: boolean; reduceOnly: boolean; - }) => - await compositeClient?.placeOrder( + }) => { + const startTimestamp = performance.now(); + + const result = await compositeClient?.placeOrder( subaccount, marketId, type, @@ -249,7 +251,17 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo execution, postOnly, reduceOnly - ), + ); + + const endTimestamp = performance.now(); + + track(AnalyticsEvent.TradePlaceOrderConfirmed, { + roundtripMs: endTimestamp - startTimestamp, + validator: compositeClient!.validatorClient.config.restEndpoint, + }); + + return result; + }, cancelOrderForSubaccount: async ({ subaccount, @@ -265,15 +277,27 @@ export const useSubaccountContext = ({ localDydxWallet }: { localDydxWallet?: Lo clobPairId: number; goodTilBlock?: number; goodTilBlockTime?: number; - }) => - await compositeClient?.cancelOrder( + }) => { + const startTimestamp = performance.now(); + + const result = await compositeClient?.cancelOrder( subaccount, clientId, orderFlags, clobPairId, goodTilBlock, goodTilBlockTime - ), + ) + + const endTimestamp = performance.now(); + + track(AnalyticsEvent.TradeCancelOrderConfirmed, { + roundtripMs: endTimestamp - startTimestamp, + validator: compositeClient!.validatorClient.config.restEndpoint, + }); + + return result; + }, }), [compositeClient] ); diff --git a/src/lib/abacus/rest.ts b/src/lib/abacus/rest.ts index 493d34e..3faa82f 100644 --- a/src/lib/abacus/rest.ts +++ b/src/lib/abacus/rest.ts @@ -2,6 +2,8 @@ import type { Nullable, kollections } from '@dydxprotocol/abacus'; import type { AbacusRestProtocol } from '@/constants/abacus'; +import { lastSuccessfulRestRequestByOrigin } from '@/hooks/useAnalytics'; + type Headers = Nullable>; type FetchResponseCallback = (p0: Nullable, p1: number) => void; @@ -46,16 +48,20 @@ class AbacusRest implements AbacusRestProtocol { }; fetch(url, options) - .then((response) => - response.text().then((data) => { - if (response.ok) { - callback(data, response.status); - } else { - // response not OK, call callback with null data and the status, this includes 400/500 status codes - callback(null, response.status); - } - }) - ) + .then(async (response) => { + const data = await response.text(); + + if (response.ok) { + callback(data, response.status); + } else { + // response not OK, call callback with null data and the status, this includes 400/500 status codes + callback(null, response.status); + } + + try { + lastSuccessfulRestRequestByOrigin[new URL(url).origin] = Date.now(); + } catch {} + }) .catch(() => callback(null, 0)); // Network error or request couldn't be made } diff --git a/src/lib/abacus/websocket.ts b/src/lib/abacus/websocket.ts index ec5cafe..450151a 100644 --- a/src/lib/abacus/websocket.ts +++ b/src/lib/abacus/websocket.ts @@ -11,6 +11,8 @@ import { import { subscriptionsByChannelId } from '@/lib/tradingView/dydxfeed/cache'; import { mapCandle } from '@/lib/tradingView/utils'; +import { lastSuccessfulWebsocketRequestByOrigin } from '@/hooks/useAnalytics'; + import { log } from '../telemetry'; const RECONNECT_INTERVAL_MS = 10_000; @@ -154,6 +156,8 @@ class AbacusWebsocket implements Omit