@@ -1888,42 +1888,46 @@ export class AnalyticsService {
18881888 1 ,
18891889 Math . ceil ( spanSeconds / DELETION_TIMELINE_BUCKETS ) ,
18901890 )
1891+ const bucketMs = bucketSeconds * 1000
1892+
1893+ // Anchor buckets to the epoch-aligned start of the window, then bucket on
1894+ // absolute unix time. Matching by integer index (rather than a formatted
1895+ // timestamp string) keeps this correct no matter how `created`'s timezone
1896+ // is rendered in the query result.
1897+ const originMs = Math . floor ( fromMs / bucketMs ) * bucketMs
1898+ const originSeconds = Math . floor ( originMs / 1000 )
1899+
1900+ const x : string [ ] = [ ]
1901+ for (
1902+ let ms = originMs ;
1903+ ms <= toMs && x . length < DELETION_TIMELINE_MAX_POINTS ;
1904+ ms += bucketMs
1905+ ) {
1906+ x . push ( dayjs . utc ( ms ) . format ( 'YYYY-MM-DD HH:mm:ss' ) )
1907+ }
1908+
1909+ const counts = new Array < number > ( x . length ) . fill ( 0 )
18911910
18921911 const { data : rows } = await clickhouse
18931912 . query ( {
18941913 query : `
18951914 SELECT
1896- toStartOfInterval( created, INTERVAL { bucketSeconds:UInt32} SECOND ) AS t ,
1915+ intDiv(toInt64(toUnixTimestamp( created)) - {originSeconds:Int64}, { bucketSeconds:UInt32}) AS bucketIndex ,
18971916 count() AS c
18981917 FROM events
18991918 WHERE ${ whereClause }
1900- GROUP BY t
1901- ORDER BY t
1919+ GROUP BY bucketIndex
1920+ ORDER BY bucketIndex
19021921 ` ,
1903- query_params : { ...baseParams , bucketSeconds } ,
1922+ query_params : { ...baseParams , originSeconds , bucketSeconds } ,
19041923 } )
1905- . then ( ( resultSet ) => resultSet . json < { t : string ; c : string } > ( ) )
1924+ . then ( ( resultSet ) => resultSet . json < { bucketIndex : string ; c : string } > ( ) )
19061925
1907- const bucketed = new Map < string , number > ( )
19081926 for ( const row of rows ) {
1909- bucketed . set ( row . t , Number ( row . c ) || 0 )
1910- }
1911-
1912- // toStartOfInterval aligns buckets to multiples of the interval since the
1913- // unix epoch, so align our JS walk the same way and fill empty buckets.
1914- const bucketMs = bucketSeconds * 1000
1915- const startMs = Math . floor ( fromMs / bucketMs ) * bucketMs
1916- const x : string [ ] = [ ]
1917- const counts : number [ ] = [ ]
1918-
1919- for (
1920- let ms = startMs ;
1921- ms <= toMs && x . length < DELETION_TIMELINE_MAX_POINTS ;
1922- ms += bucketMs
1923- ) {
1924- const key = dayjs . utc ( ms ) . format ( 'YYYY-MM-DD HH:mm:ss' )
1925- x . push ( key )
1926- counts . push ( bucketed . get ( key ) ?? 0 )
1927+ const index = Number ( row . bucketIndex )
1928+ if ( index >= 0 && index < counts . length ) {
1929+ counts [ index ] = Number ( row . c ) || 0
1930+ }
19271931 }
19281932
19291933 return { x, counts }
0 commit comments