[TRIVIAL] Add telemetry to payment interface (#28)

* Update pay function to use payerInfo with callbackUrl

- Replace infoRequests with payerInfo containing requests and callbackUrl
- Update PaymentOptions interface to use payerInfo structure
- Add callbackUrl to dataCallback capability in wallet_sendCalls
- Update all tests to use new payerInfo structure
- Update playground to use new payerInfo structure with callback URL
- Update documentation and examples

* Rename 'request' to 'type' in InfoRequest interface

- Update InfoRequest interface to use 'type' field instead of 'request'
- Update all usages in code, tests, and documentation
- Maintain consistency with the new API design

* Update callbackUrl to callbackURL to match spec

- Rename callbackUrl to callbackURL in PayerInfo interface
- Update all usages in code to use callbackURL
- Update all tests to use callbackURL
- Update documentation and examples to use callbackURL
- Update capability object to use callbackURL consistently

* refactor to payerInfoResponses

* add telemetry to the payment interface

* add cca parameters
This commit is contained in:
Spencer Stock
2025-07-15 13:43:21 -06:00
committed by GitHub
parent 397b52188f
commit 2e93a680e9
6 changed files with 232 additions and 2 deletions
@@ -0,0 +1,147 @@
import { ActionType, AnalyticsEventImportance, ComponentType, logEvent } from '../logEvent.js';
export const logPaymentStarted = ({
amount,
testnet,
correlationId,
}: {
amount: string;
testnet: boolean;
correlationId: string | undefined;
}) => {
logEvent(
'payment.pay.started',
{
action: ActionType.process,
componentType: ComponentType.unknown,
method: 'pay',
correlationId,
signerType: 'base-account',
amount,
testnet,
},
AnalyticsEventImportance.high
);
};
export const logPaymentError = ({
amount,
testnet,
correlationId,
errorMessage,
}: {
amount: string;
testnet: boolean;
correlationId: string | undefined;
errorMessage: string;
}) => {
logEvent(
'payment.pay.error',
{
action: ActionType.error,
componentType: ComponentType.unknown,
method: 'pay',
correlationId,
signerType: 'base-account',
amount,
testnet,
errorMessage,
},
AnalyticsEventImportance.high
);
};
export const logPaymentCompleted = ({
amount,
testnet,
correlationId,
}: {
amount: string;
testnet: boolean;
correlationId: string | undefined;
}) => {
logEvent(
'payment.pay.completed',
{
action: ActionType.process,
componentType: ComponentType.unknown,
method: 'pay',
correlationId,
signerType: 'base-account',
amount,
testnet,
},
AnalyticsEventImportance.high
);
};
export const logPaymentStatusCheckStarted = ({
testnet,
correlationId,
}: {
testnet: boolean;
correlationId: string | undefined;
}) => {
logEvent(
'payment.status_check.started',
{
action: ActionType.process,
componentType: ComponentType.unknown,
method: 'getPaymentStatus',
correlationId,
signerType: 'base-account',
testnet,
},
AnalyticsEventImportance.low
);
};
export const logPaymentStatusCheckCompleted = ({
testnet,
status,
correlationId,
}: {
testnet: boolean;
status: string;
correlationId: string | undefined;
}) => {
logEvent(
'payment.status_check.completed',
{
action: ActionType.process,
componentType: ComponentType.unknown,
method: 'getPaymentStatus',
correlationId,
signerType: 'base-account',
testnet,
status,
},
AnalyticsEventImportance.low
);
};
export const logPaymentStatusCheckError = ({
testnet,
correlationId,
errorMessage,
}: {
testnet: boolean;
correlationId: string | undefined;
errorMessage: string;
}) => {
logEvent(
'payment.status_check.error',
{
action: ActionType.error,
componentType: ComponentType.unknown,
method: 'getPaymentStatus',
correlationId,
errorMessage,
signerType: 'base-account',
testnet,
},
AnalyticsEventImportance.low
);
};
@@ -64,6 +64,10 @@ type CCAEventData = {
dialogContext?: string;
dialogAction?: string;
enableAutoSubAccounts?: boolean;
// Payment-specific attributes
amount?: string;
testnet?: boolean;
status?: string;
};
type AnalyticsEventData = {
@@ -106,3 +110,4 @@ export function identify(event: CCAEventData) {
export { ActionType, AnalyticsEventImportance, ComponentType };
export type { AnalyticsEventData, CCAEventData, LogEvent };
@@ -5,12 +5,23 @@ import type { PaymentStatus } from './types.js';
// Mock fetch globally
global.fetch = vi.fn();
// Mock telemetry events
vi.mock(':core/telemetry/events/payment.js', () => ({
logPaymentStatusCheckStarted: vi.fn(),
logPaymentStatusCheckCompleted: vi.fn(),
logPaymentStatusCheckError: vi.fn(),
}));
describe('getPaymentStatus', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock console methods to avoid noise in tests
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
// Mock crypto.randomUUID
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue('mock-correlation-id'),
});
});
it('should return completed status for successful payment', async () => {
@@ -1,6 +1,7 @@
import type { Hex } from 'viem';
import { decodeEventLog, formatUnits } from 'viem';
import { logPaymentStatusCheckCompleted, logPaymentStatusCheckError, logPaymentStatusCheckStarted } from ':core/telemetry/events/payment.js';
import { ERC20_TRANSFER_ABI, TOKENS } from './constants.js';
import type { PaymentStatus, PaymentStatusOptions } from './types.js';
@@ -20,6 +21,12 @@ import type { PaymentStatus, PaymentStatusOptions } from './types.js';
*/
export async function getPaymentStatus(options: PaymentStatusOptions): Promise<PaymentStatus> {
const { id, testnet = false } = options;
// Generate correlation ID for this status check
const correlationId = crypto.randomUUID();
// Log status check started
logPaymentStatusCheckStarted({ testnet, correlationId });
try {
// Get the bundler URL based on network
@@ -44,11 +51,13 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise<P
// Handle RPC errors
if (receipt.error) {
console.error('[getPaymentStatus] RPC error:', receipt.error);
const errorMessage = receipt.error.message || 'Network error';
logPaymentStatusCheckError({ testnet, correlationId, errorMessage });
return {
status: 'failed',
id: id as Hex,
message: 'Unable to check payment status. Please try again later.',
error: receipt.error.message || 'Network error',
error: errorMessage,
};
}
@@ -70,6 +79,7 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise<P
if (userOpResponse.result) {
// UserOp exists but no receipt yet - it's pending
logPaymentStatusCheckCompleted({ testnet, status: 'pending', correlationId });
const result = {
status: 'pending' as const,
id: id as Hex,
@@ -80,6 +90,7 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise<P
}
// Not found at all
logPaymentStatusCheckCompleted({ testnet, status: 'not_found', correlationId });
const result = {
status: 'not_found' as const,
id: id as Hex,
@@ -132,6 +143,7 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise<P
}
}
logPaymentStatusCheckCompleted({ testnet, status: 'completed', correlationId });
const result = {
status: 'completed' as const,
id: id as Hex,
@@ -155,6 +167,7 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise<P
}
}
logPaymentStatusCheckCompleted({ testnet, status: 'failed', correlationId });
const result = {
status: 'failed' as const,
id: id as Hex,
@@ -167,11 +180,14 @@ export async function getPaymentStatus(options: PaymentStatusOptions): Promise<P
} catch (error) {
console.error('[getPaymentStatus] Error checking status:', error);
const errorMessage = error instanceof Error ? error.message : 'Connection error';
logPaymentStatusCheckError({ testnet, correlationId, errorMessage });
const result = {
status: 'failed' as const,
id: id as Hex,
message: 'Unable to check payment status',
error: error instanceof Error ? error.message : 'Connection error',
error: errorMessage,
};
return result;
}
@@ -9,9 +9,20 @@ vi.mock('./utils/validation.js');
vi.mock('./utils/translatePayment.js');
vi.mock('./utils/sdkManager.js');
// Mock telemetry events
vi.mock(':core/telemetry/events/payment.js', () => ({
logPaymentStarted: vi.fn(),
logPaymentError: vi.fn(),
logPaymentCompleted: vi.fn(),
}));
describe('pay', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock crypto.randomUUID
vi.stubGlobal('crypto', {
randomUUID: vi.fn().mockReturnValue('mock-correlation-id'),
});
});
it('should successfully process a payment', async () => {
@@ -58,6 +69,19 @@ describe('pay', () => {
false,
undefined
);
// Verify telemetry events
const { logPaymentStarted, logPaymentCompleted } = await import(':core/telemetry/events/payment.js');
expect(logPaymentStarted).toHaveBeenCalledWith({
amount: '10.50',
testnet: false,
correlationId: 'mock-correlation-id',
});
expect(logPaymentCompleted).toHaveBeenCalledWith({
amount: '10.50',
testnet: false,
correlationId: 'mock-correlation-id',
});
});
it('should handle validation errors', async () => {
@@ -76,6 +100,20 @@ describe('pay', () => {
amount: '0',
to: '0xFe21034794A5a574B94fE4fDfD16e005F1C96e51',
});
// Verify telemetry events
const { logPaymentStarted, logPaymentError } = await import(':core/telemetry/events/payment.js');
expect(logPaymentStarted).toHaveBeenCalledWith({
amount: '0',
testnet: false,
correlationId: 'mock-correlation-id',
});
expect(logPaymentError).toHaveBeenCalledWith({
amount: '0',
testnet: false,
correlationId: 'mock-correlation-id',
errorMessage: 'Invalid amount: must be greater than 0',
});
});
it('should handle SDK execution errors', async () => {
@@ -1,3 +1,4 @@
import { logPaymentCompleted, logPaymentError, logPaymentStarted } from ':core/telemetry/events/payment.js';
import type { Address } from 'viem';
import type { PaymentOptions, PaymentResult } from './types.js';
import { executePaymentWithSDK } from './utils/sdkManager.js';
@@ -31,6 +32,12 @@ import { validateAddress, validateStringAmount } from './utils/validation.js';
*/
export async function pay(options: PaymentOptions): Promise<PaymentResult> {
const { amount, to, testnet = false, payerInfo } = options;
// Generate correlation ID for this payment request
const correlationId = crypto.randomUUID();
// Log payment started
logPaymentStarted({ amount, testnet, correlationId });
try {
validateStringAmount(amount, 2);
@@ -42,6 +49,9 @@ export async function pay(options: PaymentOptions): Promise<PaymentResult> {
// Step 3: Execute payment with SDK
const executionResult = await executePaymentWithSDK(requestParams, testnet);
// Log payment completed
logPaymentCompleted({ amount, testnet, correlationId });
// Return success result
return {
success: true,
@@ -70,6 +80,9 @@ export async function pay(options: PaymentOptions): Promise<PaymentResult> {
}
}
// Log payment error
logPaymentError({ amount, testnet, correlationId, errorMessage });
// Return error result
return {
success: false,