- 
          
 - 
                Notifications
    
You must be signed in to change notification settings  - Fork 6.9k
 
feat(status page): Add configurable heartbeat bar range options (100beats, 1..365 days) #5916
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
ad713ed
              493a5fd
              e1cf10d
              c0f09d9
              531faa9
              b441308
              ebd47f1
              2c6d410
              8d2e3f1
              dbf58b8
              f47f73d
              e16b6cf
              94adf2c
              b41d10e
              ace9ff2
              0dde42e
              992bcdb
              75fc01c
              9402c0f
              804bb39
              bf7d6de
              76b9680
              05c9ec2
              28eae65
              8b2bf8a
              1f4c4a0
              1518fd5
              adc362a
              c023015
              6defb5d
              eaace3e
              b6227e7
              3a5bedd
              100cd62
              24b6209
              6c26d32
              9cc91c3
              9e07862
              5e64226
              eb47b1f
              88b3cfc
              91221b5
              5c6cf48
              29e885b
              dc74bb7
              28ddaf1
              59043fb
              0439566
              4160045
              a15e897
              751c92b
              fd0fe64
              0028db4
              b907d62
              8a5202b
              3cecc67
              1514cbd
              d07575b
              2707aa5
              b121cc7
              71b73e9
              38a4b60
              60d879f
              49e181e
              c780c2a
              0d2c129
              c1b2f35
              8589c63
              8d7e213
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| exports.up = function (knex) { | ||
| return knex.schema.alterTable("status_page", function (table) { | ||
| table.smallint("heartbeat_bar_days").notNullable().defaultTo(0).checkBetween([ 0, 365 ]); | ||
| }); | ||
| }; | ||
| 
     | 
||
| exports.down = function (knex) { | ||
| return knex.schema.alterTable("status_page", function (table) { | ||
| table.dropColumn("heartbeat_bar_days"); | ||
| }); | ||
| }; | 
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| 
          
            
          
           | 
    @@ -845,6 +845,93 @@ class UptimeCalculator { | |
| setMigrationMode(value) { | ||
| this.migrationMode = value; | ||
| } | ||
| 
     | 
||
| /** | ||
| * Get aggregated heartbeat buckets for a specific time range | ||
| * @param {number} days Number of days to aggregate | ||
| * @param {number} targetBuckets Number of buckets to create (default 100) | ||
| * @returns {Array} Array of aggregated bucket data | ||
| */ | ||
| getAggregatedBuckets(days, targetBuckets = 100) { | ||
| const now = this.getCurrentDate(); | ||
| const startTime = now.subtract(days, "day"); | ||
| const totalMinutes = days * 60 * 24; | ||
| const bucketSizeMinutes = totalMinutes / targetBuckets; | ||
| 
     | 
||
| // Get available data from UptimeCalculator for lookup | ||
| const availableData = {}; | ||
| let rawDataPoints; | ||
| 
     | 
||
| if (days <= 1) { | ||
| const exactMinutes = Math.ceil(days * 24 * 60); | ||
| rawDataPoints = this.getDataArray(exactMinutes, "minute"); | ||
| } else if (days <= 30) { | ||
| // For 1-30 days, use hourly data (up to 720 hours) | ||
| const exactHours = Math.min(Math.ceil(days * 24), 720); | ||
| rawDataPoints = this.getDataArray(exactHours, "hour"); | ||
| } else { | ||
| // For > 30 days, use daily data | ||
| const requestDays = Math.min(days, 365); | ||
| rawDataPoints = this.getDataArray(requestDays, "day"); | ||
| } | ||
                
      
                  peaktwilight marked this conversation as resolved.
               
          
            Show resolved
            Hide resolved
         | 
||
| 
     | 
||
| // Create lookup map for available data | ||
| for (const point of rawDataPoints) { | ||
| if (point && point.timestamp) { | ||
| availableData[point.timestamp] = point; | ||
| } | ||
| } | ||
| 
     | 
||
| // Create exactly targetBuckets buckets spanning the full requested time range | ||
| const buckets = []; | ||
| for (let i = 0; i < targetBuckets; i++) { | ||
| const bucketStart = startTime.add(i * bucketSizeMinutes, "minute"); | ||
| const bucketEnd = startTime.add((i + 1) * bucketSizeMinutes, "minute"); | ||
| 
     | 
||
| buckets.push({ | ||
| start: bucketStart.unix(), | ||
| end: bucketEnd.unix(), | ||
| up: 0, | ||
| down: 0, | ||
| maintenance: 0, | ||
| pending: 0 | ||
| }); | ||
| } | ||
| 
     | 
||
| // Aggregate available data into buckets | ||
| for (const [ timestamp, dataPoint ] of Object.entries(availableData)) { | ||
| const timestampNum = parseInt(timestamp); | ||
| 
     | 
||
| // Find the appropriate bucket for this data point | ||
| // For daily data (> 30 days), timestamps are at start of day | ||
| // We need to find which bucket this day belongs to | ||
| for (let i = 0; i < buckets.length; i++) { | ||
| const bucket = buckets[i]; | ||
| 
         
      Comment on lines
    
      +905
     to 
      +909
    
   
  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think there was a misunderstanding here. We don't actually need to do this, or am I missing something? As far as I remember  There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. optimised and nested loop replaced with a single pass alg that uses currentBucketIndex to track position and only moves forward since the data is sorted. The alg complexity O(n*m) -> O(n+m). The availableData lookup map is still used because it's needed to efficiently access data points by timestamp from the rawDataPoints array. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 
 I don't think this is quite correct. Something that we could also do with   | 
||
| 
     | 
||
| if (days > 30) { | ||
| // For daily data, check if the timestamp falls within the bucket's day range | ||
| if (timestampNum >= bucket.start && timestampNum < bucket.end) { | ||
| bucket.up += dataPoint.up || 0; | ||
| bucket.down += dataPoint.down || 0; | ||
| bucket.maintenance += dataPoint.maintenance || 0; | ||
| bucket.pending += dataPoint.pending || 0; | ||
| break; | ||
| } | ||
| } else { | ||
| // For minute/hourly data, use exact timestamp matching | ||
| if (timestampNum >= bucket.start && timestampNum < bucket.end && dataPoint) { | ||
| bucket.up += dataPoint.up || 0; | ||
| bucket.down += dataPoint.down || 0; | ||
| bucket.maintenance += 0; // UptimeCalculator treats maintenance as up | ||
| bucket.pending += 0; // UptimeCalculator doesn't track pending separately | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| 
     | 
||
| return buckets; | ||
| } | ||
| } | ||
| 
     | 
||
| class UptimeDataResult { | ||
| 
          
            
          
           | 
    ||
Uh oh!
There was an error while loading. Please reload this page.