claude-session-viewer 0.3.5 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/dev.js +20 -1
- package/dist/client/assets/index-BKZOwcdy.css +1 -0
- package/dist/client/assets/index-Bo1qXVSw.js +70 -0
- package/dist/client/index.html +2 -2
- package/dist/server/claude/projects/repository.js +1 -1
- package/dist/server/claude/projects/service.js +4 -4
- package/dist/server/claude/sessions/service.js +9 -5
- package/dist/server/routes/sessionWindows.js +127 -0
- package/dist/server/routes/sessions.js +5 -4
- package/dist/server/routes/statistics.js +34 -310
- package/dist/server/statistics/aggregator.js +290 -0
- package/dist/server/statistics/service.js +226 -0
- package/dist/server/statistics/tokenStats.js +66 -0
- package/dist/server/statistics/tokenUsage.js +66 -0
- package/dist/server/statistics/utils.js +33 -0
- package/dist/server/utils/sessionWindows.js +159 -0
- package/package.json +2 -2
- package/dist/client/assets/index-BREABlIc.js +0 -69
- package/dist/client/assets/index-nqJgfpl7.css +0 -1
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
import { readdir, stat } from 'fs/promises';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { CLAUDE_DIR } from '../constants.js';
|
|
4
|
+
import { PRICING, DEFAULT_PRICING_MODEL } from '../claude/config.js';
|
|
5
|
+
import { parseJsonl } from '../utils/jsonl.js';
|
|
6
|
+
import { extractTokenUsage, calculateCost, createEmptyTokenUsage } from './tokenUsage.js';
|
|
7
|
+
import { getProjectName } from '../claude/projects/repository.js';
|
|
8
|
+
import { collectAgentDescriptions } from '../claude/sessions/agents.js';
|
|
9
|
+
import { shouldSkipSession, isAgentSession, isEmptyFile } from '../claude/sessions/filters.js';
|
|
10
|
+
/**
|
|
11
|
+
* Format date to YYYY-MM-DD in local timezone
|
|
12
|
+
*/
|
|
13
|
+
function formatDateLocal(date) {
|
|
14
|
+
const year = date.getFullYear();
|
|
15
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
16
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
17
|
+
return `${year}-${month}-${day}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create empty aggregated data structure
|
|
21
|
+
*/
|
|
22
|
+
function createEmptyAggregatedData() {
|
|
23
|
+
const hourlyMap = new Map();
|
|
24
|
+
const weekdayMap = new Map();
|
|
25
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
26
|
+
hourlyMap.set(hour, {
|
|
27
|
+
tokenUsage: createEmptyTokenUsage(),
|
|
28
|
+
sessionIds: new Set(),
|
|
29
|
+
messageCount: 0
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
for (let weekday = 0; weekday < 7; weekday++) {
|
|
33
|
+
weekdayMap.set(weekday, {
|
|
34
|
+
tokenUsage: createEmptyTokenUsage(),
|
|
35
|
+
sessionIds: new Set(),
|
|
36
|
+
messageCount: 0
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
dailyMap: new Map(),
|
|
41
|
+
projectMap: new Map(),
|
|
42
|
+
modelMap: new Map(),
|
|
43
|
+
hourlyMap,
|
|
44
|
+
weekdayMap,
|
|
45
|
+
toolUsageMap: new Map(),
|
|
46
|
+
totalMessages: 0,
|
|
47
|
+
totalSessions: 0,
|
|
48
|
+
minDate: null,
|
|
49
|
+
maxDate: null,
|
|
50
|
+
totalCacheCreation: 0,
|
|
51
|
+
totalCacheRead: 0,
|
|
52
|
+
ephemeral5mTokens: 0,
|
|
53
|
+
ephemeral1hTokens: 0,
|
|
54
|
+
allUsages: [],
|
|
55
|
+
allCosts: [],
|
|
56
|
+
totalAgentSessions: 0
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Process a single message and update aggregated data
|
|
61
|
+
*/
|
|
62
|
+
function processMessage(message, sessionId, projectId, data, toolUseIdMap // Maps tool_use_id to tool name
|
|
63
|
+
) {
|
|
64
|
+
// Process tool_result first (before extractTokenUsage check) since user messages don't have usage
|
|
65
|
+
if (message.type === 'user' && message.message?.content) {
|
|
66
|
+
const content = Array.isArray(message.message.content)
|
|
67
|
+
? message.message.content
|
|
68
|
+
: [{ type: 'text', text: message.message.content }];
|
|
69
|
+
for (const item of content) {
|
|
70
|
+
if (item.type === 'tool_result') {
|
|
71
|
+
const toolUseId = item.tool_use_id;
|
|
72
|
+
const toolName = toolUseIdMap.get(toolUseId);
|
|
73
|
+
// Count as successful if:
|
|
74
|
+
// 1. We can find the tool name from toolUseIdMap
|
|
75
|
+
// 2. Either is_error is false/undefined, OR there's no error field at all
|
|
76
|
+
if (toolName) {
|
|
77
|
+
const isError = item.is_error === true;
|
|
78
|
+
if (!isError) {
|
|
79
|
+
const stats = data.toolUsageMap.get(toolName);
|
|
80
|
+
if (stats) {
|
|
81
|
+
stats.successful++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const result = extractTokenUsage(message);
|
|
89
|
+
if (!result)
|
|
90
|
+
return;
|
|
91
|
+
const { usage, model } = result;
|
|
92
|
+
const messageDate = new Date(message.timestamp);
|
|
93
|
+
// Update min/max dates
|
|
94
|
+
if (!data.minDate || messageDate < data.minDate) {
|
|
95
|
+
data.minDate = messageDate;
|
|
96
|
+
}
|
|
97
|
+
if (!data.maxDate || messageDate > data.maxDate) {
|
|
98
|
+
data.maxDate = messageDate;
|
|
99
|
+
}
|
|
100
|
+
data.totalMessages++;
|
|
101
|
+
const pricing = PRICING[model] || PRICING[DEFAULT_PRICING_MODEL];
|
|
102
|
+
data.allUsages.push(usage);
|
|
103
|
+
const costBreakdown = calculateCost(usage, pricing);
|
|
104
|
+
data.allCosts.push(costBreakdown.totalCost);
|
|
105
|
+
// Update daily map
|
|
106
|
+
const dateKey = formatDateLocal(messageDate);
|
|
107
|
+
if (!data.dailyMap.has(dateKey)) {
|
|
108
|
+
data.dailyMap.set(dateKey, {
|
|
109
|
+
tokenUsage: createEmptyTokenUsage(),
|
|
110
|
+
sessionIds: new Set()
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const dailyData = data.dailyMap.get(dateKey);
|
|
114
|
+
dailyData.tokenUsage.inputTokens += usage.inputTokens;
|
|
115
|
+
dailyData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
116
|
+
dailyData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
|
|
117
|
+
dailyData.tokenUsage.outputTokens += usage.outputTokens;
|
|
118
|
+
dailyData.tokenUsage.totalTokens += usage.totalTokens;
|
|
119
|
+
dailyData.sessionIds.add(sessionId);
|
|
120
|
+
// Update hourly map
|
|
121
|
+
const hour = messageDate.getHours();
|
|
122
|
+
const hourData = data.hourlyMap.get(hour);
|
|
123
|
+
hourData.tokenUsage.inputTokens += usage.inputTokens;
|
|
124
|
+
hourData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
125
|
+
hourData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
|
|
126
|
+
hourData.tokenUsage.outputTokens += usage.outputTokens;
|
|
127
|
+
hourData.tokenUsage.totalTokens += usage.totalTokens;
|
|
128
|
+
hourData.sessionIds.add(sessionId);
|
|
129
|
+
hourData.messageCount++;
|
|
130
|
+
// Update weekday map
|
|
131
|
+
const weekday = messageDate.getDay();
|
|
132
|
+
const weekdayData = data.weekdayMap.get(weekday);
|
|
133
|
+
weekdayData.tokenUsage.inputTokens += usage.inputTokens;
|
|
134
|
+
weekdayData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
135
|
+
weekdayData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
|
|
136
|
+
weekdayData.tokenUsage.outputTokens += usage.outputTokens;
|
|
137
|
+
weekdayData.tokenUsage.totalTokens += usage.totalTokens;
|
|
138
|
+
weekdayData.sessionIds.add(sessionId);
|
|
139
|
+
weekdayData.messageCount++;
|
|
140
|
+
// Update project map
|
|
141
|
+
if (!data.projectMap.has(projectId)) {
|
|
142
|
+
data.projectMap.set(projectId, {
|
|
143
|
+
tokenUsage: createEmptyTokenUsage(),
|
|
144
|
+
sessionIds: new Set(),
|
|
145
|
+
name: getProjectName(projectId)
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const projectData = data.projectMap.get(projectId);
|
|
149
|
+
projectData.tokenUsage.inputTokens += usage.inputTokens;
|
|
150
|
+
projectData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
151
|
+
projectData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
|
|
152
|
+
projectData.tokenUsage.outputTokens += usage.outputTokens;
|
|
153
|
+
projectData.tokenUsage.totalTokens += usage.totalTokens;
|
|
154
|
+
// Update model map
|
|
155
|
+
if (!data.modelMap.has(model)) {
|
|
156
|
+
data.modelMap.set(model, {
|
|
157
|
+
tokenUsage: createEmptyTokenUsage(),
|
|
158
|
+
messageCount: 0
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
const modelData = data.modelMap.get(model);
|
|
162
|
+
modelData.tokenUsage.inputTokens += usage.inputTokens;
|
|
163
|
+
modelData.tokenUsage.cacheCreationTokens += usage.cacheCreationTokens;
|
|
164
|
+
modelData.tokenUsage.cacheReadTokens += usage.cacheReadTokens;
|
|
165
|
+
modelData.tokenUsage.outputTokens += usage.outputTokens;
|
|
166
|
+
modelData.tokenUsage.totalTokens += usage.totalTokens;
|
|
167
|
+
modelData.messageCount++;
|
|
168
|
+
// Update cache stats
|
|
169
|
+
data.totalCacheCreation += usage.cacheCreationTokens;
|
|
170
|
+
data.totalCacheRead += usage.cacheReadTokens;
|
|
171
|
+
const cacheCreation = message.message?.usage?.cache_creation;
|
|
172
|
+
if (cacheCreation) {
|
|
173
|
+
data.ephemeral5mTokens += cacheCreation.ephemeral_5m_input_tokens || 0;
|
|
174
|
+
data.ephemeral1hTokens += cacheCreation.ephemeral_1h_input_tokens || 0;
|
|
175
|
+
}
|
|
176
|
+
// Process tool_use in assistant messages
|
|
177
|
+
if (message.message?.role === 'assistant' && message.message?.content) {
|
|
178
|
+
const content = Array.isArray(message.message.content)
|
|
179
|
+
? message.message.content
|
|
180
|
+
: [{ type: 'text', text: message.message.content }];
|
|
181
|
+
for (const item of content) {
|
|
182
|
+
if (item.type === 'tool_use') {
|
|
183
|
+
const toolName = item.name;
|
|
184
|
+
const toolUseId = item.id;
|
|
185
|
+
// Track tool_use_id to tool name mapping
|
|
186
|
+
if (toolUseId && toolName) {
|
|
187
|
+
toolUseIdMap.set(toolUseId, toolName);
|
|
188
|
+
// Increment total uses
|
|
189
|
+
if (!data.toolUsageMap.has(toolName)) {
|
|
190
|
+
data.toolUsageMap.set(toolName, { total: 0, successful: 0 });
|
|
191
|
+
}
|
|
192
|
+
data.toolUsageMap.get(toolName).total++;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Aggregate statistics for all projects
|
|
200
|
+
*/
|
|
201
|
+
export async function aggregateAllProjects(cutoffDate) {
|
|
202
|
+
const data = createEmptyAggregatedData();
|
|
203
|
+
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
204
|
+
const projects = await readdir(projectsDir);
|
|
205
|
+
for (const project of projects) {
|
|
206
|
+
const projectPath = join(projectsDir, project);
|
|
207
|
+
const projectStat = await stat(projectPath);
|
|
208
|
+
if (!projectStat.isDirectory())
|
|
209
|
+
continue;
|
|
210
|
+
const files = await readdir(projectPath);
|
|
211
|
+
const name = getProjectName(project);
|
|
212
|
+
if (!data.projectMap.has(project)) {
|
|
213
|
+
data.projectMap.set(project, {
|
|
214
|
+
tokenUsage: createEmptyTokenUsage(),
|
|
215
|
+
sessionIds: new Set(),
|
|
216
|
+
name
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
for (const file of files) {
|
|
220
|
+
if (!file.endsWith('.jsonl'))
|
|
221
|
+
continue;
|
|
222
|
+
const filePath = join(projectPath, file);
|
|
223
|
+
const sessionId = file.replace('.jsonl', '');
|
|
224
|
+
if (isAgentSession(sessionId))
|
|
225
|
+
continue;
|
|
226
|
+
const fileStat = await stat(filePath);
|
|
227
|
+
if (isEmptyFile(fileStat.size))
|
|
228
|
+
continue;
|
|
229
|
+
const messages = await parseJsonl(filePath);
|
|
230
|
+
if (shouldSkipSession(messages))
|
|
231
|
+
continue;
|
|
232
|
+
const agentDescriptions = collectAgentDescriptions(messages);
|
|
233
|
+
const isAgent = agentDescriptions.size > 0;
|
|
234
|
+
if (isAgent) {
|
|
235
|
+
data.totalAgentSessions++;
|
|
236
|
+
}
|
|
237
|
+
const projectData = data.projectMap.get(project);
|
|
238
|
+
projectData.sessionIds.add(sessionId);
|
|
239
|
+
data.totalSessions++;
|
|
240
|
+
// Track tool_use_id to tool name mapping for this session
|
|
241
|
+
const toolUseIdMap = new Map();
|
|
242
|
+
for (const message of messages) {
|
|
243
|
+
const messageDate = new Date(message.timestamp);
|
|
244
|
+
if (messageDate < cutoffDate)
|
|
245
|
+
continue;
|
|
246
|
+
processMessage(message, sessionId, project, data, toolUseIdMap);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return data;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Aggregate statistics for a single project
|
|
254
|
+
*/
|
|
255
|
+
export async function aggregateProject(projectId, cutoffDate) {
|
|
256
|
+
const data = createEmptyAggregatedData();
|
|
257
|
+
const projectsDir = join(CLAUDE_DIR, 'projects');
|
|
258
|
+
const projectPath = join(projectsDir, projectId);
|
|
259
|
+
const files = await readdir(projectPath);
|
|
260
|
+
const name = getProjectName(projectId);
|
|
261
|
+
for (const file of files) {
|
|
262
|
+
if (!file.endsWith('.jsonl'))
|
|
263
|
+
continue;
|
|
264
|
+
const filePath = join(projectPath, file);
|
|
265
|
+
const sessionId = file.replace('.jsonl', '');
|
|
266
|
+
if (isAgentSession(sessionId))
|
|
267
|
+
continue;
|
|
268
|
+
const fileStat = await stat(filePath);
|
|
269
|
+
if (isEmptyFile(fileStat.size))
|
|
270
|
+
continue;
|
|
271
|
+
const messages = await parseJsonl(filePath);
|
|
272
|
+
if (shouldSkipSession(messages))
|
|
273
|
+
continue;
|
|
274
|
+
const agentDescriptions = collectAgentDescriptions(messages);
|
|
275
|
+
const isAgent = agentDescriptions.size > 0;
|
|
276
|
+
if (isAgent) {
|
|
277
|
+
data.totalAgentSessions++;
|
|
278
|
+
}
|
|
279
|
+
data.totalSessions++;
|
|
280
|
+
// Track tool_use_id to tool name mapping for this session
|
|
281
|
+
const toolUseIdMap = new Map();
|
|
282
|
+
for (const message of messages) {
|
|
283
|
+
const messageDate = new Date(message.timestamp);
|
|
284
|
+
if (messageDate < cutoffDate)
|
|
285
|
+
continue;
|
|
286
|
+
processMessage(message, sessionId, projectId, data, toolUseIdMap);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return data;
|
|
290
|
+
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { PRICING, DEFAULT_PRICING_MODEL, WEEKDAY_NAMES } from '../claude/config.js';
|
|
2
|
+
import { aggregateTokenUsage, calculateCost } from './tokenUsage.js';
|
|
3
|
+
import { fillMissingDates } from './utils.js';
|
|
4
|
+
import { aggregateAllProjects, aggregateProject } from './aggregator.js';
|
|
5
|
+
/**
|
|
6
|
+
* Calculate cutoff date based on days parameter
|
|
7
|
+
*/
|
|
8
|
+
function calculateCutoffDate(days) {
|
|
9
|
+
const cutoffDate = new Date();
|
|
10
|
+
if (days === 'all') {
|
|
11
|
+
cutoffDate.setFullYear(2000, 0, 1);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
const daysNum = typeof days === 'string' ? parseInt(days, 10) : days;
|
|
15
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysNum);
|
|
16
|
+
cutoffDate.setHours(0, 0, 0, 0);
|
|
17
|
+
}
|
|
18
|
+
return cutoffDate;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Get overall usage statistics across all projects
|
|
22
|
+
*/
|
|
23
|
+
export async function getOverallStatistics(days = 7) {
|
|
24
|
+
const daysParam = days.toString();
|
|
25
|
+
const cutoffDate = calculateCutoffDate(days);
|
|
26
|
+
const data = await aggregateAllProjects(cutoffDate);
|
|
27
|
+
// Calculate totals
|
|
28
|
+
const totalUsage = aggregateTokenUsage(data.allUsages);
|
|
29
|
+
const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
|
|
30
|
+
const costBreakdown = calculateCost(totalUsage, defaultPricing);
|
|
31
|
+
const savedCost = (data.totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
|
|
32
|
+
// Determine date range first (this drives all other date-based statistics)
|
|
33
|
+
const startDate = daysParam === 'all' && data.minDate
|
|
34
|
+
? data.minDate
|
|
35
|
+
: cutoffDate;
|
|
36
|
+
const endDate = new Date();
|
|
37
|
+
// Fill daily stats based on the determined date range
|
|
38
|
+
const daily = fillMissingDates(data.dailyMap, startDate, endDate);
|
|
39
|
+
// Build project stats
|
|
40
|
+
const byProject = Array.from(data.projectMap.entries())
|
|
41
|
+
.map(([project, projectData]) => ({
|
|
42
|
+
id: project,
|
|
43
|
+
name: projectData.name,
|
|
44
|
+
tokenUsage: projectData.tokenUsage,
|
|
45
|
+
sessionCount: projectData.sessionIds.size
|
|
46
|
+
}))
|
|
47
|
+
.sort((a, b) => b.tokenUsage.totalTokens - a.tokenUsage.totalTokens);
|
|
48
|
+
// Build model stats
|
|
49
|
+
const byModel = Array.from(data.modelMap.entries())
|
|
50
|
+
.map(([model, modelData]) => ({
|
|
51
|
+
model,
|
|
52
|
+
tokenUsage: modelData.tokenUsage,
|
|
53
|
+
messageCount: modelData.messageCount
|
|
54
|
+
}))
|
|
55
|
+
.sort((a, b) => b.tokenUsage.totalTokens - a.tokenUsage.totalTokens);
|
|
56
|
+
// Build tool usage stats
|
|
57
|
+
const toolUsage = Array.from(data.toolUsageMap.entries())
|
|
58
|
+
.map(([toolName, stats]) => ({
|
|
59
|
+
toolName,
|
|
60
|
+
totalUses: stats.total,
|
|
61
|
+
successfulUses: stats.successful,
|
|
62
|
+
successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
|
|
63
|
+
}))
|
|
64
|
+
.sort((a, b) => b.totalUses - a.totalUses);
|
|
65
|
+
const totalToolCalls = Array.from(data.toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
|
|
66
|
+
const agentUsageRate = data.totalSessions > 0 ? (data.totalAgentSessions / data.totalSessions) * 100 : 0;
|
|
67
|
+
// Build hourly stats
|
|
68
|
+
const byHour = Array.from(data.hourlyMap.entries())
|
|
69
|
+
.map(([hour, hourData]) => ({
|
|
70
|
+
hour,
|
|
71
|
+
sessionCount: hourData.sessionIds.size,
|
|
72
|
+
messageCount: hourData.messageCount,
|
|
73
|
+
tokenUsage: hourData.tokenUsage
|
|
74
|
+
}))
|
|
75
|
+
.sort((a, b) => a.hour - b.hour);
|
|
76
|
+
// Build weekday stats
|
|
77
|
+
const byWeekday = Array.from(data.weekdayMap.entries())
|
|
78
|
+
.map(([weekday, weekdayData]) => ({
|
|
79
|
+
weekday,
|
|
80
|
+
weekdayName: WEEKDAY_NAMES[weekday],
|
|
81
|
+
sessionCount: weekdayData.sessionIds.size,
|
|
82
|
+
messageCount: weekdayData.messageCount,
|
|
83
|
+
tokenUsage: weekdayData.tokenUsage
|
|
84
|
+
}))
|
|
85
|
+
.sort((a, b) => a.weekday - b.weekday);
|
|
86
|
+
const statistics = {
|
|
87
|
+
overview: {
|
|
88
|
+
tokenUsage: totalUsage,
|
|
89
|
+
sessionCount: data.totalSessions,
|
|
90
|
+
messageCount: data.totalMessages,
|
|
91
|
+
dateRange: {
|
|
92
|
+
start: startDate?.toISOString() || endDate.toISOString(),
|
|
93
|
+
end: endDate.toISOString()
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
daily,
|
|
97
|
+
byProject,
|
|
98
|
+
byModel,
|
|
99
|
+
cache: {
|
|
100
|
+
totalCacheCreation: data.totalCacheCreation,
|
|
101
|
+
totalCacheRead: data.totalCacheRead,
|
|
102
|
+
ephemeral5mTokens: data.ephemeral5mTokens,
|
|
103
|
+
ephemeral1hTokens: data.ephemeral1hTokens,
|
|
104
|
+
cacheHitRate: (data.totalCacheCreation + data.totalCacheRead) > 0
|
|
105
|
+
? (data.totalCacheRead / (data.totalCacheCreation + data.totalCacheRead)) * 100
|
|
106
|
+
: 0,
|
|
107
|
+
estimatedSavings: savedCost
|
|
108
|
+
},
|
|
109
|
+
cost: costBreakdown,
|
|
110
|
+
productivity: {
|
|
111
|
+
toolUsage,
|
|
112
|
+
totalToolCalls,
|
|
113
|
+
agentSessions: data.totalAgentSessions,
|
|
114
|
+
totalSessions: data.totalSessions,
|
|
115
|
+
agentUsageRate
|
|
116
|
+
},
|
|
117
|
+
trends: {
|
|
118
|
+
byHour,
|
|
119
|
+
byWeekday
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
return statistics;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Get usage statistics for a specific project
|
|
126
|
+
*/
|
|
127
|
+
export async function getProjectStatistics(projectId, days = 7) {
|
|
128
|
+
const daysParam = days.toString();
|
|
129
|
+
const cutoffDate = calculateCutoffDate(days);
|
|
130
|
+
const data = await aggregateProject(projectId, cutoffDate);
|
|
131
|
+
// Calculate totals
|
|
132
|
+
const totalUsage = aggregateTokenUsage(data.allUsages);
|
|
133
|
+
const defaultPricing = PRICING[DEFAULT_PRICING_MODEL];
|
|
134
|
+
const costBreakdown = calculateCost(totalUsage, defaultPricing);
|
|
135
|
+
const savedCost = (data.totalCacheRead / 1000000) * (defaultPricing.input - defaultPricing.cacheRead);
|
|
136
|
+
// Determine date range first (this drives all other date-based statistics)
|
|
137
|
+
const startDate = daysParam === 'all' && data.minDate
|
|
138
|
+
? data.minDate
|
|
139
|
+
: cutoffDate;
|
|
140
|
+
const endDate = new Date();
|
|
141
|
+
// Fill daily stats based on the determined date range
|
|
142
|
+
const daily = fillMissingDates(data.dailyMap, startDate, endDate);
|
|
143
|
+
// Build project stats (single project)
|
|
144
|
+
const projectData = data.projectMap.get(projectId);
|
|
145
|
+
const byProject = projectData ? [{
|
|
146
|
+
id: projectId,
|
|
147
|
+
name: projectData.name,
|
|
148
|
+
tokenUsage: totalUsage,
|
|
149
|
+
sessionCount: data.totalSessions
|
|
150
|
+
}] : [];
|
|
151
|
+
// Build model stats
|
|
152
|
+
const byModel = Array.from(data.modelMap.entries())
|
|
153
|
+
.map(([model, modelData]) => ({
|
|
154
|
+
model,
|
|
155
|
+
tokenUsage: modelData.tokenUsage,
|
|
156
|
+
messageCount: modelData.messageCount
|
|
157
|
+
}))
|
|
158
|
+
.sort((a, b) => b.tokenUsage.totalTokens - a.tokenUsage.totalTokens);
|
|
159
|
+
// Build tool usage stats
|
|
160
|
+
const toolUsage = Array.from(data.toolUsageMap.entries())
|
|
161
|
+
.map(([toolName, stats]) => ({
|
|
162
|
+
toolName,
|
|
163
|
+
totalUses: stats.total,
|
|
164
|
+
successfulUses: stats.successful,
|
|
165
|
+
successRate: stats.total > 0 ? (stats.successful / stats.total) * 100 : 0
|
|
166
|
+
}))
|
|
167
|
+
.sort((a, b) => b.totalUses - a.totalUses);
|
|
168
|
+
const totalToolCalls = Array.from(data.toolUsageMap.values()).reduce((sum, stats) => sum + stats.total, 0);
|
|
169
|
+
const agentUsageRate = data.totalSessions > 0 ? (data.totalAgentSessions / data.totalSessions) * 100 : 0;
|
|
170
|
+
// Build hourly stats
|
|
171
|
+
const byHour = Array.from(data.hourlyMap.entries())
|
|
172
|
+
.map(([hour, hourData]) => ({
|
|
173
|
+
hour,
|
|
174
|
+
sessionCount: hourData.sessionIds.size,
|
|
175
|
+
messageCount: hourData.messageCount,
|
|
176
|
+
tokenUsage: hourData.tokenUsage
|
|
177
|
+
}))
|
|
178
|
+
.sort((a, b) => a.hour - b.hour);
|
|
179
|
+
// Build weekday stats
|
|
180
|
+
const byWeekday = Array.from(data.weekdayMap.entries())
|
|
181
|
+
.map(([weekday, weekdayData]) => ({
|
|
182
|
+
weekday,
|
|
183
|
+
weekdayName: WEEKDAY_NAMES[weekday],
|
|
184
|
+
sessionCount: weekdayData.sessionIds.size,
|
|
185
|
+
messageCount: weekdayData.messageCount,
|
|
186
|
+
tokenUsage: weekdayData.tokenUsage
|
|
187
|
+
}))
|
|
188
|
+
.sort((a, b) => a.weekday - b.weekday);
|
|
189
|
+
const statistics = {
|
|
190
|
+
overview: {
|
|
191
|
+
tokenUsage: totalUsage,
|
|
192
|
+
sessionCount: data.totalSessions,
|
|
193
|
+
messageCount: data.totalMessages,
|
|
194
|
+
dateRange: {
|
|
195
|
+
start: startDate?.toISOString() || endDate.toISOString(),
|
|
196
|
+
end: endDate.toISOString()
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
daily,
|
|
200
|
+
byProject,
|
|
201
|
+
byModel,
|
|
202
|
+
cache: {
|
|
203
|
+
totalCacheCreation: data.totalCacheCreation,
|
|
204
|
+
totalCacheRead: data.totalCacheRead,
|
|
205
|
+
ephemeral5mTokens: data.ephemeral5mTokens,
|
|
206
|
+
ephemeral1hTokens: data.ephemeral1hTokens,
|
|
207
|
+
cacheHitRate: (data.totalCacheCreation + data.totalCacheRead) > 0
|
|
208
|
+
? (data.totalCacheRead / (data.totalCacheCreation + data.totalCacheRead)) * 100
|
|
209
|
+
: 0,
|
|
210
|
+
estimatedSavings: savedCost
|
|
211
|
+
},
|
|
212
|
+
cost: costBreakdown,
|
|
213
|
+
productivity: {
|
|
214
|
+
toolUsage,
|
|
215
|
+
totalToolCalls,
|
|
216
|
+
agentSessions: data.totalAgentSessions,
|
|
217
|
+
totalSessions: data.totalSessions,
|
|
218
|
+
agentUsageRate
|
|
219
|
+
},
|
|
220
|
+
trends: {
|
|
221
|
+
byHour,
|
|
222
|
+
byWeekday
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
return statistics;
|
|
226
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token statistics utilities
|
|
3
|
+
* Pure calculation functions for token usage analysis
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract token usage from a message object
|
|
7
|
+
*/
|
|
8
|
+
export function extractTokenUsage(message) {
|
|
9
|
+
if (message.type !== 'assistant' || !message.message?.usage) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const usage = message.message.usage;
|
|
13
|
+
const model = message.message.model || 'unknown';
|
|
14
|
+
return {
|
|
15
|
+
usage: {
|
|
16
|
+
inputTokens: usage.input_tokens || 0,
|
|
17
|
+
cacheCreationTokens: usage.cache_creation_input_tokens || 0,
|
|
18
|
+
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
19
|
+
outputTokens: usage.output_tokens || 0,
|
|
20
|
+
totalTokens: (usage.input_tokens || 0) +
|
|
21
|
+
(usage.cache_creation_input_tokens || 0) +
|
|
22
|
+
(usage.cache_read_input_tokens || 0) +
|
|
23
|
+
(usage.output_tokens || 0)
|
|
24
|
+
},
|
|
25
|
+
model
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Aggregate multiple token usages into a single total
|
|
30
|
+
*/
|
|
31
|
+
export function aggregateTokenUsage(usages) {
|
|
32
|
+
return usages.reduce((acc, usage) => ({
|
|
33
|
+
inputTokens: acc.inputTokens + usage.inputTokens,
|
|
34
|
+
cacheCreationTokens: acc.cacheCreationTokens + usage.cacheCreationTokens,
|
|
35
|
+
cacheReadTokens: acc.cacheReadTokens + usage.cacheReadTokens,
|
|
36
|
+
outputTokens: acc.outputTokens + usage.outputTokens,
|
|
37
|
+
totalTokens: acc.totalTokens + usage.totalTokens
|
|
38
|
+
}), createEmptyTokenUsage());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculate cost for token usage based on pricing
|
|
42
|
+
*/
|
|
43
|
+
export function calculateCost(usage, pricing) {
|
|
44
|
+
return {
|
|
45
|
+
inputCost: (usage.inputTokens / 1000000) * pricing.input,
|
|
46
|
+
outputCost: (usage.outputTokens / 1000000) * pricing.output,
|
|
47
|
+
cacheCreationCost: (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation,
|
|
48
|
+
cacheReadCost: (usage.cacheReadTokens / 1000000) * pricing.cacheRead,
|
|
49
|
+
totalCost: (usage.inputTokens / 1000000) * pricing.input +
|
|
50
|
+
(usage.outputTokens / 1000000) * pricing.output +
|
|
51
|
+
(usage.cacheCreationTokens / 1000000) * pricing.cacheCreation +
|
|
52
|
+
(usage.cacheReadTokens / 1000000) * pricing.cacheRead
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create an empty token usage object
|
|
57
|
+
*/
|
|
58
|
+
export function createEmptyTokenUsage() {
|
|
59
|
+
return {
|
|
60
|
+
inputTokens: 0,
|
|
61
|
+
cacheCreationTokens: 0,
|
|
62
|
+
cacheReadTokens: 0,
|
|
63
|
+
outputTokens: 0,
|
|
64
|
+
totalTokens: 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token statistics utilities
|
|
3
|
+
* Pure calculation functions for token usage analysis
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* Extract token usage from a message object
|
|
7
|
+
*/
|
|
8
|
+
export function extractTokenUsage(message) {
|
|
9
|
+
if (message.type !== 'assistant' || !message.message?.usage) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const usage = message.message.usage;
|
|
13
|
+
const model = message.message.model || 'unknown';
|
|
14
|
+
return {
|
|
15
|
+
usage: {
|
|
16
|
+
inputTokens: usage.input_tokens || 0,
|
|
17
|
+
cacheCreationTokens: usage.cache_creation_input_tokens || 0,
|
|
18
|
+
cacheReadTokens: usage.cache_read_input_tokens || 0,
|
|
19
|
+
outputTokens: usage.output_tokens || 0,
|
|
20
|
+
totalTokens: (usage.input_tokens || 0) +
|
|
21
|
+
(usage.cache_creation_input_tokens || 0) +
|
|
22
|
+
(usage.cache_read_input_tokens || 0) +
|
|
23
|
+
(usage.output_tokens || 0)
|
|
24
|
+
},
|
|
25
|
+
model
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Aggregate multiple token usages into a single total
|
|
30
|
+
*/
|
|
31
|
+
export function aggregateTokenUsage(usages) {
|
|
32
|
+
return usages.reduce((acc, usage) => ({
|
|
33
|
+
inputTokens: acc.inputTokens + usage.inputTokens,
|
|
34
|
+
cacheCreationTokens: acc.cacheCreationTokens + usage.cacheCreationTokens,
|
|
35
|
+
cacheReadTokens: acc.cacheReadTokens + usage.cacheReadTokens,
|
|
36
|
+
outputTokens: acc.outputTokens + usage.outputTokens,
|
|
37
|
+
totalTokens: acc.totalTokens + usage.totalTokens
|
|
38
|
+
}), createEmptyTokenUsage());
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Calculate cost for token usage based on pricing
|
|
42
|
+
*/
|
|
43
|
+
export function calculateCost(usage, pricing) {
|
|
44
|
+
return {
|
|
45
|
+
inputCost: (usage.inputTokens / 1000000) * pricing.input,
|
|
46
|
+
outputCost: (usage.outputTokens / 1000000) * pricing.output,
|
|
47
|
+
cacheCreationCost: (usage.cacheCreationTokens / 1000000) * pricing.cacheCreation,
|
|
48
|
+
cacheReadCost: (usage.cacheReadTokens / 1000000) * pricing.cacheRead,
|
|
49
|
+
totalCost: (usage.inputTokens / 1000000) * pricing.input +
|
|
50
|
+
(usage.outputTokens / 1000000) * pricing.output +
|
|
51
|
+
(usage.cacheCreationTokens / 1000000) * pricing.cacheCreation +
|
|
52
|
+
(usage.cacheReadTokens / 1000000) * pricing.cacheRead
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create an empty token usage object
|
|
57
|
+
*/
|
|
58
|
+
export function createEmptyTokenUsage() {
|
|
59
|
+
return {
|
|
60
|
+
inputTokens: 0,
|
|
61
|
+
cacheCreationTokens: 0,
|
|
62
|
+
cacheReadTokens: 0,
|
|
63
|
+
outputTokens: 0,
|
|
64
|
+
totalTokens: 0
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createEmptyTokenUsage } from './tokenUsage.js';
|
|
2
|
+
/**
|
|
3
|
+
* Format date to YYYY-MM-DD in local timezone
|
|
4
|
+
*/
|
|
5
|
+
function formatDateLocal(date) {
|
|
6
|
+
const year = date.getFullYear();
|
|
7
|
+
const month = String(date.getMonth() + 1).padStart(2, '0');
|
|
8
|
+
const day = String(date.getDate()).padStart(2, '0');
|
|
9
|
+
return `${year}-${month}-${day}`;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Fill in missing dates in daily statistics with zero values
|
|
13
|
+
* @param dailyMap Map of date string to usage data
|
|
14
|
+
* @param startDate Start date for the range (inclusive)
|
|
15
|
+
* @param endDate End date for the range (inclusive)
|
|
16
|
+
* @returns Array of daily stats with all dates filled in
|
|
17
|
+
*/
|
|
18
|
+
export function fillMissingDates(dailyMap, startDate, endDate) {
|
|
19
|
+
const daily = [];
|
|
20
|
+
const currentDate = new Date(startDate);
|
|
21
|
+
const end = new Date(endDate);
|
|
22
|
+
while (currentDate <= end) {
|
|
23
|
+
const dateKey = formatDateLocal(currentDate);
|
|
24
|
+
const data = dailyMap.get(dateKey);
|
|
25
|
+
daily.push({
|
|
26
|
+
date: dateKey,
|
|
27
|
+
tokenUsage: data?.tokenUsage || createEmptyTokenUsage(),
|
|
28
|
+
sessionCount: data?.sessionIds.size || 0
|
|
29
|
+
});
|
|
30
|
+
currentDate.setDate(currentDate.getDate() + 1);
|
|
31
|
+
}
|
|
32
|
+
return daily;
|
|
33
|
+
}
|