mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 08:18:04 +00:00
fix(ui): tighten analytics session and query contracts
This commit is contained in:
1 parent
e95b354297
commit
b2e4be310d
9 files changed
+162
-106
No files matched your search
@@ -31,6 +31,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
|
||||
|
||||
const sessions = data.sessions;
|
||||
const totalSessions = data.total;
|
||||
const sampledSessions = sessions.length;
|
||||
const hasPartialSample = data.hasMore || data.offset > 0;
|
||||
|
||||
// Calculate total cost for visible sessions
|
||||
@@ -38,6 +39,7 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
|
||||
const avgCost = totalCost / sessions.length;
|
||||
|
||||
return {
|
||||
displayedSessions: hasPartialSample ? sampledSessions : totalSessions,
|
||||
totalSessions,
|
||||
avgCost,
|
||||
hasPartialSample,
|
||||
@@ -95,12 +97,16 @@ export function SessionStatsCard({ data, isLoading, className }: SessionStatsCar
|
||||
<div className="p-2 rounded-md bg-muted/50 border text-center">
|
||||
<div className="flex items-center justify-center gap-1.5 text-blue-600 dark:text-blue-400">
|
||||
<Users className="w-4 h-4" />
|
||||
<span className="text-xl font-bold">{stats.totalSessions}</span>
|
||||
<span className="text-xl font-bold">{stats.displayedSessions}</span>
|
||||
</div>
|
||||
<p className="text-[10px] text-muted-foreground uppercase tracking-wider mt-0.5">
|
||||
{/* TODO i18n: missing key for "Total Sessions" */}
|
||||
Total Sessions
|
||||
{stats.hasPartialSample ? 'Sampled Sessions' : 'Total Sessions'}
|
||||
</p>
|
||||
{stats.hasPartialSample && (
|
||||
<p className="text-[10px] text-muted-foreground mt-0.5">
|
||||
{stats.totalSessions} total
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Avg Cost */}
|
||||
|
||||
@@ -123,7 +123,6 @@ export interface MonthlyUsage {
|
||||
export interface UsageQueryOptions {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
profile?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,8 @@ import { afterEach, vi } from 'vitest';
|
||||
// Cleanup after each test
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
// Mock matchMedia for components that use media queries
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('SessionStatsCard', () => {
|
||||
expect(screen.getByText('No session data available')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('uses the API total for overall session count while labeling subset averages as recent', () => {
|
||||
it('keeps subset session metrics explicitly sample-scoped when pagination truncates the result set', () => {
|
||||
const data = buildPaginatedSessions({
|
||||
sessions: [
|
||||
buildSession({ sessionId: 'session-1', cost: 0.08 }),
|
||||
@@ -84,8 +84,9 @@ describe('SessionStatsCard', () => {
|
||||
|
||||
render(<SessionStatsCard data={data} />, { wrapper: AllProviders });
|
||||
|
||||
expect(screen.getByText('Total Sessions')).toBeInTheDocument();
|
||||
expect(screen.getByText('9')).toBeInTheDocument();
|
||||
expect(screen.getByText('Sampled Sessions')).toBeInTheDocument();
|
||||
expect(screen.getByText('3')).toBeInTheDocument();
|
||||
expect(screen.getByText('9 total')).toBeInTheDocument();
|
||||
expect(screen.getByText('Recent Avg Cost')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('$0.08').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('codex')).toBeInTheDocument();
|
||||
|
||||
@@ -10,21 +10,22 @@ function jsonResponse(body: unknown) {
|
||||
|
||||
describe('analytics usage API contract', () => {
|
||||
beforeEach(() => {
|
||||
global.fetch = vi
|
||||
.fn()
|
||||
.mockImplementation(() => Promise.resolve(jsonResponse({ data: {} }))) as typeof fetch;
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn().mockImplementation(() => Promise.resolve(jsonResponse({ data: {} }))) as typeof fetch
|
||||
);
|
||||
});
|
||||
|
||||
it('omits unsupported profile and months query params while keeping supported filters', async () => {
|
||||
it('serializes only the supported analytics query params', async () => {
|
||||
const startDate = new Date(2026, 3, 1);
|
||||
const endDate = new Date(2026, 3, 30);
|
||||
|
||||
await usageApi.summary({ startDate, endDate, profile: 'work' });
|
||||
await usageApi.trends({ startDate, endDate, profile: 'work' });
|
||||
await usageApi.models({ startDate, endDate, profile: 'work' });
|
||||
await usageApi.sessions({ startDate, endDate, profile: 'work', limit: 50, offset: 10 });
|
||||
await usageApi.insights({ startDate, endDate, profile: 'work' });
|
||||
await usageApi.monthly({ startDate, endDate, profile: 'work' });
|
||||
await usageApi.summary({ startDate, endDate });
|
||||
await usageApi.trends({ startDate, endDate });
|
||||
await usageApi.models({ startDate, endDate });
|
||||
await usageApi.sessions({ startDate, endDate, limit: 50, offset: 10 });
|
||||
await usageApi.insights({ startDate, endDate });
|
||||
await usageApi.monthly({ startDate, endDate });
|
||||
|
||||
const urls = vi.mocked(global.fetch).mock.calls.map(([url]) => String(url));
|
||||
|
||||
@@ -34,7 +35,6 @@ describe('analytics usage API contract', () => {
|
||||
expect(urls).toContain('/api/usage/sessions?since=20260401&until=20260430&limit=50&offset=10');
|
||||
expect(urls).toContain('/api/usage/insights?since=20260401&until=20260430');
|
||||
expect(urls).toContain('/api/usage/monthly?since=20260401&until=20260430');
|
||||
expect(urls.every((url) => !url.includes('profile='))).toBe(true);
|
||||
expect(urls.every((url) => !url.includes('months='))).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user