-
Notifications
You must be signed in to change notification settings - Fork 274
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Add Telemetry foundation for Buy, Checkout, Fund, Mint, Swap, T…
…ransaction, and Wallet. (#1942)
- Loading branch information
Showing
20 changed files
with
1,148 additions
and
454 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@coinbase/onchainkit": minor | ||
--- | ||
|
||
- **feat**: Added Telemetry foundation for `Buy`, `Checkout`, `Fund`, `Mint`, `Swap`, `Transaction`, and `Wallet`. By @cpcramer #1942 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
export const ANALYTICS_API_URL = 'https://api.developer.coinbase.com/analytics'; | ||
|
||
/** | ||
* Analytics event names | ||
*/ | ||
export const ANALYTICS_EVENTS = { | ||
// Buy events | ||
BUY_FAILURE: 'buyFailure', | ||
BUY_INITIATED: 'buyInitiated', | ||
BUY_OPTION_SELECTED: 'buyOptionSelected', | ||
BUY_SUCCESS: 'buySuccess', | ||
|
||
// Checkout events | ||
CHECKOUT_FAILURE: 'checkoutFailure', | ||
CHECKOUT_INITIATED: 'checkoutInitiated', | ||
CHECKOUT_SUCCESS: 'checkoutSuccess', | ||
|
||
// Error events | ||
COMPONENT_ERROR: 'componentError', | ||
|
||
// Fund events | ||
FUND_AMOUNT_CHANGED: 'fundAmountChanged', | ||
FUND_FAILURE: 'fundFailure', | ||
FUND_INITIATED: 'fundInitiated', | ||
FUND_OPTION_SELECTED: 'fundOptionSelected', | ||
FUND_SUCCESS: 'fundSuccess', | ||
|
||
// Mint events | ||
MINT_FAILURE: 'mintFailure', | ||
MINT_INITIATED: 'mintInitiated', | ||
MINT_QUANTITY_CHANGED: 'mintQuantityChanged', | ||
MINT_SUCCESS: 'mintSuccess', | ||
|
||
// Swap events | ||
SWAP_FAILURE: 'swapFailure', | ||
SWAP_INITIATED: 'swapInitiated', | ||
SWAP_SLIPPAGE_CHANGED: 'swapSlippageChanged', | ||
SWAP_SUCCESS: 'swapSuccess', | ||
SWAP_TOKEN_DROPDOWN_SELECTED: 'swapTokenDropdownSelected', | ||
SWAP_TOKEN_SELECTED: 'swapTokenSelected', | ||
|
||
// Transaction events | ||
TRANSACTION_FAILURE: 'transactionFailure', | ||
TRANSACTION_INITIATED: 'transactionInitiated', | ||
TRANSACTION_SUCCESS: 'transactionSuccess', | ||
|
||
// Wallet events | ||
WALLET_CONNECT_ERROR: 'walletConnectError', | ||
WALLET_CONNECT_INITIATED: 'walletConnectInitiated', | ||
WALLET_CONNECT_SUCCESS: 'walletConnectSuccess', | ||
WALLET_DISCONNECT: 'walletDisconnect', | ||
WALLET_OPTION_SELECTED: 'walletOptionSelected', | ||
} as const; | ||
|
||
/** | ||
* Component names for error tracking | ||
*/ | ||
export const COMPONENT_NAMES = { | ||
BUY: 'buy', | ||
CHECKOUT: 'checkout', | ||
FUND: 'fund', | ||
MINT: 'mint', | ||
SWAP: 'swap', | ||
TRANSACTION: 'transaction', | ||
WALLET: 'wallet', | ||
} as const; | ||
|
||
/** | ||
* Buy options | ||
*/ | ||
export const BUY_OPTIONS = { | ||
APPLE_PAY: 'apple_pay', | ||
COINBASE: 'coinbase_account', | ||
DEBIT_CARD: 'debit_card', | ||
ETH: 'eth', | ||
USDC: 'usdc', | ||
} as const; | ||
|
||
/** | ||
* Fund options | ||
*/ | ||
export const FUND_OPTIONS = { | ||
APPLE_PAY: 'apple_pay', | ||
COINBASE: 'coinbase_account', | ||
DEBIT_CARD: 'debit_card', | ||
} as const; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,152 @@ | ||
import { ANALYTICS_API_URL } from '@/core/analytics/constants'; | ||
import { WalletEvent } from '@/core/analytics/types'; | ||
import { sendAnalytics } from '@/core/analytics/utils/sendAnalytics'; | ||
import { useOnchainKit } from '@/useOnchainKit'; | ||
import { cleanup, renderHook } from '@testing-library/react'; | ||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; | ||
import { useAnalytics } from './useAnalytics'; | ||
|
||
vi.mock('@/useOnchainKit', () => ({ | ||
useOnchainKit: vi.fn(), | ||
})); | ||
|
||
vi.mock('@/core/analytics/utils/sendAnalytics', () => ({ | ||
sendAnalytics: vi.fn(), | ||
})); | ||
|
||
describe('useAnalytics', () => { | ||
const mockApiKey = 'test-api-key'; | ||
const mockSessionId = 'test-session-id'; | ||
const mockAnalyticsUrl = 'https://custom-analytics.example.com'; | ||
|
||
beforeEach(() => { | ||
vi.clearAllMocks(); | ||
|
||
(useOnchainKit as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ | ||
apiKey: mockApiKey, | ||
sessionId: mockSessionId, | ||
config: { | ||
analytics: true, | ||
analyticsUrl: mockAnalyticsUrl, | ||
}, | ||
}); | ||
}); | ||
|
||
afterEach(() => { | ||
cleanup(); | ||
vi.restoreAllMocks(); | ||
}); | ||
|
||
it('should return sendAnalytics function', () => { | ||
const { result } = renderHook(() => useAnalytics()); | ||
expect(result.current.sendAnalytics).toBeDefined(); | ||
}); | ||
|
||
it('should call sendAnalytics with correct payload structure', () => { | ||
const mockTitle = 'Test App'; | ||
Object.defineProperty(global.document, 'title', { | ||
value: mockTitle, | ||
writable: true, | ||
}); | ||
|
||
const { result } = renderHook(() => useAnalytics()); | ||
const event = WalletEvent.ConnectInitiated; | ||
const data = { | ||
component: 'ConnectWallet', | ||
walletProvider: 'TestProvider', | ||
}; | ||
|
||
result.current.sendAnalytics(event, data); | ||
|
||
expect(sendAnalytics).toHaveBeenCalledWith({ | ||
url: mockAnalyticsUrl, | ||
headers: { | ||
'OnchainKit-App-Name': mockTitle, | ||
}, | ||
body: { | ||
apiKey: mockApiKey, | ||
sessionId: mockSessionId, | ||
timestamp: expect.any(Number), | ||
eventType: event, | ||
data, | ||
}, | ||
}); | ||
}); | ||
|
||
it('should not send analytics when disabled in config', () => { | ||
(useOnchainKit as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ | ||
apiKey: mockApiKey, | ||
sessionId: mockSessionId, | ||
config: { | ||
analytics: false, | ||
}, | ||
}); | ||
|
||
const { result } = renderHook(() => useAnalytics()); | ||
const event = WalletEvent.ConnectSuccess; | ||
const data = { | ||
address: '0x0000000000000000000000000000000000000000', | ||
component: 'ConnectWallet', | ||
walletProvider: 'TestProvider', | ||
}; | ||
|
||
result.current.sendAnalytics(event, data); | ||
|
||
expect(sendAnalytics).not.toHaveBeenCalled(); | ||
}); | ||
|
||
it('should use default analytics URL when not provided in config', () => { | ||
(useOnchainKit as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ | ||
apiKey: mockApiKey, | ||
sessionId: mockSessionId, | ||
config: { | ||
analytics: true, | ||
}, | ||
}); | ||
|
||
const { result } = renderHook(() => useAnalytics()); | ||
const event = WalletEvent.ConnectError; | ||
const data = { | ||
error: 'Test error message', | ||
metadata: { | ||
connector: 'TestProvider', | ||
}, | ||
}; | ||
|
||
result.current.sendAnalytics(event, data); | ||
|
||
expect(sendAnalytics).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
url: ANALYTICS_API_URL, | ||
}), | ||
); | ||
}); | ||
|
||
it('should handle undefined apiKey and sessionId', () => { | ||
(useOnchainKit as unknown as ReturnType<typeof vi.fn>).mockReturnValue({ | ||
apiKey: undefined, | ||
sessionId: undefined, | ||
config: { | ||
analytics: true, | ||
}, | ||
}); | ||
|
||
const { result } = renderHook(() => useAnalytics()); | ||
const event = WalletEvent.ConnectInitiated; | ||
const data = { | ||
component: 'ConnectWallet', | ||
walletProvider: 'TestProvider', | ||
}; | ||
|
||
result.current.sendAnalytics(event, data); | ||
|
||
expect(sendAnalytics).toHaveBeenCalledWith( | ||
expect.objectContaining({ | ||
body: expect.objectContaining({ | ||
apiKey: 'undefined', | ||
sessionId: 'undefined', | ||
}), | ||
}), | ||
); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { ANALYTICS_API_URL } from '@/core/analytics/constants'; | ||
import type { | ||
AnalyticsEvent, | ||
AnalyticsEventData, | ||
} from '@/core/analytics/types'; | ||
import { sendAnalytics } from '@/core/analytics/utils/sendAnalytics'; | ||
import { useOnchainKit } from '@/useOnchainKit'; | ||
import { useEffect, useState } from 'react'; | ||
|
||
/** | ||
* useAnalytics handles analytics events and data preparation | ||
*/ | ||
export const useAnalytics = () => { | ||
const { apiKey, sessionId, config } = useOnchainKit(); | ||
const [appName, setAppName] = useState(''); | ||
|
||
useEffect(() => { | ||
setAppName(document.title); | ||
}, []); | ||
|
||
const prepareAnalyticsPayload = ( | ||
event: AnalyticsEvent, | ||
data: AnalyticsEventData[AnalyticsEvent], | ||
) => { | ||
return { | ||
url: config?.analyticsUrl ?? ANALYTICS_API_URL, | ||
headers: { | ||
'OnchainKit-App-Name': appName, | ||
}, | ||
body: { | ||
apiKey: apiKey ?? 'undefined', | ||
sessionId: sessionId ?? 'undefined', | ||
timestamp: Date.now(), | ||
eventType: event, | ||
data, | ||
}, | ||
}; | ||
}; | ||
|
||
return { | ||
sendAnalytics: ( | ||
event: AnalyticsEvent, | ||
data: AnalyticsEventData[AnalyticsEvent], | ||
) => { | ||
// Don't send analytics if disabled | ||
if (!config?.analytics) { | ||
return; | ||
} | ||
const payload = prepareAnalyticsPayload(event, data); | ||
sendAnalytics(payload); | ||
}, | ||
}; | ||
}; |
Oops, something went wrong.