@cryptiklemur/lattice 1.6.0 → 1.8.0
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/client/src/components/analytics/AnalyticsView.tsx +66 -0
- package/client/src/components/analytics/charts/ActivityCalendar.tsx +185 -0
- package/client/src/components/analytics/charts/DailySummaryCards.tsx +83 -0
- package/client/src/components/analytics/charts/HourlyHeatmap.tsx +129 -0
- package/client/src/components/analytics/charts/PermissionBreakdown.tsx +101 -0
- package/client/src/components/analytics/charts/ProjectRadar.tsx +122 -0
- package/client/src/components/analytics/charts/SessionComplexityList.tsx +67 -0
- package/client/src/components/analytics/charts/SessionTimeline.tsx +112 -0
- package/client/src/components/analytics/charts/ToolSunburst.tsx +126 -0
- package/client/src/components/analytics/charts/ToolTreemap.tsx +108 -0
- package/package.json +1 -1
- package/server/src/analytics/engine.ts +219 -0
- package/shared/src/analytics.ts +11 -0
|
@@ -520,6 +520,216 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
|
|
|
520
520
|
|
|
521
521
|
var tokenFlowSankey: AnalyticsPayload["tokenFlowSankey"] = { nodes: sankeyNodes, links: sankeyLinks };
|
|
522
522
|
|
|
523
|
+
var activityCalendarMap = new Map<string, { count: number; tokens: number; cost: number }>();
|
|
524
|
+
for (var aci = 0; aci < filtered.length; aci++) {
|
|
525
|
+
var acSess = filtered[aci];
|
|
526
|
+
var acDate = formatDate(acSess.endTime > 0 ? acSess.endTime : acSess.startTime);
|
|
527
|
+
var acEntry = activityCalendarMap.get(acDate);
|
|
528
|
+
if (!acEntry) {
|
|
529
|
+
acEntry = { count: 0, tokens: 0, cost: 0 };
|
|
530
|
+
activityCalendarMap.set(acDate, acEntry);
|
|
531
|
+
}
|
|
532
|
+
acEntry.count++;
|
|
533
|
+
acEntry.tokens += acSess.inputTokens + acSess.outputTokens;
|
|
534
|
+
acEntry.cost += acSess.cost;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
var activityCalendar: AnalyticsPayload["activityCalendar"] = [];
|
|
538
|
+
if (dates.length > 0) {
|
|
539
|
+
var calStart = new Date(dates[0]);
|
|
540
|
+
var calEnd = new Date(dates[dates.length - 1]);
|
|
541
|
+
var calCursor = new Date(calStart);
|
|
542
|
+
while (calCursor <= calEnd) {
|
|
543
|
+
var calKey = formatDate(calCursor.getTime());
|
|
544
|
+
var calData = activityCalendarMap.get(calKey);
|
|
545
|
+
activityCalendar.push({
|
|
546
|
+
date: calKey,
|
|
547
|
+
count: calData ? calData.count : 0,
|
|
548
|
+
tokens: calData ? calData.tokens : 0,
|
|
549
|
+
cost: calData ? calData.cost : 0,
|
|
550
|
+
});
|
|
551
|
+
calCursor.setDate(calCursor.getDate() + 1);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
var hourlyHeatmap: AnalyticsPayload["hourlyHeatmap"] = [];
|
|
556
|
+
var heatmapGrid = new Map<string, number>();
|
|
557
|
+
for (var hmi = 0; hmi < filtered.length; hmi++) {
|
|
558
|
+
var hmSess = filtered[hmi];
|
|
559
|
+
if (hmSess.startTime <= 0) continue;
|
|
560
|
+
var hmDate = new Date(hmSess.startTime);
|
|
561
|
+
var hmDay = hmDate.getDay();
|
|
562
|
+
var hmHour = hmDate.getHours();
|
|
563
|
+
var hmKey = hmDay + ":" + hmHour;
|
|
564
|
+
heatmapGrid.set(hmKey, (heatmapGrid.get(hmKey) || 0) + 1);
|
|
565
|
+
}
|
|
566
|
+
for (var hd = 0; hd < 7; hd++) {
|
|
567
|
+
for (var hh = 0; hh < 24; hh++) {
|
|
568
|
+
var hhKey = hd + ":" + hh;
|
|
569
|
+
hourlyHeatmap.push({ day: hd, hour: hh, count: heatmapGrid.get(hhKey) || 0 });
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
var sessionTimeline: AnalyticsPayload["sessionTimeline"] = [];
|
|
574
|
+
var tlSorted = filtered
|
|
575
|
+
.filter(function (s) { return s.startTime > 0 && s.endTime > 0; })
|
|
576
|
+
.sort(function (a, b) { return b.startTime - a.startTime; });
|
|
577
|
+
var tlCap = Math.min(tlSorted.length, 50);
|
|
578
|
+
for (var tli = 0; tli < tlCap; tli++) {
|
|
579
|
+
var tlSess = tlSorted[tli];
|
|
580
|
+
sessionTimeline.push({
|
|
581
|
+
id: tlSess.id,
|
|
582
|
+
title: tlSess.title,
|
|
583
|
+
project: tlSess.project,
|
|
584
|
+
start: tlSess.startTime,
|
|
585
|
+
end: tlSess.endTime,
|
|
586
|
+
cost: tlSess.cost,
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
var dailySummaryMap = new Map<string, { sessions: number; cost: number; tokens: number; tools: Map<string, number>; models: Map<string, number> }>();
|
|
591
|
+
for (var dsi = 0; dsi < filtered.length; dsi++) {
|
|
592
|
+
var dsSess = filtered[dsi];
|
|
593
|
+
var dsDate = formatDate(dsSess.endTime > 0 ? dsSess.endTime : dsSess.startTime);
|
|
594
|
+
var dsEntry = dailySummaryMap.get(dsDate);
|
|
595
|
+
if (!dsEntry) {
|
|
596
|
+
dsEntry = { sessions: 0, cost: 0, tokens: 0, tools: new Map(), models: new Map() };
|
|
597
|
+
dailySummaryMap.set(dsDate, dsEntry);
|
|
598
|
+
}
|
|
599
|
+
dsEntry.sessions++;
|
|
600
|
+
dsEntry.cost += dsSess.cost;
|
|
601
|
+
dsEntry.tokens += dsSess.inputTokens + dsSess.outputTokens;
|
|
602
|
+
dsSess.tools.forEach(function (count, tool) {
|
|
603
|
+
dsEntry!.tools.set(tool, (dsEntry!.tools.get(tool) || 0) + count);
|
|
604
|
+
});
|
|
605
|
+
dsSess.models.forEach(function (val, key) {
|
|
606
|
+
dsEntry!.models.set(key, (dsEntry!.models.get(key) || 0) + val.cost);
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
var dailySummaries: AnalyticsPayload["dailySummaries"] = [];
|
|
611
|
+
var dsSortedDates = Array.from(dailySummaryMap.keys()).sort();
|
|
612
|
+
for (var dsdi = 0; dsdi < dsSortedDates.length; dsdi++) {
|
|
613
|
+
var dsd = dsSortedDates[dsdi];
|
|
614
|
+
var dsData = dailySummaryMap.get(dsd)!;
|
|
615
|
+
var topTool = "";
|
|
616
|
+
var topToolCount = 0;
|
|
617
|
+
dsData.tools.forEach(function (count, tool) {
|
|
618
|
+
if (count > topToolCount) {
|
|
619
|
+
topToolCount = count;
|
|
620
|
+
topTool = tool;
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
var modelMix: Record<string, number> = {};
|
|
624
|
+
var modelTotal = 0;
|
|
625
|
+
dsData.models.forEach(function (cost) { modelTotal += cost; });
|
|
626
|
+
if (modelTotal > 0) {
|
|
627
|
+
dsData.models.forEach(function (cost, model) {
|
|
628
|
+
modelMix[model] = Math.round((cost / modelTotal) * 100) / 100;
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
dailySummaries.push({
|
|
632
|
+
date: dsd,
|
|
633
|
+
sessions: dsData.sessions,
|
|
634
|
+
cost: dsData.cost,
|
|
635
|
+
tokens: dsData.tokens,
|
|
636
|
+
topTool: topTool,
|
|
637
|
+
modelMix: modelMix,
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
var toolTreemap: AnalyticsPayload["toolTreemap"] = [];
|
|
642
|
+
toolStats.forEach(function (val, key) {
|
|
643
|
+
toolTreemap.push({
|
|
644
|
+
name: key,
|
|
645
|
+
count: val.count,
|
|
646
|
+
avgCost: val.sessions > 0 ? val.totalCost / val.sessions : 0,
|
|
647
|
+
});
|
|
648
|
+
});
|
|
649
|
+
toolTreemap.sort(function (a, b) { return b.count - a.count; });
|
|
650
|
+
|
|
651
|
+
var toolCategoryMap: Record<string, string> = {
|
|
652
|
+
Read: "Read", Glob: "Read", Grep: "Read", LS: "Read",
|
|
653
|
+
Edit: "Write", Write: "Write", MultiEdit: "Write",
|
|
654
|
+
Bash: "Execute",
|
|
655
|
+
Agent: "AI", Skill: "AI",
|
|
656
|
+
};
|
|
657
|
+
var toolSunburst: AnalyticsPayload["toolSunburst"] = [];
|
|
658
|
+
toolStats.forEach(function (val, key) {
|
|
659
|
+
var category = toolCategoryMap[key] || "Other";
|
|
660
|
+
toolSunburst.push({ name: key, category: category, count: val.count });
|
|
661
|
+
});
|
|
662
|
+
toolSunburst.sort(function (a, b) { return b.count - a.count; });
|
|
663
|
+
|
|
664
|
+
var totalToolCalls = 0;
|
|
665
|
+
toolStats.forEach(function (val) { totalToolCalls += val.count; });
|
|
666
|
+
var permissionStats: AnalyticsPayload["permissionStats"] = {
|
|
667
|
+
allowed: totalToolCalls,
|
|
668
|
+
denied: 0,
|
|
669
|
+
alwaysAllowed: 0,
|
|
670
|
+
};
|
|
671
|
+
|
|
672
|
+
var projectRadarMap = new Map<string, { cost: number; sessions: number; totalDuration: number; durationCount: number; tools: Set<string>; totalTokens: number }>();
|
|
673
|
+
for (var pri = 0; pri < filtered.length; pri++) {
|
|
674
|
+
var prSess = filtered[pri];
|
|
675
|
+
var prEntry = projectRadarMap.get(prSess.project);
|
|
676
|
+
if (!prEntry) {
|
|
677
|
+
prEntry = { cost: 0, sessions: 0, totalDuration: 0, durationCount: 0, tools: new Set(), totalTokens: 0 };
|
|
678
|
+
projectRadarMap.set(prSess.project, prEntry);
|
|
679
|
+
}
|
|
680
|
+
prEntry.cost += prSess.cost;
|
|
681
|
+
prEntry.sessions++;
|
|
682
|
+
prEntry.totalTokens += prSess.inputTokens + prSess.outputTokens;
|
|
683
|
+
if (prSess.startTime > 0 && prSess.endTime > prSess.startTime) {
|
|
684
|
+
prEntry.totalDuration += prSess.endTime - prSess.startTime;
|
|
685
|
+
prEntry.durationCount++;
|
|
686
|
+
}
|
|
687
|
+
prSess.tools.forEach(function (_count, tool) { prEntry!.tools.add(tool); });
|
|
688
|
+
}
|
|
689
|
+
var projectRadar: AnalyticsPayload["projectRadar"] = [];
|
|
690
|
+
projectRadarMap.forEach(function (val, key) {
|
|
691
|
+
projectRadar.push({
|
|
692
|
+
project: key,
|
|
693
|
+
cost: val.cost,
|
|
694
|
+
sessions: val.sessions,
|
|
695
|
+
avgDuration: val.durationCount > 0 ? val.totalDuration / val.durationCount : 0,
|
|
696
|
+
toolDiversity: val.tools.size,
|
|
697
|
+
tokensPerSession: val.sessions > 0 ? val.totalTokens / val.sessions : 0,
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
projectRadar.sort(function (a, b) { return b.cost - a.cost; });
|
|
701
|
+
if (projectRadar.length > 5) projectRadar.length = 5;
|
|
702
|
+
|
|
703
|
+
var contextWindowSizesForComplexity: Record<string, number> = { opus: 200000, sonnet: 200000, haiku: 200000, other: 200000 };
|
|
704
|
+
var sessionComplexity: AnalyticsPayload["sessionComplexity"] = [];
|
|
705
|
+
for (var sci = 0; sci < filtered.length; sci++) {
|
|
706
|
+
var scSess = filtered[sci];
|
|
707
|
+
var scUniqueTools = scSess.tools.size;
|
|
708
|
+
var scMessages = scSess.contextMessages.length;
|
|
709
|
+
var scRunning = 0;
|
|
710
|
+
var scPrimaryModel = "other";
|
|
711
|
+
var scMaxTokens = 0;
|
|
712
|
+
scSess.models.forEach(function (val, key) {
|
|
713
|
+
if (val.tokens > scMaxTokens) { scMaxTokens = val.tokens; scPrimaryModel = key; }
|
|
714
|
+
});
|
|
715
|
+
var scWindowSize = contextWindowSizesForComplexity[scPrimaryModel] || 200000;
|
|
716
|
+
for (var scmi = 0; scmi < scSess.contextMessages.length; scmi++) {
|
|
717
|
+
scRunning += scSess.contextMessages[scmi].inputTokens;
|
|
718
|
+
}
|
|
719
|
+
var scContextPercent = Math.min((scRunning / scWindowSize) * 100, 100);
|
|
720
|
+
var scScore = (scMessages * 1) + (scUniqueTools * 5) + (scContextPercent * 0.5);
|
|
721
|
+
sessionComplexity.push({
|
|
722
|
+
id: scSess.id,
|
|
723
|
+
title: scSess.title,
|
|
724
|
+
score: Math.round(scScore * 10) / 10,
|
|
725
|
+
messages: scMessages,
|
|
726
|
+
tools: scUniqueTools,
|
|
727
|
+
contextPercent: Math.round(scContextPercent * 10) / 10,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
sessionComplexity.sort(function (a, b) { return b.score - a.score; });
|
|
731
|
+
if (sessionComplexity.length > 20) sessionComplexity.length = 20;
|
|
732
|
+
|
|
523
733
|
return {
|
|
524
734
|
totalCost: totalCost,
|
|
525
735
|
totalSessions: filtered.length,
|
|
@@ -545,6 +755,15 @@ function aggregate(sessions: SessionData[], period: AnalyticsPeriod): AnalyticsP
|
|
|
545
755
|
responseTimeData: responseTimeData,
|
|
546
756
|
contextUtilization: contextUtilization,
|
|
547
757
|
tokenFlowSankey: tokenFlowSankey,
|
|
758
|
+
activityCalendar: activityCalendar,
|
|
759
|
+
hourlyHeatmap: hourlyHeatmap,
|
|
760
|
+
sessionTimeline: sessionTimeline,
|
|
761
|
+
dailySummaries: dailySummaries,
|
|
762
|
+
toolTreemap: toolTreemap,
|
|
763
|
+
toolSunburst: toolSunburst,
|
|
764
|
+
permissionStats: permissionStats,
|
|
765
|
+
projectRadar: projectRadar,
|
|
766
|
+
sessionComplexity: sessionComplexity,
|
|
548
767
|
};
|
|
549
768
|
}
|
|
550
769
|
|
package/shared/src/analytics.ts
CHANGED
|
@@ -22,6 +22,17 @@ export interface AnalyticsPayload {
|
|
|
22
22
|
responseTimeData: Array<{ tokens: number; duration: number; model: string; sessionId: string }>;
|
|
23
23
|
contextUtilization: Array<{ messageIndex: number; contextPercent: number; sessionId: string; title: string }>;
|
|
24
24
|
tokenFlowSankey: { nodes: Array<{ name: string }>; links: Array<{ source: number; target: number; value: number }> };
|
|
25
|
+
|
|
26
|
+
activityCalendar: Array<{ date: string; count: number; tokens: number; cost: number }>;
|
|
27
|
+
hourlyHeatmap: Array<{ day: number; hour: number; count: number }>;
|
|
28
|
+
sessionTimeline: Array<{ id: string; title: string; project: string; start: number; end: number; cost: number }>;
|
|
29
|
+
dailySummaries: Array<{ date: string; sessions: number; cost: number; tokens: number; topTool: string; modelMix: Record<string, number> }>;
|
|
30
|
+
|
|
31
|
+
toolTreemap: Array<{ name: string; count: number; avgCost: number }>;
|
|
32
|
+
toolSunburst: Array<{ name: string; category: string; count: number }>;
|
|
33
|
+
permissionStats: { allowed: number; denied: number; alwaysAllowed: number };
|
|
34
|
+
projectRadar: Array<{ project: string; cost: number; sessions: number; avgDuration: number; toolDiversity: number; tokensPerSession: number }>;
|
|
35
|
+
sessionComplexity: Array<{ id: string; title: string; score: number; messages: number; tools: number; contextPercent: number }>;
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
export type AnalyticsPeriod = "24h" | "7d" | "30d" | "90d" | "all";
|