@@ -34,53 +34,122 @@ export interface SparkTopUser {
3434 totalTokens : number ;
3535}
3636
37+ /** Per-key usage within one hourly or daily bucket (for ranking teams/keys in that window). */
38+ export interface SparkBucketKeyContribution {
39+ keyId : number ;
40+ /** Spark key description (typically `"{teamName} ({scope})"`). */
41+ description : string ;
42+ count : number ;
43+ totalTokens : number ;
44+ }
45+
46+ export interface SparkAggregatedHourlyBlock {
47+ hour : string ;
48+ count : number ;
49+ totalTokens : number ;
50+ /** Highest-usage keys in this hour (by `totalTokens`, then `count`). */
51+ topKeys : SparkBucketKeyContribution [ ] ;
52+ }
53+
54+ export interface SparkAggregatedDailyBlock {
55+ date : string ;
56+ count : number ;
57+ totalTokens : number ;
58+ /** Highest-usage keys on this day (by `totalTokens`, then `count`). */
59+ topKeys : SparkBucketKeyContribution [ ] ;
60+ }
61+
3762/** Combined stats for every Spark key in a course offering (same buckets as single-key stats). */
3863export interface SparkAggregatedKeysStats {
3964 keyIds : number [ ] ;
4065 totalRequests : number ;
4166 totalTokens : number ;
4267 lastUsedAt : string | null ;
43- hourly : { hour : string ; count : number ; totalTokens : number } [ ] ;
44- daily : { date : string ; count : number ; totalTokens : number } [ ] ;
68+ hourly : SparkAggregatedHourlyBlock [ ] ;
69+ daily : SparkAggregatedDailyBlock [ ] ;
4570 topUsers : SparkTopUser [ ] ;
4671}
4772
4873type SparkStatsResponse = SparkStats & { topUsers ?: SparkTopUser [ ] } ;
4974
50- const mergeHourlySeries = (
51- series : { hour : string ; count : number ; totalTokens : number } [ ] [ ] ,
52- ) : { hour : string ; count : number ; totalTokens : number } [ ] => {
53- const combined = new Map < string , { count : number ; totalTokens : number } > ( ) ;
54- for ( const arr of series ) {
55- for ( const row of arr ) {
56- const prev = combined . get ( row . hour ) ?? { count : 0 , totalTokens : 0 } ;
57- combined . set ( row . hour , {
58- count : prev . count + row . count ,
59- totalTokens : prev . totalTokens + row . totalTokens ,
75+ const TOP_KEYS_PER_BUCKET = 10 ;
76+
77+ const buildHourlyBlocksWithTopKeys = (
78+ keys : SparkKey [ ] ,
79+ perKey : SparkStatsResponse [ ] ,
80+ ) : SparkAggregatedHourlyBlock [ ] => {
81+ const byHour = new Map < string , Map < number , SparkBucketKeyContribution > > ( ) ;
82+ for ( let i = 0 ; i < keys . length ; i ++ ) {
83+ const sparkKey = keys [ i ] ;
84+ for ( const row of perKey [ i ] . hourly ) {
85+ let m = byHour . get ( row . hour ) ;
86+ if ( ! m ) {
87+ m = new Map ( ) ;
88+ byHour . set ( row . hour , m ) ;
89+ }
90+ m . set ( sparkKey . id , {
91+ keyId : sparkKey . id ,
92+ description : sparkKey . description ,
93+ count : row . count ,
94+ totalTokens : row . totalTokens ,
6095 } ) ;
6196 }
6297 }
63- return [ ...combined . entries ( ) ]
98+ return [ ...byHour . entries ( ) ]
6499 . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
65- . map ( ( [ hour , v ] ) => ( { hour, ...v } ) ) ;
100+ . map ( ( [ hour , keyMap ] ) => {
101+ const contributions = [ ...keyMap . values ( ) ] ;
102+ const count = contributions . reduce ( ( s , c ) => s + c . count , 0 ) ;
103+ const totalTokens = contributions . reduce ( ( s , c ) => s + c . totalTokens , 0 ) ;
104+ const topKeys = [ ...contributions ]
105+ . sort (
106+ ( a , b ) =>
107+ b . totalTokens - a . totalTokens ||
108+ b . count - a . count ||
109+ a . keyId - b . keyId ,
110+ )
111+ . slice ( 0 , TOP_KEYS_PER_BUCKET ) ;
112+ return { hour, count, totalTokens, topKeys } ;
113+ } ) ;
66114} ;
67115
68- const mergeDailySeries = (
69- series : { date : string ; count : number ; totalTokens : number } [ ] [ ] ,
70- ) : { date : string ; count : number ; totalTokens : number } [ ] => {
71- const combined = new Map < string , { count : number ; totalTokens : number } > ( ) ;
72- for ( const arr of series ) {
73- for ( const row of arr ) {
74- const prev = combined . get ( row . date ) ?? { count : 0 , totalTokens : 0 } ;
75- combined . set ( row . date , {
76- count : prev . count + row . count ,
77- totalTokens : prev . totalTokens + row . totalTokens ,
116+ const buildDailyBlocksWithTopKeys = (
117+ keys : SparkKey [ ] ,
118+ perKey : SparkStatsResponse [ ] ,
119+ ) : SparkAggregatedDailyBlock [ ] => {
120+ const byDate = new Map < string , Map < number , SparkBucketKeyContribution > > ( ) ;
121+ for ( let i = 0 ; i < keys . length ; i ++ ) {
122+ const sparkKey = keys [ i ] ;
123+ for ( const row of perKey [ i ] . daily ) {
124+ let m = byDate . get ( row . date ) ;
125+ if ( ! m ) {
126+ m = new Map ( ) ;
127+ byDate . set ( row . date , m ) ;
128+ }
129+ m . set ( sparkKey . id , {
130+ keyId : sparkKey . id ,
131+ description : sparkKey . description ,
132+ count : row . count ,
133+ totalTokens : row . totalTokens ,
78134 } ) ;
79135 }
80136 }
81- return [ ...combined . entries ( ) ]
137+ return [ ...byDate . entries ( ) ]
82138 . sort ( ( [ a ] , [ b ] ) => a . localeCompare ( b ) )
83- . map ( ( [ date , v ] ) => ( { date, ...v } ) ) ;
139+ . map ( ( [ date , keyMap ] ) => {
140+ const contributions = [ ...keyMap . values ( ) ] ;
141+ const count = contributions . reduce ( ( s , c ) => s + c . count , 0 ) ;
142+ const totalTokens = contributions . reduce ( ( s , c ) => s + c . totalTokens , 0 ) ;
143+ const topKeys = [ ...contributions ]
144+ . sort (
145+ ( a , b ) =>
146+ b . totalTokens - a . totalTokens ||
147+ b . count - a . count ||
148+ a . keyId - b . keyId ,
149+ )
150+ . slice ( 0 , TOP_KEYS_PER_BUCKET ) ;
151+ return { date, count, totalTokens, topKeys } ;
152+ } ) ;
84153} ;
85154
86155const mergeTopUsers = (
@@ -353,8 +422,8 @@ export const getSparkAggregatedKeyStats = async (
353422 totalRequests : perKey . reduce ( ( s , x ) => s + x . totalRequests , 0 ) ,
354423 totalTokens : perKey . reduce ( ( s , x ) => s + x . totalTokens , 0 ) ,
355424 lastUsedAt : maxIsoDate ( perKey . map ( ( x ) => x . lastUsedAt ) ) ,
356- hourly : mergeHourlySeries ( perKey . map ( ( x ) => x . hourly ) ) ,
357- daily : mergeDailySeries ( perKey . map ( ( x ) => x . daily ) ) ,
425+ hourly : buildHourlyBlocksWithTopKeys ( keys , perKey ) ,
426+ daily : buildDailyBlocksWithTopKeys ( keys , perKey ) ,
358427 topUsers : mergeTopUsers (
359428 perKey . map ( ( x ) => x . topUsers ) ,
360429 10 ,
0 commit comments