mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-17 14:16:21 +00:00
527 lines
14 KiB
TypeScript
527 lines
14 KiB
TypeScript
/**
|
|
* Data Aggregator for Claude Code Usage Analytics
|
|
*
|
|
* Aggregates raw JSONL entries into daily, monthly, and session summaries.
|
|
* Uses model-pricing.ts for cost calculations.
|
|
*/
|
|
|
|
import { type RawUsageEntry } from '../jsonl-parser';
|
|
import { calculateCost } from '../model-pricing';
|
|
import {
|
|
type ModelBreakdown,
|
|
type DailyUsage,
|
|
type HourlyUsage,
|
|
type MonthlyUsage,
|
|
type SessionUsage,
|
|
} from './types';
|
|
|
|
// ============================================================================
|
|
// HELPER FUNCTIONS
|
|
// ============================================================================
|
|
|
|
/** Extract YYYY-MM-DD from ISO timestamp */
|
|
function extractDate(timestamp: string): string {
|
|
return timestamp.slice(0, 10);
|
|
}
|
|
|
|
/** Extract YYYY-MM from ISO timestamp */
|
|
function extractMonth(timestamp: string): string {
|
|
return timestamp.slice(0, 7);
|
|
}
|
|
|
|
/** Extract YYYY-MM-DD HH:00 from ISO timestamp */
|
|
function extractHour(timestamp: string): string {
|
|
const date = timestamp.slice(0, 10);
|
|
const hour = timestamp.slice(11, 13) || '00';
|
|
return `${date} ${hour}:00`;
|
|
}
|
|
|
|
/** Create model breakdown from accumulated data */
|
|
function createModelBreakdown(
|
|
modelName: string,
|
|
inputTokens: number,
|
|
outputTokens: number,
|
|
cacheCreationTokens: number,
|
|
cacheReadTokens: number
|
|
): ModelBreakdown {
|
|
const cost = calculateCost(
|
|
{ inputTokens, outputTokens, cacheCreationTokens, cacheReadTokens },
|
|
modelName
|
|
);
|
|
|
|
return {
|
|
modelName,
|
|
inputTokens,
|
|
outputTokens,
|
|
cacheCreationTokens,
|
|
cacheReadTokens,
|
|
cost,
|
|
};
|
|
}
|
|
|
|
/** Accumulator for per-model token counts */
|
|
interface ModelAccumulator {
|
|
inputTokens: number;
|
|
outputTokens: number;
|
|
cacheCreationTokens: number;
|
|
cacheReadTokens: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// DAILY AGGREGATION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Aggregate raw entries into daily usage summaries
|
|
* Groups by date (YYYY-MM-DD), calculates costs per model
|
|
*/
|
|
export function aggregateDailyUsage(
|
|
entries: RawUsageEntry[],
|
|
source = 'custom-parser'
|
|
): DailyUsage[] {
|
|
// Group entries by date
|
|
const byDate = new Map<string, RawUsageEntry[]>();
|
|
|
|
for (const entry of entries) {
|
|
const date = extractDate(entry.timestamp);
|
|
const existing = byDate.get(date) || [];
|
|
existing.push(entry);
|
|
byDate.set(date, existing);
|
|
}
|
|
|
|
// Build daily summaries
|
|
const dailyUsage: DailyUsage[] = [];
|
|
|
|
for (const [date, dateEntries] of byDate) {
|
|
// Aggregate by model
|
|
const modelMap = new Map<string, ModelAccumulator>();
|
|
let totalInput = 0;
|
|
let totalOutput = 0;
|
|
let totalCacheCreation = 0;
|
|
let totalCacheRead = 0;
|
|
|
|
for (const entry of dateEntries) {
|
|
const model = entry.model;
|
|
const acc = modelMap.get(model) || {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
cacheReadTokens: 0,
|
|
};
|
|
|
|
acc.inputTokens += entry.inputTokens;
|
|
acc.outputTokens += entry.outputTokens;
|
|
acc.cacheCreationTokens += entry.cacheCreationTokens;
|
|
acc.cacheReadTokens += entry.cacheReadTokens;
|
|
modelMap.set(model, acc);
|
|
|
|
totalInput += entry.inputTokens;
|
|
totalOutput += entry.outputTokens;
|
|
totalCacheCreation += entry.cacheCreationTokens;
|
|
totalCacheRead += entry.cacheReadTokens;
|
|
}
|
|
|
|
// Build model breakdowns
|
|
const modelBreakdowns: ModelBreakdown[] = [];
|
|
let totalCost = 0;
|
|
|
|
for (const [modelName, acc] of modelMap) {
|
|
const breakdown = createModelBreakdown(
|
|
modelName,
|
|
acc.inputTokens,
|
|
acc.outputTokens,
|
|
acc.cacheCreationTokens,
|
|
acc.cacheReadTokens
|
|
);
|
|
modelBreakdowns.push(breakdown);
|
|
totalCost += breakdown.cost;
|
|
}
|
|
|
|
// Sort breakdowns by cost descending
|
|
modelBreakdowns.sort((a, b) => b.cost - a.cost);
|
|
|
|
dailyUsage.push({
|
|
date,
|
|
source,
|
|
inputTokens: totalInput,
|
|
outputTokens: totalOutput,
|
|
cacheCreationTokens: totalCacheCreation,
|
|
cacheReadTokens: totalCacheRead,
|
|
cost: totalCost,
|
|
totalCost,
|
|
modelsUsed: Array.from(modelMap.keys()),
|
|
modelBreakdowns,
|
|
});
|
|
}
|
|
|
|
// Sort by date descending (most recent first)
|
|
dailyUsage.sort((a, b) => b.date.localeCompare(a.date));
|
|
|
|
return dailyUsage;
|
|
}
|
|
|
|
// ============================================================================
|
|
// HOURLY AGGREGATION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Aggregate raw entries into hourly usage summaries
|
|
* Groups by hour (YYYY-MM-DD HH:00), calculates costs per model
|
|
*/
|
|
export function aggregateHourlyUsage(
|
|
entries: RawUsageEntry[],
|
|
source = 'custom-parser'
|
|
): HourlyUsage[] {
|
|
// Group entries by hour
|
|
const byHour = new Map<string, RawUsageEntry[]>();
|
|
|
|
for (const entry of entries) {
|
|
const hour = extractHour(entry.timestamp);
|
|
const existing = byHour.get(hour) || [];
|
|
existing.push(entry);
|
|
byHour.set(hour, existing);
|
|
}
|
|
|
|
// Build hourly summaries
|
|
const hourlyUsage: HourlyUsage[] = [];
|
|
|
|
for (const [hour, hourEntries] of byHour) {
|
|
// Aggregate by model
|
|
const modelMap = new Map<string, ModelAccumulator>();
|
|
let totalInput = 0;
|
|
let totalOutput = 0;
|
|
let totalCacheCreation = 0;
|
|
let totalCacheRead = 0;
|
|
|
|
for (const entry of hourEntries) {
|
|
const model = entry.model;
|
|
const acc = modelMap.get(model) || {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
cacheReadTokens: 0,
|
|
};
|
|
|
|
acc.inputTokens += entry.inputTokens;
|
|
acc.outputTokens += entry.outputTokens;
|
|
acc.cacheCreationTokens += entry.cacheCreationTokens;
|
|
acc.cacheReadTokens += entry.cacheReadTokens;
|
|
modelMap.set(model, acc);
|
|
|
|
totalInput += entry.inputTokens;
|
|
totalOutput += entry.outputTokens;
|
|
totalCacheCreation += entry.cacheCreationTokens;
|
|
totalCacheRead += entry.cacheReadTokens;
|
|
}
|
|
|
|
// Build model breakdowns
|
|
const modelBreakdowns: ModelBreakdown[] = [];
|
|
let totalCost = 0;
|
|
|
|
for (const [modelName, acc] of modelMap) {
|
|
const breakdown = createModelBreakdown(
|
|
modelName,
|
|
acc.inputTokens,
|
|
acc.outputTokens,
|
|
acc.cacheCreationTokens,
|
|
acc.cacheReadTokens
|
|
);
|
|
modelBreakdowns.push(breakdown);
|
|
totalCost += breakdown.cost;
|
|
}
|
|
|
|
// Sort breakdowns by cost descending
|
|
modelBreakdowns.sort((a, b) => b.cost - a.cost);
|
|
|
|
hourlyUsage.push({
|
|
hour,
|
|
source,
|
|
inputTokens: totalInput,
|
|
outputTokens: totalOutput,
|
|
cacheCreationTokens: totalCacheCreation,
|
|
cacheReadTokens: totalCacheRead,
|
|
cost: totalCost,
|
|
totalCost,
|
|
modelsUsed: Array.from(modelMap.keys()),
|
|
modelBreakdowns,
|
|
});
|
|
}
|
|
|
|
// Sort by hour descending (most recent first)
|
|
hourlyUsage.sort((a, b) => b.hour.localeCompare(a.hour));
|
|
|
|
return hourlyUsage;
|
|
}
|
|
|
|
// ============================================================================
|
|
// MONTHLY AGGREGATION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Aggregate raw entries into monthly usage summaries
|
|
* Groups by month (YYYY-MM), calculates costs per model
|
|
*/
|
|
export function aggregateMonthlyUsage(
|
|
entries: RawUsageEntry[],
|
|
source = 'custom-parser'
|
|
): MonthlyUsage[] {
|
|
// Group entries by month
|
|
const byMonth = new Map<string, RawUsageEntry[]>();
|
|
|
|
for (const entry of entries) {
|
|
const month = extractMonth(entry.timestamp);
|
|
const existing = byMonth.get(month) || [];
|
|
existing.push(entry);
|
|
byMonth.set(month, existing);
|
|
}
|
|
|
|
// Build monthly summaries
|
|
const monthlyUsage: MonthlyUsage[] = [];
|
|
|
|
for (const [month, monthEntries] of byMonth) {
|
|
// Aggregate by model
|
|
const modelMap = new Map<string, ModelAccumulator>();
|
|
let totalInput = 0;
|
|
let totalOutput = 0;
|
|
let totalCacheCreation = 0;
|
|
let totalCacheRead = 0;
|
|
|
|
for (const entry of monthEntries) {
|
|
const model = entry.model;
|
|
const acc = modelMap.get(model) || {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
cacheReadTokens: 0,
|
|
};
|
|
|
|
acc.inputTokens += entry.inputTokens;
|
|
acc.outputTokens += entry.outputTokens;
|
|
acc.cacheCreationTokens += entry.cacheCreationTokens;
|
|
acc.cacheReadTokens += entry.cacheReadTokens;
|
|
modelMap.set(model, acc);
|
|
|
|
totalInput += entry.inputTokens;
|
|
totalOutput += entry.outputTokens;
|
|
totalCacheCreation += entry.cacheCreationTokens;
|
|
totalCacheRead += entry.cacheReadTokens;
|
|
}
|
|
|
|
// Build model breakdowns
|
|
const modelBreakdowns: ModelBreakdown[] = [];
|
|
let totalCost = 0;
|
|
|
|
for (const [modelName, acc] of modelMap) {
|
|
const breakdown = createModelBreakdown(
|
|
modelName,
|
|
acc.inputTokens,
|
|
acc.outputTokens,
|
|
acc.cacheCreationTokens,
|
|
acc.cacheReadTokens
|
|
);
|
|
modelBreakdowns.push(breakdown);
|
|
totalCost += breakdown.cost;
|
|
}
|
|
|
|
// Sort breakdowns by cost descending
|
|
modelBreakdowns.sort((a, b) => b.cost - a.cost);
|
|
|
|
monthlyUsage.push({
|
|
month,
|
|
source,
|
|
inputTokens: totalInput,
|
|
outputTokens: totalOutput,
|
|
cacheCreationTokens: totalCacheCreation,
|
|
cacheReadTokens: totalCacheRead,
|
|
totalCost,
|
|
modelsUsed: Array.from(modelMap.keys()),
|
|
modelBreakdowns,
|
|
});
|
|
}
|
|
|
|
// Sort by month descending (most recent first)
|
|
monthlyUsage.sort((a, b) => b.month.localeCompare(a.month));
|
|
|
|
return monthlyUsage;
|
|
}
|
|
|
|
// ============================================================================
|
|
// SESSION AGGREGATION
|
|
// ============================================================================
|
|
|
|
/**
|
|
* Aggregate raw entries into session usage summaries
|
|
* Groups by sessionId, tracks last activity and versions
|
|
*/
|
|
export function aggregateSessionUsage(
|
|
entries: RawUsageEntry[],
|
|
source = 'custom-parser'
|
|
): SessionUsage[] {
|
|
// Group entries by sessionId
|
|
const bySession = new Map<string, RawUsageEntry[]>();
|
|
|
|
for (const entry of entries) {
|
|
if (!entry.sessionId) continue;
|
|
const existing = bySession.get(entry.sessionId) || [];
|
|
existing.push(entry);
|
|
bySession.set(entry.sessionId, existing);
|
|
}
|
|
|
|
// Build session summaries
|
|
const sessionUsage: SessionUsage[] = [];
|
|
|
|
for (const [sessionId, sessionEntries] of bySession) {
|
|
const orderedEntries = [...sessionEntries].sort((a, b) =>
|
|
a.timestamp.localeCompare(b.timestamp)
|
|
);
|
|
|
|
// Aggregate by model
|
|
const modelMap = new Map<string, ModelAccumulator>();
|
|
const versions = new Set<string>();
|
|
let totalInput = 0;
|
|
let totalOutput = 0;
|
|
let totalCacheCreation = 0;
|
|
let totalCacheRead = 0;
|
|
let lastActivity = '';
|
|
let projectPath = '';
|
|
let target: string | undefined;
|
|
|
|
for (const entry of orderedEntries) {
|
|
const model = entry.model;
|
|
const acc = modelMap.get(model) || {
|
|
inputTokens: 0,
|
|
outputTokens: 0,
|
|
cacheCreationTokens: 0,
|
|
cacheReadTokens: 0,
|
|
};
|
|
|
|
acc.inputTokens += entry.inputTokens;
|
|
acc.outputTokens += entry.outputTokens;
|
|
acc.cacheCreationTokens += entry.cacheCreationTokens;
|
|
acc.cacheReadTokens += entry.cacheReadTokens;
|
|
modelMap.set(model, acc);
|
|
|
|
totalInput += entry.inputTokens;
|
|
totalOutput += entry.outputTokens;
|
|
totalCacheCreation += entry.cacheCreationTokens;
|
|
totalCacheRead += entry.cacheReadTokens;
|
|
|
|
// Track latest timestamp
|
|
if (entry.timestamp > lastActivity) {
|
|
lastActivity = entry.timestamp;
|
|
}
|
|
|
|
// Track versions
|
|
if (entry.version) {
|
|
versions.add(entry.version);
|
|
}
|
|
|
|
// Use project path from entry
|
|
if (entry.projectPath) {
|
|
projectPath = entry.projectPath;
|
|
}
|
|
|
|
if (entry.target) {
|
|
target = entry.target;
|
|
}
|
|
}
|
|
|
|
// Build model breakdowns
|
|
const modelBreakdowns: ModelBreakdown[] = [];
|
|
let totalCost = 0;
|
|
|
|
for (const [modelName, acc] of modelMap) {
|
|
const breakdown = createModelBreakdown(
|
|
modelName,
|
|
acc.inputTokens,
|
|
acc.outputTokens,
|
|
acc.cacheCreationTokens,
|
|
acc.cacheReadTokens
|
|
);
|
|
modelBreakdowns.push(breakdown);
|
|
totalCost += breakdown.cost;
|
|
}
|
|
|
|
// Sort breakdowns by cost descending
|
|
modelBreakdowns.sort((a, b) => b.cost - a.cost);
|
|
|
|
sessionUsage.push({
|
|
sessionId,
|
|
projectPath,
|
|
inputTokens: totalInput,
|
|
outputTokens: totalOutput,
|
|
cacheCreationTokens: totalCacheCreation,
|
|
cacheReadTokens: totalCacheRead,
|
|
cost: totalCost,
|
|
totalCost,
|
|
lastActivity,
|
|
versions: Array.from(versions),
|
|
modelsUsed: Array.from(modelMap.keys()),
|
|
modelBreakdowns,
|
|
source,
|
|
target,
|
|
});
|
|
}
|
|
|
|
// Sort by last activity descending (most recent first)
|
|
sessionUsage.sort((a, b) => b.lastActivity.localeCompare(a.lastActivity));
|
|
|
|
return sessionUsage;
|
|
}
|
|
|
|
// ============================================================================
|
|
// MAIN DATA LOADER (drop-in replacement for better-ccusage)
|
|
// ============================================================================
|
|
|
|
import { scanProjectsDirectory, type ParserOptions } from '../jsonl-parser';
|
|
|
|
/**
|
|
* Load daily usage data (replaces better-ccusage loadDailyUsageData)
|
|
*/
|
|
export async function loadDailyUsageData(options?: ParserOptions): Promise<DailyUsage[]> {
|
|
const entries = await scanProjectsDirectory(options);
|
|
return aggregateDailyUsage(entries);
|
|
}
|
|
|
|
/**
|
|
* Load hourly usage data for today's chart
|
|
*/
|
|
export async function loadHourlyUsageData(options?: ParserOptions): Promise<HourlyUsage[]> {
|
|
const entries = await scanProjectsDirectory(options);
|
|
return aggregateHourlyUsage(entries);
|
|
}
|
|
|
|
/**
|
|
* Load monthly usage data (replaces better-ccusage loadMonthlyUsageData)
|
|
*/
|
|
export async function loadMonthlyUsageData(options?: ParserOptions): Promise<MonthlyUsage[]> {
|
|
const entries = await scanProjectsDirectory(options);
|
|
return aggregateMonthlyUsage(entries);
|
|
}
|
|
|
|
/**
|
|
* Load session data (replaces better-ccusage loadSessionData)
|
|
*/
|
|
export async function loadSessionData(options?: ParserOptions): Promise<SessionUsage[]> {
|
|
const entries = await scanProjectsDirectory(options);
|
|
return aggregateSessionUsage(entries);
|
|
}
|
|
|
|
/**
|
|
* Load all usage data in a single pass (more efficient)
|
|
*/
|
|
export async function loadAllUsageData(options?: ParserOptions): Promise<{
|
|
daily: DailyUsage[];
|
|
hourly: HourlyUsage[];
|
|
monthly: MonthlyUsage[];
|
|
session: SessionUsage[];
|
|
}> {
|
|
const entries = await scanProjectsDirectory(options);
|
|
return {
|
|
daily: aggregateDailyUsage(entries),
|
|
hourly: aggregateHourlyUsage(entries),
|
|
monthly: aggregateMonthlyUsage(entries),
|
|
session: aggregateSessionUsage(entries),
|
|
};
|
|
}
|