railwatch 0.9.4 → 1.1.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 +63 -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 +18 -152
- data/lib/railwatch/engine.rb +9 -118
- data/lib/railwatch/patches.rb +0 -11
- data/lib/railwatch/reporter.rb +13 -20
- 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
|
@@ -1,258 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class IssuesController < DashboardController
|
|
5
|
-
BULK_LIMIT = 200
|
|
6
|
-
|
|
7
|
-
before_action :set_issue, only: %i[show update merge_candidates]
|
|
8
|
-
rescue_from Issue::InvalidMerge, with: :invalid_merge
|
|
9
|
-
|
|
10
|
-
def index
|
|
11
|
-
scope = Issue.all
|
|
12
|
-
scope = scope.where(status: params[:status]) if Issue::STATUSES.include?(params[:status].to_s)
|
|
13
|
-
scope = scope.where(status: "open") unless params.key?(:status)
|
|
14
|
-
scope = scope.where(application_id: params[:application_id]) if params[:application_id].present?
|
|
15
|
-
scope = scope.where(environment_id: params[:environment_id]) if params[:environment_id].present?
|
|
16
|
-
scope = scope.where(kind: params[:kind]) if Issue::KINDS.include?(params[:kind].to_s)
|
|
17
|
-
scope = scope.where(assignee_id: Viewer.user.id) if params[:mine] == "1"
|
|
18
|
-
# Same "key:value free text" grammar as the other list pages: today the
|
|
19
|
-
# only key is source, which is how you pull out the browser's own errors.
|
|
20
|
-
parsed = FilterQuery.parse(params[:q])
|
|
21
|
-
scope = scope.where(source: parsed[:fields]["source"]) if parsed[:fields]["source"].present?
|
|
22
|
-
scope = scope.where("title LIKE ?", "%#{Issue.sanitize_sql_like(parsed[:text])}%") if parsed[:text].present?
|
|
23
|
-
render inertia: { issues: scope.recent.limit(200).map { |i| row(i) },
|
|
24
|
-
filters: params.permit(:status, :application_id, :environment_id, :kind, :mine, :q).to_h,
|
|
25
|
-
counts: Issue.all.group(:status).count,
|
|
26
|
-
kindCounts: Issue.all.open.group(:kind).count,
|
|
27
|
-
members: User.default.then { |u| [ { id: u.id, name: u.name } ] } }
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def show
|
|
31
|
-
env = @issue.environment
|
|
32
|
-
return render_detection_issue(env) unless @issue.kind == "exception"
|
|
33
|
-
|
|
34
|
-
from = 30.days.ago
|
|
35
|
-
occurrences = env.with_telemetry do
|
|
36
|
-
Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).recent.limit(50)
|
|
37
|
-
.map { |e| { id: e.id, message: e.message.first(300), handled: e.handled, occurred_at: e.occurred_at, execution_id: e.execution_id, execution_source: e.execution_source, execution_preview: e.execution_preview, user_ref: e.user_ref, tenant: e.app_tenant, deploy: e.deploy, server: e.server } }
|
|
38
|
-
end
|
|
39
|
-
latest = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).recent.first }
|
|
40
|
-
browser_breadcrumbs = browser_breadcrumbs_for(latest)
|
|
41
|
-
breadcrumbs = latest&.execution_id ? env.with_telemetry { breadcrumbs_for(latest) } : []
|
|
42
|
-
daily_counts = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).group(Arel.sql("date(occurred_at)")).count }.transform_keys(&:to_s)
|
|
43
|
-
daily = (from.to_date..Date.current).map { |d| { day: d.to_s, count: daily_counts[d.to_s].to_i } }
|
|
44
|
-
by_deploy = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).group(:deploy).count }
|
|
45
|
-
by_tenant = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).between(from, Time.current).where.not(app_tenant: nil).group(:app_tenant).count }
|
|
46
|
-
attachments = env.with_telemetry { attachments_for_issue }
|
|
47
|
-
# What a split by message would produce, and what the fingerprint that
|
|
48
|
-
# grouped these occurrences was -- both drive the Grouping panel.
|
|
49
|
-
messages = env.with_telemetry { Telemetry::Exception.where(group_hash: @issue.group_hash).recent.limit(Issue::SPLIT_SAMPLE).pluck(:message) }.tally
|
|
50
|
-
render inertia: {
|
|
51
|
-
issue: row(@issue).merge(first_seen_at: @issue.first_seen_at, resolved_at: @issue.resolved_at, resolved_in_deploy: @issue.resolved_in_deploy, regressed_at: @issue.regressed_at, sample: @issue.sample),
|
|
52
|
-
detection: nil,
|
|
53
|
-
latest: latest && { id: latest.id, class_name: latest.class_name, message: latest.message, handled: latest.handled, severity: latest.severity, source: latest.source, file: latest.file, line: latest.line, frames: latest.frames, cause: latest.cause, locals: latest.locals, context: context_without_breadcrumbs(latest), ruby_version: latest.ruby_version, rails_version: latest.rails_version, execution_id: latest.execution_id, execution_source: latest.execution_source, execution_preview: latest.execution_preview, deploy: latest.deploy },
|
|
54
|
-
breadcrumbs: breadcrumbs, browser_breadcrumbs: browser_breadcrumbs,
|
|
55
|
-
fingerprint: (latest&.fingerprint).presence || Array(@issue.sample["fingerprint"]),
|
|
56
|
-
fingerprint_source: latest&.fingerprint_source || @issue.sample["fingerprint_source"],
|
|
57
|
-
distinct_messages: messages.size,
|
|
58
|
-
top_messages: messages.sort_by { |_, n| -n }.first(5).map { |message, n| { message: message.to_s.first(200), count: n } },
|
|
59
|
-
occurrences: occurrences, daily: daily, by_deploy: by_deploy, by_tenant: by_tenant, attachments: attachments,
|
|
60
|
-
comments: @issue.comments.order(:created_at).map { |c| { id: c.id, body: c.body, user: c.user&.name || c.author_name || "Linear", source: c.source, created_at: c.created_at } },
|
|
61
|
-
activities: @issue.activities.order(:created_at).map { |a| activity_row(a) },
|
|
62
|
-
members: User.default.then { |u| [ { id: u.id, name: u.name } ] },
|
|
63
|
-
deploys: env.deploys.recent.limit(20).map { |d| { deploy: d.deploy, ref: d.short_ref, at: d.deployed_at } },
|
|
64
|
-
related_issues: related_issues,
|
|
65
|
-
merged_into: @issue.merged_into && { id: @issue.merged_into.id, key: @issue.merged_into.key, title: @issue.merged_into.title },
|
|
66
|
-
merged_issues: @issue.merged_issues.map { |i| { id: i.id, key: i.key, title: i.title } },
|
|
67
|
-
alerts: @issue.alerts.order(created_at: :desc).map { |a| alert_row(a) }
|
|
68
|
-
}
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def merge_candidates
|
|
72
|
-
query = params[:q].to_s.strip.first(200)
|
|
73
|
-
scope = @issue.environment.issues.open.where.not(id: @issue.id)
|
|
74
|
-
if query.present?
|
|
75
|
-
pattern = "%#{Issue.sanitize_sql_like(query)}%"
|
|
76
|
-
key_match = query.upcase.match(/\A#{Regexp.escape(@issue.application.issue_prefix)}-(\d+)\z/)
|
|
77
|
-
scope = if key_match
|
|
78
|
-
scope.where("title LIKE :pattern OR number = :number", pattern: pattern, number: key_match[1].to_i)
|
|
79
|
-
else
|
|
80
|
-
scope.where("title LIKE ?", pattern)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
render json: { issues: scope.order(last_seen_at: :desc, id: :desc).limit(20)
|
|
85
|
-
.map { |issue| { id: issue.id, key: issue.key, title: issue.title } } }
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
def update
|
|
89
|
-
notice = "Issue updated"
|
|
90
|
-
case params[:action_name]
|
|
91
|
-
when "resolve" then @issue.resolve!(deploy: params[:deploy].presence || @issue.environment.deploys.recent.first&.deploy)
|
|
92
|
-
when "ignore" then @issue.ignore!
|
|
93
|
-
when "reopen" then @issue.reopen!
|
|
94
|
-
when "assign" then @issue.update!(assignee: assignee_from(params[:assignee_id]))
|
|
95
|
-
when "assign_me" then @issue.update!(assignee_id: Viewer.user.id)
|
|
96
|
-
when "priority" then @issue.update!(priority: params[:priority])
|
|
97
|
-
when "merge" then @issue.merge_into!(@issue.environment.issues.open.find(params[:target_id]))
|
|
98
|
-
when "unmerge" then @issue.unmerge!
|
|
99
|
-
when "split" then notice = split_notice(@issue.split!(by: params[:by].to_s))
|
|
100
|
-
else @issue.update!(params.permit(:title))
|
|
101
|
-
end
|
|
102
|
-
redirect_to issue_path(@issue), notice: notice
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
def bulk
|
|
106
|
-
ids = Array(params[:ids]).first(BULK_LIMIT)
|
|
107
|
-
scope = Issue.all.where(id: ids)
|
|
108
|
-
if params[:action_name] == "merge"
|
|
109
|
-
bulk_merge(scope)
|
|
110
|
-
else
|
|
111
|
-
scope.find_each do |issue|
|
|
112
|
-
case params[:action_name]
|
|
113
|
-
when "resolve" then issue.resolve!(deploy: issue.environment.deploys.recent.first&.deploy)
|
|
114
|
-
when "ignore" then issue.ignore!
|
|
115
|
-
when "priority" then issue.update!(priority: params[:value])
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
redirect_back fallback_location: issues_path, notice: "#{ids.size} issue(s) updated"
|
|
120
|
-
end
|
|
121
|
-
|
|
122
|
-
private
|
|
123
|
-
|
|
124
|
-
# A stale selection can contain issues that have already been merged since
|
|
125
|
-
# the list rendered. Ignore those sources, choose an open target, and wrap
|
|
126
|
-
# the whole operation so a concurrent state change cannot partially fold a
|
|
127
|
-
# bulk selection before the model rejects it.
|
|
128
|
-
def bulk_merge(scope)
|
|
129
|
-
issues = scope.order(:first_seen_at, :id).to_a
|
|
130
|
-
target = issues.find(&:open?)
|
|
131
|
-
return unless target
|
|
132
|
-
|
|
133
|
-
Issue.transaction do
|
|
134
|
-
issues.each do |issue|
|
|
135
|
-
next if issue.id == target.id || issue.environment_id != target.environment_id || issue.merged?
|
|
136
|
-
|
|
137
|
-
issue.merge_into!(target)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def invalid_merge(error)
|
|
143
|
-
redirect_back fallback_location: (@issue ? issue_path(@issue) : issues_path), alert: error.message
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
def render_detection_issue(env)
|
|
147
|
-
render inertia: {
|
|
148
|
-
issue: row(@issue).merge(first_seen_at: @issue.first_seen_at, resolved_at: @issue.resolved_at,
|
|
149
|
-
resolved_in_deploy: @issue.resolved_in_deploy, regressed_at: @issue.regressed_at, sample: @issue.sample),
|
|
150
|
-
detection: IssueDetectionPresenter.new(@issue).as_json,
|
|
151
|
-
latest: nil, breadcrumbs: [], browser_breadcrumbs: [], fingerprint: [], fingerprint_source: nil,
|
|
152
|
-
distinct_messages: 0, top_messages: [], occurrences: [], daily: [], by_deploy: {}, by_tenant: {}, attachments: [],
|
|
153
|
-
comments: @issue.comments.order(:created_at).map { |c| { id: c.id, body: c.body, user: c.user.name, created_at: c.created_at } },
|
|
154
|
-
activities: @issue.activities.order(:created_at).map { |a| activity_row(a) },
|
|
155
|
-
members: User.default.then { |u| [ { id: u.id, name: u.name } ] },
|
|
156
|
-
deploys: env.deploys.recent.limit(20).map { |d| { deploy: d.deploy, ref: d.short_ref, at: d.deployed_at } },
|
|
157
|
-
related_issues: related_issues,
|
|
158
|
-
merged_into: @issue.merged_into && { id: @issue.merged_into.id, key: @issue.merged_into.key, title: @issue.merged_into.title },
|
|
159
|
-
merged_issues: @issue.merged_issues.map { |i| { id: i.id, key: i.key, title: i.title } },
|
|
160
|
-
alerts: @issue.alerts.order(created_at: :desc).map { |a| alert_row(a) }
|
|
161
|
-
}
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
# Sentry-style breadcrumbs: everything the execution did before it raised
|
|
165
|
-
# (queries, cache, outgoing calls, logs, jobs), newest last, capped at 30.
|
|
166
|
-
def breadcrumbs_for(exception)
|
|
167
|
-
exe = Telemetry::Execution.find_by(execution_id: exception.execution_id) or return []
|
|
168
|
-
ExecutionPresenter.new(exe, @issue.environment).timeline_entries
|
|
169
|
-
.select { |e| e[:offset] <= ((exception.occurred_at - exe.occurred_at) * 1000.0) + 1 }
|
|
170
|
-
.reject { |e| e[:type] == "exception" && e[:id] == exception.id }
|
|
171
|
-
.last(30)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# A browser exception's execution is the beacon POST that carried it, whose
|
|
175
|
-
# timeline says nothing about the crash. What it has instead is the trail
|
|
176
|
-
# the browser client recorded -- console errors, clicks, navigations --
|
|
177
|
-
# which the gem stores in the exception's context.
|
|
178
|
-
def browser_breadcrumbs_for(exception)
|
|
179
|
-
trail = browser_context(exception)["breadcrumbs"]
|
|
180
|
-
return [] unless trail.is_a?(Array)
|
|
181
|
-
trail.filter_map { |crumb| crumb.slice("at", "kind", "text") if crumb.is_a?(Hash) }
|
|
182
|
-
end
|
|
183
|
-
|
|
184
|
-
# The same context with the trail taken back out, since the page renders it
|
|
185
|
-
# as a breadcrumb list rather than as raw JSON in the context panel.
|
|
186
|
-
def context_without_breadcrumbs(exception)
|
|
187
|
-
context = parsed_context(exception)
|
|
188
|
-
return exception.context unless context["browser"].is_a?(Hash) && context["browser"].key?("breadcrumbs")
|
|
189
|
-
context.merge("browser" => context["browser"].except("breadcrumbs")).to_json
|
|
190
|
-
end
|
|
191
|
-
|
|
192
|
-
def browser_context(exception)
|
|
193
|
-
context = parsed_context(exception)
|
|
194
|
-
context["browser"].is_a?(Hash) ? context["browser"] : {}
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Exception context is a JSON string the gem built; anything else in that
|
|
198
|
-
# column is an older or garbled record, and reads as empty rather than 500.
|
|
199
|
-
def parsed_context(exception)
|
|
200
|
-
parsed = JSON.parse(exception&.context.to_s)
|
|
201
|
-
parsed.is_a?(Hash) ? parsed : {}
|
|
202
|
-
rescue JSON::ParserError
|
|
203
|
-
{}
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
# Files the app attached while raising this exception, from any execution.
|
|
207
|
-
# `data` is left out of the select: it is the whole gzipped file and the
|
|
208
|
-
# page only links to the download.
|
|
209
|
-
def attachments_for_issue
|
|
210
|
-
Telemetry::Attachment.where(exception_group_hash: @issue.group_hash).recent.limit(20)
|
|
211
|
-
.select(:id, :name, :content_type, :bytes, :truncated, :occurred_at, :execution_id, :execution_preview)
|
|
212
|
-
.map { |a| { id: a.id, name: a.name, content_type: a.content_type, bytes: a.bytes, truncated: a.truncated,
|
|
213
|
-
viewable: a.viewable?, occurred_at: a.occurred_at, execution_id: a.execution_id, execution_preview: a.execution_preview } }
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
# split! no-ops when there is nothing to split apart (the button is hidden
|
|
217
|
-
# then, but a stale page can still ask), so say which one happened.
|
|
218
|
-
def split_notice(created)
|
|
219
|
-
created.empty? ? "Nothing to split \u2014 every recent occurrence is already in its own issue" : "Split into #{created.size} issue(s)"
|
|
220
|
-
end
|
|
221
|
-
|
|
222
|
-
def set_issue
|
|
223
|
-
@issue = Issue.all.find(params[:id])
|
|
224
|
-
end
|
|
225
|
-
|
|
226
|
-
# Only a member of the current account can be assigned; nil clears.
|
|
227
|
-
def assignee_from(id)
|
|
228
|
-
id.present? ? User.find(id) : nil
|
|
229
|
-
end
|
|
230
|
-
|
|
231
|
-
# Same exception class seen in other environments of the same application:
|
|
232
|
-
# issue titles are stored as "ClassName: message", so we match on the
|
|
233
|
-
# "ClassName:" prefix.
|
|
234
|
-
def related_issues
|
|
235
|
-
class_name = @issue.title.to_s.split(":", 2).first
|
|
236
|
-
return [] if class_name.blank?
|
|
237
|
-
Issue.all.where.not(id: @issue.id)
|
|
238
|
-
.where("title LIKE ?", "#{Issue.sanitize_sql_like(class_name)}:%").limit(20)
|
|
239
|
-
.map { |i| { id: i.id, key: i.key, title: i.title, status: i.status, environment: { id: i.environment.id, name: i.environment.name } } }
|
|
240
|
-
end
|
|
241
|
-
|
|
242
|
-
def row(i)
|
|
243
|
-
{ id: i.id, key: i.key, title: i.title, kind: i.kind, status: i.status, priority: i.priority, culprit: i.culprit, occurrences: i.occurrences,
|
|
244
|
-
affected_users: i.affected_users, first_seen_at: i.first_seen_at, last_seen_at: i.last_seen_at, assignee: i.assignee && { id: i.assignee.id, name: i.assignee.name },
|
|
245
|
-
fingerprint_source: i.sample["fingerprint_source"], source: i.source,
|
|
246
|
-
application: { id: i.application.id, name: i.application.name }, environment: { id: i.environment.id, name: i.environment.name } }
|
|
247
|
-
end
|
|
248
|
-
|
|
249
|
-
def activity_row(a)
|
|
250
|
-
{ id: a.id, kind: a.kind, data: a.data, user: a.user && { id: a.user.id, name: a.user.name }, created_at: a.created_at }
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def alert_row(a)
|
|
254
|
-
{ id: a.id, event: a.event, status: a.status, sent_at: a.sent_at, error: a.error,
|
|
255
|
-
integration: { kind: a.integration.kind, name: a.integration.name } }
|
|
256
|
-
end
|
|
257
|
-
end
|
|
258
|
-
end
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class JobsController < DashboardController
|
|
5
|
-
include TelemetryIdentity
|
|
6
|
-
|
|
7
|
-
def index
|
|
8
|
-
from, to = window_range
|
|
9
|
-
page_data = nil
|
|
10
|
-
page = lambda do
|
|
11
|
-
page_data ||= telemetry do
|
|
12
|
-
scope = FilterQuery.apply(Telemetry::Execution.jobs, resource: :jobs, query: params[:q], from: from, to: to)
|
|
13
|
-
rows, meta = Telemetry::CursorPage.call(scope, cursor: params[:cursor], limit: params[:limit],
|
|
14
|
-
context: telemetry_cursor_context(:jobs))
|
|
15
|
-
people = origin_people(rows)
|
|
16
|
-
[ rows.map { |r| job_row(r, people) }, meta ]
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
render inertia: {
|
|
20
|
-
classes: -> { grouped("job_attempt", limit: 200) }, series: -> { series("job_attempt") },
|
|
21
|
-
queues: -> { queue_stats(from, to) }, recent: InertiaRails.merge { page.call.first },
|
|
22
|
-
pagination: -> { page.call.last }, deploys: -> { deploys_in_window }, q: params[:q].to_s
|
|
23
|
-
}
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def klass
|
|
27
|
-
group_hash = params[:group_hash]
|
|
28
|
-
from, to = window_range
|
|
29
|
-
name, attempts = telemetry do
|
|
30
|
-
base = Telemetry::Execution.jobs.where(group_hash: group_hash)
|
|
31
|
-
rows = FilterQuery.apply(base, resource: :jobs,
|
|
32
|
-
query: params[:q], from: from, to: to).recent.limit(200).to_a
|
|
33
|
-
people = origin_people(rows)
|
|
34
|
-
[ base.between(from, to).pick(:name), rows.map { |row| job_row(row, people) } ]
|
|
35
|
-
end
|
|
36
|
-
render inertia: "jobs/klass", props: { name: name, group_hash: group_hash,
|
|
37
|
-
summary: summary_with_delta("job_attempt", group_hash: group_hash),
|
|
38
|
-
series: series("job_attempt", group_hash: group_hash), attempts: attempts,
|
|
39
|
-
deploys: deploys_in_window, q: params[:q].to_s }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def show
|
|
43
|
-
exe = telemetry { Telemetry::Execution.find_by!(execution_id: params[:id]) }
|
|
44
|
-
render inertia: "executions/show", props: ExecutionPresenter.new(exe, environment).props
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
private
|
|
48
|
-
|
|
49
|
-
def queue_stats(from, to)
|
|
50
|
-
telemetry do
|
|
51
|
-
Telemetry::Execution.jobs.between(from, to).group(:queue)
|
|
52
|
-
.pluck(:queue, Arel.sql("COUNT(*)"), Arel.sql("AVG(queue_latency)"), Arel.sql("SUM(CASE WHEN outcome = 'failed' THEN 1 ELSE 0 END)"))
|
|
53
|
-
.map { |q, c, lat, f| { queue: q, count: c, avg_latency: (lat.to_f / 1000.0).round(1), failed: f } }
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def job_row(r, people)
|
|
58
|
-
{ execution_id: r.execution_id, name: r.name, outcome: r.outcome, queue: r.queue, attempt: r.attempt, duration: r.duration_ms.round(2),
|
|
59
|
-
queue_latency: r.queue_latency && (r.queue_latency / 1000.0).round(1), occurred_at: r.occurred_at, exception_preview: r.exception_preview,
|
|
60
|
-
job_id: r.job_id, deploy: r.deploy }.merge(origin_identity(r, people))
|
|
61
|
-
end
|
|
62
|
-
end
|
|
63
|
-
end
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class LlmCallsController < DashboardController
|
|
5
|
-
NANOS_PER_DOLLAR = 1_000_000_000.0
|
|
6
|
-
|
|
7
|
-
def index
|
|
8
|
-
from, to = window_range
|
|
9
|
-
fields = FilterQuery.parse(params[:q]).fetch(:fields)
|
|
10
|
-
models = telemetry { model_rows(from, to) }
|
|
11
|
-
tools = telemetry { tool_rows(from, to) }
|
|
12
|
-
rows = telemetry { recent_rows(fields) }
|
|
13
|
-
render inertia: { models: models, tools: tools, recent: rows,
|
|
14
|
-
totals: totals(models, tools),
|
|
15
|
-
# Two record types over one table, the way Execution
|
|
16
|
-
# feeds request/job_attempt/... Waiting on a model is
|
|
17
|
-
# seconds of somebody else's compute; a tool call is
|
|
18
|
-
# your own code in milliseconds. One p95 over both
|
|
19
|
-
# describes neither.
|
|
20
|
-
series: series("llm_call"), tool_series: series("llm_tool"),
|
|
21
|
-
q: params[:q].to_s }
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
|
|
26
|
-
# One row per provider/model/operation group, from rollups: tokens and
|
|
27
|
-
# spend come out of Rollup#extra, which sums them on merge, so a 30-day
|
|
28
|
-
# window costs the same few rows as an hour.
|
|
29
|
-
def model_rows(from, to)
|
|
30
|
-
Telemetry::Rollup.for_type("llm_call").between(from, to).to_a
|
|
31
|
-
.group_by(&:group_hash).map { |group_hash, group_rows| model_row(group_hash, group_rows, from, to) }
|
|
32
|
-
.sort_by { |r| [ -(r[:cost] || 0), -r[:count] ] }.first(200)
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Tools have no tokens and no price, so they rank by how much time they
|
|
36
|
-
# actually cost the request: slowest first.
|
|
37
|
-
def tool_rows(from, to)
|
|
38
|
-
Telemetry::Rollup.for_type("llm_tool").between(from, to).to_a
|
|
39
|
-
.group_by(&:group_hash).map do |group_hash, group_rows|
|
|
40
|
-
summary = Telemetry::Rollup.summarize(group_rows)
|
|
41
|
-
{ group_hash: group_hash, name: group_rows.max_by(&:bucket).name,
|
|
42
|
-
count: group_rows.sum(&:count), errors: group_rows.sum(&:error_count),
|
|
43
|
-
avg: (summary[:avg] / 1000.0).round(2), p95: (summary[:p95] / 1000.0).round(2),
|
|
44
|
-
max: (summary[:max] / 1000.0).round(2),
|
|
45
|
-
sparkline: Telemetry::Aggregations.sparkline(group_rows, from, to) }
|
|
46
|
-
end.sort_by { |r| -r[:p95] }.first(200)
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def model_row(group_hash, group_rows, from, to)
|
|
50
|
-
count = group_rows.sum(&:count)
|
|
51
|
-
summary = Telemetry::Rollup.summarize(group_rows)
|
|
52
|
-
nanos = sum_extra(group_rows, "cost_nanos")
|
|
53
|
-
priced = sum_extra(group_rows, "priced")
|
|
54
|
-
unpriced = sum_extra(group_rows, "unpriced")
|
|
55
|
-
{
|
|
56
|
-
group_hash: group_hash, name: group_rows.max_by(&:bucket).name, count: count,
|
|
57
|
-
errors: group_rows.sum(&:error_count),
|
|
58
|
-
input_tokens: sum_extra(group_rows, "input_tokens"), output_tokens: sum_extra(group_rows, "output_tokens"),
|
|
59
|
-
cache_read_tokens: sum_extra(group_rows, "cache_read_tokens"),
|
|
60
|
-
cache_write_tokens: sum_extra(group_rows, "cache_write_tokens"),
|
|
61
|
-
# nil, not 0, when nothing in the group was priced: a table that shows
|
|
62
|
-
# $0.00 for an unpriced model is lying about the bill. `unpriced`
|
|
63
|
-
# carries how many calls the figure leaves out when it is partial.
|
|
64
|
-
cost: priced.zero? ? nil : nanos / NANOS_PER_DOLLAR, unpriced: unpriced,
|
|
65
|
-
# Neither of these is an error, so neither shows in the error rate.
|
|
66
|
-
truncated: sum_extra(group_rows, "truncated"),
|
|
67
|
-
with_attachments: sum_extra(group_rows, "with_attachments"),
|
|
68
|
-
avg: (summary[:avg] / 1000.0).round(2), p95: (summary[:p95] / 1000.0).round(2),
|
|
69
|
-
sparkline: Telemetry::Aggregations.sparkline(group_rows, from, to)
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def sum_extra(rows, key)
|
|
74
|
-
rows.sum { |r| r.extra[key].to_i }
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
# `count` is model calls only. Tool calls are counted separately rather
|
|
78
|
-
# than folded in: they cost no tokens and no money, so adding them to the
|
|
79
|
-
# headline number inflates it with work that never reached a provider.
|
|
80
|
-
def totals(models, tools)
|
|
81
|
-
priced = models.reject { |m| m[:cost].nil? }
|
|
82
|
-
{
|
|
83
|
-
count: models.sum { |m| m[:count] }, errors: models.sum { |m| m[:errors] },
|
|
84
|
-
tool_count: tools.sum { |t| t[:count] }, tool_errors: tools.sum { |t| t[:errors] },
|
|
85
|
-
input_tokens: models.sum { |m| m[:input_tokens] }, output_tokens: models.sum { |m| m[:output_tokens] },
|
|
86
|
-
cost: priced.empty? ? nil : priced.sum { |m| m[:cost] }, unpriced: models.sum { |m| m[:unpriced] }
|
|
87
|
-
}
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def recent_rows(fields)
|
|
91
|
-
scope = Telemetry::LlmCall.between(*window_range)
|
|
92
|
-
scope = scope.where("model LIKE ?", "%#{Telemetry::LlmCall.sanitize_sql_like(fields['model'])}%") if fields["model"].present?
|
|
93
|
-
scope = scope.where(provider: fields["provider"]) if fields["provider"].present?
|
|
94
|
-
scope = scope.where(operation: fields["operation"]) if fields["operation"].present?
|
|
95
|
-
scope = scope.where(workflow_id: fields["workflow"]) if fields["workflow"].present?
|
|
96
|
-
scope = scope.where(finish_reason: fields["finish"]) if fields["finish"].present?
|
|
97
|
-
# "attachments:image" is the question a document-heavy app actually asks.
|
|
98
|
-
if fields["attachments"].present?
|
|
99
|
-
scope = if fields["attachments"] == "any"
|
|
100
|
-
scope.where.not(attachments: nil)
|
|
101
|
-
else
|
|
102
|
-
scope.where("attachment_types LIKE ?", "%#{Telemetry::LlmCall.sanitize_sql_like(fields['attachments'])}%")
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
scope.recent.limit(200).map { |r| row_props(r) }
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def row_props(call)
|
|
109
|
-
{ id: call.id, operation: call.operation, provider: call.provider, model: call.model,
|
|
110
|
-
response_model: call.response_model, tool_name: call.tool_name, duration: call.duration_ms.round(1),
|
|
111
|
-
status: call.status, error: call.error, streaming: call.streaming, message_count: call.message_count,
|
|
112
|
-
tool_count: call.tool_count, input_tokens: call.input_tokens, output_tokens: call.output_tokens,
|
|
113
|
-
cache_read_tokens: call.cache_read_tokens, cache_write_tokens: call.cache_write_tokens,
|
|
114
|
-
thinking_tokens: call.thinking_tokens, cost: call.cost, cost_reported: call.cost_reported,
|
|
115
|
-
workflow_id: call.workflow_id, workflow_name: call.workflow_name,
|
|
116
|
-
workflow_step_name: call.workflow_step_name,
|
|
117
|
-
finish_reason: call.finish_reason, provider_request_id: call.provider_request_id,
|
|
118
|
-
tools: call.tools, attachments: call.attachments, attachment_types: call.attachment_types,
|
|
119
|
-
attachment_names: call.attachment_names, tool_call_id: call.tool_call_id, params: call.params,
|
|
120
|
-
prompt: call.prompt, completion: call.completion, occurred_at: call.occurred_at,
|
|
121
|
-
execution_id: call.execution_id, execution_preview: call.execution_preview }
|
|
122
|
-
end
|
|
123
|
-
end
|
|
124
|
-
end
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class LogsController < DashboardController
|
|
5
|
-
def index
|
|
6
|
-
from, to = window_range
|
|
7
|
-
parsed = FilterQuery.parse(params[:q])
|
|
8
|
-
page_data = nil
|
|
9
|
-
page = lambda do
|
|
10
|
-
page_data ||= telemetry do
|
|
11
|
-
filtered = FilterQuery.apply(Telemetry::Log.all, resource: :logs, query: params[:q], from: from, to: to)
|
|
12
|
-
rows, meta = Telemetry::CursorPage.call(filtered, cursor: params[:cursor], limit: params[:limit],
|
|
13
|
-
context: telemetry_cursor_context(:logs))
|
|
14
|
-
[ rows.map { |l| log_row(l) }, meta, Telemetry::Log.fts_snippets(rows.map(&:id), parsed[:text]) ]
|
|
15
|
-
end
|
|
16
|
-
end
|
|
17
|
-
render inertia: {
|
|
18
|
-
logs: InertiaRails.merge { page.call[0] }, pagination: -> { page.call[1] },
|
|
19
|
-
counts: -> { level_counts(from, to) }, highlights: InertiaRails.deep_merge { page.call[2] }, q: params[:q].to_s
|
|
20
|
-
}
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
private
|
|
24
|
-
|
|
25
|
-
# The level facet counts the same rows the search does, except for the
|
|
26
|
-
# selected level itself, so switching level never disagrees with the table.
|
|
27
|
-
def level_counts(from, to)
|
|
28
|
-
telemetry do
|
|
29
|
-
FilterQuery.apply(Telemetry::Log.all, resource: :logs, query: params[:q], from: from, to: to,
|
|
30
|
-
except: %w[level status]).group(:level).count
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def log_row(log)
|
|
35
|
-
{ id: log.id, level: log.level, message: log.message.first(2000), tags: log.tags, occurred_at: log.occurred_at,
|
|
36
|
-
execution_id: log.execution_id, execution_source: log.execution_source, execution_preview: log.execution_preview,
|
|
37
|
-
source: log.source, tenant: log.app_tenant, user_ref: log.user_ref, context: log.context }
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class MailsController < DashboardController
|
|
5
|
-
def index
|
|
6
|
-
fields = FilterQuery.parse(params[:q]).fetch(:fields)
|
|
7
|
-
rows = telemetry do
|
|
8
|
-
scope = Telemetry::Mail.between(*window_range)
|
|
9
|
-
scope = scope.where(mailer: fields["mailer"]) if fields["mailer"].present?
|
|
10
|
-
# Mail rows have no "kind" column; delivery_method (smtp/test/deliver_later
|
|
11
|
-
# etc.) is the closest fit for a kind: filter token.
|
|
12
|
-
scope = scope.where(delivery_method: fields["kind"]) if fields["kind"].present?
|
|
13
|
-
scope.recent.limit(200).to_a
|
|
14
|
-
end
|
|
15
|
-
render inertia: { mailers: grouped("mail", limit: 100), series: series("mail"), q: params[:q].to_s,
|
|
16
|
-
recent: rows.map { |m| { id: m.id, mailer: m.mailer, subject: m.subject, to: m.to, cc: m.cc, bcc: m.bcc, attachments: m.attachments, delivery_method: m.delivery_method, duration: m.duration_ms.round(1), failed: m.failed, occurred_at: m.occurred_at, execution_id: m.execution_id, execution_preview: m.execution_preview } } }
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class NotificationsController < DashboardController
|
|
5
|
-
def index
|
|
6
|
-
rows = telemetry { Telemetry::Notification.between(*window_range).recent.limit(200).to_a }
|
|
7
|
-
render inertia: { groups: grouped("notification", limit: 100),
|
|
8
|
-
recent: rows.map { |n| { id: n.id, notifier: n.notifier, delivery_method: n.delivery_method, duration: n.duration_ms.round(1), failed: n.failed, occurred_at: n.occurred_at, execution_id: n.execution_id, execution_preview: n.execution_preview } } }
|
|
9
|
-
end
|
|
10
|
-
end
|
|
11
|
-
end
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class OutgoingRequestsController < DashboardController
|
|
5
|
-
def index
|
|
6
|
-
fields = FilterQuery.parse(params[:q]).fetch(:fields)
|
|
7
|
-
rows = telemetry do
|
|
8
|
-
scope = Telemetry::OutgoingRequest.between(*window_range)
|
|
9
|
-
scope = scope.where("host LIKE ?", "%#{Telemetry::OutgoingRequest.sanitize_sql_like(fields['host'])}%") if fields["host"].present?
|
|
10
|
-
if (range = FilterQuery.status_range(fields["status"]))
|
|
11
|
-
scope = scope.where(status_code: range)
|
|
12
|
-
end
|
|
13
|
-
scope.recent.limit(200).to_a
|
|
14
|
-
end
|
|
15
|
-
render inertia: { hosts: grouped("outgoing_request", limit: 100), series: series("outgoing_request"), q: params[:q].to_s,
|
|
16
|
-
recent: rows.map { |r| { id: r.id, host: r.host, method: r.method, url: r.url, status_code: r.status_code, duration: r.duration_ms.round(1), request_size: r.request_size, response_size: r.response_size, error: r.error, source: r.source, occurred_at: r.occurred_at, execution_id: r.execution_id, execution_preview: r.execution_preview, response_body: r.response_body } } }
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
end
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class OverviewController < DashboardController
|
|
5
|
-
def show
|
|
6
|
-
requests = grouped("request", limit: 8, order: :p95)
|
|
7
|
-
jobs = grouped("job_attempt", limit: 8)
|
|
8
|
-
render inertia: {
|
|
9
|
-
totals: { requests: summary_with_delta("request"), jobs: summary_with_delta("job_attempt") },
|
|
10
|
-
request_series: series("request"),
|
|
11
|
-
job_series: series("job_attempt"),
|
|
12
|
-
slow_routes: requests,
|
|
13
|
-
top_jobs: jobs,
|
|
14
|
-
issues: environment.issues.open.recent.limit(8).map { |i| issue_row(i) },
|
|
15
|
-
deploys: deploys_in_window,
|
|
16
|
-
release_health: release_health,
|
|
17
|
-
processes: telemetry { Telemetry::Process.recent.limit(20).group_by(&:server).transform_values { |ps| ps.first.slice(:role, :deploy, :ruby_version, :rails_version, :railwatch_version, :booted_at) } }
|
|
18
|
-
}
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
private
|
|
22
|
-
|
|
23
|
-
# Crash-free sessions for the release that is live now, over the page's
|
|
24
|
-
# window. nil (and the stat is not rendered) until the environment reports
|
|
25
|
-
# sessions at all.
|
|
26
|
-
def release_health
|
|
27
|
-
deploy = environment.deploys.recent.first or return nil
|
|
28
|
-
health = telemetry { Telemetry::ReleaseHealth.for_deploy(deploy.deploy, *window_range) }
|
|
29
|
-
return nil if health[:crash_free_sessions].nil?
|
|
30
|
-
health.slice(:sessions, :crash_free_sessions).merge(deploy: deploy.deploy, ref: deploy.short_ref)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def issue_row(i)
|
|
34
|
-
{ id: i.id, key: i.key, title: i.title, kind: i.kind, status: i.status, priority: i.priority, occurrences: i.occurrences,
|
|
35
|
-
affected_users: i.affected_users, last_seen_at: i.last_seen_at, culprit: i.culprit }
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class PeopleController < DashboardController
|
|
5
|
-
def index
|
|
6
|
-
from, to = window_range
|
|
7
|
-
active = telemetry do
|
|
8
|
-
Telemetry::Execution.requests.between(from, to).where.not(user_ref: nil).group(:user_ref)
|
|
9
|
-
.pluck(:user_ref, Arel.sql("COUNT(*)"), Arel.sql("SUM(CASE WHEN status >= 500 THEN 1 ELSE 0 END)"), Arel.sql("MAX(occurred_at)"))
|
|
10
|
-
.map { |ref, c, e, last| { ref: ref, requests: c, errors: e, last_seen_at: last } }.sort_by { |r| -r[:requests] }.first(200)
|
|
11
|
-
end
|
|
12
|
-
people = telemetry { Telemetry::Person.where(ref: active.map { |a| a[:ref] }).index_by(&:ref) }
|
|
13
|
-
render inertia: { people: active.map { |a| a.merge(name: people[a[:ref]]&.display_name, email: people[a[:ref]]&.email, tenant: people[a[:ref]]&.app_tenant) } }
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
def show
|
|
17
|
-
ref = params[:id]
|
|
18
|
-
person = telemetry { Telemetry::Person.find_by(ref: ref) }
|
|
19
|
-
executions = telemetry { Telemetry::Execution.where(user_ref: ref).between(*window_range).recent.limit(100).map { |r| { execution_id: r.execution_id, kind: r.kind, name: r.name, status: r.status, outcome: r.outcome, duration: r.duration_ms.round(1), occurred_at: r.occurred_at, exception_preview: r.exception_preview } } }
|
|
20
|
-
exception_groups = telemetry { Telemetry::Exception.where(user_ref: ref).between(*window_range).group(:group_hash).count }
|
|
21
|
-
issues = environment.issues.where(group_hash: exception_groups.keys).map { |i| { id: i.id, key: i.key, title: i.title, status: i.status, count: exception_groups[i.group_hash] } }
|
|
22
|
-
render inertia: { person: person && { person_ref: person.ref, name: person.display_name, email: person.email, tenant: person.app_tenant, first_seen_at: person.first_seen_at, last_seen_at: person.last_seen_at }, person_ref: ref, executions: executions, issues: issues }
|
|
23
|
-
end
|
|
24
|
-
end
|
|
25
|
-
end
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Railwatch
|
|
4
|
-
class ProcessesController < DashboardController
|
|
5
|
-
def index
|
|
6
|
-
live_since = Telemetry::HealthSample::LIVE_WINDOW.ago
|
|
7
|
-
samples, series, queues, live_servers, processes = telemetry do
|
|
8
|
-
live = Telemetry::HealthSample.live(live_since).to_a
|
|
9
|
-
[ live, Telemetry::HealthSample.series(*window_range, bucket: Telemetry::Aggregations::STEPS.fetch(step_key)), Telemetry::HealthSample.queue_depths(live),
|
|
10
|
-
live.map(&:server) | Telemetry::Execution.servers_since(live_since),
|
|
11
|
-
Telemetry::Process.recent.limit(200).to_a ]
|
|
12
|
-
end
|
|
13
|
-
render inertia: { samples: samples.map { |s| sample_row(s) }, series: series, queues: queues,
|
|
14
|
-
silent_servers: environment.expected_servers - live_servers.compact,
|
|
15
|
-
processes: processes.map { |p| process_row(p) } }
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
private
|
|
19
|
-
|
|
20
|
-
def sample_row(s)
|
|
21
|
-
{ id: s.id, server: s.server, role: s.role, pid: s.pid, deploy: s.deploy, sampled_at: s.sampled_at,
|
|
22
|
-
threads_busy: s.threads_busy, threads_max: s.threads_max, backlog: s.backlog,
|
|
23
|
-
pool_busy: s.pool_busy, pool_size: s.pool_size, pool_waiting: s.pool_waiting,
|
|
24
|
-
queue_depth: s.queue_depth, queue_latency: s.queue_latency && (s.queue_latency / 1000.0).round(1), memory: s.memory }
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def process_row(p)
|
|
28
|
-
{ id: p.id, booted_at: p.booted_at, pid: p.pid, role: p.role, server: p.server, deploy: p.deploy,
|
|
29
|
-
ruby_version: p.ruby_version, rails_version: p.rails_version, railwatch_version: p.railwatch_version,
|
|
30
|
-
boot_seconds: p.boot_seconds, detail: p.detail }
|
|
31
|
-
end
|
|
32
|
-
end
|
|
33
|
-
end
|