railwatch 0.6.0 → 0.6.1

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.
Files changed (143) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +42 -26
  3. data/CHANGELOG.md +28 -0
  4. data/README.md +40 -29
  5. data/app/jobs/railwatch/rollup_job.rb +3 -1
  6. data/app/models/railwatch/ingest/rollup_absorber.rb +1 -1
  7. data/docs/ai-and-mcp.md +9 -3
  8. data/docs/configuration.md +68 -27
  9. data/docs/embedded.md +76 -43
  10. data/docs/faq.md +22 -10
  11. data/docs/getting-started.md +116 -41
  12. data/docs/records.md +13 -10
  13. data/docs/replacing-nightwatch.md +16 -14
  14. data/docs/replacing-sentry.md +19 -10
  15. data/docs/security.md +24 -3
  16. data/docs/self-hosting.md +9 -1
  17. data/docs/testing.md +14 -4
  18. data/docs/troubleshooting.md +62 -23
  19. data/lib/generators/railwatch/install/install_generator.rb +5 -4
  20. data/lib/generators/railwatch/install/templates/initializer.rb.tt +2 -2
  21. data/lib/railwatch/version.rb +1 -1
  22. data/llms.txt +21 -18
  23. data/public/railwatch/assets/{app-layout-2xAKndVD.js → app-layout-Zc0v-hYh.js} +1 -1
  24. data/public/railwatch/assets/{app-wordmark-Bt6XqURo.js → app-wordmark-BA_60AVb.js} +1 -1
  25. data/public/railwatch/assets/{appearance-DyCfeh9o.js → appearance-CcfP9tZ7.js} +1 -1
  26. data/public/railwatch/assets/application-BrN3Sz94.css +1 -0
  27. data/public/railwatch/assets/{arrow-up-EmePsTKP.js → arrow-up-CLQ-7heQ.js} +1 -1
  28. data/public/railwatch/assets/{auth-layout-BuwogWMZ.js → auth-layout-C05gEBIQ.js} +1 -1
  29. data/public/railwatch/assets/{badge-Dv7I-ebP.js → badge-DRae8XwK.js} +1 -1
  30. data/public/railwatch/assets/{braces-iZ7MEfYP.js → braces-rSYydpLY.js} +1 -1
  31. data/public/railwatch/assets/{card-DqdEEKcJ.js → card-DPjFKfen.js} +1 -1
  32. data/public/railwatch/assets/{chart-C4k7Ub9W.js → chart-DWh7l8yM.js} +1 -1
  33. data/public/railwatch/assets/{chart-hover-2yZG3w2u.js → chart-hover-CfoZUY4J.js} +1 -1
  34. data/public/railwatch/assets/{chart-panel-CONkZcn9.js → chart-panel-CC49WTQL.js} +1 -1
  35. data/public/railwatch/assets/{checkbox-DKbThbxr.js → checkbox-DPkLUiwM.js} +1 -1
  36. data/public/railwatch/assets/{code-CYahYQHZ.js → code-CLmYS6FU.js} +1 -1
  37. data/public/railwatch/assets/{copy-block-CbJum9s8.js → copy-block-CGcXxp8J.js} +1 -1
  38. data/public/railwatch/assets/{copy-id-JmvLfkmx.js → copy-id-vBYHQwxg.js} +1 -1
  39. data/public/railwatch/assets/{cursor-load-more-C1ylbqqL.js → cursor-load-more-Ddwxe5dZ.js} +1 -1
  40. data/public/railwatch/assets/{data-table-CVKR2wkq.js → data-table-CzKTEE-O.js} +1 -1
  41. data/public/railwatch/assets/{edit-BwSEVrTG.js → edit-B1kmWkzd.js} +1 -1
  42. data/public/railwatch/assets/{edit-BXF5oNYA.js → edit-D9cx4pbG.js} +1 -1
  43. data/public/railwatch/assets/{edit-D5BRDwLT.js → edit-yv6j9p-V.js} +1 -1
  44. data/public/railwatch/assets/{empty-state-Bn48Gplm.js → empty-state-CW4wclK_.js} +1 -1
  45. data/public/railwatch/assets/{env-layout-CKLL8Ds7.js → env-layout-Kz7wks1x.js} +1 -1
  46. data/public/railwatch/assets/{execution-path-DWovhgLi.js → execution-path-FAuIzOXB.js} +1 -1
  47. data/public/railwatch/assets/{filter-bar-xcbJC93z.js → filter-bar-CJDjWFib.js} +1 -1
  48. data/public/railwatch/assets/{flamegraph-BNbNIuKT.js → flamegraph-EaGkP2NT.js} +1 -1
  49. data/public/railwatch/assets/{frames-Bk39xfJG.js → frames-zZuIaNH9.js} +1 -1
  50. data/public/railwatch/assets/{google-sign-in-button-B5as2ps1.js → google-sign-in-button-2_zgbgVy.js} +1 -1
  51. data/public/railwatch/assets/{index-XjXcV-EO.js → index-B49SWz7K.js} +1 -1
  52. data/public/railwatch/assets/{index-B0xgzHuN.js → index-BHlY4wKe.js} +1 -1
  53. data/public/railwatch/assets/{index-CiaeS-n7.js → index-BZpPtyFY.js} +1 -1
  54. data/public/railwatch/assets/{index-BfitxVBv.js → index-BeVK6jCL.js} +1 -1
  55. data/public/railwatch/assets/{index-C-EwH4lO.js → index-BfbSo01U.js} +1 -1
  56. data/public/railwatch/assets/{index-CrA7jBTK.js → index-BfgncAv6.js} +1 -1
  57. data/public/railwatch/assets/{index-D0mKj3Hg.js → index-BhNszK1k.js} +1 -1
  58. data/public/railwatch/assets/{index-CDXlITyG.js → index-Bu01uWvw.js} +1 -1
  59. data/public/railwatch/assets/{index-w_skgMmK.js → index-C1s_hK3p.js} +1 -1
  60. data/public/railwatch/assets/{index-Zkt4W_CM.js → index-C8Cggnbw.js} +1 -1
  61. data/public/railwatch/assets/{index-BDVShWMs.js → index-CBip6V4z.js} +1 -1
  62. data/public/railwatch/assets/{index-DeLtMWou.js → index-CMJGss5R.js} +1 -1
  63. data/public/railwatch/assets/{index-LKgpCzoR.js → index-CRo3yK20.js} +1 -1
  64. data/public/railwatch/assets/{index-gCvuyG1l.js → index-CWB_p2J8.js} +1 -1
  65. data/public/railwatch/assets/{index-C8JIdb3_.js → index-CWHpndGc.js} +1 -1
  66. data/public/railwatch/assets/{index-CBFCtJAR.js → index-Ca_S4Sc3.js} +1 -1
  67. data/public/railwatch/assets/{index-o1xqGvLo.js → index-CanPDDOa.js} +1 -1
  68. data/public/railwatch/assets/{index-Bd4YpOIO.js → index-Cie90Yat.js} +1 -1
  69. data/public/railwatch/assets/{index-C_hQmVTP.js → index-CiepQ_pR.js} +1 -1
  70. data/public/railwatch/assets/{index-BRvRkfob.js → index-Cm1uCIGN.js} +1 -1
  71. data/public/railwatch/assets/{index-dpG-19ZN.js → index-CzitnnSC.js} +1 -1
  72. data/public/railwatch/assets/{index-Vj93BTR5.js → index-DEUfClv3.js} +1 -1
  73. data/public/railwatch/assets/index-DJKwo-mI.js +1 -0
  74. data/public/railwatch/assets/{index-cTzBycCg.js → index-DK6y0YHp.js} +1 -1
  75. data/public/railwatch/assets/{index-B-FXAo1w.js → index-DMNPpLH9.js} +1 -1
  76. data/public/railwatch/assets/{index-BeQw69GM.js → index-DZO1mSfX.js} +1 -1
  77. data/public/railwatch/assets/{index-BVWapDeB.js → index-Db5wj3M5.js} +1 -1
  78. data/public/railwatch/assets/{index-DV5MdmpY.js → index-Dcy5WktB.js} +1 -1
  79. data/public/railwatch/assets/{index-aPCZX-pW.js → index-DvG0-7Lx.js} +1 -1
  80. data/public/railwatch/assets/{index-Atp1FTgX.js → index-Dvj1wuka.js} +1 -1
  81. data/public/railwatch/assets/{index-COd9lT84.js → index-KyZX46qX.js} +1 -1
  82. data/public/railwatch/assets/{index-S0NVaLj9.js → index-Ze-KP-sl.js} +1 -1
  83. data/public/railwatch/assets/{index-DaE-1xhx.js → index-mhRbWLBM.js} +1 -1
  84. data/public/railwatch/assets/{index-DdVg9LKX.js → index-x099JL5f.js} +1 -1
  85. data/public/railwatch/assets/{index-CcennT28.js → index-x28zb_nf.js} +1 -1
  86. data/public/railwatch/assets/{inertia-CqnzqPVD.js → inertia-Cuyz2ZHO.js} +2 -2
  87. data/public/railwatch/assets/{input-error-ByT28UdP.js → input-error-hog6gGxg.js} +1 -1
  88. data/public/railwatch/assets/{json-viewer-Bnu9RsJL.js → json-viewer-DH2W9HXf.js} +1 -1
  89. data/public/railwatch/assets/{klass-nLiBV-Az.js → klass-DHbDelLk.js} +1 -1
  90. data/public/railwatch/assets/{label-1nSzpSB6.js → label-DdCBgiUn.js} +1 -1
  91. data/public/railwatch/assets/{layout-D7lE1Doo.js → layout-Cueyl7c5.js} +1 -1
  92. data/public/railwatch/assets/{live-dot-UdzOfzJb.js → live-dot-BZgYTYdt.js} +1 -1
  93. data/public/railwatch/assets/{nav-BRCgp2w3.js → nav-BSSGObDZ.js} +1 -1
  94. data/public/railwatch/assets/{new-COyDKOOb.js → new-B8FSb8Bl.js} +1 -1
  95. data/public/railwatch/assets/{new-Cd9zPNJ8.js → new-C39_v2Ll.js} +1 -1
  96. data/public/railwatch/assets/{new-vjRzERL3.js → new-DVOPwaC5.js} +1 -1
  97. data/public/railwatch/assets/{new-C60G72DR.js → new-Dd40nvJR.js} +1 -1
  98. data/public/railwatch/assets/{new-DMJGKjmH.js → new-SxnYe1SE.js} +1 -1
  99. data/public/railwatch/assets/{new-Omvf-c9s.js → new-eP5vKD3Y.js} +1 -1
  100. data/public/railwatch/assets/{onboarding-gUjJQc-6.js → onboarding-CUpZl5KB.js} +1 -1
  101. data/public/railwatch/assets/{origin-identity-M1YUPYS0.js → origin-identity-BYt2uiuo.js} +1 -1
  102. data/public/railwatch/assets/{percentile-picker-BxIVblBw.js → percentile-picker-CeQgllxD.js} +1 -1
  103. data/public/railwatch/assets/{relative-time-DQ4kjowZ.js → relative-time-D5UbF4oO.js} +1 -1
  104. data/public/railwatch/assets/{release-health-Cb1tMOOO.js → release-health-uP-GF3_V.js} +1 -1
  105. data/public/railwatch/assets/{route-BJkoEm7r.js → route-DQAY8JEr.js} +1 -1
  106. data/public/railwatch/assets/{segmented-C4oWE73o.js → segmented-BeIe4uqk.js} +1 -1
  107. data/public/railwatch/assets/{select-ocdQJdQo.js → select-2R16457Z.js} +1 -1
  108. data/public/railwatch/assets/{separator-Ct2UcZ2x.js → separator-D5I0UCB5.js} +1 -1
  109. data/public/railwatch/assets/series-chart-gnrzhmM6.js +1 -0
  110. data/public/railwatch/assets/show-2BkeNRUC.js +2 -0
  111. data/public/railwatch/assets/{show-BLJOyLb_.js → show-B0X1hRQH.js} +1 -1
  112. data/public/railwatch/assets/{show-C8TkpEgm.js → show-BKUV5l5q.js} +1 -1
  113. data/public/railwatch/assets/{show-BAjfddEn.js → show-BQJD_CsF.js} +1 -1
  114. data/public/railwatch/assets/{show-C6OiJ6Hr.js → show-BhmD6Sxp.js} +1 -1
  115. data/public/railwatch/assets/{show-CEhPVMMT.js → show-C3KcnVo2.js} +1 -1
  116. data/public/railwatch/assets/{show-CX_pab9S.js → show-C95cHd06.js} +1 -1
  117. data/public/railwatch/assets/{show-Bsu1rvnK.js → show-CdB4uVQS.js} +1 -1
  118. data/public/railwatch/assets/{show-yEOrk6Pt.js → show-CoCqcVIp.js} +1 -1
  119. data/public/railwatch/assets/{show-DDVsv-uS.js → show-D2LqjEsU.js} +1 -1
  120. data/public/railwatch/assets/{show-D6lgzL6n.js → show-DNpnKyTh.js} +1 -1
  121. data/public/railwatch/assets/{show-eKazflRI.js → show-DOlTxbig.js} +1 -1
  122. data/public/railwatch/assets/{show-Dwiq-8hA.js → show-DQCkttL8.js} +1 -1
  123. data/public/railwatch/assets/{show-CBFBiBY2.js → show-DcxesipC.js} +1 -1
  124. data/public/railwatch/assets/{show-BoVyMQS6.js → show-IGJoc_X0.js} +1 -1
  125. data/public/railwatch/assets/{show-gtwKsTsl.js → show-YsNpqbcI.js} +1 -1
  126. data/public/railwatch/assets/{show-B4bwI9x2.js → show-qNV6H8SH.js} +1 -1
  127. data/public/railwatch/assets/{sort-header-CPM6fhXv.js → sort-header-mEJUM3Av.js} +1 -1
  128. data/public/railwatch/assets/{sparkline-cell-CXCxXPWt.js → sparkline-cell-Dwg7awwh.js} +1 -1
  129. data/public/railwatch/assets/{stat-BNEfE8X1.js → stat-ZtxGU8lE.js} +1 -1
  130. data/public/railwatch/assets/{status-badge-D0aylrM5.js → status-badge-CH5P-Xkj.js} +1 -1
  131. data/public/railwatch/assets/{tenant-path-C4aM_cO0.js → tenant-path-CQoP-BeF.js} +1 -1
  132. data/public/railwatch/assets/{text-link-CP4lqUi6.js → text-link-DHJ8BXx5.js} +1 -1
  133. data/public/railwatch/assets/{textarea-DzJo2ds4.js → textarea-BeQtQyl5.js} +1 -1
  134. data/public/railwatch/assets/{timeline-BNcJ9PMc.js → timeline-CaKQVu48.js} +1 -1
  135. data/public/railwatch/assets/{transition-BX2M1iaq.js → transition-ksDpqhKJ.js} +1 -1
  136. data/public/railwatch/assets/{use-clipboard-C1ApSwzA.js → use-clipboard-DNefo-ky.js} +1 -1
  137. data/public/railwatch/assets/{use-live-DWwLg1Nj.js → use-live-DMTuhKfB.js} +1 -1
  138. data/public/railwatch/manifest.json +1286 -1286
  139. metadata +116 -116
  140. data/public/railwatch/assets/application-B7h1MIhi.css +0 -1
  141. data/public/railwatch/assets/index-DiQ4mqT_.js +0 -1
  142. data/public/railwatch/assets/series-chart-D6upsrSn.js +0 -1
  143. data/public/railwatch/assets/show-CV-lG2b6.js +0 -2
@@ -8,7 +8,10 @@ depends on a conditional row.
8
8
 
9
9
  Install Railwatch first ([`getting-started.md`](getting-started.md)); you
10
10
  can run both for a day if you want to compare, since neither knows about
11
- the other.
11
+ the other. The default install is embedded, with the dashboard at
12
+ `/railwatch` and no token. The token and alert steps below apply when
13
+ you send to Railwatch Cloud, either as a cloud install or by exporting
14
+ from embedded.
12
15
 
13
16
  ## Decide whether Railwatch covers your workload
14
17
 
@@ -30,7 +33,7 @@ Railwatch does not claim those broader capabilities.
30
33
  | Ruby profiling | Conditional | Requires `vernier` or `stackprof`; there is no profiler bundled into the SDK. |
31
34
  | Browser monitoring | Partial | The optional Inertia client reports visits, Web Vitals, browser errors, and breadcrumbs. Session Replay, native/mobile SDKs, and Sentry's full browser/source-map workflow are outside the currently released Rails-server replacement. |
32
35
  | Runtime compatibility | Narrow today | The currently proved pair is Ruby 3.4 + Rails 8.1. A maintained compatibility matrix and any safe lowering of requirements are tracked by [#26](https://github.com/Rebulk/railwatch/issues/26). |
33
- | Managed integrations and operations | Partial | Railwatch Cloud supports its documented email, Slack, and webhook paths plus self-hosting. It does not promise Sentry's broader integration catalog. |
36
+ | Managed integrations and operations | Partial | Railwatch Cloud delivers alerts to email, Slack, webhooks and Linear, plus self-hosting; embedded mode records alerts without delivering them. It does not promise Sentry's broader integration catalog. |
34
37
  | SQL value privacy | Supported by default | Query records carry normalized SQL shapes without literal values, and Active Record binds are never sent. Raw SQL and query plans are separate opt-ins; either can contain values. |
35
38
 
36
39
  For a Rails 8.1 application whose work enters through Rack and Active Job,
@@ -70,9 +73,11 @@ deploy config once the app boots without it.
70
73
  `config/initializers/railwatch.rb` (written by the install generator) is
71
74
  where every option from `Sentry.init` lands. The mapping:
72
75
 
73
- - **`dsn:`** becomes `RAILWATCH_TOKEN`, one token per environment, created
74
- in Railwatch Cloud. Self-hosting adds `RAILWATCH_INGEST_URL`. The token is
75
- also the on/off switch: with it blank, Railwatch installs nothing.
76
+ - **`dsn:`** has no equivalent in embedded mode, which stores telemetry in
77
+ the app. For Railwatch Cloud it becomes `RAILWATCH_TOKEN`, one token per
78
+ environment, created in Railwatch Cloud. Self-hosting adds
79
+ `RAILWATCH_INGEST_URL`. On a cloud install the token is also the on/off
80
+ switch: with it blank, Railwatch installs nothing.
76
81
  - **`environment:`** becomes `c.environment`, which defaults to
77
82
  `Rails.env` — set it only to report under a different name.
78
83
  - **`release:`** becomes `c.deploy`, which auto-detects the release from
@@ -111,7 +116,9 @@ where every option from `Sentry.init` lands. The mapping:
111
116
  - **Rack `X-Request-Start` queue time** needs no setting: it is parsed
112
117
  into `queue_time` on every `request` record.
113
118
 
114
- A worked initializer, roughly what a `Sentry.init` block turns into:
119
+ A worked initializer for a cloud install, roughly what a `Sentry.init`
120
+ block turns into (an embedded install has `c.transport = :local` instead
121
+ of the token):
115
122
 
116
123
  ```ruby
117
124
  # config/initializers/railwatch.rb
@@ -415,9 +422,9 @@ startRailwatch({
415
422
 
416
423
  | Sentry | Railwatch |
417
424
  |---|---|
418
- | `Sentry.init({ dsn })` | `startRailwatch()`. There is no DSN: the beacon posts to the app's own origin and the *server* decides whether to record it (`c.beacon_enabled`, `RAILWATCH_TOKEN`). The gate you already have on whether `startRailwatch()` runs at all is the only gate. |
425
+ | `Sentry.init({ dsn })` | `startRailwatch()`. There is no DSN: the beacon posts to the app's own origin and the *server* decides whether to record it (`c.beacon_enabled`, and whether Railwatch is enabled). The gate you already have on whether `startRailwatch()` runs at all is the only gate. |
419
426
  | `release` | Automatic. The record is stamped with `c.deploy`, the same release the server records carry, so a browser issue and a server issue from one deploy line up without a matching pair of settings to get wrong. |
420
- | `environment` | Automatic — the ingest token identifies the environment. |
427
+ | `environment` | Automatic — the embedded databases belong to one environment, and on Railwatch Cloud the ingest token identifies it. |
421
428
  | `ignoreErrors` | `startRailwatch({ ignoreErrors })`. Strings match anywhere in the message; regexes are tested against it. Both `ResizeObserver` messages are ignored by default. |
422
429
  | `denyUrls` | `startRailwatch({ denyUrls })`, matched against the top stack frame's URL. `/extensions\//i`, `/^chrome:\/\//i`, and `/^moz-extension:\/\//i` are denied by default, **and** any frame from an origin that isn't the app's own is dropped — extensions, injected widgets, tag managers. |
423
430
  | `Sentry.setUser` | Server-side. The beacon is a same-origin POST carrying the session cookie, so the server resolves the user the same way it does for a request (`Railwatch.user`) when `Current.user` or Warden is set by middleware; an app that authenticates in a `before_action` gives Railwatch the same lookup with `c.beacon_user { \|request\| ... }`. Nothing the browser sends names the user, so it cannot be forged. |
@@ -561,8 +568,10 @@ hook matching `rails runner …` previews, or `sentry_runner_noise.rb` under
561
568
  | Inertia visit timing | Real browser page-visit duration, prop byte size, partial reloads, SSR time, and Core Web Vitals, from a client the generator installs. |
562
569
  | Spec matchers as a CI gate | `have_railwatch_queries`, `have_railwatch_n_plus_one`, `have_railwatch_outgoing_requests` fail the pull request that regresses a hot path. |
563
570
  | Zero app-DB writes | The gem holds records in memory and ships them from a background thread; a bench gate asserts no `INSERT`/`UPDATE`/`DELETE` ever originates in `lib/railwatch`. This is why it is safe on single-writer SQLite. |
564
- | One SQLite database per environment | The platform stores each monitored environment's telemetry in its own database file, which makes retention pruning, backup, and restore per-environment operations. |
565
- | An MCP server | AI assistants can ask what broke after the last deploy, list slow routes, read an execution's timeline, and search logs ([`ai-and-mcp.md`](ai-and-mcp.md)). |
571
+ | A dashboard inside the app | The default install serves the whole dashboard at `/railwatch` from two SQLite files the app owns, with no hosted service at all ([`embedded.md`](embedded.md)). |
572
+ | One SQLite database per environment | Railwatch Cloud stores each monitored environment's telemetry in its own database file, which makes retention pruning, backup, and restore per-environment operations. |
573
+ | LLM calls | RubyLLM calls are recorded with tokens, cost, finish reason, tool calls and workflows, in the request or job that made them ([`records.md`](records.md#llm_call)). |
574
+ | An MCP server | With Railwatch Cloud, AI assistants can ask what broke after the last deploy, list slow routes, read an execution's timeline, and search logs ([`ai-and-mcp.md`](ai-and-mcp.md)). |
566
575
 
567
576
  ## See also
568
577
 
data/docs/security.md CHANGED
@@ -3,8 +3,25 @@
3
3
  This review covers the open-source gem that runs in a customer's Rails
4
4
  application. Railwatch Cloud is a separate service and is outside this review.
5
5
 
6
+ ## Embedded dashboard
7
+
8
+ In embedded mode (the installer's default) the gem also serves a dashboard at
9
+ the engine's mount, showing every query, log line and exception the app
10
+ recorded. HTTP Basic authentication is on by default. With no credentials
11
+ configured, every dashboard request is 401 in every environment except
12
+ development, where it is open; the app logs a warning at boot outside
13
+ development, and `railwatch:doctor` reports the gate. Credentials, once set,
14
+ apply in development too. An app that turns Basic off gates pages with its own
15
+ `base_controller_class` or a routes constraint; live updates over Action Cable
16
+ are refused unless Basic, a `dashboard_user` resolver, or `dashboard_open`
17
+ authorizes them. See [Embedded mode](embedded.md#authentication).
18
+
19
+ The writer socket that web workers hand batches to is created with a 0700
20
+ directory and a 0600 socket, so only the app's own user can connect.
21
+
6
22
  ## Transport
7
23
 
24
+ This applies to a cloud install and to export from an embedded install.
8
25
  `Railwatch::Transport::Http` sends gzip NDJSON with `Net::HTTP`. HTTPS explicitly
9
26
  sets OpenSSL's `VERIFY_PEER`; a spec pins that setting. The transport does not
10
27
  implement redirect handling, so a redirect response is treated as a permanent
@@ -35,8 +52,9 @@ configuration and record specs:
35
52
  filtered. `Locals.inspect_value` rescues an `inspect` implementation that
36
53
  raises; this behavior is covered by an exception-record spec.
37
54
  - `capture_exception_source` is true. Source lines surrounding in-application
38
- backtrace frames are sent to Railwatch Cloud. Disable it if source context is
39
- outside the application's telemetry policy.
55
+ backtrace frames are recorded, and sent to Railwatch Cloud when the install
56
+ reports or exports there. Disable it if source context is outside the
57
+ application's telemetry policy.
40
58
 
41
59
  Log record messages are sent exactly as supplied to `Rails.logger`. Railwatch
42
60
  does not attempt to parse and partially filter `key=value` text because doing
@@ -46,7 +64,8 @@ and `Railwatch.reject_logs` or `RAILWATCH_IGNORE_LOGS=true` can omit log records
46
64
 
47
65
  ## Browser beacon
48
66
 
49
- `POST /railwatch/beacon` is the gem's only inbound unauthenticated endpoint. It
67
+ `POST /railwatch/beacon` is the gem's only inbound endpoint that is
68
+ unauthenticated by design. It
50
69
  is rate-limited per client IP through the Rails cache (120 requests per minute
51
70
  by default), limited to a 256 KiB body, and capped at 50 visits and 50 errors
52
71
  per request. Nested error stacks, messages, breadcrumbs, context, visit partial
@@ -88,6 +107,8 @@ application/deployment secret and must not be committed.
88
107
  application-specific credentials and personal data.
89
108
  - Keep secrets out of exception messages, log text, source files, tenant ids,
90
109
  user resolvers, and custom context.
110
+ - In embedded mode, set dashboard credentials (or your own gate) before
111
+ deploying; see [Embedded mode](embedded.md#authentication).
91
112
  - Use HTTPS in production and keep TLS verification enabled.
92
113
  - Put a request-body limit at the reverse proxy when the public beacon is
93
114
  enabled, and choose a shared cache if rate limits must span processes.
data/docs/self-hosting.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Self-hosting
2
2
 
3
+ This page is for sending telemetry to a Railwatch Cloud you run
4
+ yourself. An [embedded](embedded.md) install keeps everything inside
5
+ your app and needs none of it.
6
+
3
7
  Railwatch Cloud is a Rails app you can run yourself. The gem doesn't care
4
8
  which install it talks to — point it at yours and everything works the
5
9
  same.
@@ -17,12 +21,16 @@ end
17
21
  `ingest_url` defaults to `https://railwatch.rebulk.com`, so this is the one
18
22
  setting a self-hosted install always needs; everything the gem sends —
19
23
  records, ping, deploys — hangs off that host. Pass `--url=` to the
20
- installer to have it written for you:
24
+ installer to have it written for you (it implies `--cloud`):
21
25
 
22
26
  ```sh
23
27
  bin/rails generate railwatch:install --url=https://telemetry.example.com
24
28
  ```
25
29
 
30
+ To keep an embedded install and mirror it to your platform, set
31
+ `RAILWATCH_INGEST_URL` and `RAILWATCH_TOKEN` and turn on
32
+ [export](embedded.md#three-ways-to-run-it) instead.
33
+
26
34
  ## Getting a token
27
35
 
28
36
  On your install: sign up, create an application, then create an
data/docs/testing.md CHANGED
@@ -27,10 +27,20 @@ require "railwatch/minitest"
27
27
  ```
28
28
 
29
29
  Railwatch must be *enabled* in the test environment or every block would look
30
- empty. `config.enabled?` is true when `config.enabled` is set and a token is
31
- present, so set any non-blank `RAILWATCH_TOKEN` for the test env. Records go to
32
- an in-memory transport, never over the network. If Railwatch is disabled, the
33
- matchers raise `Railwatch::SpecHelper::Disabled` rather than quietly passing.
30
+ empty. An embedded install (`transport = :local`) is enabled with no token. A
31
+ cloud install needs a token present, so set any non-blank `RAILWATCH_TOKEN` for
32
+ the test env. Either way records go to an in-memory transport, never over the
33
+ network or into the telemetry database. If Railwatch is disabled, the matchers
34
+ raise `Railwatch::SpecHelper::Disabled` rather than quietly passing.
35
+
36
+ An embedded install adds its two databases to the `test` environment too, and
37
+ Rails' schema check refuses to run the suite while their migrations are
38
+ pending. `bin/rails db:test:prepare` does not create them, because they keep no
39
+ schema file; run this once locally and as a CI setup step:
40
+
41
+ ```sh
42
+ RAILS_ENV=test bin/rails db:prepare
43
+ ```
34
44
 
35
45
  Sampling is forced on for the block, so a fractional `c.sample` in the app's
36
46
  test config can't turn an assertion into one that never fires either.
@@ -8,20 +8,48 @@ below are keyed to those lines.
8
8
  bin/rails railwatch:doctor
9
9
  ```
10
10
 
11
- The task exits non-zero only when **token** or **ingest reachable**
12
- fails. Everything else is informational. A `✗` there means a feature
11
+ The first lines depend on the transport. An embedded install checks its
12
+ databases, writer and dashboard; a cloud install checks its token and
13
+ ingest host. The task exits non-zero only on the lines marked fatal
14
+ below. Everything else is informational: a `✗` there means a feature
13
15
  isn't wired, not that the install is broken.
14
16
 
17
+ Embedded (`transport = :local`):
18
+
19
+ | Doctor line | What a `✗` means |
20
+ |---|---|
21
+ | `export` | Only shown with `export_enabled` on. Export is on and cannot work: no token, no URL, a plain-HTTP URL, or an unsupported policy. Fatal. |
22
+ | `export destination` | The export queue is blocked or deferred; the line gives the reason. `bin/rails railwatch:export:rebind` clears a credential block. |
23
+ | `railwatch database`, `railwatch_telemetry database` | The database is missing from `config/database.yml` for this environment. Fatal. Re-run the install generator. |
24
+ | `railwatch migrations`, `railwatch_telemetry migrations` | Migrations are pending. Fatal. Run `bin/rails db:prepare`. |
25
+ | `telemetry disk` | The telemetry database is not in incremental auto-vacuum, so pruning never shrinks the file. See [Embedded mode](embedded.md#giving-the-disk-back). |
26
+ | `maintenance` | No maintenance tick in the last ten minutes. Expected when the app is stopped: the clock runs in the app's processes, not in rake. |
27
+ | `writer process` | The writer socket is not answering, `plugin :railwatch` is missing from `config/puma.rb`, or the socket path is over Linux's 108-byte limit. Expected when the app is stopped. |
28
+ | `last write` | Shown when the writer answers: nothing written in five minutes. With the app serving traffic, the writer is stuck or workers are not reaching it. |
29
+ | `dashboard access` | HTTP Basic is on with no credentials outside development, so every page is 401; or Basic is off and nothing else is declared. Run `bin/rails railwatch:authentication:configure`, or see [Embedded mode](embedded.md#authentication). |
30
+ | `json compatibility` | The installed `json` gem cannot decode on this Rails; see [below](#binjobs-dies-in-a-loop-with-wrong-number-of-arguments-given-2-expected-1). |
31
+ | `recurring.yml` | `config/recurring.yml` still lists `Railwatch::*` jobs from a pre-release. Remove them. |
32
+
33
+ Cloud (`transport = :http`):
34
+
15
35
  | Doctor line | What a `✗` means |
16
36
  |---|---|
17
37
  | `token` | `RAILWATCH_TOKEN` is unset or empty. Fatal: nothing is recorded at all. |
38
+ | `token storage` | A plaintext token is in a file Git tracks. Fatal. Move it to credentials or a secret manager. |
18
39
  | `ingest url` | `ingest_url` isn't a parseable HTTP(S) URL. |
40
+ | `ingest transport security` | The ingest URL is plain HTTP on a non-loopback host without `RAILWATCH_ALLOW_HTTP=true`. |
19
41
  | `ingest reachable` | `GET {ingest_url}/ingest/ping` didn't return success. Fatal. The ping carries the token, so a missing or wrong token fails this line too; fix `token` first. |
42
+
43
+ Both:
44
+
45
+ | Doctor line | What a `✗` means |
46
+ |---|---|
20
47
  | `request middleware` | `Railwatch::Middleware::Request` isn't in the stack, so requests aren't executions. |
21
48
  | `engine mounted` | `mount Railwatch::Engine, at: "/railwatch"` is missing from `config/routes.rb`; the browser beacon has nowhere to post. |
22
49
  | `deploy` | `config.deploy` is unset — records ship, charts get no deploy markers. |
23
50
  | `sample rates` | Never fails; it prints the effective rate per execution kind. |
24
51
  | `ignored record types` | Never fails; it prints what `c.ignore` is dropping. |
52
+ | `interactive sessions` | Never fails; it prints whether consoles are captured and the runner scratch paths. |
25
53
  | `kamal post-deploy hook` | `.kamal/hooks/post-deploy` is missing or doesn't mention Railwatch. Only matters if you deploy with Kamal. |
26
54
  | `browser client` | `app/frontend/lib/railwatch.ts` isn't there. Only matters for Inertia visit timing. |
27
55
  | `browser client imported` | The client exists but nothing calls `startRailwatch()` — no `startRailwatch` found in `app/frontend/entrypoints`. Visits won't report. |
@@ -33,30 +61,35 @@ isn't wired, not that the install is broken.
33
61
  **Symptom.** The environment's pages stay empty however much traffic the
34
62
  app takes.
35
63
 
36
- Work down this list. The first five are the same root cause seen from
64
+ Work down this list. Most of it is the same root cause seen from
37
65
  different angles: Railwatch decided not to record.
38
66
 
39
- **The token is missing or blank.** `Railwatch.enabled?` is
40
- `config.enabled && token.present?`. With no token the engine's
41
- `railwatch.subscribe` initializer returns early, so no subscribers and no
42
- patches are installed at all. This is by design, so the gem is inert in
43
- development. Fix: set `RAILWATCH_TOKEN`, restart, and re-run
44
- `railwatch:doctor`. The `token` line prints the first 6 characters and
45
- the length, which is enough to spot a truncated or quoted value.
67
+ **Embedded: the writer is not writing.** Run `railwatch:doctor` with the
68
+ app serving traffic. `writer process` and `last write` say whether the
69
+ writer is up and when it last wrote; the migrations lines catch a
70
+ database that was never prepared.
71
+
72
+ **Cloud: the token is missing or blank.** With `transport = :http`,
73
+ `Railwatch.enabled?` is `config.enabled && token.present?`. With no token
74
+ the engine's `railwatch.subscribe` initializer returns early, so no
75
+ subscribers and no patches are installed at all. Fix: set
76
+ `RAILWATCH_TOKEN`, restart, and re-run `railwatch:doctor`. The `token`
77
+ line prints the first 6 characters and the length, which is enough to
78
+ spot a truncated or quoted value.
46
79
 
47
- **The token is wrong.** A 401 from the ingest marks the transport
80
+ **Cloud: the token is wrong.** A 401 from the ingest marks the transport
48
81
  permanently unauthorized: no further flush is attempted for the lifetime
49
82
  of that process. Fixing the env var isn't enough. Restart the process.
50
83
  `railwatch:doctor`'s `ingest reachable` line catches this before you
51
84
  deploy.
52
85
 
53
- **`RAILWATCH_INGEST_URL` points somewhere else.** Records go where you sent
54
- them. `railwatch:status` prints the URL it is actually using. Compare it
55
- against the platform you're looking at. Self-hosting: see
86
+ **Cloud: `RAILWATCH_INGEST_URL` points somewhere else.** Records go where
87
+ you sent them. `railwatch:status` prints the URL it is actually using.
88
+ Compare it against the platform you're looking at. Self-hosting: see
56
89
  [`self-hosting.md`](self-hosting.md).
57
90
 
58
91
  **`config.enabled` is false.** `RAILWATCH_ENABLED=0` (or `false`/`no`/`off`)
59
- turns everything off with a valid token present.
92
+ turns everything off, embedded or not.
60
93
 
61
94
  **Sample rates are at zero.** `c.sample = { requests: 0.0 }` means no
62
95
  request records. So does the per-route `railwatch_never_sample` macro on
@@ -70,11 +103,11 @@ rather than a broken install.
70
103
  built. The `ignored record types` doctor line prints the list. Ignoring
71
104
  `:queries` also drops `n_plus_one`, since both key off `:queries`.
72
105
 
73
- **You're looking at the test environment.** Requiring `railwatch/rspec`
74
- (or `railwatch/minitest`) swaps the reporter's transport for an in-memory
75
- one. A suite records normally but never sends anything over the
76
- network. Independently: the health sampler, the session flusher, and the
77
- profiler all refuse to start when `Rails.env.test?`.
106
+ **You're looking at the test environment.** The spec helpers swap the
107
+ reporter's transport for an in-memory one, so a suite records normally
108
+ but never sends or stores anything. Independently: the health sampler,
109
+ the session flusher, and the profiler all refuse to start when
110
+ `Rails.env.test?`.
78
111
 
79
112
  Still nothing? Set `RAILWATCH_DEBUG=1` and restart. Internal diagnostics go
80
113
  to stderr prefixed `[railwatch]`. They never go to `Rails.logger`, so they
@@ -296,7 +329,13 @@ hook below.
296
329
 
297
330
  **Cause and fix**, in the order the hook itself checks:
298
331
 
299
- - **`RAILWATCH_TOKEN` isn't exported to the hook.** The first thing
332
+ - **Embedded: `RAILWATCH_TRANSPORT=local` isn't exported to the hook.**
333
+ The hook reads the deployer's environment, not your initializer. With
334
+ that variable set it runs `bin/rails railwatch:deploy[$KAMAL_VERSION]`
335
+ in the primary container, which writes the marker to the embedded
336
+ database. Without it, the hook treats the install as a cloud one and
337
+ exits at the next check, since an embedded install has no token.
338
+ - **Cloud: `RAILWATCH_TOKEN` isn't exported to the hook.** The next thing
300
339
  `.kamal/hooks/post-deploy` does is `[ -z "$RAILWATCH_TOKEN" ] && exit 0`.
301
340
  The hook runs on the deployer machine, in your shell, not in a
302
341
  container. So a token that only exists in `.kamal/secrets` for the
@@ -317,8 +356,8 @@ it exits 0 regardless.
317
356
 
318
357
  ## Log search finds less than it should
319
358
 
320
- **Symptom.** On a Postgres-backed platform install, log search matches
321
- fewer lines and highlights nothing.
359
+ **Symptom.** On a self-hosted platform backed by Postgres, log search
360
+ matches fewer lines and highlights nothing.
322
361
 
323
362
  **Cause.** Full-text search uses SQLite's FTS5 (`logs_fts`). The
324
363
  platform checks for both a SQLite adapter *and* the `logs_fts` table.
@@ -258,10 +258,11 @@ module Railwatch
258
258
  config/puma.rb Puma forks one Railwatch writer process that
259
259
  writes every batch and runs the maintenance clock, so no web
260
260
  process ever holds the telemetry database. No job worker.
261
- 4. Optional: mirror to Railwatch Cloud for alerts that still
262
- arrive when this app is down, MCP for your AI assistant, and
263
- every app in one place. Set RAILWATCH_TOKEN and
264
- c.export_enabled = true (docs/embedded.md).
261
+ 4. Optional: mirror to Railwatch Cloud for alerts delivered even
262
+ when this app is down, MCP for your AI assistant, and every
263
+ app in one place. Get a token (bin/rails railwatch:token), set
264
+ RAILWATCH_TOKEN, then c.export_enabled = true in the
265
+ initializer (docs/embedded.md).
265
266
  STEPS
266
267
  return
267
268
  end
@@ -5,8 +5,8 @@
5
5
  Railwatch.configure do |c|
6
6
  <% if local? -%>
7
7
  # Telemetry stays in this app's own railwatch_telemetry database and the
8
- # dashboard is served at /railwatch. No token, no cloud. Put the mount
9
- # behind your own authentication; this only names who is looking.
8
+ # dashboard is served at /railwatch. No token, no cloud. Who can open the
9
+ # dashboard is the Access block below.
10
10
  c.transport = :local # RAILWATCH_TRANSPORT
11
11
  c.ignored_request_paths += ["/railwatch", %r{\A/railwatch/}]
12
12
  # c.issue_prefix = "APP" # RAILWATCH_ISSUE_PREFIX; issue keys like APP-12
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Railwatch
4
- VERSION = "0.6.0"
4
+ VERSION = "0.6.1"
5
5
  end
data/llms.txt CHANGED
@@ -3,39 +3,42 @@
3
3
  > Railwatch is a Ruby gem that instruments a Rails application end to end —
4
4
  > requests, jobs, scheduled tasks, rake/runner commands, database queries and
5
5
  > N+1s, exceptions, cache, mail, notifications, broadcasts, outgoing HTTP,
6
- > Active Storage, view renders, and logs — links every one of them into a
7
- > single execution tree by `execution_id`/`trace_id`, and writes them from a
8
- > background thread into the app's own SQLite files (embedded, the default) or
9
- > to Railwatch Cloud. It replaces a separate APM and a
6
+ > Active Storage, view renders, RubyLLM calls, and logs — links every one of
7
+ > them into a single execution tree by `execution_id`/`trace_id`, and writes
8
+ > them from a background thread into two SQLite files the app owns (embedded,
9
+ > the default) or to Railwatch Cloud. It replaces a separate APM and a
10
10
  > separate error tracker with one gem and one configuration block, costs
11
11
  > under a millisecond of CPU per request, and never writes to the
12
- > application's own database. Install is `bundle add railwatch` followed by
13
- > `bin/rails generate railwatch:install`, which by default keeps everything in
14
- > the app (two SQLite databases, the dashboard at `/railwatch`); with
15
- > `--cloud` or a token option it ships to Railwatch Cloud instead. It writes
16
- > the initializer, mounts
17
- > the beacon engine, adds a Kamal post-deploy hook and the Inertia browser
18
- > client where the app has them, wires the test matchers, and then runs
19
- > `bin/rails railwatch:doctor` to print a ✓/✗ line for every piece.
12
+ > application's primary database. Install is `bundle add railwatch` followed
13
+ > by `bin/rails generate railwatch:install`, which by default keeps
14
+ > everything in the app (two SQLite databases, a Puma writer process, the
15
+ > dashboard at `/railwatch`, open in development and password-protected
16
+ > elsewhere); with `--cloud` or a token option it sends to Railwatch Cloud
17
+ > instead, and an embedded install can export to the cloud as well. It
18
+ > writes the initializer, mounts the engine, adds a Kamal post-deploy hook
19
+ > and the Inertia browser client where the app has them, and wires the test
20
+ > matchers. `bin/rails railwatch:doctor` prints a ✓/✗ line for every piece.
20
21
 
21
22
  ## Docs
22
23
 
23
- - [README](README.md): what Railwatch is, the install, sampling, spans, profiling, tail sampling, and the Sentry option mapping table.
24
- - [Getting started](docs/getting-started.md): five-minute install for a Rails 8 app, where the token comes from, the three optional lines, and deploying under Kamal, Docker/Heroku/Render, or no deploy tool at all.
24
+ - [README](README.md): what Railwatch is, the install, embedded mode, and where each document fits.
25
+ - [Getting started](docs/getting-started.md): the two-command embedded install, the Railwatch Cloud install and where its token comes from, the three optional lines, and deploying under Kamal, Docker/Heroku/Render, or no deploy tool at all.
26
+ - [Embedded mode](docs/embedded.md): the default install — the dashboard inside the app, its two SQLite databases, dashboard authentication, the Puma writer process, maintenance without a job worker, disk reclaim, and optional export to Railwatch Cloud.
25
27
  - [Configuration](docs/configuration.md): every configuration option and its `RAILWATCH_*` environment variable, plus the full public facade, redaction, rejection, transport and buffering behaviour, and the rake tasks.
26
- - [Record types](docs/records.md): every record type the gem ships and every attribute on it, sourced from the code that builds it.
28
+ - [Record types](docs/records.md): every record type the gem ships and every attribute on it, including RubyLLM calls (tokens, cost, finish reason, tool calls, workflows), sourced from the code that builds it.
27
29
  - [Testing](docs/testing.md): the RSpec and Minitest matchers (`have_railwatch_queries`, `have_railwatch_n_plus_one`, `record_railwatch_span`, `record_railwatch_exception`, `have_railwatch_outgoing_requests`) and a CI performance-gate recipe.
28
30
  - [Production source maps](docs/source-maps.md): hidden Vite maps, private upload before publishing assets, safe opt-in deletion, resolved browser stacks and default issue grouping.
29
- - [AI assistants and MCP](docs/ai-and-mcp.md): the MCP endpoint, how to get a token, paste-ready client configuration for Claude Code, Claude Desktop, Cursor, VS Code, and Zed, and every tool, prompt, and resource the server exposes.
31
+ - [AI assistants and MCP](docs/ai-and-mcp.md): Railwatch Cloud's MCP endpoint, how to get a token, paste-ready client configuration for Claude Code, Claude Desktop, Cursor, VS Code, and Zed, and every tool, prompt, and resource the server exposes.
30
32
  - [Replacing Sentry](docs/replacing-sentry.md): a step-by-step migration — removing the gems, porting each option, rewriting each call site, breadcrumbs, spans, profiling, attachments, `before_send`, fingerprints, and release health.
31
33
  - [Coming from Laravel Nightwatch](docs/replacing-nightwatch.md): the record-type mapping, what "execution" means in Railwatch, sampling parity, and the facade method names in Ruby.
32
34
  - [Self-hosting](docs/self-hosting.md): pointing the gem at a self-hosted Railwatch Cloud, creating a token there, and checking the connection.
33
- - [Troubleshooting](docs/troubleshooting.md): every failure mode paired with the `railwatch:doctor` line it shows up as — no records, doubled scheduled tasks, WebMock in specs, Puma fork, tail-sampling memory, missing profiler, missing deploy marker, Kamal hook, log search, empty tenants.
35
+ - [Troubleshooting](docs/troubleshooting.md): every `railwatch:doctor` line, embedded and cloud, and the failure modes behind them — no records, doubled scheduled tasks, WebMock in specs, Puma fork, tail-sampling memory, missing profiler, missing deploy marker, Kamal hook, log search, empty tenants.
36
+ - [Security](docs/security.md): transport, capture defaults, the browser beacon, token handling, and application responsibilities.
34
37
  - [FAQ](docs/faq.md): overhead numbers and how they are measured, retention, what is redacted by default versus opt-in, SQLite, and what happens when the platform is unreachable.
35
38
 
36
39
  ## For coding agents
37
40
 
38
- - [AGENTS.md](AGENTS.md): how to install and use Railwatch from inside a Rails app — the facade methods, the spec matchers, `railwatch:doctor`, and the MCP hookup.
41
+ - [AGENTS.md](AGENTS.md): how to install and use Railwatch from inside a Rails app — the facade methods, the spec matchers, `railwatch:doctor`, and the Railwatch Cloud MCP hookup.
39
42
 
40
43
  ## Optional
41
44