railwatch 0.9.4 → 1.0.0
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/AGENTS.md +37 -33
- data/CHANGELOG.md +50 -0
- data/README.md +51 -60
- data/config/routes.rb +0 -59
- data/docs/ai-and-mcp.md +9 -8
- data/docs/configuration.md +35 -66
- data/docs/faq.md +17 -25
- data/docs/getting-started.md +105 -117
- data/docs/replacing-nightwatch.md +8 -7
- data/docs/replacing-sentry.md +12 -16
- data/docs/security.md +14 -20
- data/docs/self-hosting.md +24 -22
- data/docs/testing.md +6 -14
- data/docs/troubleshooting.md +12 -46
- data/lib/generators/railwatch/install/install_generator.rb +12 -240
- data/lib/generators/railwatch/install/templates/initializer.rb.tt +10 -40
- data/lib/generators/railwatch/install/templates/post-deploy +0 -7
- data/lib/railwatch/authentication.rb +6 -5
- data/lib/railwatch/configuration.rb +21 -151
- data/lib/railwatch/engine.rb +9 -118
- data/lib/railwatch/patches.rb +0 -11
- data/lib/railwatch/reporter.rb +3 -19
- data/lib/railwatch/subscribers/process_info.rb +1 -2
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +4 -62
- data/lib/tasks/railwatch_tasks.rake +3 -325
- data/llms.txt +23 -25
- metadata +3 -453
- data/app/channels/railwatch/environment_channel.rb +0 -62
- data/app/controllers/concerns/railwatch/telemetry_identity.rb +0 -25
- data/app/controllers/railwatch/alerts_controller.rb +0 -48
- data/app/controllers/railwatch/anomaly_rules_controller.rb +0 -31
- data/app/controllers/railwatch/attachments_controller.rb +0 -22
- data/app/controllers/railwatch/broadcasts_controller.rb +0 -15
- data/app/controllers/railwatch/cache_events_controller.rb +0 -30
- data/app/controllers/railwatch/commands_controller.rb +0 -16
- data/app/controllers/railwatch/comments_controller.rb +0 -11
- data/app/controllers/railwatch/dashboard_controller.rb +0 -70
- data/app/controllers/railwatch/deploys_controller.rb +0 -34
- data/app/controllers/railwatch/deprecations_controller.rb +0 -54
- data/app/controllers/railwatch/environment_scoped.rb +0 -81
- data/app/controllers/railwatch/exceptions_controller.rb +0 -52
- data/app/controllers/railwatch/executions_controller.rb +0 -13
- data/app/controllers/railwatch/issues_controller.rb +0 -258
- data/app/controllers/railwatch/jobs_controller.rb +0 -63
- data/app/controllers/railwatch/llm_calls_controller.rb +0 -124
- data/app/controllers/railwatch/logs_controller.rb +0 -40
- data/app/controllers/railwatch/mails_controller.rb +0 -19
- data/app/controllers/railwatch/notifications_controller.rb +0 -11
- data/app/controllers/railwatch/outgoing_requests_controller.rb +0 -19
- data/app/controllers/railwatch/overview_controller.rb +0 -38
- data/app/controllers/railwatch/people_controller.rb +0 -25
- data/app/controllers/railwatch/processes_controller.rb +0 -33
- data/app/controllers/railwatch/profiles_controller.rb +0 -65
- data/app/controllers/railwatch/queries_controller.rb +0 -59
- data/app/controllers/railwatch/releases_controller.rb +0 -75
- data/app/controllers/railwatch/requests_controller.rb +0 -37
- data/app/controllers/railwatch/saved_views_controller.rb +0 -53
- data/app/controllers/railwatch/scheduled_tasks_controller.rb +0 -45
- data/app/controllers/railwatch/spans_controller.rb +0 -47
- data/app/controllers/railwatch/storage_ops_controller.rb +0 -15
- data/app/controllers/railwatch/tenants_controller.rb +0 -44
- data/app/controllers/railwatch/thresholds_controller.rb +0 -47
- data/app/controllers/railwatch/traces_controller.rb +0 -72
- data/app/controllers/railwatch/transactions_controller.rb +0 -21
- data/app/controllers/railwatch/view_renders_controller.rb +0 -28
- data/app/controllers/railwatch/visits_controller.rb +0 -53
- data/app/frontend/components/app-content.tsx +0 -26
- data/app/frontend/components/app-logo-icon.tsx +0 -23
- data/app/frontend/components/app-logo.tsx +0 -14
- data/app/frontend/components/app-shell.tsx +0 -38
- data/app/frontend/components/app-sidebar-header.tsx +0 -54
- data/app/frontend/components/app-sidebar.tsx +0 -94
- data/app/frontend/components/app-wordmark.tsx +0 -29
- data/app/frontend/components/breadcrumbs.tsx +0 -46
- data/app/frontend/components/icon.tsx +0 -16
- data/app/frontend/components/input-error.tsx +0 -18
- data/app/frontend/components/nav-footer.tsx +0 -46
- data/app/frontend/components/nav-main.tsx +0 -41
- data/app/frontend/components/nav-user.tsx +0 -50
- data/app/frontend/components/railwatch/chart-hover.tsx +0 -34
- data/app/frontend/components/railwatch/chart-panel.tsx +0 -170
- data/app/frontend/components/railwatch/code.tsx +0 -28
- data/app/frontend/components/railwatch/command-palette.tsx +0 -132
- data/app/frontend/components/railwatch/copy-block.tsx +0 -52
- data/app/frontend/components/railwatch/copy-id.tsx +0 -49
- data/app/frontend/components/railwatch/cursor-load-more.tsx +0 -39
- data/app/frontend/components/railwatch/data-table.tsx +0 -195
- data/app/frontend/components/railwatch/digit-roll.tsx +0 -312
- data/app/frontend/components/railwatch/empty-state.tsx +0 -159
- data/app/frontend/components/railwatch/env-nav.tsx +0 -130
- data/app/frontend/components/railwatch/env-switcher.tsx +0 -145
- data/app/frontend/components/railwatch/filter-bar.tsx +0 -153
- data/app/frontend/components/railwatch/flamegraph.tsx +0 -324
- data/app/frontend/components/railwatch/frames.tsx +0 -295
- data/app/frontend/components/railwatch/json-viewer.tsx +0 -159
- data/app/frontend/components/railwatch/live-dot.tsx +0 -78
- data/app/frontend/components/railwatch/live-toggle.tsx +0 -76
- data/app/frontend/components/railwatch/metric-chart.tsx +0 -112
- data/app/frontend/components/railwatch/mobile-page-switcher.tsx +0 -89
- data/app/frontend/components/railwatch/onboarding.tsx +0 -91
- data/app/frontend/components/railwatch/origin-identity.tsx +0 -84
- data/app/frontend/components/railwatch/page-header.tsx +0 -42
- data/app/frontend/components/railwatch/percentile-picker.tsx +0 -18
- data/app/frontend/components/railwatch/relative-time.tsx +0 -18
- data/app/frontend/components/railwatch/saved-views.tsx +0 -232
- data/app/frontend/components/railwatch/segmented.tsx +0 -64
- data/app/frontend/components/railwatch/series-chart.tsx +0 -400
- data/app/frontend/components/railwatch/shortcuts-dialog.tsx +0 -89
- data/app/frontend/components/railwatch/signal-toast.tsx +0 -121
- data/app/frontend/components/railwatch/sort-header.tsx +0 -37
- data/app/frontend/components/railwatch/source-link.tsx +0 -57
- data/app/frontend/components/railwatch/sparkline-cell.tsx +0 -14
- data/app/frontend/components/railwatch/sql-block.tsx +0 -116
- data/app/frontend/components/railwatch/stat.tsx +0 -185
- data/app/frontend/components/railwatch/status-badge.tsx +0 -79
- data/app/frontend/components/railwatch/step-picker.tsx +0 -57
- data/app/frontend/components/railwatch/timeline.tsx +0 -405
- data/app/frontend/components/railwatch/utilisation-bar.tsx +0 -44
- data/app/frontend/components/railwatch/window-picker.tsx +0 -147
- data/app/frontend/components/ui/avatar.tsx +0 -51
- data/app/frontend/components/ui/badge.tsx +0 -46
- data/app/frontend/components/ui/breadcrumb.tsx +0 -109
- data/app/frontend/components/ui/button.tsx +0 -60
- data/app/frontend/components/ui/card.tsx +0 -92
- data/app/frontend/components/ui/chart.tsx +0 -374
- data/app/frontend/components/ui/checkbox.tsx +0 -32
- data/app/frontend/components/ui/collapsible.tsx +0 -31
- data/app/frontend/components/ui/command.tsx +0 -182
- data/app/frontend/components/ui/dialog.tsx +0 -158
- data/app/frontend/components/ui/dropdown-menu.tsx +0 -255
- data/app/frontend/components/ui/hover-card.tsx +0 -42
- data/app/frontend/components/ui/input.tsx +0 -21
- data/app/frontend/components/ui/label.tsx +0 -24
- data/app/frontend/components/ui/popover.tsx +0 -87
- data/app/frontend/components/ui/select.tsx +0 -185
- data/app/frontend/components/ui/separator.tsx +0 -28
- data/app/frontend/components/ui/sheet.tsx +0 -137
- data/app/frontend/components/ui/sidebar.tsx +0 -726
- data/app/frontend/components/ui/skeleton.tsx +0 -13
- data/app/frontend/components/ui/sonner.tsx +0 -41
- data/app/frontend/components/ui/table.tsx +0 -114
- data/app/frontend/components/ui/tabs.tsx +0 -91
- data/app/frontend/components/ui/textarea.tsx +0 -18
- data/app/frontend/components/ui/tooltip.tsx +0 -61
- data/app/frontend/components/user-info.tsx +0 -32
- data/app/frontend/components/user-menu-content.tsx +0 -94
- data/app/frontend/entrypoints/application.css +0 -244
- data/app/frontend/entrypoints/inertia.tsx +0 -92
- data/app/frontend/fonts/CommitMono-Bold.woff2 +0 -0
- data/app/frontend/fonts/CommitMono-Regular.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Black.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Bold.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Medium.woff2 +0 -0
- data/app/frontend/fonts/Roboto-Regular.woff2 +0 -0
- data/app/frontend/hooks/use-appearance.tsx +0 -67
- data/app/frontend/hooks/use-clipboard.ts +0 -32
- data/app/frontend/hooks/use-flash.tsx +0 -23
- data/app/frontend/hooks/use-hotkeys.ts +0 -63
- data/app/frontend/hooks/use-initials.tsx +0 -15
- data/app/frontend/hooks/use-live.ts +0 -205
- data/app/frontend/hooks/use-mobile-navigation.ts +0 -8
- data/app/frontend/hooks/use-mobile.ts +0 -21
- data/app/frontend/hooks/use-percentile.ts +0 -24
- data/app/frontend/hooks/use-reduced-motion.ts +0 -25
- data/app/frontend/hooks/use-row-nav.ts +0 -106
- data/app/frontend/hooks/use-step.ts +0 -27
- data/app/frontend/hooks/use-ticker.ts +0 -12
- data/app/frontend/hooks/use-window.ts +0 -58
- data/app/frontend/layouts/app/app-sidebar-layout.tsx +0 -107
- data/app/frontend/layouts/app-layout.tsx +0 -21
- data/app/frontend/layouts/env-layout.tsx +0 -53
- data/app/frontend/layouts/persistent-layout.tsx +0 -18
- data/app/frontend/lib/cable.ts +0 -10
- data/app/frontend/lib/env-pages.ts +0 -236
- data/app/frontend/lib/execution-path.ts +0 -55
- data/app/frontend/lib/explain.ts +0 -50
- data/app/frontend/lib/filter.ts +0 -25
- data/app/frontend/lib/flamegraph.ts +0 -155
- data/app/frontend/lib/format.ts +0 -71
- data/app/frontend/lib/keyboard.ts +0 -18
- data/app/frontend/lib/mount.ts +0 -23
- data/app/frontend/lib/source-link.ts +0 -217
- data/app/frontend/lib/utils.ts +0 -6
- data/app/frontend/pages/alerts/index.tsx +0 -171
- data/app/frontend/pages/broadcasts/index.tsx +0 -171
- data/app/frontend/pages/cache_events/index.tsx +0 -127
- data/app/frontend/pages/commands/index.tsx +0 -143
- data/app/frontend/pages/deploys/index.tsx +0 -94
- data/app/frontend/pages/deploys/show.tsx +0 -309
- data/app/frontend/pages/deprecations/index.tsx +0 -176
- data/app/frontend/pages/exceptions/index.tsx +0 -271
- data/app/frontend/pages/executions/show.tsx +0 -821
- data/app/frontend/pages/issues/index.tsx +0 -399
- data/app/frontend/pages/issues/show.tsx +0 -1367
- data/app/frontend/pages/jobs/index.tsx +0 -381
- data/app/frontend/pages/jobs/klass.tsx +0 -215
- data/app/frontend/pages/llm_calls/index.tsx +0 -664
- data/app/frontend/pages/logs/index.tsx +0 -303
- data/app/frontend/pages/mails/index.tsx +0 -218
- data/app/frontend/pages/notifications/index.tsx +0 -161
- data/app/frontend/pages/outgoing_requests/index.tsx +0 -309
- data/app/frontend/pages/overview/show.tsx +0 -333
- data/app/frontend/pages/people/index.tsx +0 -102
- data/app/frontend/pages/people/show.tsx +0 -149
- data/app/frontend/pages/processes/index.tsx +0 -335
- data/app/frontend/pages/profiles/index.tsx +0 -110
- data/app/frontend/pages/profiles/show.tsx +0 -105
- data/app/frontend/pages/queries/index.tsx +0 -492
- data/app/frontend/pages/queries/show.tsx +0 -276
- data/app/frontend/pages/releases/index.tsx +0 -190
- data/app/frontend/pages/releases/release-health.tsx +0 -68
- data/app/frontend/pages/releases/release-path.ts +0 -25
- data/app/frontend/pages/releases/show.tsx +0 -262
- data/app/frontend/pages/requests/index.tsx +0 -235
- data/app/frontend/pages/requests/route.tsx +0 -159
- data/app/frontend/pages/scheduled_tasks/index.tsx +0 -220
- data/app/frontend/pages/spans/index.tsx +0 -229
- data/app/frontend/pages/spans/show.tsx +0 -220
- data/app/frontend/pages/storage_ops/index.tsx +0 -150
- data/app/frontend/pages/tenants/index.tsx +0 -236
- data/app/frontend/pages/tenants/show.tsx +0 -477
- data/app/frontend/pages/tenants/tenant-path.ts +0 -26
- data/app/frontend/pages/thresholds/index.tsx +0 -414
- data/app/frontend/pages/traces/show.tsx +0 -208
- data/app/frontend/pages/transactions/index.tsx +0 -220
- data/app/frontend/pages/view_renders/index.tsx +0 -241
- data/app/frontend/pages/visits/index.tsx +0 -321
- data/app/frontend/public/icon.png +0 -0
- data/app/frontend/public/icon.svg +0 -5
- data/app/frontend/routes/index.d.ts +0 -2136
- data/app/frontend/routes/index.js +0 -2112
- data/app/frontend/types/globals.d.ts +0 -9
- data/app/frontend/types/index.ts +0 -288
- data/app/frontend/types/vite-env.d.ts +0 -9
- data/app/helpers/railwatch/assets_helper.rb +0 -52
- data/app/jobs/railwatch/anomaly_scan_job.rb +0 -11
- data/app/jobs/railwatch/application_job.rb +0 -7
- data/app/jobs/railwatch/auto_resolve_issues_job.rb +0 -19
- data/app/jobs/railwatch/check_scheduled_tasks_job.rb +0 -113
- data/app/jobs/railwatch/detect_anomalies_job.rb +0 -171
- data/app/jobs/railwatch/detect_performance_issues_job.rb +0 -95
- data/app/jobs/railwatch/group_exceptions_job.rb +0 -91
- data/app/jobs/railwatch/optimize_telemetry_job.rb +0 -23
- data/app/jobs/railwatch/performance_scan_job.rb +0 -11
- data/app/jobs/railwatch/prune_telemetry_job.rb +0 -129
- data/app/jobs/railwatch/release_health_rollup_job.rb +0 -64
- data/app/jobs/railwatch/rollup_catchup_job.rb +0 -21
- data/app/jobs/railwatch/rollup_job.rb +0 -132
- data/app/jobs/railwatch/scheduled_task_scan_job.rb +0 -11
- data/app/models/concerns/railwatch/detection_snapshotting.rb +0 -20
- data/app/models/railwatch/alert.rb +0 -229
- data/app/models/railwatch/alert_rule.rb +0 -121
- data/app/models/railwatch/anomaly_rule.rb +0 -51
- data/app/models/railwatch/application.rb +0 -44
- data/app/models/railwatch/application_record.rb +0 -33
- data/app/models/railwatch/comment.rb +0 -36
- data/app/models/railwatch/deploy.rb +0 -67
- data/app/models/railwatch/environment.rb +0 -68
- data/app/models/railwatch/execution_presenter.rb +0 -185
- data/app/models/railwatch/filter_query.rb +0 -184
- data/app/models/railwatch/followup_receipt.rb +0 -27
- data/app/models/railwatch/ingest/batch.rb +0 -355
- data/app/models/railwatch/ingest/mapper.rb +0 -585
- data/app/models/railwatch/ingest/payload.rb +0 -96
- data/app/models/railwatch/ingest/rollup_absorber.rb +0 -170
- data/app/models/railwatch/ingest/writer.rb +0 -137
- data/app/models/railwatch/issue.rb +0 -277
- data/app/models/railwatch/issue_activity.rb +0 -23
- data/app/models/railwatch/issue_detection_presenter.rb +0 -309
- data/app/models/railwatch/issue_detection_snapshot.rb +0 -77
- data/app/models/railwatch/maintenance_task.rb +0 -53
- data/app/models/railwatch/saved_view.rb +0 -63
- data/app/models/railwatch/telemetry/aggregations.rb +0 -251
- data/app/models/railwatch/telemetry/attachment.rb +0 -31
- data/app/models/railwatch/telemetry/bounded_gzip.rb +0 -101
- data/app/models/railwatch/telemetry/broadcast.rb +0 -13
- data/app/models/railwatch/telemetry/cache_event.rb +0 -16
- data/app/models/railwatch/telemetry/child.rb +0 -53
- data/app/models/railwatch/telemetry/cursor_page.rb +0 -99
- data/app/models/railwatch/telemetry/deprecation.rb +0 -13
- data/app/models/railwatch/telemetry/enqueued_job.rb +0 -13
- data/app/models/railwatch/telemetry/exception.rb +0 -28
- data/app/models/railwatch/telemetry/execution.rb +0 -152
- data/app/models/railwatch/telemetry/export_delivery.rb +0 -32
- data/app/models/railwatch/telemetry/export_destination.rb +0 -66
- data/app/models/railwatch/telemetry/health_sample.rb +0 -72
- data/app/models/railwatch/telemetry/ingest_batch.rb +0 -31
- data/app/models/railwatch/telemetry/llm_call.rb +0 -37
- data/app/models/railwatch/telemetry/log.rb +0 -91
- data/app/models/railwatch/telemetry/mail.rb +0 -13
- data/app/models/railwatch/telemetry/n_plus_one.rb +0 -92
- data/app/models/railwatch/telemetry/notification.rb +0 -13
- data/app/models/railwatch/telemetry/outgoing_request.rb +0 -13
- data/app/models/railwatch/telemetry/person.rb +0 -50
- data/app/models/railwatch/telemetry/process.rb +0 -10
- data/app/models/railwatch/telemetry/profile.rb +0 -37
- data/app/models/railwatch/telemetry/query.rb +0 -57
- data/app/models/railwatch/telemetry/query_shape.rb +0 -45
- data/app/models/railwatch/telemetry/release_health.rb +0 -69
- data/app/models/railwatch/telemetry/rollup.rb +0 -157
- data/app/models/railwatch/telemetry/session.rb +0 -24
- data/app/models/railwatch/telemetry/span.rb +0 -19
- data/app/models/railwatch/telemetry/storage_op.rb +0 -13
- data/app/models/railwatch/telemetry/tenant.rb +0 -240
- data/app/models/railwatch/telemetry/transaction.rb +0 -13
- data/app/models/railwatch/telemetry/view_render.rb +0 -13
- data/app/models/railwatch/telemetry/visit.rb +0 -24
- data/app/models/railwatch/telemetry_record.rb +0 -107
- data/app/models/railwatch/threshold.rb +0 -66
- data/app/models/railwatch/user.rb +0 -47
- data/app/models/railwatch/viewer.rb +0 -13
- data/app/models/railwatch/window.rb +0 -66
- data/app/views/layouts/railwatch/dashboard.html.erb +0 -25
- data/db/railwatch_migrate/20260916000000_create_railwatch_tables.rb +0 -151
- data/db/railwatch_migrate/20260917000000_create_railwatch_maintenance_tasks.rb +0 -18
- data/db/railwatch_migrate/20260917120000_create_railwatch_followup_receipts.rb +0 -20
- data/db/railwatch_migrate/20260918120000_widen_host_user_ids.rb +0 -71
- data/db/railwatch_telemetry_migrate/20260903000000_enable_incremental_vacuum.rb +0 -62
- data/db/railwatch_telemetry_migrate/20260903000001_create_telemetry.rb +0 -481
- data/db/railwatch_telemetry_migrate/20260903000002_rename_tenant_to_app_tenant.rb +0 -14
- data/db/railwatch_telemetry_migrate/20260903000003_add_statement_count_to_transactions.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260903000004_add_role_and_channel.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260903000005_add_locals_to_exceptions.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260903000006_add_spans_health_vitals_and_fts.rb +0 -82
- data/db/railwatch_telemetry_migrate/20260903000007_rename_span_attributes_to_payload.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260903000008_add_profiles_and_attachments.rb +0 -60
- data/db/railwatch_telemetry_migrate/20260903000009_add_truncated_to_attachments.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260903000010_create_sessions_and_release_health.rb +0 -51
- data/db/railwatch_telemetry_migrate/20260903000011_add_fingerprint_to_exceptions.rb +0 -11
- data/db/railwatch_telemetry_migrate/20260904000012_add_failed_to_broadcasts.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260904010000_add_filter_cursor_indexes.rb +0 -37
- data/db/railwatch_telemetry_migrate/20260904020000_add_n_plus_ones_execution_id_index.rb +0 -11
- data/db/railwatch_telemetry_migrate/20260904120000_create_query_shapes.rb +0 -15
- data/db/railwatch_telemetry_migrate/20260906120000_add_backpressure_factor_to_ingest_batches.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260907000000_rename_lantern_version_on_processes.rb +0 -10
- data/db/railwatch_telemetry_migrate/20260913000000_rename_nightrail_version_on_processes.rb +0 -9
- data/db/railwatch_telemetry_migrate/20260914000000_drop_orphan_durable_ingest_tables.rb +0 -18
- data/db/railwatch_telemetry_migrate/20260914010000_drop_orphan_durable_ingest_columns.rb +0 -25
- data/db/railwatch_telemetry_migrate/20260915000000_create_llm_calls.rb +0 -55
- data/db/railwatch_telemetry_migrate/20260915120000_add_detail_to_llm_calls.rb +0 -21
- data/db/railwatch_telemetry_migrate/20260915200000_add_explained_index_to_queries.rb +0 -16
- data/db/railwatch_telemetry_migrate/20260915210000_add_slowest_index_to_queries.rb +0 -18
- data/db/railwatch_telemetry_migrate/20260917010000_add_batch_ledger_to_ingest_batches.rb +0 -16
- data/db/railwatch_telemetry_migrate/20260919000100_create_export_queue.rb +0 -146
- data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +0 -34
- data/db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb +0 -15
- data/db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb +0 -17
- data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +0 -10
- data/docs/embedded.md +0 -503
- data/lib/puma/plugin/railwatch.rb +0 -215
- data/lib/railwatch/dashboard_assets.rb +0 -45
- data/lib/railwatch/embedded.rb +0 -50
- data/lib/railwatch/export/client.rb +0 -124
- data/lib/railwatch/export/lease.rb +0 -56
- data/lib/railwatch/export/outbox.rb +0 -361
- data/lib/railwatch/export/policy.rb +0 -79
- data/lib/railwatch/export/sender.rb +0 -130
- data/lib/railwatch/ingest_request_body_limit.rb +0 -10
- data/lib/railwatch/maintenance.rb +0 -194
- data/lib/railwatch/patches/migration_busy_timeout.rb +0 -73
- data/lib/railwatch/transport/local.rb +0 -78
- data/lib/railwatch/transport/socket.rb +0 -183
- data/lib/railwatch/writer.rb +0 -370
- data/public/railwatch/assets/CommitMono-Bold-D6h61ieg.woff2 +0 -0
- data/public/railwatch/assets/CommitMono-Regular-zr8w7Obm.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Black-auA4GeOK.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Bold-CJLnO8j1.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Medium-Cm2bwKpj.woff2 +0 -0
- data/public/railwatch/assets/Roboto-Regular-Chaq1-PV.woff2 +0 -0
- data/public/railwatch/assets/app-layout-by7G3sa1.js +0 -41
- data/public/railwatch/assets/application-Cc5v_Vk9.css +0 -1
- data/public/railwatch/assets/arrow-up-DCZ7emux.js +0 -1
- data/public/railwatch/assets/braces-CykT5_5u.js +0 -1
- data/public/railwatch/assets/card-C-PPSA0g.js +0 -1
- data/public/railwatch/assets/chart-Cov79YC5.js +0 -39
- data/public/railwatch/assets/chart-hover-DdK4E-2h.js +0 -1
- data/public/railwatch/assets/chart-panel-BPaYRXJB.js +0 -1
- data/public/railwatch/assets/code-BYjC4YZ_.js +0 -1
- data/public/railwatch/assets/copy-id-LPfoj9cC.js +0 -1
- data/public/railwatch/assets/cursor-load-more-DSdhgpGl.js +0 -1
- data/public/railwatch/assets/data-table-CQ_YRWQs.js +0 -1
- data/public/railwatch/assets/env-layout-CdMWbUkm.js +0 -1
- data/public/railwatch/assets/execution-path-7MYt-Qbn.js +0 -1
- data/public/railwatch/assets/filter-bar-CrzpQ_vD.js +0 -1
- data/public/railwatch/assets/flamegraph-BIFEdTAg.js +0 -2
- data/public/railwatch/assets/format-B9SDkrWj.js +0 -1
- data/public/railwatch/assets/frames-C5t-x4f0.js +0 -1
- data/public/railwatch/assets/index--NQ7us-L.js +0 -1
- data/public/railwatch/assets/index-2kgYe_99.js +0 -1
- data/public/railwatch/assets/index-4YEPHxtO.js +0 -1
- data/public/railwatch/assets/index-AOCC1GDJ.js +0 -1
- data/public/railwatch/assets/index-BEo-u0eO.js +0 -1
- data/public/railwatch/assets/index-BJ1V3w2N.js +0 -1
- data/public/railwatch/assets/index-BPoHsBKb.js +0 -1
- data/public/railwatch/assets/index-BZzyGUM3.js +0 -1
- data/public/railwatch/assets/index-BgVq7yKL.js +0 -1
- data/public/railwatch/assets/index-BqFGEZvW.js +0 -1
- data/public/railwatch/assets/index-ByuHJ1CH.js +0 -1
- data/public/railwatch/assets/index-C2j50AL2.js +0 -1
- data/public/railwatch/assets/index-CLEdTkvW.js +0 -1
- data/public/railwatch/assets/index-CWgq8qBu.js +0 -1
- data/public/railwatch/assets/index-CXQ7_oVj.js +0 -2
- data/public/railwatch/assets/index-CmyYrjS2.js +0 -1
- data/public/railwatch/assets/index-Cn9_HjDo.js +0 -1
- data/public/railwatch/assets/index-CsMAm9UK.js +0 -1
- data/public/railwatch/assets/index-D0Kk_oGY.js +0 -1
- data/public/railwatch/assets/index-DC9ZptJh.js +0 -1
- data/public/railwatch/assets/index-DNzg7jmV.js +0 -1
- data/public/railwatch/assets/index-DQ8uJO5f.js +0 -1
- data/public/railwatch/assets/index-Deq14BxB.js +0 -1
- data/public/railwatch/assets/index-IaV3Blzp.js +0 -1
- data/public/railwatch/assets/index-TYNWcJQs.js +0 -1
- data/public/railwatch/assets/index-WonH2fcZ.js +0 -1
- data/public/railwatch/assets/index-WxWUaH_d.js +0 -1
- data/public/railwatch/assets/index-mUNrl4ky.js +0 -1
- data/public/railwatch/assets/inertia-DfNRARuA.js +0 -97
- data/public/railwatch/assets/json-viewer-DMsF5glp.js +0 -1
- data/public/railwatch/assets/klass-CmHYk5rz.js +0 -1
- data/public/railwatch/assets/origin-identity-1R3bqa3R.js +0 -1
- data/public/railwatch/assets/percentile-picker-73z5PAps.js +0 -1
- data/public/railwatch/assets/relative-time-Dsc_cCdX.js +0 -1
- data/public/railwatch/assets/release-health-BV5OaOWd.js +0 -1
- data/public/railwatch/assets/route-CWh58C7K.js +0 -1
- data/public/railwatch/assets/segmented-DB4zh-Am.js +0 -1
- data/public/railwatch/assets/select-DBKjFlOX.js +0 -1
- data/public/railwatch/assets/series-chart-BaiDG795.js +0 -1
- data/public/railwatch/assets/show-B4ICCFGh.js +0 -1
- data/public/railwatch/assets/show-B5lOU9Gv.js +0 -1
- data/public/railwatch/assets/show-BYPB74t4.js +0 -2
- data/public/railwatch/assets/show-C7A0m2M8.js +0 -1
- data/public/railwatch/assets/show-DT5X1zqE.js +0 -1
- data/public/railwatch/assets/show-D_NBdXsn.js +0 -2
- data/public/railwatch/assets/show-DgPCnRg8.js +0 -1
- data/public/railwatch/assets/show-Yl69HF0x.js +0 -1
- data/public/railwatch/assets/show-_QxuUQCm.js +0 -3
- data/public/railwatch/assets/show-g__jnN36.js +0 -1
- data/public/railwatch/assets/show-l4ztfjl9.js +0 -1
- data/public/railwatch/assets/sort-header-BcG8ulEH.js +0 -1
- data/public/railwatch/assets/source-link-B2183i2-.js +0 -1
- data/public/railwatch/assets/sparkline-cell-B47ipscB.js +0 -1
- data/public/railwatch/assets/stat-DT2hhsqS.js +0 -1
- data/public/railwatch/assets/status-badge-CmbRKlY7.js +0 -1
- data/public/railwatch/assets/tenant-path-C4rNGauE.js +0 -1
- data/public/railwatch/assets/use-clipboard-oTNeP4ax.js +0 -1
- data/public/railwatch/assets/use-live-CLZZj245.js +0 -1
- data/public/railwatch/icon.png +0 -0
- data/public/railwatch/icon.svg +0 -5
- data/public/railwatch/manifest.json +0 -1245
- data/public/railwatch/rails-vite.json +0 -1
- /data/{app/frontend/lib → lib/generators/railwatch/install/templates}/railwatch.ts +0 -0
data/app/frontend/types/index.ts
DELETED
|
@@ -1,288 +0,0 @@
|
|
|
1
|
-
import type { LucideIcon } from "lucide-react"
|
|
2
|
-
|
|
3
|
-
export interface Auth {
|
|
4
|
-
user: User
|
|
5
|
-
session: Pick<Session, "id" | "recently_authenticated">
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface BreadcrumbItem {
|
|
9
|
-
title: string
|
|
10
|
-
href: string
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface NavItem {
|
|
14
|
-
title: string
|
|
15
|
-
href: string
|
|
16
|
-
icon?: LucideIcon | null
|
|
17
|
-
isActive?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface FlashData {
|
|
21
|
-
alert?: string
|
|
22
|
-
warning?: string
|
|
23
|
-
notice?: string
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface AccountSummary {
|
|
27
|
-
id: number
|
|
28
|
-
name: string
|
|
29
|
-
slug: string
|
|
30
|
-
plan: string
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface EnvironmentSummary {
|
|
34
|
-
id: number
|
|
35
|
-
name: string
|
|
36
|
-
slug: string
|
|
37
|
-
last_seen_at: string | null
|
|
38
|
-
paused: boolean
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ApplicationSummary {
|
|
42
|
-
id: number
|
|
43
|
-
name: string
|
|
44
|
-
slug: string
|
|
45
|
-
issue_prefix: string
|
|
46
|
-
environments: EnvironmentSummary[]
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface EnvironmentContext {
|
|
50
|
-
id: number
|
|
51
|
-
name: string
|
|
52
|
-
slug: string
|
|
53
|
-
application_id: number
|
|
54
|
-
application_name: string
|
|
55
|
-
issue_prefix: string
|
|
56
|
-
last_seen_at: string | null
|
|
57
|
-
paused: boolean
|
|
58
|
-
token_prefix: string
|
|
59
|
-
repository_url: string | null
|
|
60
|
-
default_branch: string
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type Window = "1h" | "6h" | "24h" | "7d" | "30d" | "custom"
|
|
64
|
-
|
|
65
|
-
// Chart bucket width; the server offers the subset that fits the window.
|
|
66
|
-
export type Step = "1m" | "5m" | "15m" | "1h" | "6h" | "1d"
|
|
67
|
-
|
|
68
|
-
export interface WindowRange {
|
|
69
|
-
from: string
|
|
70
|
-
to: string
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export type TelemetryTimeParams =
|
|
74
|
-
| { window: Exclude<Window, "custom">; from?: never; to?: never }
|
|
75
|
-
| { window?: never; from: string; to: string }
|
|
76
|
-
|
|
77
|
-
export interface CursorMeta {
|
|
78
|
-
limit: number
|
|
79
|
-
next_cursor: string | null
|
|
80
|
-
has_more: boolean
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface SharedProps {
|
|
84
|
-
auth: Auth
|
|
85
|
-
account: AccountSummary | null
|
|
86
|
-
accounts: { id: number; name: string }[]
|
|
87
|
-
applications: ApplicationSummary[]
|
|
88
|
-
environment?: EnvironmentContext
|
|
89
|
-
telemetry_freshness?: {
|
|
90
|
-
received_at: string | null
|
|
91
|
-
aggregated_at?: string | null
|
|
92
|
-
processing_lag_seconds: number | null
|
|
93
|
-
}
|
|
94
|
-
window?: Window
|
|
95
|
-
range?: WindowRange
|
|
96
|
-
step?: Step
|
|
97
|
-
steps?: Step[]
|
|
98
|
-
flash: FlashData
|
|
99
|
-
google_oauth: boolean
|
|
100
|
-
embedded?: boolean
|
|
101
|
-
saved_views?: SavedView[]
|
|
102
|
-
[key: string]: unknown
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
export interface User {
|
|
106
|
-
id: number
|
|
107
|
-
name: string
|
|
108
|
-
email: string
|
|
109
|
-
provider: string | null
|
|
110
|
-
avatar?: string
|
|
111
|
-
verified: boolean
|
|
112
|
-
editor: string
|
|
113
|
-
editor_root: string | null
|
|
114
|
-
created_at: string
|
|
115
|
-
updated_at: string
|
|
116
|
-
[key: string]: unknown
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export interface Session {
|
|
120
|
-
id: string
|
|
121
|
-
user_agent: string
|
|
122
|
-
ip_address: string
|
|
123
|
-
created_at: string
|
|
124
|
-
recently_authenticated: boolean
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Telemetry shapes shared by pages
|
|
128
|
-
|
|
129
|
-
export type Percentile = "p50" | "p95" | "p99"
|
|
130
|
-
|
|
131
|
-
// One chart bucket. Durations are null where nothing ran, so a latency
|
|
132
|
-
// line breaks over a quiet bucket instead of dropping to zero.
|
|
133
|
-
export interface SeriesPoint {
|
|
134
|
-
t: string
|
|
135
|
-
count: number
|
|
136
|
-
errors: number
|
|
137
|
-
client_errors: number
|
|
138
|
-
avg: number | null
|
|
139
|
-
p50: number | null
|
|
140
|
-
p95: number | null
|
|
141
|
-
p99: number | null
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export interface GroupRow {
|
|
145
|
-
group_hash: string
|
|
146
|
-
name: string
|
|
147
|
-
count: number
|
|
148
|
-
errors: number
|
|
149
|
-
client_errors: number
|
|
150
|
-
avg: number
|
|
151
|
-
p50: number
|
|
152
|
-
p95: number
|
|
153
|
-
p99: number
|
|
154
|
-
max: number
|
|
155
|
-
sparkline: number[]
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export interface Summary {
|
|
159
|
-
count: number
|
|
160
|
-
errors: number
|
|
161
|
-
client_errors: number
|
|
162
|
-
avg: number
|
|
163
|
-
p50: number
|
|
164
|
-
p95: number
|
|
165
|
-
p99: number
|
|
166
|
-
max: number
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
export interface SummaryWithDelta {
|
|
170
|
-
current: Summary
|
|
171
|
-
previous: Summary
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
// Release health: crash-free rates are null, not 0, when there were no
|
|
175
|
-
// sessions (or no users) to divide by.
|
|
176
|
-
export interface ReleaseHealthSummary {
|
|
177
|
-
sessions: number
|
|
178
|
-
users: number
|
|
179
|
-
crash_free_sessions: number | null
|
|
180
|
-
crash_free_users: number | null
|
|
181
|
-
errored: number | null
|
|
182
|
-
avg_duration_ms: number | null
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
export interface SessionStatusPoint {
|
|
186
|
-
t: string
|
|
187
|
-
sessions: number
|
|
188
|
-
ok: number
|
|
189
|
-
errored: number
|
|
190
|
-
crashed: number
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export interface DeployMarker {
|
|
194
|
-
deploy: string
|
|
195
|
-
ref: string
|
|
196
|
-
at: string
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
export interface IssueRow {
|
|
200
|
-
id: number
|
|
201
|
-
key: string
|
|
202
|
-
title: string
|
|
203
|
-
kind: "exception" | "performance"
|
|
204
|
-
status: "open" | "resolved" | "ignored" | "merged"
|
|
205
|
-
priority: "low" | "normal" | "high" | "urgent"
|
|
206
|
-
culprit?: string | null
|
|
207
|
-
occurrences: number
|
|
208
|
-
affected_users: number
|
|
209
|
-
first_seen_at?: string
|
|
210
|
-
last_seen_at: string
|
|
211
|
-
assignee?: { id: number; name: string } | null
|
|
212
|
-
fingerprint_source?: string | null
|
|
213
|
-
source?: string | null
|
|
214
|
-
application?: { id: number; name: string }
|
|
215
|
-
environment?: { id: number; name: string }
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export interface ExecutionRow {
|
|
219
|
-
execution_id: string
|
|
220
|
-
name: string
|
|
221
|
-
kind?: string
|
|
222
|
-
status?: number | null
|
|
223
|
-
outcome?: string | null
|
|
224
|
-
duration: number
|
|
225
|
-
occurred_at: string
|
|
226
|
-
user_ref?: string | null
|
|
227
|
-
tenant?: string | null
|
|
228
|
-
exception_preview?: string | null
|
|
229
|
-
inertia_component?: string | null
|
|
230
|
-
queries?: number
|
|
231
|
-
deploy?: string | null
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
export interface Frame {
|
|
235
|
-
file: string
|
|
236
|
-
line: number
|
|
237
|
-
function: string
|
|
238
|
-
in_app: boolean
|
|
239
|
-
code?: Record<string, string>
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export interface ExceptionDetail {
|
|
243
|
-
id: number
|
|
244
|
-
class_name: string
|
|
245
|
-
message: string
|
|
246
|
-
handled: boolean
|
|
247
|
-
severity?: string
|
|
248
|
-
source?: string
|
|
249
|
-
file?: string | null
|
|
250
|
-
line?: number | null
|
|
251
|
-
frames: Frame[]
|
|
252
|
-
cause?: { class: string; message: string } | null
|
|
253
|
-
locals?: Record<string, string> | null
|
|
254
|
-
context?: string | null
|
|
255
|
-
occurred_at?: string
|
|
256
|
-
execution_id?: string | null
|
|
257
|
-
execution_source?: string | null
|
|
258
|
-
execution_preview?: string | null
|
|
259
|
-
deploy?: string | null
|
|
260
|
-
group_hash?: string
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
export interface TimelineEntry {
|
|
264
|
-
type: string
|
|
265
|
-
id: number
|
|
266
|
-
offset: number
|
|
267
|
-
duration: number | null
|
|
268
|
-
label: string
|
|
269
|
-
stage: string | null
|
|
270
|
-
sql?: string | null
|
|
271
|
-
source?: string | null
|
|
272
|
-
detail?: Record<string, string | number | boolean | null>
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// A named filter on one environment page, shared through every env page's
|
|
276
|
-
// props. `url` is built server-side from the page's route helper.
|
|
277
|
-
export interface SavedView {
|
|
278
|
-
id: number
|
|
279
|
-
name: string
|
|
280
|
-
page: string
|
|
281
|
-
query: string | null
|
|
282
|
-
window: string | null
|
|
283
|
-
params: Record<string, string>
|
|
284
|
-
pinned: boolean
|
|
285
|
-
shared: boolean
|
|
286
|
-
mine: boolean
|
|
287
|
-
url: string
|
|
288
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Emits the dashboard's script and stylesheet tags from the manifest Vite
|
|
5
|
-
# wrote at gem build time. The gem ships the built bundle under
|
|
6
|
-
# public/railwatch, so a host app needs no Node toolchain and no asset
|
|
7
|
-
# pipeline integration: the engine serves those files itself and this
|
|
8
|
-
# helper does the little that rails_vite's vite_tags would have done.
|
|
9
|
-
module AssetsHelper
|
|
10
|
-
MANIFEST = Railwatch::Engine.root.join("public/railwatch/manifest.json")
|
|
11
|
-
ASSET_PREFIX = "/railwatch/assets"
|
|
12
|
-
|
|
13
|
-
def railwatch_asset_tags(nonce: nil)
|
|
14
|
-
manifest = railwatch_manifest
|
|
15
|
-
tags = []
|
|
16
|
-
preloaded = Set.new
|
|
17
|
-
%w[app/frontend/entrypoints/application.css app/frontend/entrypoints/inertia.tsx].each do |entry|
|
|
18
|
-
info = manifest.fetch(entry)
|
|
19
|
-
Array(info["imports"]).each do |key|
|
|
20
|
-
file = manifest.fetch(key)["file"]
|
|
21
|
-
next unless preloaded.add?(file)
|
|
22
|
-
tags << tag.link(rel: "modulepreload", href: "#{ASSET_PREFIX}/#{file}", nonce: nonce)
|
|
23
|
-
end
|
|
24
|
-
tags << if info["file"].end_with?(".css")
|
|
25
|
-
tag.link(rel: "stylesheet", href: "#{ASSET_PREFIX}/#{info['file']}", nonce: nonce)
|
|
26
|
-
else
|
|
27
|
-
tag.script(src: "#{ASSET_PREFIX}/#{info['file']}", type: "module", nonce: nonce)
|
|
28
|
-
end
|
|
29
|
-
Array(info["css"]).each { |css| tags << tag.link(rel: "stylesheet", href: "#{ASSET_PREFIX}/#{css}", nonce: nonce) }
|
|
30
|
-
end
|
|
31
|
-
safe_join(tags, "\n")
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def railwatch_font_preloads
|
|
35
|
-
safe_join(railwatch_manifest.select { |k, _| k.end_with?(".woff2") && k.include?("Roboto") }.map { |_, v|
|
|
36
|
-
tag.link(rel: "preload", href: "#{ASSET_PREFIX}/#{v['file']}", as: "font", type: "font/woff2", crossorigin: "anonymous")
|
|
37
|
-
}, "\n")
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# The bundle digest doubles as the Inertia asset version: a gem upgrade
|
|
41
|
-
# changes it and every open tab does a full reload on its next visit.
|
|
42
|
-
def self.digest
|
|
43
|
-
@digest ||= Digest::SHA256.file(MANIFEST).hexdigest[0, 16]
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def railwatch_manifest
|
|
49
|
-
@railwatch_manifest ||= JSON.parse(File.read(MANIFEST))
|
|
50
|
-
end
|
|
51
|
-
end
|
|
52
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Auto-resolves open issues that have gone quiet for a configured number of
|
|
5
|
-
# days (nil = off). Resolved with no deploy attached, so any recurrence is
|
|
6
|
-
# treated as a regression rather than a duplicate. Runs daily (recurring.yml).
|
|
7
|
-
class AutoResolveIssuesJob < ApplicationJob
|
|
8
|
-
queue_as :default
|
|
9
|
-
|
|
10
|
-
def perform
|
|
11
|
-
days = Railwatch::Embedded::Account.auto_resolve_after_days
|
|
12
|
-
return unless days
|
|
13
|
-
|
|
14
|
-
Issue.open.where("last_seen_at < ?", days.days.ago).find_each do |issue|
|
|
15
|
-
issue.resolve!(deploy: nil)
|
|
16
|
-
end
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Flags scheduled tasks that have not run when expected. Uses the schedule
|
|
5
|
-
# string the gem sends (Solid Queue's cron / natural language) parsed with
|
|
6
|
-
# fugit, which Solid Queue already depends on.
|
|
7
|
-
class CheckScheduledTasksJob < ApplicationJob
|
|
8
|
-
include DetectionSnapshotting
|
|
9
|
-
|
|
10
|
-
queue_as :default
|
|
11
|
-
|
|
12
|
-
GRACE = 5.minutes
|
|
13
|
-
# A deploy restarts the worker, and Solid Queue skips (does not replay) the
|
|
14
|
-
# recurring ticks that fell inside the restart window. A tick missed within
|
|
15
|
-
# this long after a deploy is the deploy, not the scheduler.
|
|
16
|
-
DEPLOY_GRACE = 15.minutes
|
|
17
|
-
|
|
18
|
-
def perform(environment)
|
|
19
|
-
require "fugit"
|
|
20
|
-
tasks = environment.with_telemetry do
|
|
21
|
-
last_runs = Telemetry::Execution.scheduled.where("occurred_at > ?", 30.days.ago).group(:task_key).maximum(:occurred_at)
|
|
22
|
-
schedules = Telemetry::Execution.latest_schedules(last_runs.keys)
|
|
23
|
-
last_runs.map { |key, last| [ key, last, schedules[key] ] }
|
|
24
|
-
end
|
|
25
|
-
last_deploy_at = environment.deploys.maximum(:deployed_at)
|
|
26
|
-
configured = environment.with_telemetry { Telemetry::HealthSample.recurring_task_keys }
|
|
27
|
-
open_missed = environment.issues.open.where("group_hash LIKE 'missed:%'").index_by(&:group_hash)
|
|
28
|
-
tasks.each do |key, last_run, schedule|
|
|
29
|
-
next if schedule.blank?
|
|
30
|
-
# A task taken out of config/recurring.yml stops running on purpose.
|
|
31
|
-
# Its history would otherwise read as a miss every window for 30 days
|
|
32
|
-
# (reconcile_ingest_batches was live for four minutes and flagged 62
|
|
33
|
-
# times). The gem ships the schedule Solid Queue is actually running
|
|
34
|
-
# on every health sample; a key absent from the newest manifest is
|
|
35
|
-
# removed, not missed. No manifest yet means an older gem: judge from
|
|
36
|
-
# history alone, as before.
|
|
37
|
-
if configured && !configured.include?(key)
|
|
38
|
-
resolve_missed!(open_missed["missed:#{key}"], "Auto-resolved: #{key} is no longer a configured recurring task.")
|
|
39
|
-
next
|
|
40
|
-
end
|
|
41
|
-
cron = Fugit.parse(schedule) or next
|
|
42
|
-
expected = cron.next_time(last_run).to_t
|
|
43
|
-
if expected > Time.current - GRACE
|
|
44
|
-
# The task is back on schedule; a missed-run issue for it heals on its own.
|
|
45
|
-
resolve_missed!(open_missed["missed:#{key}"], "Auto-resolved: #{key} ran again on schedule.")
|
|
46
|
-
next
|
|
47
|
-
end
|
|
48
|
-
next if last_deploy_at && expected.between?(last_deploy_at - DEPLOY_GRACE, last_deploy_at + DEPLOY_GRACE)
|
|
49
|
-
diagnosis = diagnose(key, expected)
|
|
50
|
-
issue, outcome = Issue.record_occurrence!(
|
|
51
|
-
environment: environment, group_hash: "missed:#{key}", kind: "performance",
|
|
52
|
-
title: "Scheduled task #{key} missed its run (expected #{expected.utc.iso8601})#{diagnosis[:title_suffix]}",
|
|
53
|
-
culprit: key, occurred_at: Time.current, deploy: nil, user_ref: nil,
|
|
54
|
-
sample: { task_key: key, schedule: schedule, last_run: last_run.iso8601, expected: expected.iso8601 }.merge(diagnosis[:sample]))
|
|
55
|
-
next unless outcome == :new || outcome == :regressed
|
|
56
|
-
|
|
57
|
-
# Only on a new or regressed issue: the detectors re-run every few minutes
|
|
58
|
-
# and capturing a snapshot on every breached window would query telemetry
|
|
59
|
-
# for an issue nobody is newly looking at.
|
|
60
|
-
latest = environment.with_telemetry { Telemetry::Execution.scheduled.where(task_key: key).recent.first }
|
|
61
|
-
issue.update!(sample: issue.sample.merge("detection" => IssueDetectionSnapshot.capture(
|
|
62
|
-
environment: environment, kind: "performance", telemetry_type: "scheduled_task",
|
|
63
|
-
telemetry_group_hash: latest.group_hash, target: key, metric: "missed", from: expected, to: Time.current,
|
|
64
|
-
representative_from: last_run,
|
|
65
|
-
rule: { type: "schedule", task_key: key, schedule: schedule,
|
|
66
|
-
last_run: last_run.iso8601, expected: expected.iso8601 }
|
|
67
|
-
)))
|
|
68
|
-
payload = { issue_key: issue.key, title: issue.title, environment: environment.name }
|
|
69
|
-
environment.application.alert_rules.where(event: "threshold").find_each do |rule|
|
|
70
|
-
next unless rule.matches?(issue: issue, payload: payload)
|
|
71
|
-
rule.fire!(event: "threshold", issue: issue, payload: payload)
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
private
|
|
77
|
-
|
|
78
|
-
# Where the run went, from Solid Queue's own ledger when it is in this
|
|
79
|
-
# process. A RecurringExecution row is written by the scheduler the moment
|
|
80
|
-
# it enqueues a run, so its presence without a scheduled_task execution
|
|
81
|
-
# means "enqueued, never performed" (no worker, or a wedged one); its
|
|
82
|
-
# absence means the scheduler itself never fired. The hosted platform, and
|
|
83
|
-
# any adapter other than Solid Queue, only ever sees the absence of the
|
|
84
|
-
# run and reports it as before.
|
|
85
|
-
def diagnose(key, expected)
|
|
86
|
-
return NO_DIAGNOSIS unless defined?(::SolidQueue::RecurringExecution)
|
|
87
|
-
|
|
88
|
-
enqueued_at = Railwatch.ignore { ::SolidQueue::RecurringExecution.where(task_key: key).where("run_at >= ?", expected - 1.minute).maximum(:run_at) }
|
|
89
|
-
workers = Railwatch.ignore { ::SolidQueue::Process.where(kind: "Worker").count }
|
|
90
|
-
if enqueued_at
|
|
91
|
-
{ title_suffix: workers.zero? ? ": enqueued, but no Solid Queue worker is running" : ": enqueued, not yet performed",
|
|
92
|
-
sample: { enqueued_at: enqueued_at.iso8601, workers: workers, cause: workers.zero? ? "no_worker" : "backlog" } }
|
|
93
|
-
else
|
|
94
|
-
{ title_suffix: ": the scheduler never enqueued it",
|
|
95
|
-
sample: { enqueued_at: nil, workers: workers, cause: "scheduler" } }
|
|
96
|
-
end
|
|
97
|
-
rescue StandardError
|
|
98
|
-
NO_DIAGNOSIS
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
NO_DIAGNOSIS = { title_suffix: "", sample: {} }.freeze
|
|
102
|
-
|
|
103
|
-
# Sentry Crons marked a monitor OK again on the next successful check-in;
|
|
104
|
-
# the same here: once the task has run inside its window again, the open
|
|
105
|
-
# missed-run issue resolves with an activity row, so it does not sit open
|
|
106
|
-
# until someone notices it is stale.
|
|
107
|
-
def resolve_missed!(issue, body)
|
|
108
|
-
return unless issue
|
|
109
|
-
issue.resolve!
|
|
110
|
-
issue.activities.create!(kind: "comment", data: { body: body })
|
|
111
|
-
end
|
|
112
|
-
end
|
|
113
|
-
end
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
# Compares each enabled anomaly rule's current window against the same clock
|
|
5
|
-
# window on each of the previous `baseline_days` days and opens an anomaly
|
|
6
|
-
# issue when the current value sits above the baseline's mean plus
|
|
7
|
-
# `deviation` standard deviations. Runs on a schedule via AnomalyScanJob.
|
|
8
|
-
class DetectAnomaliesJob < ApplicationJob
|
|
9
|
-
include DetectionSnapshotting
|
|
10
|
-
|
|
11
|
-
queue_as :default
|
|
12
|
-
|
|
13
|
-
TYPE_FOR = DetectPerformanceIssuesJob::TYPE_FOR
|
|
14
|
-
# Fewer baseline days than this and the standard deviation is noise.
|
|
15
|
-
MIN_BASELINE_DAYS = AnomalyRule::MIN_BASELINE_DAYS
|
|
16
|
-
# Volume metrics on a handful of events swing wildly; ignore them.
|
|
17
|
-
MIN_EVENTS = 20
|
|
18
|
-
COOL_DOWN = 60.minutes
|
|
19
|
-
MAX_GROUPS_PER_RULE = 50
|
|
20
|
-
MAX_CURRENT_ROWS_PER_RULE = MAX_GROUPS_PER_RULE * 25
|
|
21
|
-
MAX_BASELINE_ROWS_PER_RULE = 50_000
|
|
22
|
-
|
|
23
|
-
def perform(environment)
|
|
24
|
-
now = Time.current
|
|
25
|
-
environment.anomaly_rules.enabled.find_each do |rule|
|
|
26
|
-
detect(environment, rule, now).each { |detection| open_issue(environment, rule, now, detection) }
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
private
|
|
31
|
-
|
|
32
|
-
def detect(environment, rule, now)
|
|
33
|
-
from = now - rule.window_minutes.minutes
|
|
34
|
-
type = TYPE_FOR.fetch(rule.target_kind)
|
|
35
|
-
environment.with_telemetry do
|
|
36
|
-
groups = current_groups(rule, type, from, now)
|
|
37
|
-
baseline = baseline_rows(rule, type, groups.keys, from, now)
|
|
38
|
-
return [] unless baseline
|
|
39
|
-
|
|
40
|
-
groups.filter_map do |group_hash, (name, summary)|
|
|
41
|
-
anomaly_for(rule, group_hash, name, summary, baseline.fetch(group_hash, []), from, now)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Rollups are hourly, so a window shorter than an hour reads the whole
|
|
47
|
-
# enclosing hour bucket (Rollup.between rounds `from` down); the baseline
|
|
48
|
-
# windows are shifted by whole days and land on the same hour, so the
|
|
49
|
-
# comparison stays like-for-like.
|
|
50
|
-
def current_groups(rule, type, from, to)
|
|
51
|
-
scope = Telemetry::Rollup.for_type(type).between(from, to)
|
|
52
|
-
scope = scope.where(name: rule.target) unless rule.target == "*"
|
|
53
|
-
group_hashes = scope.reorder(nil).group(:group_hash).order(Arel.sql("SUM(count) DESC"), :group_hash)
|
|
54
|
-
.limit(MAX_GROUPS_PER_RULE + 1).pluck(:group_hash)
|
|
55
|
-
if group_hashes.length > MAX_GROUPS_PER_RULE
|
|
56
|
-
Rails.logger.warn("anomaly detector evaluated only the #{MAX_GROUPS_PER_RULE} busiest groups rule_id=#{rule.id}")
|
|
57
|
-
group_hashes.pop
|
|
58
|
-
end
|
|
59
|
-
rows = scope.where(group_hash: group_hashes).order(:bucket, :group_hash).limit(MAX_CURRENT_ROWS_PER_RULE + 1).to_a
|
|
60
|
-
if rows.length > MAX_CURRENT_ROWS_PER_RULE
|
|
61
|
-
Rails.logger.error("anomaly detector skipped rule_id=#{rule.id}: more than #{MAX_CURRENT_ROWS_PER_RULE} current rollups")
|
|
62
|
-
return {}
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
rows.group_by(&:group_hash)
|
|
66
|
-
.transform_values { |group_rows| [ group_rows.first.name, Telemetry::Rollup.summarize(group_rows) ] }
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def anomaly_for(rule, group_hash, name, summary, baseline_rows, from, to)
|
|
70
|
-
return if cooling_down?(rule, group_hash)
|
|
71
|
-
current = value_for(rule.metric, summary, rule.window_minutes)
|
|
72
|
-
return if current.nil?
|
|
73
|
-
return if %w[count error_rate].include?(rule.metric) && summary[:count] < MIN_EVENTS
|
|
74
|
-
|
|
75
|
-
baseline = baseline_values(rule, baseline_rows, from, to)
|
|
76
|
-
return if baseline.size < MIN_BASELINE_DAYS
|
|
77
|
-
mean = baseline.sum / baseline.size
|
|
78
|
-
stddev = Math.sqrt(baseline.sum { |v| (v - mean)**2 } / baseline.size)
|
|
79
|
-
return unless current > mean + (rule.deviation * stddev) && current > mean * 1.25
|
|
80
|
-
|
|
81
|
-
# A perfectly flat baseline has no σ to measure against, so report the
|
|
82
|
-
# rule's own threshold rather than an infinite one.
|
|
83
|
-
sigmas = stddev.positive? ? (current - mean) / stddev : rule.deviation
|
|
84
|
-
{ group_hash: group_hash, name: name, current: current.round(2), mean: mean.round(2),
|
|
85
|
-
stddev: stddev.round(2), sigmas: sigmas.round(2) }
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# The same clock window on each of the previous `baseline_days` days. Days
|
|
89
|
-
# with no rollups at all are left out rather than counted as zero.
|
|
90
|
-
def baseline_values(rule, rows, from, to)
|
|
91
|
-
(1..rule.baseline_days).filter_map do |days|
|
|
92
|
-
day_rows = rows.select { |row| row.bucket.between?((from - days.days).beginning_of_hour, to - days.days) }
|
|
93
|
-
next if day_rows.empty?
|
|
94
|
-
value_for(rule.metric, Telemetry::Rollup.summarize(day_rows), rule.window_minutes)
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
# One query for every group's baseline, reading only the hour buckets the
|
|
99
|
-
# baseline windows actually cover rather than the whole `baseline_days` span.
|
|
100
|
-
def baseline_rows(rule, type, group_hashes, from, to)
|
|
101
|
-
return {} if group_hashes.empty?
|
|
102
|
-
|
|
103
|
-
rows = Telemetry::Rollup.for_type(type)
|
|
104
|
-
.where(group_hash: group_hashes, bucket: baseline_buckets(rule, from, to))
|
|
105
|
-
.order(:bucket, :group_hash).limit(MAX_BASELINE_ROWS_PER_RULE + 1).to_a
|
|
106
|
-
if rows.length > MAX_BASELINE_ROWS_PER_RULE
|
|
107
|
-
Rails.logger.error("anomaly detector skipped rule_id=#{rule.id}: more than #{MAX_BASELINE_ROWS_PER_RULE} baseline rollups")
|
|
108
|
-
return nil
|
|
109
|
-
end
|
|
110
|
-
rows.group_by(&:group_hash)
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# The hour buckets covered by the same clock window on each of the previous
|
|
114
|
-
# `baseline_days` days.
|
|
115
|
-
def baseline_buckets(rule, from, to)
|
|
116
|
-
(1..rule.baseline_days).flat_map do |days|
|
|
117
|
-
bucket = (from - days.days).beginning_of_hour
|
|
118
|
-
last = to - days.days
|
|
119
|
-
buckets = []
|
|
120
|
-
while bucket <= last
|
|
121
|
-
buckets << bucket
|
|
122
|
-
bucket += 1.hour
|
|
123
|
-
end
|
|
124
|
-
buckets
|
|
125
|
-
end
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
def value_for(metric, summary, window_minutes)
|
|
129
|
-
case metric
|
|
130
|
-
when "p95" then summary[:p95] / 1000.0
|
|
131
|
-
when "avg" then summary[:avg] / 1000.0
|
|
132
|
-
when "count" then summary[:count] / window_minutes.to_f
|
|
133
|
-
when "error_rate" then summary[:count].zero? ? nil : (summary[:errors] * 100.0 / summary[:count])
|
|
134
|
-
end
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
# Reads the primary database: one open anomaly issue per (rule, group) is
|
|
138
|
-
# enough for an hour, however often the scan runs.
|
|
139
|
-
def cooling_down?(rule, group_hash)
|
|
140
|
-
Issue.where(environment_id: rule.environment_id, group_hash: issue_group_hash(rule, group_hash))
|
|
141
|
-
.where(last_seen_at: COOL_DOWN.ago..).exists?
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
def issue_group_hash(rule, group_hash)
|
|
145
|
-
"anomaly:#{rule.id}:#{group_hash}"
|
|
146
|
-
end
|
|
147
|
-
|
|
148
|
-
def open_issue(environment, rule, now, detection)
|
|
149
|
-
issue, outcome = Issue.record_occurrence!(
|
|
150
|
-
environment: environment, group_hash: issue_group_hash(rule, detection[:group_hash]), kind: "anomaly",
|
|
151
|
-
title: "#{rule.metric} of #{detection[:name]} is #{detection[:sigmas].round(1)}σ above its #{rule.baseline_days}-day baseline (#{detection[:current]} vs #{detection[:mean]})",
|
|
152
|
-
culprit: detection[:name], occurred_at: now, deploy: nil, user_ref: nil,
|
|
153
|
-
sample: { rule_id: rule.id, metric: rule.metric, current: detection[:current], mean: detection[:mean],
|
|
154
|
-
stddev: detection[:stddev], sigmas: detection[:sigmas], window_minutes: rule.window_minutes })
|
|
155
|
-
carry_detection(issue, outcome) do
|
|
156
|
-
IssueDetectionSnapshot.capture(
|
|
157
|
-
environment: environment, kind: "anomaly", telemetry_type: TYPE_FOR.fetch(rule.target_kind),
|
|
158
|
-
telemetry_group_hash: detection[:group_hash], target: detection[:name], metric: rule.metric,
|
|
159
|
-
from: now - rule.window_minutes.minutes, to: now, value: detection[:current],
|
|
160
|
-
baseline: detection.slice(:mean, :stddev, :sigmas).merge(days: rule.baseline_days, deviation: rule.deviation),
|
|
161
|
-
rule: { type: "anomaly", id: rule.id, target_kind: rule.target_kind, target: rule.target }
|
|
162
|
-
)
|
|
163
|
-
end
|
|
164
|
-
if outcome == :new || outcome == :regressed
|
|
165
|
-
issue.fire_alerts!("anomaly", rule_id: rule.id, metric: rule.metric, current: detection[:current],
|
|
166
|
-
mean: detection[:mean], sigmas: detection[:sigmas])
|
|
167
|
-
end
|
|
168
|
-
rule.update_columns(last_fired_at: now)
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
end
|