feat: add project usage analytics with ECharts
This commit is contained in:
@@ -10,6 +10,7 @@ const DEFAULT_MAX_RANGE_DAYS = 370;
|
||||
const DEFAULT_RECENT_LIMIT = 50;
|
||||
const DEFAULT_MCP_TOOL_LIMIT = 200;
|
||||
const DEFAULT_SKILL_LIMIT = 100;
|
||||
const DEFAULT_PROJECT_LIMIT = 100;
|
||||
const DEFAULT_MCP_DETAIL_LIMIT = 250;
|
||||
const BUILD_BATCH_SIZE = 1;
|
||||
const UPSERT_DEBOUNCE_MS = 300;
|
||||
@@ -292,6 +293,7 @@ function createTrendBucket(date) {
|
||||
mcpCalls: 0,
|
||||
mcpFailures: 0,
|
||||
skillMentions: 0,
|
||||
projects: 0,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -316,9 +318,11 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
const recentLimit = clampInteger(params.recentLimit, DEFAULT_RECENT_LIMIT, 1, 100);
|
||||
const mcpToolLimit = clampInteger(params.mcpToolLimit, DEFAULT_MCP_TOOL_LIMIT, 1, 500);
|
||||
const skillLimit = clampInteger(params.skillLimit, DEFAULT_SKILL_LIMIT, 1, 500);
|
||||
const projectLimit = clampInteger(params.projectLimit, DEFAULT_PROJECT_LIMIT, 1, 500);
|
||||
const mcpDetailLimit = clampInteger(params.mcpDetailLimit, DEFAULT_MCP_DETAIL_LIMIT, 1, 500);
|
||||
const dayKey = createDayKeyFormatter(range.timeZone);
|
||||
const trendMap = createTrendBuckets(range, dayKey);
|
||||
const trendProjectKeys = new Map(Array.from(trendMap.keys(), (key) => [key, new Set()]));
|
||||
const overview = {
|
||||
newSessions: 0,
|
||||
messages: 0,
|
||||
@@ -327,10 +331,14 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
mcpCalls: 0,
|
||||
mcpFailures: 0,
|
||||
skillMentions: 0,
|
||||
projects: 0,
|
||||
projectSessions: 0,
|
||||
unassignedProjectSessions: 0,
|
||||
};
|
||||
const mcpStatus = { completed: 0, failed: 0, other: 0 };
|
||||
const mcpTools = new Map();
|
||||
const skills = new Map();
|
||||
const projects = new Map();
|
||||
const mcpRecentCalls = [];
|
||||
const recentSessions = [];
|
||||
let retainedMessages = 0;
|
||||
@@ -338,12 +346,15 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
for (const doc of documents || []) {
|
||||
if (!doc || !doc.sessionId) continue;
|
||||
retainedMessages += Math.max(0, Number(doc.sourceMessageCount || 0));
|
||||
const projectCwd = cleanDisplayText(String(doc.cwd || ''), 1200).replace(/\\/g, '/').replace(/\/+$/, '');
|
||||
const projectName = cleanDisplayText(String(doc.projectName || basenameFromCwd(projectCwd)), 240);
|
||||
const projectKey = projectName ? (projectCwd ? `cwd:${projectCwd}` : `name:${projectName}`) : '';
|
||||
const sessionSummary = {
|
||||
sessionId: doc.sessionId,
|
||||
title: doc.title || '未命名会话',
|
||||
agent: doc.agent || 'codex',
|
||||
cwd: doc.cwd || '',
|
||||
projectName: doc.projectName || '',
|
||||
projectName,
|
||||
created: doc.created || null,
|
||||
lastActivity: null,
|
||||
newSession: false,
|
||||
@@ -361,7 +372,10 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
sessionSummary.newSession = true;
|
||||
sessionSummary.lastActivity = new Date(createdMs).toISOString();
|
||||
const bucket = trendMap.get(dayKey(createdMs));
|
||||
if (bucket) bucket.newSessions += 1;
|
||||
if (bucket) {
|
||||
bucket.newSessions += 1;
|
||||
if (projectKey) trendProjectKeys.get(bucket.date)?.add(projectKey);
|
||||
}
|
||||
}
|
||||
|
||||
for (const event of Array.isArray(doc.events) ? doc.events : []) {
|
||||
@@ -373,6 +387,7 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
}
|
||||
const bucket = trendMap.get(dayKey(timestampMs));
|
||||
if (!bucket) continue;
|
||||
if (projectKey) trendProjectKeys.get(bucket.date)?.add(projectKey);
|
||||
|
||||
if (event.type === 'message') {
|
||||
overview.messages += 1;
|
||||
@@ -448,7 +463,34 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
}
|
||||
}
|
||||
|
||||
if (sessionSummary.lastActivity) recentSessions.push(sessionSummary);
|
||||
if (sessionSummary.lastActivity) {
|
||||
if (projectKey) {
|
||||
const current = projects.get(projectKey) || {
|
||||
key: projectKey,
|
||||
name: projectName,
|
||||
cwd: projectCwd,
|
||||
sessions: 0,
|
||||
messages: 0,
|
||||
mcpCalls: 0,
|
||||
lastActiveAt: null,
|
||||
};
|
||||
current.sessions += 1;
|
||||
current.messages += sessionSummary.messages;
|
||||
current.mcpCalls += sessionSummary.mcpCalls;
|
||||
if (!current.lastActiveAt || sessionSummary.lastActivity > current.lastActiveAt) {
|
||||
current.lastActiveAt = sessionSummary.lastActivity;
|
||||
}
|
||||
projects.set(projectKey, current);
|
||||
overview.projectSessions += 1;
|
||||
} else {
|
||||
overview.unassignedProjectSessions += 1;
|
||||
}
|
||||
recentSessions.push(sessionSummary);
|
||||
}
|
||||
}
|
||||
|
||||
for (const bucket of trendMap.values()) {
|
||||
bucket.projects = trendProjectKeys.get(bucket.date)?.size || 0;
|
||||
}
|
||||
|
||||
const mcpToolRows = Array.from(mcpTools.values());
|
||||
@@ -460,6 +502,12 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
|| a.server.localeCompare(b.server)
|
||||
|| a.tool.localeCompare(b.tool));
|
||||
const skillRows = Array.from(skills.values()).sort((a, b) => b.uses - a.uses || a.name.localeCompare(b.name));
|
||||
const projectRows = Array.from(projects.values()).sort((a, b) => b.messages - a.messages
|
||||
|| b.mcpCalls - a.mcpCalls
|
||||
|| b.sessions - a.sessions
|
||||
|| a.name.localeCompare(b.name)
|
||||
|| a.key.localeCompare(b.key));
|
||||
overview.projects = projectRows.length;
|
||||
recentSessions.sort((a, b) => String(b.lastActivity).localeCompare(String(a.lastActivity)) || a.sessionId.localeCompare(b.sessionId));
|
||||
mcpRecentCalls.sort((a, b) => b.timestamp.localeCompare(a.timestamp)
|
||||
|| a.server.localeCompare(b.server)
|
||||
@@ -479,6 +527,7 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
row.detailCallsReturned = returnedDetailCounts.get(`${row.server}\u0000${row.tool}`) || 0;
|
||||
}
|
||||
const returnedSkills = skillRows.slice(0, skillLimit);
|
||||
const returnedProjects = projectRows.slice(0, projectLimit);
|
||||
|
||||
return {
|
||||
schemaVersion: SCHEMA_VERSION,
|
||||
@@ -501,6 +550,8 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
returnedMcpTools: returnedMcpTools.length,
|
||||
distinctSkills: skillRows.length,
|
||||
returnedSkills: returnedSkills.length,
|
||||
distinctProjects: projectRows.length,
|
||||
returnedProjects: returnedProjects.length,
|
||||
lastBuiltAt: meta.lastBuiltAt || null,
|
||||
mcpTimestampBasis: 'assistant_message',
|
||||
skillBasis: 'explicit_composer_mention',
|
||||
@@ -511,12 +562,14 @@ function aggregateUsageStatistics(documents, params = {}, meta = {}) {
|
||||
limits: {
|
||||
mcpTools: mcpToolLimit,
|
||||
skills: skillLimit,
|
||||
projects: projectLimit,
|
||||
mcpRecentCalls: mcpDetailLimit,
|
||||
recentSessions: recentLimit,
|
||||
},
|
||||
mcpTools: returnedMcpTools,
|
||||
mcpRecentCalls: returnedMcpRecentCalls,
|
||||
skills: returnedSkills,
|
||||
projects: returnedProjects,
|
||||
recentSessions: recentSessions.slice(0, recentLimit),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user