@@ -59,94 +59,91 @@ export default class RedisHelper {
5959 }
6060
6161 public async getChartDataFromRedis (
62- groupingBy : 'hours' | 'days' ,
63- rangeValue : number ,
62+ startDate : string ,
63+ endDate : string ,
64+ groupBy : number , // minutes: 1=minute, 60=hour, 1440=day
6465 timezoneOffset = 0 ,
6566 projectId = '' ,
6667 groupHash = ''
6768 ) : Promise < { timestamp : number ; count : number } [ ] > {
6869 if ( ! this . redisClient . isOpen ) {
6970 throw new Error ( 'Redis client not connected' ) ;
7071 }
71-
72- const suffix = groupingBy === 'hours' ? 'hourly' : 'daily' ;
72+
73+ // Determine suffix based on groupBy
74+ let suffix : string ;
75+ if ( groupBy === 1 ) {
76+ suffix = 'minutely' ;
77+ } else if ( groupBy === 60 ) {
78+ suffix = 'hourly' ;
79+ } else if ( groupBy === 1440 ) {
80+ suffix = 'daily' ;
81+ } else {
82+ // For custom intervals, fallback to minutely with aggregation
83+ suffix = 'minutely' ;
84+ }
85+
7386 const key = groupHash
7487 ? `ts:events:${ groupHash } :${ suffix } `
7588 : projectId
7689 ? `ts:events:${ projectId } :${ suffix } `
7790 : `ts:events:${ suffix } ` ;
78-
79- const now = Date . now ( ) ;
80-
81- // определяем начало выборки
82- const fromDate = new Date ( now ) ;
83- if ( groupingBy === 'hours' ) {
84- fromDate . setMinutes ( 0 , 0 , 0 ) ;
91+
92+ // Parse dates (support ISO string or Unix timestamp in seconds)
93+ const start = typeof startDate === 'string' && startDate . includes ( '-' )
94+ ? new Date ( startDate ) . getTime ( )
95+ : Number ( startDate ) * 1000 ;
96+ const end = typeof endDate === 'string' && endDate . includes ( '-' )
97+ ? new Date ( endDate ) . getTime ( )
98+ : Number ( endDate ) * 1000 ;
99+
100+ const bucketMs = groupBy * 60 * 1000 ;
101+
102+ let result : [ string , string ] [ ] = [ ] ;
103+ try {
104+ // Use aggregation to sum events within each bucket
105+ // Since we now use TS.ADD (not TS.INCRBY), each sample is 1, so SUM gives us count
106+ result = ( await this . redisClient . sendCommand ( [
107+ 'TS.RANGE' ,
108+ key ,
109+ start . toString ( ) ,
110+ end . toString ( ) ,
111+ 'AGGREGATION' ,
112+ 'sum' ,
113+ bucketMs . toString ( ) ,
114+ ] ) ) as [ string , string ] [ ] | [ ] ;
115+ } catch ( err : any ) {
116+ if ( err . message . includes ( 'TSDB: the key does not exist' ) ) {
117+ console . warn ( `[Redis] Key ${ key } does not exist, returning zeroed data` ) ;
118+ result = [ ] ;
85119 } else {
86- fromDate . setHours ( 0 , 0 , 0 , 0 ) ;
87- }
88- fromDate . setMilliseconds ( fromDate . getMilliseconds ( ) - ( groupingBy === 'hours' ? rangeValue * 60 * 60 * 1000 : rangeValue * 24 * 60 * 60 * 1000 ) ) ;
89- const from = fromDate . getTime ( ) ;
90-
91- let result : [ string , string ] [ ] = [ ] ;
92- try {
93- result = ( await this . redisClient . sendCommand ( [
94- 'TS.RANGE' ,
95- key ,
96- from . toString ( ) ,
97- now . toString ( ) ,
98- ] ) ) as [ string , string ] [ ] | [ ] ;
99- } catch ( err : any ) {
100- if ( err . message . includes ( 'TSDB: the key does not exist' ) ) {
101- console . warn ( `[Redis] Key ${ key } does not exist, returning zeroed data` ) ;
102- result = [ ] ;
103- } else {
104- throw err ;
105- }
106- }
107-
108- // агрегируем события по интервалу
109- const dataPoints : { [ ts : number ] : number } = { } ;
110- for ( const [ tsStr ] of result ) {
111- const tsMs = Number ( tsStr ) ;
112- const date = new Date ( tsMs ) ;
113-
114- let intervalStart : number ;
115- if ( groupingBy === 'hours' ) {
116- date . setMinutes ( 0 , 0 , 0 ) ;
117- intervalStart = Date . UTC ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , date . getUTCDate ( ) , date . getUTCHours ( ) ) ;
118- } else {
119- date . setHours ( 0 , 0 , 0 , 0 ) ;
120- intervalStart = Date . UTC ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , date . getUTCDate ( ) ) ;
121- }
122-
123- const intervalWithOffset = intervalStart + timezoneOffset * 60 * 1000 ;
124- dataPoints [ intervalWithOffset ] = ( dataPoints [ intervalWithOffset ] || 0 ) + 1 ;
120+ throw err ;
125121 }
126-
127- // заполняем пропущенные интервалы нулями
128- const filled : { timestamp : number ; count : number } [ ] = [ ] ;
129- const nowDate = new Date ( now ) ;
130-
131- for ( let i = 0 ; i < rangeValue ; i ++ ) {
132- const date = new Date ( nowDate ) ;
133-
134- if ( groupingBy === 'hours' ) {
135- date . setHours ( date . getHours ( ) - i , 0 , 0 , 0 ) ;
136- var intervalStart = Date . UTC ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , date . getUTCDate ( ) , date . getUTCHours ( ) ) ;
137- } else {
138- date . setDate ( date . getDate ( ) - i ) ;
139- date . setHours ( 0 , 0 , 0 , 0 ) ;
140- var intervalStart = Date . UTC ( date . getUTCFullYear ( ) , date . getUTCMonth ( ) , date . getUTCDate ( ) ) ;
141- }
142-
143- const intervalWithOffset = intervalStart + timezoneOffset * 60 * 1000 ;
144- filled . push ( {
145- timestamp : Math . floor ( intervalWithOffset / 1000 ) ,
146- count : dataPoints [ intervalWithOffset ] || 0 ,
147- } ) ;
148- }
149-
150- return filled . sort ( ( a , b ) => a . timestamp - b . timestamp ) ;
122+ }
123+
124+ // Transform data from Redis
125+ const dataPoints : { [ ts : number ] : number } = { } ;
126+ for ( const [ tsStr , valStr ] of result ) {
127+ const tsMs = Number ( tsStr ) ;
128+ dataPoints [ tsMs ] = Number ( valStr ) || 0 ;
129+ }
130+
131+ // Fill missing intervals with zeros
132+ const filled : { timestamp : number ; count : number } [ ] = [ ] ;
133+ let current = start ;
134+
135+ // Round current to the nearest bucket boundary
136+ current = Math . floor ( current / bucketMs ) * bucketMs ;
137+
138+ while ( current <= end ) {
139+ const count = dataPoints [ current ] || 0 ;
140+ filled . push ( {
141+ timestamp : Math . floor ( ( current + timezoneOffset * 60 * 1000 ) / 1000 ) ,
142+ count,
143+ } ) ;
144+ current += bucketMs ;
145+ }
146+
147+ return filled . sort ( ( a , b ) => a . timestamp - b . timestamp ) ;
151148 }
152149}
0 commit comments