diff --git a/packages/analytics/src/__tests__/pagination.test.ts b/packages/analytics/src/__tests__/pagination.test.ts new file mode 100644 index 0000000..7c0effe --- /dev/null +++ b/packages/analytics/src/__tests__/pagination.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it, vi } from 'vitest'; + +import { AnalyticsResource } from '../index.js'; + +import type { HttpClient } from '@astroid/core'; +import type { AgentSpendingRow, BudgetUtilizationRow } from '@astroid/types'; + +/** Minimal HttpClient stand-in that records GET calls and returns stubbed data. */ +function makeClient() { + const calls: Array<{ path: string; query?: Record }> = []; + const handler = vi.fn(); + + const client = { + get: vi.fn(async (path: string, opts?: { query?: Record }) => { + calls.push({ path, query: opts?.query }); + const data = await handler(path, opts?.query); + return { data, meta: { page: 1, limit: 20, total: data.length, totalPages: 1, hasNextPage: false, hasPreviousPage: false } }; + }), + post: vi.fn(), + patch: vi.fn(), + delete: vi.fn(), + } as unknown as HttpClient; + + return { client, calls, handler }; +} + +describe('AnalyticsResource pagination', () => { + const agentRow: AgentSpendingRow = { + agentId: 'agt_1', + agentName: 'Payment Bot', + totalSpent: '120.50', + transactionCount: 7, + averageRisk: 12, + }; + + const budgetRow: BudgetUtilizationRow = { + budgetId: 'bud_1', + budgetName: 'Marketing', + limit: '1000.00', + spent: '400.00', + remaining: '600.00', + utilization: 0.4, + }; + + it('listAgents serializes analytics filters and pagination into the querystring', async () => { + const { client, calls, handler } = makeClient(); + handler.mockResolvedValueOnce([agentRow]); + const resource = new AnalyticsResource(client); + + const result = await resource.listAgents({ + from: '2026-01-01', + to: '2026-01-31', + currency: 'USDC', + page: 2, + limit: 25, + order: 'desc', + sort: 'totalSpent', + }); + + expect(calls).toEqual([ + { + path: '/analytics/agents', + query: { + from: '2026-01-01', + to: '2026-01-31', + currency: 'USDC', + page: 2, + limit: 25, + order: 'desc', + sort: 'totalSpent', + }, + }, + ]); + expect(result.data).toEqual([agentRow]); + expect(result.meta.page).toBe(1); + }); + + it('listAgents returns an empty page when there are no rows', async () => { + const { client, handler } = makeClient(); + handler.mockResolvedValueOnce([]); + const resource = new AnalyticsResource(client); + + const result = await resource.listAgents({}); + expect(result.data).toEqual([]); + expect(result.meta.total).toBe(0); + }); + + it('listBudgets serializes pagination and parses utilization rows', async () => { + const { client, calls, handler } = makeClient(); + handler.mockResolvedValueOnce([budgetRow]); + const resource = new AnalyticsResource(client); + + const result = await resource.listBudgets({ + from: '2026-01-01', + page: 1, + limit: 50, + order: 'asc', + }); + + expect(calls).toEqual([ + { + path: '/analytics/budgets', + query: { from: '2026-01-01', page: 1, limit: 50, order: 'asc' }, + }, + ]); + expect(result.data).toEqual([budgetRow]); + expect(result.data[0].utilization).toBe(0.4); + }); +}); \ No newline at end of file diff --git a/packages/analytics/src/index.ts b/packages/analytics/src/index.ts index 9c7353f..6a1ecee 100644 --- a/packages/analytics/src/index.ts +++ b/packages/analytics/src/index.ts @@ -1,4 +1,95 @@ -export * from './metrics.js'; -export * from './aggregations.js'; -export * from './exporter.js'; -export * from './time-series.js'; +/** + * `@astroid/analytics` — read-only reporting resource. + * + * Thin, typed wrappers over the `GET /analytics/*` endpoints. Every method + * returns a chart-ready object (time series, distributions, per-agent and + * per-budget rows) and accepts the shared {@link AnalyticsQuery} filters. + * + * @packageDocumentation + */ + +import { Resource } from '@astroid/core'; +import type { + AgentAnalytics, + AgentSpendingRow, + AnalyticsListParams, + AnalyticsOverview, + AnalyticsQuery, + BudgetAnalytics, + BudgetUtilizationRow, + CashflowReport, + Paginated, + RiskReport, +} from '@astroid/types'; + +export { + exportToCSV, + exportToJSON, + formatTransactionForExport, + flattenRecordForExport, + escapeCsvValue, + type CsvColumn, + type CsvExportOptions, + type JsonExportOptions, +} from './exporter.js'; + +/** + * The `analytics` namespace on the Astroid client. + * + * All methods are read-only and safe to call frequently; they aggregate over + * the organization's transactions, agents, and budgets for the requested window + * and granularity. + */ +export class AnalyticsResource extends Resource { + /** Headline dashboard metrics plus the spending trend. */ + async overview(query: AnalyticsQuery = {}): Promise { + return this.getData('/analytics/overview', { ...query }); + } + + /** Inflow/outflow/net cashflow over the requested window. */ + async cashflow(query: AnalyticsQuery = {}): Promise { + return this.getData('/analytics/cashflow', { ...query }); + } + + /** Spending report (alias of the cashflow endpoint's outflow view). */ + async spending(query: AnalyticsQuery = {}): Promise { + return this.getData('/analytics/spending', { ...query }); + } + + /** Risk distribution, average score, and trend. */ + async risk(query: AnalyticsQuery = {}): Promise { + return this.getData('/analytics/risk', { ...query }); + } + + /** Per-agent spending and risk breakdown. */ + async agents(query: AnalyticsQuery = {}): Promise { + return this.getData('/analytics/agents', { ...query }); + } + + /** Per-budget utilization breakdown. */ + async budgets(query: AnalyticsQuery = {}): Promise { + return this.getData('/analytics/budgets', { ...query }); + } + + /** + * Densely paginated per-agent performance rows. + * + * Unlike {@link AnalyticsResource.agents} (which returns the full aggregate + * in one payload), this endpoint is cursor/page-aware so clients can page + * through large historical sets without loading everything at once. Accepts + * the shared {@link AnalyticsListParams} filters plus pagination controls. + */ + async listAgents(query: AnalyticsListParams = {}): Promise> { + return this.listData('/analytics/agents', { ...query }); + } + + /** + * Densely paginated per-budget utilization rows. + * + * Use when there are many budgets and you want to page through them with + * `page`/`limit`/`order` rather than fetch every row in a single response. + */ + async listBudgets(query: AnalyticsListParams = {}): Promise> { + return this.listData('/analytics/budgets', { ...query }); + } +} diff --git a/packages/types/src/analytics.ts b/packages/types/src/analytics.ts index fd7d723..2c38eae 100644 --- a/packages/types/src/analytics.ts +++ b/packages/types/src/analytics.ts @@ -1,21 +1,36 @@ export type Timeframe = 'hour' | 'day' | 'week' | 'month' | 'year' | string; -export interface AnalyticsQueryParams { - startDate?: string; - endDate?: string; - timeframe?: Timeframe; - asset?: string; - walletId?: string; +import type { DecimalString } from './entities.js'; +import type { RiskBand } from './enums.js'; +import type { Paginated, PaginationParams } from './common.js'; + +/** Query parameters accepted by analytics endpoints. */ +export interface AnalyticsQuery { + from?: string; + to?: string; agentId?: string; } -export interface TimeSeriesMetricPoint { - timestamp: string; - volume: string; - fee: string; - count: number; - successCount: number; - failureCount: number; +/** + * Analytics list queries: the shared {@link AnalyticsQuery} filters plus + * standard pagination controls (`page`, `limit`, `sort`, `order`, `search`). + * + * Applied to the tabular analytics endpoints (per-agent and per-budget rows) so + * clients can page through large historical result sets without pulling the + * full payload into memory. + */ +export interface AnalyticsListParams extends AnalyticsQuery, PaginationParams {} + +/** + * Alias for a paginated analytics results payload. Keeps the narrow, row-level + * item type explicit at call sites (e.g. {@link AgentSpendingRow}). + */ +export type PaginatedResponse = Paginated; + +/** A single (timestamp, value) point in a time series. */ +export interface TimeSeriesPoint { + date: string; + value: number; } export interface VolumeSummary {