rails_pulse 0.4.2 → 0.5.0.pre.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -1
- data/README.md +15 -4
- data/app/assets/stylesheets/rails_pulse/components/chart.css +11 -0
- data/app/assets/stylesheets/rails_pulse/components/descriptive_list.css +19 -1
- data/app/assets/stylesheets/rails_pulse/components/row.css +3 -17
- data/app/controllers/concerns/chart_table_concern.rb +28 -69
- data/app/controllers/concerns/deployment_markers_concern.rb +3 -1
- data/app/controllers/concerns/metric_card_concern.rb +6 -7
- data/app/controllers/rails_pulse/api/v1/base_controller.rb +133 -0
- data/app/controllers/rails_pulse/api/v1/capabilities_controller.rb +27 -0
- data/app/controllers/rails_pulse/api/v1/coverage_controller.rb +158 -0
- data/app/controllers/rails_pulse/api/v1/deployments_controller.rb +40 -0
- data/app/controllers/rails_pulse/api/v1/exceptions_controller.rb +77 -0
- data/app/controllers/rails_pulse/api/v1/insights_controller.rb +62 -0
- data/app/controllers/rails_pulse/api/v1/job_runs_controller.rb +34 -0
- data/app/controllers/rails_pulse/api/v1/jobs_controller.rb +172 -0
- data/app/controllers/rails_pulse/api/v1/queries_controller.rb +123 -0
- data/app/controllers/rails_pulse/api/v1/requests_controller.rb +74 -0
- data/app/controllers/rails_pulse/api/v1/routes_controller.rb +103 -0
- data/app/controllers/rails_pulse/application_controller.rb +1 -1
- data/app/controllers/rails_pulse/dashboard_controller.rb +25 -21
- data/app/controllers/rails_pulse/deployments_controller.rb +23 -13
- data/app/controllers/rails_pulse/exceptions_controller.rb +10 -12
- data/app/controllers/rails_pulse/jobs_controller.rb +1 -2
- data/app/controllers/rails_pulse/queries_controller.rb +6 -5
- data/app/controllers/rails_pulse/requests_controller.rb +15 -14
- data/app/controllers/rails_pulse/routes_controller.rb +4 -8
- data/app/helpers/rails_pulse/application_helper.rb +15 -0
- data/app/helpers/rails_pulse/chart_helper.rb +9 -2
- data/app/helpers/rails_pulse/formatting_helper.rb +11 -12
- data/app/javascript/rails_pulse/controllers/chart_controller.js +80 -43
- data/app/jobs/rails_pulse/backfill_summaries_job.rb +4 -2
- data/app/jobs/rails_pulse/cloud_health_job.rb +11 -0
- data/app/jobs/rails_pulse/cloud_sync_job.rb +11 -0
- data/app/jobs/rails_pulse/summary_job.rb +13 -0
- data/app/models/concerns/rails_pulse/has_metadata.rb +15 -0
- data/app/models/concerns/rails_pulse/taggable.rb +5 -12
- data/app/models/rails_pulse/cards/base.rb +20 -11
- data/app/models/rails_pulse/charts/base.rb +5 -18
- data/app/models/rails_pulse/charts/percentile_chart_base.rb +3 -5
- data/app/models/rails_pulse/cleanup_run.rb +32 -0
- data/app/models/rails_pulse/cloud/buffered_batch.rb +55 -0
- data/app/models/rails_pulse/cloud/installation.rb +55 -0
- data/app/models/rails_pulse/dashboard/charts/response_time_percentiles.rb +4 -5
- data/app/models/rails_pulse/dashboard/charts/throughput_and_errors.rb +4 -5
- data/app/models/rails_pulse/dashboard/concerns/attention_classification.rb +104 -0
- data/app/models/rails_pulse/dashboard/concerns/time_range_helper.rb +25 -2
- data/app/models/rails_pulse/dashboard/health_summary.rb +38 -13
- data/app/models/rails_pulse/dashboard/needs_attention.rb +31 -98
- data/app/models/rails_pulse/dashboard/storage_pressure.rb +39 -63
- data/app/models/rails_pulse/dashboard/storage_status.rb +130 -39
- data/app/models/rails_pulse/deployment.rb +2 -7
- data/app/models/rails_pulse/event.rb +41 -0
- data/app/models/rails_pulse/exception_occurrence.rb +1 -1
- data/app/models/rails_pulse/exceptions/cards/exception_rate.rb +3 -6
- data/app/models/rails_pulse/exceptions/cards/open_groups.rb +1 -4
- data/app/models/rails_pulse/exceptions/cards/total_occurrences.rb +3 -6
- data/app/models/rails_pulse/exceptions/charts/occurrence_volume.rb +4 -7
- data/app/models/rails_pulse/job_run.rb +1 -1
- data/app/models/rails_pulse/jobs/cards/failure_rate.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/p95_duration.rb +8 -14
- data/app/models/rails_pulse/jobs/cards/total_runs.rb +5 -11
- data/app/models/rails_pulse/jobs/charts/duration.rb +1 -2
- data/app/models/rails_pulse/jobs/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/jobs/charts/failure_rate.rb +1 -2
- data/app/models/rails_pulse/like_pattern.rb +35 -0
- data/app/models/rails_pulse/queries/cards/average_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/cards/database_load.rb +0 -9
- data/app/models/rails_pulse/queries/cards/execution_rate.rb +5 -11
- data/app/models/rails_pulse/queries/cards/percentile_query_times.rb +6 -12
- data/app/models/rails_pulse/queries/charts/database_load.rb +5 -8
- data/app/models/rails_pulse/queries/charts/execution_volume.rb +1 -1
- data/app/models/rails_pulse/request.rb +1 -1
- data/app/models/rails_pulse/routes/cards/error_rates.rb +5 -15
- data/app/models/rails_pulse/routes/cards/percentile_response_times.rb +5 -17
- data/app/models/rails_pulse/routes/cards/request_count_totals.rb +5 -17
- data/app/models/rails_pulse/routes/charts/error_rate.rb +1 -1
- data/app/models/rails_pulse/routes/charts/request_volume.rb +1 -1
- data/app/models/rails_pulse/summary.rb +5 -0
- data/app/models/rails_pulse/tables/base.rb +5 -6
- data/app/models/rails_pulse/time_range.rb +309 -0
- data/app/models/rails_pulse/time_window.rb +15 -0
- data/app/models/rails_pulse/writer_heartbeat.rb +90 -0
- data/app/serializers/rails_pulse/api/v1/deployment_serializer.rb +23 -0
- data/app/serializers/rails_pulse/api/v1/exception_group_serializer.rb +23 -0
- data/app/serializers/rails_pulse/api/v1/exception_occurrence_serializer.rb +22 -0
- data/app/serializers/rails_pulse/api/v1/job_run_serializer.rb +28 -0
- data/app/serializers/rails_pulse/api/v1/job_serializer.rb +26 -0
- data/app/serializers/rails_pulse/api/v1/query_serializer.rb +25 -0
- data/app/serializers/rails_pulse/api/v1/request_serializer.rb +22 -0
- data/app/serializers/rails_pulse/api/v1/route_serializer.rb +19 -0
- data/app/services/rails_pulse/cloud/batch.rb +83 -0
- data/app/services/rails_pulse/cloud/client.rb +68 -0
- data/app/services/rails_pulse/cloud/combined_metrics.rb +49 -0
- data/app/services/rails_pulse/cloud/health_item.rb +77 -0
- data/app/services/rails_pulse/cloud/path_prefix.rb +50 -0
- data/app/services/rails_pulse/cloud/preview.rb +83 -0
- data/app/services/rails_pulse/cloud/query_shape.rb +195 -0
- data/app/services/rails_pulse/cloud/record_items.rb +60 -0
- data/app/services/rails_pulse/cloud/route_pattern.rb +111 -0
- data/app/services/rails_pulse/cloud/summary_items.rb +188 -0
- data/app/services/rails_pulse/cloud/sync.rb +275 -0
- data/app/services/rails_pulse/config_recommendations.rb +132 -0
- data/app/services/rails_pulse/deployment_comparison.rb +177 -0
- data/app/services/rails_pulse/exception_capture_service.rb +1 -1
- data/app/services/rails_pulse/operations/change_point.rb +1 -1
- data/app/services/rails_pulse/operations/comparison.rb +1 -1
- data/app/services/rails_pulse/period_insights.rb +131 -0
- data/app/services/rails_pulse/sql_query_normalizer.rb +222 -122
- data/app/services/rails_pulse/summary_service/from_child_periods.rb +161 -0
- data/app/services/rails_pulse/summary_service/from_raw_rows.rb +115 -0
- data/app/services/rails_pulse/summary_service/metrics.rb +38 -0
- data/app/services/rails_pulse/summary_service.rb +121 -133
- data/app/services/rails_pulse/tag_filter_service.rb +5 -5
- data/app/views/layouts/rails_pulse/_time_range_selector.html.erb +4 -1
- data/app/views/rails_pulse/components/_operation_details_popover.html.erb +1 -1
- data/app/views/rails_pulse/components/_table_head.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_chart_panel.html.erb +6 -1
- data/app/views/rails_pulse/dashboard/_deployments_panel.html.erb +2 -2
- data/app/views/rails_pulse/dashboard/_health_badge.html.erb +29 -20
- data/app/views/rails_pulse/dashboard/_storage_panel.html.erb +13 -3
- data/app/views/rails_pulse/dashboard/index.html.erb +16 -2
- data/app/views/rails_pulse/jobs/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/jobs/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/queries/index.html.erb +1 -1
- data/app/views/rails_pulse/queries/show.html.erb +1 -1
- data/app/views/rails_pulse/requests/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/_chart_tabs.html.erb +1 -0
- data/app/views/rails_pulse/routes/index.html.erb +1 -1
- data/app/views/rails_pulse/routes/show.html.erb +1 -1
- data/app/views/rails_pulse/shared/_aggregation_zone_badge.html.erb +4 -0
- data/app/views/rails_pulse/storage/show.html.erb +39 -2
- data/config/routes.rb +23 -0
- data/db/rails_pulse_cloud_schema.rb +58 -0
- data/db/rails_pulse_migrate/20260506000001_create_rails_pulse_exceptions.rb +1 -1
- data/db/rails_pulse_migrate/20260921000001_create_rails_pulse_events.rb +21 -0
- data/db/rails_pulse_schema.rb +20 -2
- data/exe/rails-pulse +7 -0
- data/lib/generators/rails_pulse/base_methods.rb +19 -0
- data/lib/generators/rails_pulse/install_cloud_generator.rb +84 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_cloud_schema.rb +58 -0
- data/lib/generators/rails_pulse/templates/db/rails_pulse_schema.rb +20 -2
- data/lib/generators/rails_pulse/templates/migrations/install_rails_pulse_cloud_tables.rb +30 -0
- data/lib/generators/rails_pulse/templates/rails_pulse.rb +71 -19
- data/lib/generators/rails_pulse/upgrade_generator.rb +9 -16
- data/lib/rails_pulse/cleanup_service.rb +37 -20
- data/lib/rails_pulse/cli/agent_files/agents.md +89 -0
- data/lib/rails_pulse/cli/agent_files/claude_skill.md +182 -0
- data/lib/rails_pulse/cli/base_command.rb +37 -0
- data/lib/rails_pulse/cli/client.rb +64 -0
- data/lib/rails_pulse/cli/config.rb +87 -0
- data/lib/rails_pulse/cli/configure.rb +75 -0
- data/lib/rails_pulse/cli/coverage.rb +91 -0
- data/lib/rails_pulse/cli/deployments.rb +54 -0
- data/lib/rails_pulse/cli/exceptions.rb +116 -0
- data/lib/rails_pulse/cli/formatter.rb +31 -0
- data/lib/rails_pulse/cli/insights.rb +70 -0
- data/lib/rails_pulse/cli/install.rb +145 -0
- data/lib/rails_pulse/cli/job_runs.rb +53 -0
- data/lib/rails_pulse/cli/jobs.rb +71 -0
- data/lib/rails_pulse/cli/main.rb +76 -0
- data/lib/rails_pulse/cli/mcp.rb +47 -0
- data/lib/rails_pulse/cli/queries.rb +67 -0
- data/lib/rails_pulse/cli/requests.rb +57 -0
- data/lib/rails_pulse/cli/routes.rb +73 -0
- data/lib/rails_pulse/configuration.rb +100 -2
- data/lib/rails_pulse/current.rb +11 -0
- data/lib/rails_pulse/engine.rb +12 -3
- data/lib/rails_pulse/installers/config_updater.rb +9 -2
- data/lib/rails_pulse/job_run_collector.rb +15 -15
- data/lib/rails_pulse/mcp/server.rb +78 -0
- data/lib/rails_pulse/mcp/tools/coverage.rb +96 -0
- data/lib/rails_pulse/mcp/tools/deployments.rb +117 -0
- data/lib/rails_pulse/mcp/tools/endpoint.rb +167 -0
- data/lib/rails_pulse/mcp/tools/errors.rb +104 -0
- data/lib/rails_pulse/mcp/tools/exception_detail.rb +118 -0
- data/lib/rails_pulse/mcp/tools/exceptions.rb +128 -0
- data/lib/rails_pulse/mcp/tools/helpers.rb +162 -0
- data/lib/rails_pulse/mcp/tools/insights.rb +99 -0
- data/lib/rails_pulse/mcp/tools/jobs.rb +155 -0
- data/lib/rails_pulse/mcp/tools/queries.rb +142 -0
- data/lib/rails_pulse/mcp/tools/routes.rb +108 -0
- data/lib/rails_pulse/mcp/tools/slow_requests.rb +118 -0
- data/lib/rails_pulse/middleware/request_collector.rb +9 -9
- data/lib/rails_pulse/scoped_inflector.rb +35 -0
- data/lib/rails_pulse/statistics.rb +27 -0
- data/lib/rails_pulse/subscribers/exception_subscriber.rb +2 -2
- data/lib/rails_pulse/subscribers/operation_subscriber.rb +15 -29
- data/lib/rails_pulse/tasks/status_reporter.rb +201 -0
- data/lib/rails_pulse/tracker.rb +76 -14
- data/lib/rails_pulse/version.rb +1 -1
- data/lib/rails_pulse.rb +0 -4
- data/lib/tasks/rails_pulse.rake +6 -0
- data/lib/tasks/rails_pulse_tasks.rake +7 -0
- data/public/rails-pulse-assets/rails-pulse.css +1 -1
- data/public/rails-pulse-assets/rails-pulse.js +13 -13
- metadata +123 -24
- data/app/controllers/concerns/page_timings.rb +0 -10
- data/app/controllers/concerns/response_range_concern.rb +0 -45
- data/app/controllers/concerns/time_range_concern.rb +0 -152
- data/app/controllers/concerns/zoom_range_concern.rb +0 -86
|
@@ -2,10 +2,12 @@ import { Controller } from "@hotwired/stimulus"
|
|
|
2
2
|
|
|
3
3
|
export default class extends Controller {
|
|
4
4
|
static values = {
|
|
5
|
-
type: String,
|
|
6
|
-
data: Object,
|
|
7
|
-
options: Object,
|
|
8
|
-
theme: String
|
|
5
|
+
type: String, // "bar", "line", "area", "sparkline"
|
|
6
|
+
data: Object, // Chart data
|
|
7
|
+
options: Object, // ECharts configuration
|
|
8
|
+
theme: String, // ECharts theme
|
|
9
|
+
timezone: String, // IANA zone summaries were bucketed in (e.g. "Etc/UTC")
|
|
10
|
+
timezoneLabel: String // Human label for the same zone (e.g. "UTC")
|
|
9
11
|
}
|
|
10
12
|
|
|
11
13
|
connect() {
|
|
@@ -401,6 +403,46 @@ export default class extends Controller {
|
|
|
401
403
|
return this.getSafeFormatter(cleanString)
|
|
402
404
|
}
|
|
403
405
|
|
|
406
|
+
// Formats in the aggregation zone, not the browser's — SummaryJob fixes a
|
|
407
|
+
// day's boundary at write time, so browser-zone display can flip the date.
|
|
408
|
+
formatDailyDate(date, extraOptions = {}) {
|
|
409
|
+
const timeZone = this.timezoneValue || undefined
|
|
410
|
+
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone, ...extraOptions })
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// "Mon D, HH:MM" — the hourly-granularity counterpart to formatDailyDate.
|
|
414
|
+
formatDailyDateTime(date) {
|
|
415
|
+
const timeZone = this.timezoneValue || undefined
|
|
416
|
+
return date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false, timeZone })
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// "HH:MM" only, same zone as the above.
|
|
420
|
+
formatHourMinute(date) {
|
|
421
|
+
const timeZone = this.timezoneValue || undefined
|
|
422
|
+
return date.toLocaleString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false, timeZone })
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Year comparison in the same zone as the label it decorates, so a point
|
|
426
|
+
// near New Year is not tagged with a year the aggregation zone has not
|
|
427
|
+
// reached yet (or has already left).
|
|
428
|
+
isCurrentYear(date) {
|
|
429
|
+
const options = { year: 'numeric', timeZone: this.timezoneValue || undefined }
|
|
430
|
+
return date.toLocaleDateString('en-US', options) === new Date().toLocaleDateString('en-US', options)
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Hourly vs. daily comes from the formatter key Ruby chose, not ECharts'
|
|
434
|
+
// generated axisValueLabel (unreliable for some axis-pointer/series combos).
|
|
435
|
+
isHourlyAxis() {
|
|
436
|
+
return this.optionsValue?.xAxis?.axisLabel?.formatter === "time"
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Appends " (Zone)" to a tooltip's date/time string when a zone label is
|
|
440
|
+
// available, so a hover always says which zone the point is in.
|
|
441
|
+
withZoneSuffix(dateString) {
|
|
442
|
+
const zone = this.timezoneLabelValue || ''
|
|
443
|
+
return zone ? `${dateString} (${zone})` : dateString
|
|
444
|
+
}
|
|
445
|
+
|
|
404
446
|
/**
|
|
405
447
|
* Returns a safe formatter function based on the formatter string.
|
|
406
448
|
* This prevents arbitrary code execution by using a whitelist approach.
|
|
@@ -455,7 +497,7 @@ export default class extends Controller {
|
|
|
455
497
|
return value.toString()
|
|
456
498
|
}
|
|
457
499
|
|
|
458
|
-
return
|
|
500
|
+
return this.formatDailyDate(date)
|
|
459
501
|
}
|
|
460
502
|
return value
|
|
461
503
|
},
|
|
@@ -463,17 +505,14 @@ export default class extends Controller {
|
|
|
463
505
|
// Time only (formatted as "HH:00" to match Rails Pulse formatters)
|
|
464
506
|
'time': (value) => {
|
|
465
507
|
if (typeof value === 'number' || typeof value === 'string') {
|
|
466
|
-
// Convert to number if string
|
|
467
508
|
const numValue = typeof value === 'string' ? parseInt(value) : value
|
|
468
509
|
const date = new Date(numValue)
|
|
469
510
|
|
|
470
|
-
|
|
471
|
-
if (isNaN(hours)) {
|
|
511
|
+
if (isNaN(date.getTime())) {
|
|
472
512
|
return value.toString()
|
|
473
513
|
}
|
|
474
514
|
|
|
475
|
-
|
|
476
|
-
return hours.toString().padStart(2, '0') + ':' + minutes.toString().padStart(2, '0')
|
|
515
|
+
return this.formatHourMinute(date)
|
|
477
516
|
}
|
|
478
517
|
return value
|
|
479
518
|
},
|
|
@@ -501,9 +540,7 @@ export default class extends Controller {
|
|
|
501
540
|
|
|
502
541
|
const data = params[0]
|
|
503
542
|
const date = new Date(data.axisValue)
|
|
504
|
-
const
|
|
505
|
-
const minutes = date.getMinutes().toString().padStart(2, '0')
|
|
506
|
-
const dateString = `${hours}:${minutes}`
|
|
543
|
+
const dateString = this.withZoneSuffix(this.formatHourMinute(date))
|
|
507
544
|
const value = parseInt(data.data)
|
|
508
545
|
|
|
509
546
|
return `${dateString} <br /> ${data.marker} ${value} ms`
|
|
@@ -515,7 +552,7 @@ export default class extends Controller {
|
|
|
515
552
|
|
|
516
553
|
const data = params[0]
|
|
517
554
|
const date = new Date(data.axisValue)
|
|
518
|
-
const dateString =
|
|
555
|
+
const dateString = this.withZoneSuffix(this.formatDailyDate(date))
|
|
519
556
|
const value = parseInt(data.data)
|
|
520
557
|
|
|
521
558
|
return `${dateString} <br /> ${data.marker} ${value} ms`
|
|
@@ -527,9 +564,7 @@ export default class extends Controller {
|
|
|
527
564
|
|
|
528
565
|
const data = params[0]
|
|
529
566
|
const date = new Date(data.axisValue)
|
|
530
|
-
const
|
|
531
|
-
const minutes = date.getMinutes().toString().padStart(2, '0')
|
|
532
|
-
const dateString = `${hours}:${minutes}`
|
|
567
|
+
const dateString = this.withZoneSuffix(this.formatHourMinute(date))
|
|
533
568
|
|
|
534
569
|
return `${dateString} <br /> ${data.marker} ${data.data}`
|
|
535
570
|
},
|
|
@@ -540,7 +575,7 @@ export default class extends Controller {
|
|
|
540
575
|
|
|
541
576
|
const data = params[0]
|
|
542
577
|
const date = new Date(data.axisValue)
|
|
543
|
-
const dateString =
|
|
578
|
+
const dateString = this.withZoneSuffix(this.formatDailyDate(date))
|
|
544
579
|
|
|
545
580
|
return `${dateString} <br /> ${data.marker} ${data.data}`
|
|
546
581
|
},
|
|
@@ -558,13 +593,14 @@ export default class extends Controller {
|
|
|
558
593
|
const timestamp = Number(axisValue)
|
|
559
594
|
const date = new Date(timestamp)
|
|
560
595
|
if (!isNaN(date.getTime())) {
|
|
561
|
-
dateString =
|
|
562
|
-
|
|
563
|
-
day: 'numeric',
|
|
564
|
-
year: date.getFullYear() !== new Date().getFullYear() ? 'numeric' : undefined
|
|
596
|
+
dateString = this.formatDailyDate(date, {
|
|
597
|
+
year: this.isCurrentYear(date) ? undefined : 'numeric'
|
|
565
598
|
})
|
|
566
599
|
}
|
|
567
600
|
}
|
|
601
|
+
// Category axis sends an already-formatted label (e.g. "Apr 28")
|
|
602
|
+
// instead of a timestamp — still needs the suffix below.
|
|
603
|
+
dateString = this.withZoneSuffix(dateString)
|
|
568
604
|
|
|
569
605
|
// Build tooltip HTML with all series
|
|
570
606
|
let html = `${dateString}<br/>`
|
|
@@ -587,7 +623,7 @@ export default class extends Controller {
|
|
|
587
623
|
if (typeof numValue === 'number' && !isNaN(numValue) && numValue > 1000000000000) {
|
|
588
624
|
const date = new Date(numValue)
|
|
589
625
|
if (!isNaN(date.getTime())) {
|
|
590
|
-
return
|
|
626
|
+
return this.formatDailyDate(date)
|
|
591
627
|
}
|
|
592
628
|
}
|
|
593
629
|
|
|
@@ -599,23 +635,26 @@ export default class extends Controller {
|
|
|
599
635
|
'tooltip_with_timestamp': (params) => {
|
|
600
636
|
if (!Array.isArray(params) || params.length === 0) return ''
|
|
601
637
|
|
|
602
|
-
//
|
|
603
|
-
//
|
|
638
|
+
// axisValue is the raw value — a ms timestamp for time axis, an
|
|
639
|
+
// already-formatted category string (e.g. "Apr 28") otherwise
|
|
604
640
|
const axisValue = params[0].axisValue
|
|
605
641
|
const ts = Number(axisValue)
|
|
606
642
|
let dateString
|
|
607
643
|
|
|
608
644
|
if (!isNaN(ts) && ts > 1000000000000) {
|
|
609
645
|
const date = new Date(ts)
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
dateString =
|
|
614
|
-
? date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })
|
|
615
|
-
: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
646
|
+
// Sub-day granularity comes from the formatter Ruby chose for this
|
|
647
|
+
// chart, not from ECharts' own generated axisValueLabel (unreliable
|
|
648
|
+
// for some axis-pointer/series combinations — see isHourlyAxis()).
|
|
649
|
+
dateString = this.isHourlyAxis() ? this.formatDailyDateTime(date) : this.formatDailyDate(date)
|
|
616
650
|
} else {
|
|
651
|
+
// Category axis (daily/weekly/monthly charts send a plain,
|
|
652
|
+
// already-formatted label like "Apr 28" instead of an epoch
|
|
653
|
+
// timestamp) — still needs the zone suffix, same as the time-axis
|
|
654
|
+
// branch above, or daily-grouped charts silently lose it.
|
|
617
655
|
dateString = axisValue
|
|
618
656
|
}
|
|
657
|
+
dateString = this.withZoneSuffix(dateString)
|
|
619
658
|
|
|
620
659
|
// Build tooltip HTML with all series
|
|
621
660
|
let html = `${dateString}<br/>`
|
|
@@ -643,12 +682,13 @@ export default class extends Controller {
|
|
|
643
682
|
if (typeof numValue === 'number' && !isNaN(numValue) && numValue > 1000000000000) {
|
|
644
683
|
const date = new Date(numValue)
|
|
645
684
|
if (!isNaN(date.getTime())) {
|
|
646
|
-
|
|
647
|
-
const minutes = date.getMinutes().toString().padStart(2, '0')
|
|
648
|
-
axisValue = `${hours}:${minutes}`
|
|
685
|
+
axisValue = this.formatHourMinute(date)
|
|
649
686
|
}
|
|
650
687
|
}
|
|
651
|
-
//
|
|
688
|
+
// Either the hour just computed above, or an already-formatted daily
|
|
689
|
+
// label (like "Apr 5") — both need the suffix, or daily sparklines
|
|
690
|
+
// silently lose it.
|
|
691
|
+
axisValue = this.withZoneSuffix(axisValue)
|
|
652
692
|
|
|
653
693
|
// Show marker, series name, and value (e.g., "● P95: 150")
|
|
654
694
|
return `${axisValue}<br/>${data.marker} ${seriesName}: ${value}`
|
|
@@ -669,11 +709,10 @@ export default class extends Controller {
|
|
|
669
709
|
if (typeof numValue === 'number' && !isNaN(numValue) && numValue > 1000000000000) {
|
|
670
710
|
const date = new Date(numValue)
|
|
671
711
|
if (!isNaN(date.getTime())) {
|
|
672
|
-
|
|
673
|
-
const minutes = date.getMinutes().toString().padStart(2, '0')
|
|
674
|
-
axisValue = `${hours}:${minutes}`
|
|
712
|
+
axisValue = this.formatHourMinute(date)
|
|
675
713
|
}
|
|
676
714
|
}
|
|
715
|
+
axisValue = this.withZoneSuffix(axisValue)
|
|
677
716
|
|
|
678
717
|
return `${axisValue}<br/>${data.marker} ${seriesName}: ${value}`
|
|
679
718
|
},
|
|
@@ -688,14 +727,12 @@ export default class extends Controller {
|
|
|
688
727
|
|
|
689
728
|
if (!isNaN(ts) && ts > 1000000000000) {
|
|
690
729
|
const date = new Date(ts)
|
|
691
|
-
|
|
692
|
-
const isHourly = /^\d{2}:\d{2}$/.test(axisLabel)
|
|
693
|
-
dateString = isHourly
|
|
694
|
-
? date.toLocaleString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: false })
|
|
695
|
-
: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
|
|
730
|
+
dateString = this.isHourlyAxis() ? this.formatDailyDateTime(date) : this.formatDailyDate(date)
|
|
696
731
|
} else {
|
|
732
|
+
// Category axis (daily/weekly/monthly) — see tooltip_with_timestamp.
|
|
697
733
|
dateString = axisValue
|
|
698
734
|
}
|
|
735
|
+
dateString = this.withZoneSuffix(dateString)
|
|
699
736
|
|
|
700
737
|
let html = `${dateString}<br/>`
|
|
701
738
|
params.forEach(param => {
|
|
@@ -6,8 +6,10 @@ module RailsPulse
|
|
|
6
6
|
# @param period_types [Array<String>] Period types to backfill (default: ["hour", "day"])
|
|
7
7
|
# @return [nil]
|
|
8
8
|
def perform(start_date, end_date, period_types = [ "hour", "day" ])
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
# A Date or date string means midnight in the aggregation time zone, so
|
|
10
|
+
# backfilled periods land on the boundaries the scheduled job writes.
|
|
11
|
+
start_date = start_date.in_time_zone
|
|
12
|
+
end_date = end_date.in_time_zone
|
|
11
13
|
|
|
12
14
|
period_types.each do |period_type|
|
|
13
15
|
backfill_period(period_type, start_date, end_date)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# Sends the health update for the minute just ended to Rails Pulse Cloud,
|
|
3
|
+
# deployments recorded since the last run, and any buffered batches whose
|
|
4
|
+
# retry is due. Schedule it every minute alongside SummaryJob. Does nothing
|
|
5
|
+
# unless config.cloud is set.
|
|
6
|
+
class CloudHealthJob < ApplicationJob
|
|
7
|
+
def perform
|
|
8
|
+
Cloud::Sync.new.minutely
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# Sends newly summarized hours, changed exception groups and deployments to
|
|
3
|
+
# Rails Pulse Cloud. SummaryJob enqueues it once its summaries are written,
|
|
4
|
+
# so it needs no schedule of its own. Does nothing unless config.cloud is
|
|
5
|
+
# set.
|
|
6
|
+
class CloudSyncJob < ApplicationJob
|
|
7
|
+
def perform
|
|
8
|
+
Cloud::Sync.new.hourly
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -19,6 +19,8 @@ module RailsPulse
|
|
|
19
19
|
# Check if we should run monthly summary (first day of month)
|
|
20
20
|
process_monthly_summary((target_hour.to_date - 1.month).beginning_of_month) if first_of_month?(target_hour)
|
|
21
21
|
end
|
|
22
|
+
|
|
23
|
+
enqueue_cloud_sync
|
|
22
24
|
rescue StandardError => e
|
|
23
25
|
log_error(e)
|
|
24
26
|
raise
|
|
@@ -26,6 +28,17 @@ module RailsPulse
|
|
|
26
28
|
|
|
27
29
|
private
|
|
28
30
|
|
|
31
|
+
# Cloud receives an hour only once it is summarized, so the sync follows
|
|
32
|
+
# this job rather than running on a schedule of its own. A queue that
|
|
33
|
+
# refuses the job must not fail the summaries already written.
|
|
34
|
+
def enqueue_cloud_sync
|
|
35
|
+
return unless RailsPulse.configuration.cloud.enabled?
|
|
36
|
+
|
|
37
|
+
CloudSyncJob.perform_later
|
|
38
|
+
rescue StandardError => e
|
|
39
|
+
RailsPulse.logger.error "Could not enqueue the Rails Pulse Cloud sync: #{e.class} - #{e.message}"
|
|
40
|
+
end
|
|
41
|
+
|
|
29
42
|
def midnight?(time)
|
|
30
43
|
time.hour == 0
|
|
31
44
|
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module HasMetadata
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# metadata can be valid JSON that isn't an object (an array, a scalar)
|
|
6
|
+
def metadata_hash
|
|
7
|
+
return {} if metadata.blank?
|
|
8
|
+
|
|
9
|
+
parsed = JSON.parse(metadata)
|
|
10
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
11
|
+
rescue JSON::ParserError
|
|
12
|
+
{}
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -5,25 +5,18 @@ module RailsPulse
|
|
|
5
5
|
# Tag validation constants
|
|
6
6
|
TAG_NAME_REGEX = /\A[a-z0-9_-]+\z/i
|
|
7
7
|
MAX_TAG_LENGTH = 50
|
|
8
|
-
# Escape character for LIKE patterns over the serialized tags column.
|
|
9
|
-
# Never appears in a valid tag name, and needs no quoting on any adapter.
|
|
10
|
-
LIKE_ESCAPE = "!".freeze
|
|
11
|
-
|
|
12
8
|
included do
|
|
13
9
|
# Callbacks
|
|
14
10
|
before_save :ensure_tags_is_array
|
|
15
11
|
|
|
16
|
-
# Scopes with table name qualification to avoid ambiguity.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
# character — without it `my\_tag` matches a literal backslash there
|
|
20
|
-
# and the filter silently matches nothing. `!` is used rather than `\`
|
|
21
|
-
# because MySQL and PostgreSQL disagree on how to quote a backslash.
|
|
12
|
+
# Scopes with table name qualification to avoid ambiguity. Tags are
|
|
13
|
+
# matched against the serialized column, so the quoted tag name is
|
|
14
|
+
# escaped by LikePattern and paired with its ESCAPE clause.
|
|
22
15
|
scope :with_tag, ->(tag) {
|
|
23
|
-
where("#{table_name}.tags LIKE ?
|
|
16
|
+
where("#{table_name}.tags LIKE ? #{RailsPulse::LikePattern::CLAUSE}", "%\"#{RailsPulse::LikePattern.escape(tag)}\"%")
|
|
24
17
|
}
|
|
25
18
|
scope :without_tag, ->(tag) {
|
|
26
|
-
where.not("#{table_name}.tags LIKE ?
|
|
19
|
+
where.not("#{table_name}.tags LIKE ? #{RailsPulse::LikePattern::CLAUSE}", "%\"#{RailsPulse::LikePattern.escape(tag)}\"%")
|
|
27
20
|
}
|
|
28
21
|
scope :with_tags, -> { where("#{table_name}.tags IS NOT NULL AND #{table_name}.tags != '[]'") }
|
|
29
22
|
end
|
|
@@ -3,14 +3,21 @@ require "active_support/number_helper"
|
|
|
3
3
|
module RailsPulse
|
|
4
4
|
module Cards
|
|
5
5
|
class Base
|
|
6
|
+
def initialize(window: nil, subject: nil, period: 7, period_type: "day", disabled_tags: [], show_non_tagged: true)
|
|
7
|
+
@window = window
|
|
8
|
+
@subject = subject
|
|
9
|
+
@period = period
|
|
10
|
+
@period_type = period_type
|
|
11
|
+
@disabled_tags = disabled_tags
|
|
12
|
+
@show_non_tagged = show_non_tagged
|
|
13
|
+
end
|
|
14
|
+
|
|
6
15
|
private
|
|
7
16
|
|
|
8
|
-
# nil unless the caller passed
|
|
9
|
-
#
|
|
17
|
+
# nil unless the caller passed a window, so the "trailing @period
|
|
18
|
+
# days/hours" fallback below still applies by default.
|
|
10
19
|
def time_window
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@time_window = RailsPulse::TimeWindow.build(@start_time, @end_time)
|
|
20
|
+
@window
|
|
14
21
|
end
|
|
15
22
|
|
|
16
23
|
def now
|
|
@@ -74,14 +81,16 @@ module RailsPulse
|
|
|
74
81
|
end
|
|
75
82
|
|
|
76
83
|
def subject_id
|
|
77
|
-
|
|
78
|
-
instance_variable_get(:@job)&.id ||
|
|
79
|
-
instance_variable_get(:@query)&.id ||
|
|
80
|
-
instance_variable_get(:@route)&.id
|
|
84
|
+
@subject&.id
|
|
81
85
|
end
|
|
82
86
|
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
# A SELECT fragment comparing against the card's window, with the
|
|
88
|
+
# bounds bound as values (:current_start, :range_start) rather than
|
|
89
|
+
# interpolated into the SQL string.
|
|
90
|
+
def window_sql(fragment)
|
|
91
|
+
Arel.sql(
|
|
92
|
+
RailsPulse::Summary.sanitize_sql_array([ fragment, { current_start: current_window_start, range_start: range_start } ])
|
|
93
|
+
)
|
|
85
94
|
end
|
|
86
95
|
|
|
87
96
|
# Enhanced sparkline generation (support hour and day)
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
module Charts
|
|
3
3
|
class Base
|
|
4
|
-
def initialize(ransack_query:, period_type: nil, subject: nil,
|
|
4
|
+
def initialize(ransack_query:, period_type: nil, subject: nil, window: nil, start_duration: nil, disabled_tags: [], show_non_tagged: true)
|
|
5
5
|
@ransack_query = ransack_query
|
|
6
6
|
@period_type = period_type
|
|
7
7
|
@subject = subject
|
|
8
|
-
@
|
|
9
|
-
@end_time = end_time
|
|
8
|
+
@window = window
|
|
10
9
|
@start_duration = start_duration
|
|
11
10
|
@disabled_tags = disabled_tags
|
|
12
11
|
@show_non_tagged = show_non_tagged
|
|
13
|
-
|
|
14
|
-
# Support legacy parameter names (job, query, route)
|
|
15
|
-
@subject ||= options[:job] || options[:query] || options[:route]
|
|
16
12
|
end
|
|
17
13
|
|
|
18
14
|
private
|
|
@@ -28,23 +24,14 @@ module RailsPulse
|
|
|
28
24
|
.then { |q| @subject ? q.where(summarizable_id: @subject.id) : q }
|
|
29
25
|
end
|
|
30
26
|
|
|
31
|
-
# Common helper for time step calculation
|
|
32
|
-
def time_step
|
|
33
|
-
@period_type.to_s == "hour" ? 3600 : 86400
|
|
34
|
-
end
|
|
35
|
-
|
|
36
27
|
# Abstract method - must be implemented by subclasses
|
|
37
28
|
def summarizable_type
|
|
38
29
|
raise NotImplementedError, "#{self.class} must implement summarizable_type"
|
|
39
30
|
end
|
|
40
31
|
|
|
41
|
-
#
|
|
42
|
-
def pad_data_with_zeros(raw_data
|
|
43
|
-
|
|
44
|
-
(start_time.to_i..end_time.to_i).step(step) do |timestamp|
|
|
45
|
-
padded[timestamp] = raw_data[timestamp] || 0
|
|
46
|
-
end
|
|
47
|
-
end
|
|
32
|
+
# Every bucket in the window, with zero where there is no data.
|
|
33
|
+
def pad_data_with_zeros(raw_data)
|
|
34
|
+
@window.bucket_timestamps(@period_type).index_with { |timestamp| raw_data[timestamp] || 0 }
|
|
48
35
|
end
|
|
49
36
|
end
|
|
50
37
|
end
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
module RailsPulse
|
|
2
2
|
module Charts
|
|
3
3
|
class PercentileChartBase
|
|
4
|
-
def initialize(ransack_query:, period_type: nil, subject: nil,
|
|
4
|
+
def initialize(ransack_query:, period_type: nil, subject: nil, window: nil, start_duration: nil, disabled_tags: [], show_non_tagged: true)
|
|
5
5
|
@ransack_query = ransack_query
|
|
6
6
|
@period_type = period_type
|
|
7
7
|
@subject = subject
|
|
8
|
-
@
|
|
9
|
-
@end_time = end_time
|
|
8
|
+
@window = window
|
|
10
9
|
@start_duration = start_duration
|
|
11
10
|
@disabled_tags = disabled_tags
|
|
12
11
|
@show_non_tagged = show_non_tagged
|
|
@@ -48,9 +47,8 @@ module RailsPulse
|
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
# Convert to final values (weighted averages) and pad missing data
|
|
51
|
-
step = @period_type.to_s == "hour" ? 3600 : 86400
|
|
52
50
|
daily_data = {}
|
|
53
|
-
(@
|
|
51
|
+
@window.bucket_timestamps(@period_type).each do |timestamp|
|
|
54
52
|
if raw_data[timestamp]
|
|
55
53
|
count = raw_data[timestamp][:total_count]
|
|
56
54
|
daily_data[timestamp] = {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
# One CleanupService run, stored as an Event of kind cleanup_run: `value`
|
|
3
|
+
# is the rows it deleted, `outcome` completed or failed, and `metadata` the
|
|
4
|
+
# per-table counts and any stages that failed. rails_pulse:status reads the
|
|
5
|
+
# latest to say whether cleanup is running at all; nothing else depends on
|
|
6
|
+
# these rows, so event retention prunes them like any other kind.
|
|
7
|
+
class CleanupRun
|
|
8
|
+
KIND = "cleanup_run".freeze
|
|
9
|
+
SUBJECT = "cleanup".freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def events
|
|
13
|
+
RailsPulse::Event.of_kind(KIND)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def record!(stats:, failed_stages: [], ran_at: Time.current)
|
|
17
|
+
RailsPulse::Event.create!(
|
|
18
|
+
kind: KIND,
|
|
19
|
+
subject: SUBJECT,
|
|
20
|
+
outcome: failed_stages.any? ? "failed" : "completed",
|
|
21
|
+
value: stats[:total_deleted].to_i,
|
|
22
|
+
occurred_at: ran_at,
|
|
23
|
+
metadata: { time_based: stats[:time_based], count_based: stats[:count_based], failed_stages: failed_stages }.to_json
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def latest
|
|
28
|
+
events.recent.first
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
require "zlib"
|
|
2
|
+
|
|
3
|
+
module RailsPulse
|
|
4
|
+
module Cloud
|
|
5
|
+
# A batch waiting to be sent to Rails Pulse Cloud, stored gzipped. It
|
|
6
|
+
# keeps its batch_id across retries, so Cloud recognises a resend. The
|
|
7
|
+
# buffer holds at most MAX_AGE or MAX_BYTES of batches, dropping the
|
|
8
|
+
# oldest first, so an outage cannot grow it without bound.
|
|
9
|
+
class BufferedBatch < RailsPulse::ApplicationRecord
|
|
10
|
+
self.table_name = "rails_pulse_cloud_batches"
|
|
11
|
+
|
|
12
|
+
MAX_AGE = 7.days
|
|
13
|
+
MAX_BYTES = 50.megabytes
|
|
14
|
+
|
|
15
|
+
validates :batch_id, :payload, :byte_size, :item_count, :next_attempt_at, presence: true
|
|
16
|
+
|
|
17
|
+
scope :due, ->(now = Time.current) { where(next_attempt_at: ..now).order(:created_at, :id) }
|
|
18
|
+
|
|
19
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
20
|
+
%w[batch_id byte_size item_count attempts next_attempt_at created_at]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
24
|
+
[]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.enqueue!(batch, now: Time.current)
|
|
28
|
+
payload = Zlib.gzip(batch.to_json)
|
|
29
|
+
create!(batch_id: batch.batch_id, payload: payload, byte_size: payload.bytesize, item_count: batch.items.size,
|
|
30
|
+
next_attempt_at: now)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Drops batches past MAX_AGE, then the oldest until the rest fit in
|
|
34
|
+
# MAX_BYTES. Returns how many were dropped.
|
|
35
|
+
def self.prune!(now = Time.current)
|
|
36
|
+
dropped = where(created_at: ...(now - MAX_AGE)).delete_all
|
|
37
|
+
total = sum(:byte_size)
|
|
38
|
+
return dropped if total <= MAX_BYTES
|
|
39
|
+
|
|
40
|
+
order(:created_at, :id).pluck(:id, :byte_size).each do |id, size|
|
|
41
|
+
break if total <= MAX_BYTES
|
|
42
|
+
|
|
43
|
+
dropped += where(id: id).delete_all
|
|
44
|
+
total -= size
|
|
45
|
+
end
|
|
46
|
+
dropped
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# The stored batch as a hash with string keys.
|
|
50
|
+
def contents
|
|
51
|
+
JSON.parse(Zlib.gunzip(payload))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module RailsPulse
|
|
2
|
+
module Cloud
|
|
3
|
+
# The one row naming this Rails Pulse database to Rails Pulse Cloud, and
|
|
4
|
+
# where its sync has got to. Hosts that share the database share the row,
|
|
5
|
+
# so their data is counted once; hosts with separate databases are
|
|
6
|
+
# separate installations, and Cloud adds them together.
|
|
7
|
+
class Installation < RailsPulse::ApplicationRecord
|
|
8
|
+
self.table_name = "rails_pulse_cloud_installations"
|
|
9
|
+
|
|
10
|
+
validates :installation_id, presence: true
|
|
11
|
+
|
|
12
|
+
def self.ransackable_attributes(_auth_object = nil)
|
|
13
|
+
%w[installation_id last_success_at last_error_at paused_until created_at]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.ransackable_associations(_auth_object = nil)
|
|
17
|
+
[]
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# The row, created with a new installation ID the first time it is
|
|
21
|
+
# asked for. A second process creating it at the same moment loses on
|
|
22
|
+
# the unique index and reads the winner's.
|
|
23
|
+
def self.current
|
|
24
|
+
first || create!(installation_id: SecureRandom.uuid)
|
|
25
|
+
rescue ActiveRecord::RecordNotUnique
|
|
26
|
+
first!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# The row if one exists, without creating it.
|
|
30
|
+
def self.existing
|
|
31
|
+
first
|
|
32
|
+
rescue ActiveRecord::ActiveRecordError
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def paused?(now = Time.current)
|
|
37
|
+
paused_until.present? && paused_until > now
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def record_success!(deprecated_on: nil, now: Time.current)
|
|
41
|
+
update!(last_success_at: now, last_error: nil, last_error_at: nil, paused_until: nil, pause_reason: nil,
|
|
42
|
+
contract_deprecated_on: deprecated_on)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def record_error!(message, now: Time.current)
|
|
46
|
+
update!(last_error: message.to_s.truncate(2_000), last_error_at: now)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pause!(until_time, reason, now: Time.current)
|
|
50
|
+
update!(paused_until: until_time, pause_reason: reason.to_s.truncate(2_000),
|
|
51
|
+
last_error: reason.to_s.truncate(2_000), last_error_at: now)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -2,13 +2,12 @@ module RailsPulse
|
|
|
2
2
|
module Dashboard
|
|
3
3
|
module Charts
|
|
4
4
|
class ResponseTimePercentiles
|
|
5
|
-
def initialize(disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day",
|
|
5
|
+
def initialize(disabled_tags: [], show_non_tagged: true, period: 7, period_type: "day", window: nil)
|
|
6
6
|
@disabled_tags = disabled_tags
|
|
7
7
|
@show_non_tagged = show_non_tagged
|
|
8
8
|
@period = period
|
|
9
9
|
@period_type = period_type
|
|
10
|
-
@
|
|
11
|
-
@end_time = end_time
|
|
10
|
+
@window = window
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def to_chart_data
|
|
@@ -169,10 +168,10 @@ module RailsPulse
|
|
|
169
168
|
|
|
170
169
|
private
|
|
171
170
|
|
|
172
|
-
# nil unless
|
|
171
|
+
# nil unless a window was given, so the default
|
|
173
172
|
# "recent" view keeps using the trailing-@period-days fallback below.
|
|
174
173
|
def time_window
|
|
175
|
-
@
|
|
174
|
+
@window
|
|
176
175
|
end
|
|
177
176
|
|
|
178
177
|
def default_hour_range
|