railwatch 0.6.1 → 0.8.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/CHANGELOG.md +36 -0
- data/app/frontend/components/app-content.tsx +26 -0
- data/app/frontend/components/app-logo-icon.tsx +23 -0
- data/app/frontend/components/app-logo.tsx +14 -0
- data/app/frontend/components/app-shell.tsx +38 -0
- data/app/frontend/components/app-sidebar-header.tsx +54 -0
- data/app/frontend/components/app-sidebar.tsx +94 -0
- data/app/frontend/components/app-wordmark.tsx +29 -0
- data/app/frontend/components/breadcrumbs.tsx +46 -0
- data/app/frontend/components/icon.tsx +16 -0
- data/app/frontend/components/input-error.tsx +18 -0
- data/app/frontend/components/nav-footer.tsx +46 -0
- data/app/frontend/components/nav-main.tsx +41 -0
- data/app/frontend/components/nav-user.tsx +50 -0
- data/app/frontend/components/railwatch/chart-hover.tsx +34 -0
- data/app/frontend/components/railwatch/chart-panel.tsx +170 -0
- data/app/frontend/components/railwatch/code.tsx +28 -0
- data/app/frontend/components/railwatch/command-palette.tsx +132 -0
- data/app/frontend/components/railwatch/copy-block.tsx +52 -0
- data/app/frontend/components/railwatch/copy-id.tsx +49 -0
- data/app/frontend/components/railwatch/cursor-load-more.tsx +39 -0
- data/app/frontend/components/railwatch/data-table.tsx +195 -0
- data/app/frontend/components/railwatch/digit-roll.tsx +312 -0
- data/app/frontend/components/railwatch/empty-state.tsx +159 -0
- data/app/frontend/components/railwatch/env-nav.tsx +130 -0
- data/app/frontend/components/railwatch/env-switcher.tsx +145 -0
- data/app/frontend/components/railwatch/filter-bar.tsx +153 -0
- data/app/frontend/components/railwatch/flamegraph.tsx +324 -0
- data/app/frontend/components/railwatch/frames.tsx +295 -0
- data/app/frontend/components/railwatch/json-viewer.tsx +159 -0
- data/app/frontend/components/railwatch/live-dot.tsx +78 -0
- data/app/frontend/components/railwatch/live-toggle.tsx +76 -0
- data/app/frontend/components/railwatch/metric-chart.tsx +112 -0
- data/app/frontend/components/railwatch/mobile-page-switcher.tsx +89 -0
- data/app/frontend/components/railwatch/onboarding.tsx +91 -0
- data/app/frontend/components/railwatch/origin-identity.tsx +84 -0
- data/app/frontend/components/railwatch/page-header.tsx +42 -0
- data/app/frontend/components/railwatch/percentile-picker.tsx +18 -0
- data/app/frontend/components/railwatch/relative-time.tsx +18 -0
- data/app/frontend/components/railwatch/saved-views.tsx +232 -0
- data/app/frontend/components/railwatch/segmented.tsx +64 -0
- data/app/frontend/components/railwatch/series-chart.tsx +400 -0
- data/app/frontend/components/railwatch/shortcuts-dialog.tsx +89 -0
- data/app/frontend/components/railwatch/signal-toast.tsx +121 -0
- data/app/frontend/components/railwatch/sort-header.tsx +37 -0
- data/app/frontend/components/railwatch/source-link.tsx +57 -0
- data/app/frontend/components/railwatch/sparkline-cell.tsx +14 -0
- data/app/frontend/components/railwatch/sql-block.tsx +116 -0
- data/app/frontend/components/railwatch/stat.tsx +173 -0
- data/app/frontend/components/railwatch/status-badge.tsx +79 -0
- data/app/frontend/components/railwatch/step-picker.tsx +57 -0
- data/app/frontend/components/railwatch/timeline.tsx +405 -0
- data/app/frontend/components/railwatch/utilisation-bar.tsx +44 -0
- data/app/frontend/components/railwatch/window-picker.tsx +147 -0
- data/app/frontend/components/ui/avatar.tsx +51 -0
- data/app/frontend/components/ui/badge.tsx +46 -0
- data/app/frontend/components/ui/breadcrumb.tsx +109 -0
- data/app/frontend/components/ui/button.tsx +60 -0
- data/app/frontend/components/ui/card.tsx +92 -0
- data/app/frontend/components/ui/chart.tsx +374 -0
- data/app/frontend/components/ui/checkbox.tsx +32 -0
- data/app/frontend/components/ui/collapsible.tsx +31 -0
- data/app/frontend/components/ui/command.tsx +182 -0
- data/app/frontend/components/ui/dialog.tsx +158 -0
- data/app/frontend/components/ui/dropdown-menu.tsx +255 -0
- data/app/frontend/components/ui/hover-card.tsx +42 -0
- data/app/frontend/components/ui/input.tsx +21 -0
- data/app/frontend/components/ui/label.tsx +24 -0
- data/app/frontend/components/ui/popover.tsx +87 -0
- data/app/frontend/components/ui/select.tsx +185 -0
- data/app/frontend/components/ui/separator.tsx +28 -0
- data/app/frontend/components/ui/sheet.tsx +137 -0
- data/app/frontend/components/ui/sidebar.tsx +726 -0
- data/app/frontend/components/ui/skeleton.tsx +13 -0
- data/app/frontend/components/ui/sonner.tsx +41 -0
- data/app/frontend/components/ui/table.tsx +114 -0
- data/app/frontend/components/ui/tabs.tsx +91 -0
- data/app/frontend/components/ui/textarea.tsx +18 -0
- data/app/frontend/components/ui/tooltip.tsx +61 -0
- data/app/frontend/components/user-info.tsx +32 -0
- data/app/frontend/components/user-menu-content.tsx +94 -0
- data/app/frontend/entrypoints/application.css +244 -0
- data/app/frontend/entrypoints/inertia.tsx +95 -0
- 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 +67 -0
- data/app/frontend/hooks/use-clipboard.ts +32 -0
- data/app/frontend/hooks/use-flash.tsx +23 -0
- data/app/frontend/hooks/use-hotkeys.ts +63 -0
- data/app/frontend/hooks/use-initials.tsx +15 -0
- data/app/frontend/hooks/use-live.ts +179 -0
- data/app/frontend/hooks/use-mobile-navigation.ts +8 -0
- data/app/frontend/hooks/use-mobile.ts +21 -0
- data/app/frontend/hooks/use-percentile.ts +24 -0
- data/app/frontend/hooks/use-reduced-motion.ts +25 -0
- data/app/frontend/hooks/use-row-nav.ts +106 -0
- data/app/frontend/hooks/use-step.ts +27 -0
- data/app/frontend/hooks/use-ticker.ts +12 -0
- data/app/frontend/hooks/use-window.ts +58 -0
- data/app/frontend/layouts/app/app-sidebar-layout.tsx +107 -0
- data/app/frontend/layouts/app-layout.tsx +21 -0
- data/app/frontend/layouts/env-layout.tsx +53 -0
- data/app/frontend/layouts/persistent-layout.tsx +18 -0
- data/app/frontend/lib/cable.ts +10 -0
- data/app/frontend/lib/env-pages.ts +236 -0
- data/app/frontend/lib/execution-path.ts +55 -0
- data/app/frontend/lib/explain.ts +50 -0
- data/app/frontend/lib/filter.ts +25 -0
- data/app/frontend/lib/flamegraph.ts +155 -0
- data/app/frontend/lib/format.ts +71 -0
- data/app/frontend/lib/keyboard.ts +18 -0
- data/app/frontend/lib/source-link.ts +217 -0
- data/app/frontend/lib/utils.ts +6 -0
- data/app/frontend/pages/alerts/index.tsx +171 -0
- data/app/frontend/pages/broadcasts/index.tsx +171 -0
- data/app/frontend/pages/cache_events/index.tsx +127 -0
- data/app/frontend/pages/commands/index.tsx +143 -0
- data/app/frontend/pages/deploys/index.tsx +94 -0
- data/app/frontend/pages/deploys/show.tsx +309 -0
- data/app/frontend/pages/deprecations/index.tsx +176 -0
- data/app/frontend/pages/exceptions/index.tsx +271 -0
- data/app/frontend/pages/executions/show.tsx +821 -0
- data/app/frontend/pages/issues/index.tsx +399 -0
- data/app/frontend/pages/issues/show.tsx +1367 -0
- data/app/frontend/pages/jobs/index.tsx +381 -0
- data/app/frontend/pages/jobs/klass.tsx +215 -0
- data/app/frontend/pages/llm_calls/index.tsx +664 -0
- data/app/frontend/pages/logs/index.tsx +303 -0
- data/app/frontend/pages/mails/index.tsx +218 -0
- data/app/frontend/pages/notifications/index.tsx +161 -0
- data/app/frontend/pages/outgoing_requests/index.tsx +309 -0
- data/app/frontend/pages/overview/show.tsx +333 -0
- data/app/frontend/pages/people/index.tsx +102 -0
- data/app/frontend/pages/people/show.tsx +149 -0
- data/app/frontend/pages/processes/index.tsx +335 -0
- data/app/frontend/pages/profiles/index.tsx +110 -0
- data/app/frontend/pages/profiles/show.tsx +105 -0
- data/app/frontend/pages/queries/index.tsx +492 -0
- data/app/frontend/pages/queries/show.tsx +276 -0
- data/app/frontend/pages/releases/index.tsx +190 -0
- data/app/frontend/pages/releases/release-health.tsx +68 -0
- data/app/frontend/pages/releases/release-path.ts +25 -0
- data/app/frontend/pages/releases/show.tsx +262 -0
- data/app/frontend/pages/requests/index.tsx +235 -0
- data/app/frontend/pages/requests/route.tsx +159 -0
- data/app/frontend/pages/scheduled_tasks/index.tsx +220 -0
- data/app/frontend/pages/spans/index.tsx +229 -0
- data/app/frontend/pages/spans/show.tsx +220 -0
- data/app/frontend/pages/storage_ops/index.tsx +150 -0
- data/app/frontend/pages/tenants/index.tsx +236 -0
- data/app/frontend/pages/tenants/show.tsx +477 -0
- data/app/frontend/pages/tenants/tenant-path.ts +26 -0
- data/app/frontend/pages/thresholds/index.tsx +414 -0
- data/app/frontend/pages/traces/show.tsx +208 -0
- data/app/frontend/pages/transactions/index.tsx +220 -0
- data/app/frontend/pages/view_renders/index.tsx +241 -0
- data/app/frontend/pages/visits/index.tsx +321 -0
- data/app/frontend/public/icon.png +0 -0
- data/app/frontend/public/icon.svg +5 -0
- data/app/frontend/routes/index.d.ts +2136 -0
- data/app/frontend/routes/index.js +2112 -0
- data/app/frontend/types/globals.d.ts +9 -0
- data/app/frontend/types/index.ts +288 -0
- data/app/frontend/types/vite-env.d.ts +9 -0
- data/app/models/railwatch/filter_query.rb +5 -5
- data/app/models/railwatch/telemetry/execution.rb +47 -0
- data/app/models/railwatch/telemetry/rollup.rb +73 -9
- data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb +34 -0
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch.rb +8 -3
- data/public/railwatch/assets/app-layout-LvxN9xSS.js +41 -0
- data/public/railwatch/assets/application-Cc5v_Vk9.css +1 -0
- data/public/railwatch/assets/{arrow-up-CLQ-7heQ.js → arrow-up-DG01k9AL.js} +1 -1
- data/public/railwatch/assets/{braces-rSYydpLY.js → braces-C3w7pfxS.js} +1 -1
- data/public/railwatch/assets/{card-DPjFKfen.js → card-Dpwlr0uY.js} +1 -1
- data/public/railwatch/assets/{chart-DWh7l8yM.js → chart-DJHv3R7W.js} +1 -1
- data/public/railwatch/assets/{chart-hover-CfoZUY4J.js → chart-hover-CEH-DtXO.js} +1 -1
- data/public/railwatch/assets/{chart-panel-CC49WTQL.js → chart-panel-CUdIzJvk.js} +1 -1
- data/public/railwatch/assets/code-BRZprwun.js +1 -0
- data/public/railwatch/assets/copy-id-CwhGGiwF.js +1 -0
- data/public/railwatch/assets/cursor-load-more-uF1_955Q.js +1 -0
- data/public/railwatch/assets/data-table-DYi-N0hm.js +1 -0
- data/public/railwatch/assets/env-layout-HdcOaPPX.js +1 -0
- data/public/railwatch/assets/{execution-path-FAuIzOXB.js → execution-path-CElXXFO_.js} +1 -1
- data/public/railwatch/assets/filter-bar-DEv0G_34.js +1 -0
- data/public/railwatch/assets/{flamegraph-EaGkP2NT.js → flamegraph-SKD9yxW9.js} +2 -2
- data/public/railwatch/assets/frames-CUpyJQDy.js +1 -0
- data/public/railwatch/assets/index-2W-ppwap.js +1 -0
- data/public/railwatch/assets/index-74ocuU5d.js +1 -0
- data/public/railwatch/assets/{index-C8Cggnbw.js → index-B8NA62Lz.js} +2 -2
- data/public/railwatch/assets/index-BBCEkLqp.js +1 -0
- data/public/railwatch/assets/index-BDcglJzX.js +1 -0
- data/public/railwatch/assets/index-B_KFb9LS.js +1 -0
- data/public/railwatch/assets/index-BqK_VKom.js +1 -0
- data/public/railwatch/assets/index-BxCXBHP2.js +1 -0
- data/public/railwatch/assets/index-C1jc1OoH.js +1 -0
- data/public/railwatch/assets/index-C7zREldp.js +1 -0
- data/public/railwatch/assets/index-CEetm4oC.js +1 -0
- data/public/railwatch/assets/index-CIaZkokF.js +1 -0
- data/public/railwatch/assets/index-CNH85TZp.js +1 -0
- data/public/railwatch/assets/index-Ce4SB5AB.js +1 -0
- data/public/railwatch/assets/index-CiRMSYMn.js +1 -0
- data/public/railwatch/assets/index-CjxXgOs-.js +1 -0
- data/public/railwatch/assets/index-ClL-PYEV.js +1 -0
- data/public/railwatch/assets/index-CteZ6fop.js +1 -0
- data/public/railwatch/assets/index-CvqfEExO.js +1 -0
- data/public/railwatch/assets/index-CxTLT-Qh.js +1 -0
- data/public/railwatch/assets/index-DJd3OUSd.js +1 -0
- data/public/railwatch/assets/index-DPD-rLp6.js +1 -0
- data/public/railwatch/assets/index-DYK_dps4.js +1 -0
- data/public/railwatch/assets/index-DrayARrI.js +1 -0
- data/public/railwatch/assets/index-HRXGir-0.js +1 -0
- data/public/railwatch/assets/index-UJOUALr_.js +1 -0
- data/public/railwatch/assets/index-d-3J7gQ5.js +1 -0
- data/public/railwatch/assets/index-oQrvBpOm.js +1 -0
- data/public/railwatch/assets/inertia-FoqYZAk8.js +97 -0
- data/public/railwatch/assets/json-viewer-CxqMojEm.js +1 -0
- data/public/railwatch/assets/klass-DEF-Vat5.js +1 -0
- data/public/railwatch/assets/origin-identity-Y6gvfQVR.js +1 -0
- data/public/railwatch/assets/{percentile-picker-CeQgllxD.js → percentile-picker-E2aUdLxx.js} +1 -1
- data/public/railwatch/assets/relative-time-DcJbkKlN.js +1 -0
- data/public/railwatch/assets/{release-health-uP-GF3_V.js → release-health-C8WSCDKD.js} +1 -1
- data/public/railwatch/assets/route-qO5vX1p-.js +1 -0
- data/public/railwatch/assets/{segmented-BeIe4uqk.js → segmented-BWee8uc9.js} +1 -1
- data/public/railwatch/assets/select-G7rNBzv0.js +1 -0
- data/public/railwatch/assets/series-chart-C5nXIUc5.js +1 -0
- data/public/railwatch/assets/show-BEWisLQw.js +1 -0
- data/public/railwatch/assets/show-BEa9PGu1.js +2 -0
- data/public/railwatch/assets/{show-YsNpqbcI.js → show-BiT_Aloc.js} +3 -3
- data/public/railwatch/assets/show-BpCO49d3.js +1 -0
- data/public/railwatch/assets/show-CH7yxt7R.js +1 -0
- data/public/railwatch/assets/show-CTO8Lvd2.js +1 -0
- data/public/railwatch/assets/show-DnYsDMUt.js +2 -0
- data/public/railwatch/assets/show-DyzHM527.js +1 -0
- data/public/railwatch/assets/show-Nfpxs8su.js +1 -0
- data/public/railwatch/assets/show-ddjUMo9G.js +1 -0
- data/public/railwatch/assets/show-ksjbSzDH.js +1 -0
- data/public/railwatch/assets/{sort-header-mEJUM3Av.js → sort-header-uOq5VlrB.js} +1 -1
- data/public/railwatch/assets/{sparkline-cell-Dwg7awwh.js → sparkline-cell-DgGij0cu.js} +1 -1
- data/public/railwatch/assets/stat-BTzNKTm2.js +1 -0
- data/public/railwatch/assets/status-badge-BDkOXa-C.js +1 -0
- data/public/railwatch/assets/{tenant-path-CQoP-BeF.js → tenant-path-DU0ezKSU.js} +1 -1
- data/public/railwatch/assets/{use-clipboard-DNefo-ky.js → use-clipboard-DoENmlWY.js} +1 -1
- data/public/railwatch/assets/{use-live-DMTuhKfB.js → use-live-CE_fN5_6.js} +1 -1
- data/public/railwatch/manifest.json +612 -1591
- metadata +243 -117
- data/public/railwatch/assets/app-layout-Zc0v-hYh.js +0 -1
- data/public/railwatch/assets/app-wordmark-BA_60AVb.js +0 -1
- data/public/railwatch/assets/appearance-CcfP9tZ7.js +0 -1
- data/public/railwatch/assets/application-BrN3Sz94.css +0 -1
- data/public/railwatch/assets/auth-layout-C05gEBIQ.js +0 -1
- data/public/railwatch/assets/badge-DRae8XwK.js +0 -1
- data/public/railwatch/assets/checkbox-DPkLUiwM.js +0 -1
- data/public/railwatch/assets/code-CLmYS6FU.js +0 -1
- data/public/railwatch/assets/copy-DaQMJWoT.js +0 -1
- data/public/railwatch/assets/copy-block-CGcXxp8J.js +0 -1
- data/public/railwatch/assets/copy-id-vBYHQwxg.js +0 -1
- data/public/railwatch/assets/cursor-load-more-Ddwxe5dZ.js +0 -1
- data/public/railwatch/assets/data-table-CzKTEE-O.js +0 -1
- data/public/railwatch/assets/edit-B1kmWkzd.js +0 -8
- data/public/railwatch/assets/edit-D9cx4pbG.js +0 -1
- data/public/railwatch/assets/edit-yv6j9p-V.js +0 -1
- data/public/railwatch/assets/empty-state-CW4wclK_.js +0 -1
- data/public/railwatch/assets/env-layout-Kz7wks1x.js +0 -1
- data/public/railwatch/assets/filter-bar-CJDjWFib.js +0 -1
- data/public/railwatch/assets/frames-zZuIaNH9.js +0 -1
- data/public/railwatch/assets/google-sign-in-button-2_zgbgVy.js +0 -1
- data/public/railwatch/assets/index-B49SWz7K.js +0 -1
- data/public/railwatch/assets/index-BHlY4wKe.js +0 -1
- data/public/railwatch/assets/index-BZpPtyFY.js +0 -1
- data/public/railwatch/assets/index-BeVK6jCL.js +0 -1
- data/public/railwatch/assets/index-BfbSo01U.js +0 -1
- data/public/railwatch/assets/index-BfgncAv6.js +0 -1
- data/public/railwatch/assets/index-BhNszK1k.js +0 -1
- data/public/railwatch/assets/index-Bu01uWvw.js +0 -1
- data/public/railwatch/assets/index-C1s_hK3p.js +0 -1
- data/public/railwatch/assets/index-CBip6V4z.js +0 -1
- data/public/railwatch/assets/index-CMJGss5R.js +0 -1
- data/public/railwatch/assets/index-CRo3yK20.js +0 -1
- data/public/railwatch/assets/index-CWB_p2J8.js +0 -1
- data/public/railwatch/assets/index-CWHpndGc.js +0 -1
- data/public/railwatch/assets/index-Ca_S4Sc3.js +0 -1
- data/public/railwatch/assets/index-CanPDDOa.js +0 -1
- data/public/railwatch/assets/index-Cie90Yat.js +0 -1
- data/public/railwatch/assets/index-CiepQ_pR.js +0 -1
- data/public/railwatch/assets/index-Cm1uCIGN.js +0 -1
- data/public/railwatch/assets/index-CzitnnSC.js +0 -1
- data/public/railwatch/assets/index-DEUfClv3.js +0 -1
- data/public/railwatch/assets/index-DJKwo-mI.js +0 -1
- data/public/railwatch/assets/index-DK6y0YHp.js +0 -1
- data/public/railwatch/assets/index-DMNPpLH9.js +0 -1
- data/public/railwatch/assets/index-DZO1mSfX.js +0 -1
- data/public/railwatch/assets/index-Db5wj3M5.js +0 -1
- data/public/railwatch/assets/index-Dcy5WktB.js +0 -1
- data/public/railwatch/assets/index-DvG0-7Lx.js +0 -1
- data/public/railwatch/assets/index-Dvj1wuka.js +0 -1
- data/public/railwatch/assets/index-KyZX46qX.js +0 -1
- data/public/railwatch/assets/index-Ze-KP-sl.js +0 -1
- data/public/railwatch/assets/index-mhRbWLBM.js +0 -1
- data/public/railwatch/assets/index-x099JL5f.js +0 -1
- data/public/railwatch/assets/index-x28zb_nf.js +0 -1
- data/public/railwatch/assets/inertia-Cuyz2ZHO.js +0 -97
- data/public/railwatch/assets/input-error-hog6gGxg.js +0 -1
- data/public/railwatch/assets/json-viewer-DH2W9HXf.js +0 -1
- data/public/railwatch/assets/klass-DHbDelLk.js +0 -1
- data/public/railwatch/assets/label-DdCBgiUn.js +0 -1
- data/public/railwatch/assets/layout-Cueyl7c5.js +0 -1
- data/public/railwatch/assets/live-dot-BZgYTYdt.js +0 -41
- data/public/railwatch/assets/nav-BSSGObDZ.js +0 -1
- data/public/railwatch/assets/new-B8FSb8Bl.js +0 -1
- data/public/railwatch/assets/new-C39_v2Ll.js +0 -1
- data/public/railwatch/assets/new-DVOPwaC5.js +0 -1
- data/public/railwatch/assets/new-Dd40nvJR.js +0 -1
- data/public/railwatch/assets/new-SxnYe1SE.js +0 -1
- data/public/railwatch/assets/new-eP5vKD3Y.js +0 -1
- data/public/railwatch/assets/onboarding-CUpZl5KB.js +0 -1
- data/public/railwatch/assets/origin-identity-BYt2uiuo.js +0 -1
- data/public/railwatch/assets/relative-time-D5UbF4oO.js +0 -1
- data/public/railwatch/assets/route-DQAY8JEr.js +0 -1
- data/public/railwatch/assets/select-2R16457Z.js +0 -1
- data/public/railwatch/assets/separator-D5I0UCB5.js +0 -1
- data/public/railwatch/assets/series-chart-gnrzhmM6.js +0 -1
- data/public/railwatch/assets/show-2BkeNRUC.js +0 -2
- data/public/railwatch/assets/show-B0X1hRQH.js +0 -1
- data/public/railwatch/assets/show-BKUV5l5q.js +0 -1
- data/public/railwatch/assets/show-BQJD_CsF.js +0 -6
- data/public/railwatch/assets/show-BhmD6Sxp.js +0 -1
- data/public/railwatch/assets/show-C3KcnVo2.js +0 -1
- data/public/railwatch/assets/show-C95cHd06.js +0 -2
- data/public/railwatch/assets/show-CdB4uVQS.js +0 -1
- data/public/railwatch/assets/show-CoCqcVIp.js +0 -1
- data/public/railwatch/assets/show-D2LqjEsU.js +0 -1
- data/public/railwatch/assets/show-DNpnKyTh.js +0 -1
- data/public/railwatch/assets/show-DOlTxbig.js +0 -1
- data/public/railwatch/assets/show-DQCkttL8.js +0 -1
- data/public/railwatch/assets/show-DcxesipC.js +0 -1
- data/public/railwatch/assets/show-IGJoc_X0.js +0 -1
- data/public/railwatch/assets/show-qNV6H8SH.js +0 -2
- data/public/railwatch/assets/stat-ZtxGU8lE.js +0 -1
- data/public/railwatch/assets/status-badge-CH5P-Xkj.js +0 -1
- data/public/railwatch/assets/text-link-DHJ8BXx5.js +0 -1
- data/public/railwatch/assets/textarea-BeQtQyl5.js +0 -1
- data/public/railwatch/assets/timeline-CaKQVu48.js +0 -1
- data/public/railwatch/assets/transition-ksDpqhKJ.js +0 -5
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
}
|
|
@@ -74,11 +74,12 @@ module Railwatch
|
|
|
74
74
|
resource = resource.to_sym
|
|
75
75
|
parsed = parse(query)
|
|
76
76
|
fields = parsed[:fields].except(*Array(except).map(&:to_s))
|
|
77
|
-
|
|
77
|
+
range = bounded_time_range(fields, from, to)
|
|
78
|
+
scope = scope.where(occurred_at: range)
|
|
78
79
|
scope = apply_common(scope, fields)
|
|
79
80
|
|
|
80
81
|
case resource
|
|
81
|
-
when :jobs then apply_jobs(scope, fields, parsed[:text])
|
|
82
|
+
when :jobs then apply_jobs(scope, fields, parsed[:text], range)
|
|
82
83
|
when :exceptions then apply_exceptions(scope, fields, parsed[:text])
|
|
83
84
|
when :logs then apply_logs(scope, fields, parsed[:text])
|
|
84
85
|
when :queries then apply_queries(scope, fields, parsed[:text])
|
|
@@ -120,15 +121,14 @@ module Railwatch
|
|
|
120
121
|
end
|
|
121
122
|
private_class_method :apply_common
|
|
122
123
|
|
|
123
|
-
def self.apply_jobs(scope, fields, text)
|
|
124
|
+
def self.apply_jobs(scope, fields, text, range)
|
|
124
125
|
outcome = fields["outcome"].presence || fields["status"].presence
|
|
125
126
|
scope = scope.where(queue: fields["queue"]) if fields["queue"].present?
|
|
126
127
|
scope = scope.where(outcome: outcome) if outcome
|
|
127
128
|
scope = scope.where(name: fields["class"]) if fields["class"].present?
|
|
128
129
|
scope = scope.where(job_id: fields["job_id"]) if fields["job_id"].present?
|
|
129
130
|
return scope unless text.present?
|
|
130
|
-
|
|
131
|
-
scope.where("name LIKE :pattern OR exception_preview LIKE :pattern", pattern: pattern)
|
|
131
|
+
scope.merge(Telemetry::Execution.named_like("job_attempt", text, range, previews: true))
|
|
132
132
|
end
|
|
133
133
|
private_class_method :apply_jobs
|
|
134
134
|
|
|
@@ -17,6 +17,53 @@ module Railwatch
|
|
|
17
17
|
scope :between, ->(from, to) { where(occurred_at: from..to) }
|
|
18
18
|
scope :failed, -> { where("status >= 500 OR outcome = 'failed'") }
|
|
19
19
|
|
|
20
|
+
# Executions of `kind` in [from, to] whose name contains `text`, or
|
|
21
|
+
# (with previews: true) whose exception preview does. A LIKE on the rows
|
|
22
|
+
# themselves reads every execution in the window until it has a page:
|
|
23
|
+
# 32 s for a week of the platform's own requests when the text is rare.
|
|
24
|
+
# Names come from a few hundred groups, so the text is matched against
|
|
25
|
+
# the window's rollups and the rows are fetched by group_hash. Rollups
|
|
26
|
+
# trail ingest by up to a minute, so the last FRESH_NAMES are matched on
|
|
27
|
+
# the rows themselves. Only failures carry a preview, and a partial
|
|
28
|
+
# index holds just those rows.
|
|
29
|
+
#
|
|
30
|
+
# Each branch names its index and the outer lookup is by rowid, so the
|
|
31
|
+
# plan does not depend on planner statistics (an embedded install's are
|
|
32
|
+
# sampled, and sampled statistics make kind look selective). When the
|
|
33
|
+
# text is common the old walk is the fast one -- a page of matches
|
|
34
|
+
# turns up in the first few thousand rows -- so a text the rollups say
|
|
35
|
+
# matches DENSE_MATCHES rows or more keeps it. On a copy of the
|
|
36
|
+
# platform's own tenant, a week of job attempts: a text matching
|
|
37
|
+
# nothing took 657 ms walking and 27 ms here; one matching 32,766 took
|
|
38
|
+
# 11 ms walking and 215 ms here.
|
|
39
|
+
FRESH_NAMES = 5.minutes
|
|
40
|
+
DENSE_MATCHES = 1_000
|
|
41
|
+
|
|
42
|
+
# `range` is used as given, so an exclusive or empty one (FilterQuery's
|
|
43
|
+
# after:/before: can narrow a window to nothing) stays that way.
|
|
44
|
+
def self.named_like(kind, text, range, previews: false)
|
|
45
|
+
from, to = range.begin, range.end
|
|
46
|
+
pattern = "%#{sanitize_sql_like(text)}%"
|
|
47
|
+
window = where(kind: kind, occurred_at: range)
|
|
48
|
+
groups = Rollup.for_type(kind).between(from, to).where("name LIKE ? ESCAPE '\\'", pattern)
|
|
49
|
+
if !TelemetryRecord.sqlite? || groups.sum(:count) >= DENSE_MATCHES
|
|
50
|
+
like = previews ? "name LIKE :pattern ESCAPE '\\' OR exception_preview LIKE :pattern ESCAPE '\\'" : "name LIKE :pattern ESCAPE '\\'"
|
|
51
|
+
return window.where(like, pattern: pattern)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
by_group = indexed_by("index_executions_on_group_hash_and_occurred_at")
|
|
55
|
+
.where(group_hash: groups.distinct.select(:group_hash), occurred_at: range, kind: kind)
|
|
56
|
+
fresh = where(kind: kind, occurred_at: [ from, to - FRESH_NAMES ].max..to).where("name LIKE ? ESCAPE '\\'", pattern)
|
|
57
|
+
ids = [ by_group, fresh ]
|
|
58
|
+
ids << indexed_by("idx_executions_with_preview").where(kind: kind, occurred_at: range)
|
|
59
|
+
.where.not(exception_preview: nil).where("exception_preview LIKE ? ESCAPE '\\'", pattern) if previews
|
|
60
|
+
from("#{quoted_table_name} NOT INDEXED").where(kind: kind, occurred_at: range)
|
|
61
|
+
.where(ids.map { |branch| "#{quoted_table_name}.id IN (#{branch.select(:id).to_sql})" }.join(" OR "))
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.indexed_by(index) = unscoped.from("#{quoted_table_name} INDEXED BY #{index}")
|
|
65
|
+
private_class_method :indexed_by
|
|
66
|
+
|
|
20
67
|
# Servers that reported an execution since `time`. Asked once per kind:
|
|
21
68
|
# there is no index led by occurred_at alone, so the obvious
|
|
22
69
|
# `where(occurred_at: time..).distinct.pluck(:server)` scans the whole
|
|
@@ -55,21 +55,16 @@ module Railwatch
|
|
|
55
55
|
def self.summarize(relation)
|
|
56
56
|
rows = relation.to_a
|
|
57
57
|
return { count: 0, errors: 0, client_errors: 0, avg: 0, p50: 0, p95: 0, p99: 0, max: 0, extra: {} } if rows.empty?
|
|
58
|
-
|
|
59
|
-
# `+` built a brand-new digest from both operands' centroids on every
|
|
60
|
-
# row, so merging N rows re-pushed every earlier centroid N times:
|
|
61
|
-
# 311 rows took 1.4s where merge! takes 0.5s, for the same percentiles.
|
|
62
|
-
merged = TDigest::TDigest.new(0.01)
|
|
63
|
-
rows.each { |r| merged.merge!(TDigest::TDigest.from_bytes(r.digest)) if r.digest }
|
|
58
|
+
p50, p95, p99 = merged_percentiles(rows, [ 0.5, 0.95, 0.99 ])
|
|
64
59
|
count = rows.sum(&:count)
|
|
65
60
|
{
|
|
66
61
|
count: count,
|
|
67
62
|
errors: rows.sum(&:error_count),
|
|
68
63
|
client_errors: rows.sum(&:client_error_count),
|
|
69
64
|
avg: count.zero? ? 0 : (rows.sum(&:duration_sum) / count),
|
|
70
|
-
p50:
|
|
71
|
-
p95:
|
|
72
|
-
p99:
|
|
65
|
+
p50: p50,
|
|
66
|
+
p95: p95,
|
|
67
|
+
p99: p99,
|
|
73
68
|
max: rows.map(&:duration_max).max,
|
|
74
69
|
# Everything a type puts in `extra` -- llm_call's cost_nanos and
|
|
75
70
|
# token counts, cache_event's hits and misses -- merged the way
|
|
@@ -80,6 +75,75 @@ module Railwatch
|
|
|
80
75
|
}
|
|
81
76
|
end
|
|
82
77
|
|
|
78
|
+
# Percentiles of the union of every row's centroids, read straight out
|
|
79
|
+
# of the stored bytes. Merging through TDigest pushed each centroid into
|
|
80
|
+
# a red-black tree one at a time: 2.8 s for a week of the platform's
|
|
81
|
+
# own query rollups (18,674 rows) and 7 s for thirty days, on every
|
|
82
|
+
# cold page load. Sorting the centroids once and walking their
|
|
83
|
+
# cumulative counts answers the same question without re-clustering:
|
|
84
|
+
# 353 ms and 1 s for those two windows. The answer is the first
|
|
85
|
+
# centroid whose running total reaches n * p, which is nearest rank --
|
|
86
|
+
# the rule raw_series uses for sub-hour buckets, so both readings of a
|
|
87
|
+
# window agree. (TDigest#percentile compares each centroid's midpoint
|
|
88
|
+
# instead, which puts the median of 1,000 sevens and 300 nines at 9.)
|
|
89
|
+
def self.merged_percentiles(rows, ps)
|
|
90
|
+
means = []
|
|
91
|
+
counts = []
|
|
92
|
+
rows.each { |row| centroids(row.digest, means, counts) if row.digest }
|
|
93
|
+
return ps.map { 0 } if means.empty?
|
|
94
|
+
|
|
95
|
+
order = (0...means.size).sort_by { |i| means[i] }
|
|
96
|
+
total = counts.sum
|
|
97
|
+
targets = ps.map { |p| total * p }
|
|
98
|
+
found = []
|
|
99
|
+
seen = 0
|
|
100
|
+
order.each do |i|
|
|
101
|
+
seen += counts[i]
|
|
102
|
+
found << means[i] while found.size < targets.size && seen >= targets[found.size]
|
|
103
|
+
break if found.size == targets.size
|
|
104
|
+
end
|
|
105
|
+
found << means[order.last] while found.size < targets.size
|
|
106
|
+
found.map(&:to_i)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Appends one stored digest's centroid means and weights, in either of
|
|
110
|
+
# the tdigest gem's encodings (TDigest.from_bytes reads the same two).
|
|
111
|
+
def self.centroids(bytes, means, counts)
|
|
112
|
+
format, _compression, size = bytes.unpack("LdL")
|
|
113
|
+
case format
|
|
114
|
+
when TDigest::TDigest::VERBOSE_ENCODING
|
|
115
|
+
means.concat(bytes.unpack("@16d#{size}"))
|
|
116
|
+
counts.concat(bytes.unpack("@#{16 + 8 * size}L#{size}"))
|
|
117
|
+
when TDigest::TDigest::SMALL_ENCODING
|
|
118
|
+
# Means are delta-encoded 4-byte floats; weights are 7-bit varints,
|
|
119
|
+
# one byte each unless a centroid holds 128 or more samples.
|
|
120
|
+
mean = 0.0
|
|
121
|
+
bytes.unpack("@16f#{size}").each { |delta| means << (mean += delta) }
|
|
122
|
+
weights = bytes.byteslice(16 + 4 * size, bytes.bytesize).unpack("C*")
|
|
123
|
+
if weights.size == size
|
|
124
|
+
counts.concat(weights)
|
|
125
|
+
else
|
|
126
|
+
at = 0
|
|
127
|
+
size.times do
|
|
128
|
+
byte = weights[at]
|
|
129
|
+
at += 1
|
|
130
|
+
weight = byte & 0x7f
|
|
131
|
+
shift = 7
|
|
132
|
+
while byte & 0x80 != 0
|
|
133
|
+
byte = weights[at] || 0
|
|
134
|
+
at += 1
|
|
135
|
+
weight += (byte & 0x7f) << shift
|
|
136
|
+
shift += 7
|
|
137
|
+
end
|
|
138
|
+
counts << weight
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
else
|
|
142
|
+
raise ArgumentError, "unknown t-digest encoding #{format}"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
private_class_method :centroids
|
|
146
|
+
|
|
83
147
|
def self.merge_extras(rows)
|
|
84
148
|
rows.each_with_object({}) do |row, out|
|
|
85
149
|
row.extra.each do |key, value|
|
data/db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Two dashboard aggregates read every row in the window and only a few
|
|
4
|
+
# narrow columns of each: the Jobs page's per-queue breakdown and the
|
|
5
|
+
# Processes page's health series. Through (kind, occurred_at) and
|
|
6
|
+
# (sampled_at) SQLite finds the rows in order but then fetches each one
|
|
7
|
+
# from the table, and on a telemetry file much larger than memory those
|
|
8
|
+
# rows are scattered pages on disk. On the platform's own 30 GB tenant,
|
|
9
|
+
# over a week: 38 s for the queue breakdown (491,000 job attempts) and
|
|
10
|
+
# 19 s for the health series (263,000 samples), with the GVL held the
|
|
11
|
+
# whole time, so every other request in the process waited too.
|
|
12
|
+
#
|
|
13
|
+
# Both indexes carry every column their query reads, so the whole answer
|
|
14
|
+
# comes from one contiguous index range: 335 ms and 370 ms on a copy of
|
|
15
|
+
# the same file. They cost 151 MB and 29 MB there, and took 13 s and 2 s
|
|
16
|
+
# to build.
|
|
17
|
+
#
|
|
18
|
+
# broadcasts is the one windowed table with no index led by occurred_at,
|
|
19
|
+
# so the Broadcasts page's "recent" list sorted the whole window to show
|
|
20
|
+
# 100 rows. Every other raw table has had this index since it was created.
|
|
21
|
+
#
|
|
22
|
+
# Free-text search over jobs and requests also matches the exception
|
|
23
|
+
# preview, which only a failed execution carries (15 of 491,000 job
|
|
24
|
+
# attempts in that same week). A partial index over just those rows lets
|
|
25
|
+
# Execution.named_like seek them instead of reading the window.
|
|
26
|
+
class AddCoveringIndexesForDashboardAggregates < ActiveRecord::Migration[8.1]
|
|
27
|
+
def change
|
|
28
|
+
add_index :executions, [ :kind, :occurred_at, :queue, :outcome, :queue_latency ], name: "idx_executions_queue_stats"
|
|
29
|
+
add_index :health_samples, [ :sampled_at, :threads_max, :threads_busy, :backlog, :queue_depth, :queue_latency ],
|
|
30
|
+
name: "idx_health_samples_series"
|
|
31
|
+
add_index :broadcasts, :occurred_at
|
|
32
|
+
add_index :executions, [ :kind, :occurred_at ], name: "idx_executions_with_preview", where: "exception_preview IS NOT NULL"
|
|
33
|
+
end
|
|
34
|
+
end
|
data/lib/railwatch/version.rb
CHANGED
data/lib/railwatch.rb
CHANGED
|
@@ -634,15 +634,20 @@ module Railwatch
|
|
|
634
634
|
end
|
|
635
635
|
|
|
636
636
|
# The one browser client, relative to the gem root. The dashboard bundle
|
|
637
|
-
# is built from it
|
|
638
|
-
# only app/frontend file it does), and a host with its own frontend build
|
|
639
|
-
# (Railwatch Cloud) imports it from browser_client_path.
|
|
637
|
+
# is built from it and the install generator copies it into a host.
|
|
640
638
|
BROWSER_CLIENT = "app/frontend/lib/railwatch.ts"
|
|
641
639
|
|
|
642
640
|
def self.browser_client_path
|
|
643
641
|
File.expand_path("../#{BROWSER_CLIENT}", __dir__)
|
|
644
642
|
end
|
|
645
643
|
|
|
644
|
+
# The dashboard's frontend source. Railwatch Cloud builds its bundle from
|
|
645
|
+
# this directory plus its own hosting pages, so there is one copy of every
|
|
646
|
+
# dashboard page and component.
|
|
647
|
+
def self.frontend_path
|
|
648
|
+
File.expand_path("../app/frontend", __dir__)
|
|
649
|
+
end
|
|
650
|
+
|
|
646
651
|
# One realistic wire record per record type, exactly as the gem's own
|
|
647
652
|
# subscribers emit it, generated by spec/railwatch/wire_fixtures_spec.rb.
|
|
648
653
|
# A receiver tests its mapper against these instead of hand-writing what
|