Skip to content

Commit

Permalink
chore: add telemetry to Earn (#1982)
Browse files Browse the repository at this point in the history
  • Loading branch information
dschlabach authored Feb 13, 2025
1 parent d889d1b commit f6b1235
Show file tree
Hide file tree
Showing 8 changed files with 461 additions and 4 deletions.
9 changes: 9 additions & 0 deletions src/core/analytics/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ export const ANALYTICS_EVENTS = {
WALLET_CONNECT_SUCCESS: 'walletConnectSuccess',
WALLET_DISCONNECT: 'walletDisconnect',
WALLET_OPTION_SELECTED: 'walletOptionSelected',

// Earn events
EARN_DEPOSIT_INITIATED: 'earnDepositInitiated',
EARN_DEPOSIT_SUCCESS: 'earnDepositSuccess',
EARN_DEPOSIT_FAILURE: 'earnDepositFailure',
EARN_WITHDRAW_INITIATED: 'earnWithdrawInitiated',
EARN_WITHDRAW_SUCCESS: 'earnWithdrawSuccess',
EARN_WITHDRAW_FAILURE: 'earnWithdrawFailure',
} as const;

/**
Expand All @@ -63,6 +71,7 @@ export const COMPONENT_NAMES = {
SWAP: 'swap',
TRANSACTION: 'transaction',
WALLET: 'wallet',
EARN: 'earn',
} as const;

/**
Expand Down
63 changes: 63 additions & 0 deletions src/core/analytics/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,18 @@ export enum FundEvent {
FundSuccess = 'fundSuccess',
}

/**
* Earn component events
*/
export enum EarnEvent {
EarnDepositInitiated = 'earnDepositInitiated',
EarnDepositSuccess = 'earnDepositSuccess',
EarnDepositFailure = 'earnDepositFailure',
EarnWithdrawInitiated = 'earnWithdrawInitiated',
EarnWithdrawSuccess = 'earnWithdrawSuccess',
EarnWithdrawFailure = 'earnWithdrawFailure',
}

/**
* Generic error events across components
* Used for error tracking and monitoring
Expand All @@ -125,6 +137,7 @@ export type AnalyticsEvent =
| MintEvent
| TransactionEvent
| FundEvent
| EarnEvent
| ErrorEvent;

/**
Expand Down Expand Up @@ -320,6 +333,48 @@ export type FundEventData = {
};
};

/**
* Earn component events data
*/
export type EarnEventData = {
[EarnEvent.EarnDepositInitiated]: CommonAnalyticsData & {
amount: number;
address: string;
tokenAddress: string;
vaultAddress: string;
};
[EarnEvent.EarnDepositSuccess]: CommonAnalyticsData & {
amount: number;
address: string;
tokenAddress: string;
vaultAddress: string;
};
[EarnEvent.EarnDepositFailure]: CommonAnalyticsData & {
amount: number;
address: string;
tokenAddress: string;
vaultAddress: string;
};
[EarnEvent.EarnWithdrawInitiated]: CommonAnalyticsData & {
amount: number;
address: string;
tokenAddress: string;
vaultAddress: string;
};
[EarnEvent.EarnWithdrawSuccess]: CommonAnalyticsData & {
amount: number;
address: string;
tokenAddress: string;
vaultAddress: string;
};
[EarnEvent.EarnWithdrawFailure]: CommonAnalyticsData & {
amount: number;
address: string;
tokenAddress: string;
vaultAddress: string;
};
};

// Update main AnalyticsEventData type to include all component events
export type AnalyticsEventData = {
// Wallet events
Expand Down Expand Up @@ -368,6 +423,14 @@ export type AnalyticsEventData = {
[FundEvent.FundOptionSelected]: FundEventData[FundEvent.FundOptionSelected];
[FundEvent.FundSuccess]: FundEventData[FundEvent.FundSuccess];

// Earn events
[EarnEvent.EarnDepositInitiated]: EarnEventData[EarnEvent.EarnDepositInitiated];
[EarnEvent.EarnDepositSuccess]: EarnEventData[EarnEvent.EarnDepositSuccess];
[EarnEvent.EarnDepositFailure]: EarnEventData[EarnEvent.EarnDepositFailure];
[EarnEvent.EarnWithdrawInitiated]: EarnEventData[EarnEvent.EarnWithdrawInitiated];
[EarnEvent.EarnWithdrawSuccess]: EarnEventData[EarnEvent.EarnWithdrawSuccess];
[EarnEvent.EarnWithdrawFailure]: EarnEventData[EarnEvent.EarnWithdrawFailure];

// Error events
[ErrorEvent.ComponentError]: CommonAnalyticsData & {
component: string;
Expand Down
7 changes: 5 additions & 2 deletions src/earn/components/DepositButton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useDepositAnalytics } from '@/earn/hooks/useDepositAnalytics';
import { cn } from '@/styles/theme';
import {
type LifecycleStatus,
Expand All @@ -21,11 +22,13 @@ export function DepositButton({ className }: DepositButtonReact) {
updateLifecycleStatus,
refetchWalletBalance,
} = useEarnContext();

const [depositedAmount, setDepositedAmount] = useState('');
const { setTransactionState } = useDepositAnalytics(depositedAmount);

const handleOnStatus = useCallback(
(status: LifecycleStatus) => {
setTransactionState(status.statusName);

if (status.statusName === 'transactionPending') {
updateLifecycleStatus({ statusName: 'transactionPending' });
}
Expand All @@ -38,7 +41,7 @@ export function DepositButton({ className }: DepositButtonReact) {
updateLifecycleStatus(status);
}
},
[updateLifecycleStatus],
[updateLifecycleStatus, setTransactionState],
);

const handleOnSuccess = useCallback(
Expand Down
6 changes: 4 additions & 2 deletions src/earn/components/WithdrawButton.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useWithdrawAnalytics } from '@/earn/hooks/useWithdrawAnalytics';
import { cn } from '@/styles/theme';
import {
type LifecycleStatus,
Expand All @@ -21,11 +22,12 @@ export function WithdrawButton({ className }: WithdrawButtonReact) {
withdrawAmountError,
vaultToken,
} = useEarnContext();

const [withdrawnAmount, setWithdrawnAmount] = useState('');
const { setTransactionState } = useWithdrawAnalytics(withdrawnAmount);

const handleOnStatus = useCallback(
(status: LifecycleStatus) => {
setTransactionState(status.statusName);
if (status.statusName === 'transactionPending') {
updateLifecycleStatus({ statusName: 'transactionPending' });
}
Expand All @@ -38,7 +40,7 @@ export function WithdrawButton({ className }: WithdrawButtonReact) {
updateLifecycleStatus(status);
}
},
[updateLifecycleStatus],
[updateLifecycleStatus, setTransactionState],
);

const handleOnSuccess = useCallback(
Expand Down
136 changes: 136 additions & 0 deletions src/earn/hooks/useDepositAnalytics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { EarnEvent } from '@/core/analytics/types';
import { useEarnContext } from '@/earn/components/EarnProvider';
import { useDepositAnalytics } from '@/earn/hooks/useDepositAnalytics';
import { act, renderHook } from '@testing-library/react';
import { type Mock, beforeEach, describe, expect, it, vi } from 'vitest';

const mockSendAnalytics = vi.fn();
vi.mock('../../core/analytics/hooks/useAnalytics', () => ({
useAnalytics: vi.fn(() => ({
sendAnalytics: mockSendAnalytics,
})),
}));

vi.mock('@/earn/components/EarnProvider', () => ({
useEarnContext: vi.fn(),
}));

describe('useDepositAnalytics', () => {
const mockContextData = {
vaultAddress: '0xvault',
vaultToken: {
symbol: 'TEST',
address: '0xtoken',
},
recipientAddress: '0xrecipient',
depositAmount: '100',
};

beforeEach(() => {
vi.clearAllMocks();
(useEarnContext as Mock).mockReturnValue(mockContextData);
});

it('should send initiated analytics when transaction starts building', () => {
const { result } = renderHook(() => useDepositAnalytics('50'));

act(() => {
result.current.setTransactionState('buildingTransaction');
});

expect(mockSendAnalytics).toHaveBeenCalledWith(
EarnEvent.EarnDepositInitiated,
{
amount: 100,
address: '0xrecipient',
tokenAddress: '0xtoken',
vaultAddress: '0xvault',
},
);
});

it('should send success analytics only once', () => {
const { result } = renderHook(() => useDepositAnalytics('50'));

act(() => {
result.current.setTransactionState('success');
result.current.setTransactionState('success');
});

expect(mockSendAnalytics).toHaveBeenCalledTimes(1);
expect(mockSendAnalytics).toHaveBeenCalledWith(
EarnEvent.EarnDepositSuccess,
{
amount: 100,
address: '0xrecipient',
tokenAddress: '0xtoken',
vaultAddress: '0xvault',
},
);
});

it('should send error analytics only once', () => {
const { result } = renderHook(() => useDepositAnalytics('50'));

act(() => {
result.current.setTransactionState('error');
result.current.setTransactionState('error');
});

expect(mockSendAnalytics).toHaveBeenCalledTimes(1);
expect(mockSendAnalytics).toHaveBeenCalledWith(
EarnEvent.EarnDepositFailure,
{
amount: 100,
address: '0xrecipient',
tokenAddress: '0xtoken',
vaultAddress: '0xvault',
},
);
});

it('should use depositedAmount when depositAmount is 0', () => {
(useEarnContext as Mock).mockReturnValue({
...mockContextData,
depositAmount: '0',
});

const { result } = renderHook(() => useDepositAnalytics('50'));

act(() => {
result.current.setTransactionState('buildingTransaction');
});

expect(mockSendAnalytics).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
amount: 50,
}),
);
});

it('should handle missing context values', () => {
(useEarnContext as Mock).mockReturnValue({
vaultAddress: '0xvault',
vaultToken: null,
recipientAddress: null,
depositAmount: '100',
});

const { result } = renderHook(() => useDepositAnalytics('50'));

act(() => {
result.current.setTransactionState('buildingTransaction');
});

expect(mockSendAnalytics).toHaveBeenCalledWith(
EarnEvent.EarnDepositInitiated,
{
amount: 100,
address: '',
tokenAddress: '',
vaultAddress: '0xvault',
},
);
});
});
54 changes: 54 additions & 0 deletions src/earn/hooks/useDepositAnalytics.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { useAnalytics } from '@/core/analytics/hooks/useAnalytics';
import { EarnEvent } from '@/core/analytics/types';
import { useEarnContext } from '@/earn/components/EarnProvider';
import type { LifecycleStatus } from '@/transaction/types';
import { useEffect, useMemo, useRef, useState } from 'react';

export const useDepositAnalytics = (depositedAmount: string) => {
const [transactionState, setTransactionState] = useState<
LifecycleStatus['statusName'] | null
>(null);
// Undesirable, but required because Transaction emits multiple success and error events
const successSent = useRef(false);
const errorSent = useRef(false);
const { sendAnalytics } = useAnalytics();
const { vaultAddress, vaultToken, recipientAddress, depositAmount } =
useEarnContext();

const analyticsData = useMemo(
() => ({
amount: Number(depositAmount) || Number(depositedAmount), // fall back to depositedAmount to avoid sending 0
address: recipientAddress ?? '',
tokenAddress: vaultToken?.address ?? '',
vaultAddress,
}),
[
depositedAmount,
depositAmount,
recipientAddress,
vaultToken?.address,
vaultAddress,
],
);

useEffect(() => {
if (transactionState === 'buildingTransaction') {
successSent.current = false; // in case user does a second deposit
sendAnalytics(EarnEvent.EarnDepositInitiated, analyticsData);
}

if (transactionState === 'success' && !successSent.current) {
successSent.current = true;
sendAnalytics(EarnEvent.EarnDepositSuccess, analyticsData);
}

if (transactionState === 'error' && !errorSent.current) {
errorSent.current = true;
sendAnalytics(EarnEvent.EarnDepositFailure, analyticsData);
}
}, [transactionState, analyticsData, sendAnalytics]);

return {
setTransactionState,
};
};
Loading

0 comments on commit f6b1235

Please sign in to comment.