railwatch 0.5.1 → 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 (154) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +45 -25
  3. data/CHANGELOG.md +147 -0
  4. data/README.md +50 -31
  5. data/app/controllers/railwatch/dashboard_controller.rb +1 -0
  6. data/app/jobs/railwatch/rollup_job.rb +3 -1
  7. data/app/models/railwatch/application_record.rb +2 -2
  8. data/app/models/railwatch/ingest/rollup_absorber.rb +1 -1
  9. data/app/models/railwatch/telemetry_record.rb +2 -2
  10. data/docs/ai-and-mcp.md +9 -3
  11. data/docs/configuration.md +97 -40
  12. data/docs/embedded.md +96 -43
  13. data/docs/faq.md +28 -15
  14. data/docs/getting-started.md +121 -42
  15. data/docs/records.md +13 -10
  16. data/docs/replacing-nightwatch.md +16 -14
  17. data/docs/replacing-sentry.md +19 -10
  18. data/docs/security.md +24 -3
  19. data/docs/self-hosting.md +9 -1
  20. data/docs/testing.md +14 -4
  21. data/docs/troubleshooting.md +72 -29
  22. data/lib/generators/railwatch/install/install_generator.rb +32 -16
  23. data/lib/generators/railwatch/install/templates/initializer.rb.tt +6 -6
  24. data/lib/puma/plugin/railwatch.rb +48 -3
  25. data/lib/railwatch/configuration.rb +16 -1
  26. data/lib/railwatch/engine.rb +14 -3
  27. data/lib/railwatch/reporter.rb +52 -16
  28. data/lib/railwatch/transport/http.rb +4 -9
  29. data/lib/railwatch/version.rb +1 -1
  30. data/lib/railwatch.rb +23 -1
  31. data/lib/tasks/railwatch_tasks.rake +11 -4
  32. data/llms.txt +21 -14
  33. data/public/railwatch/assets/{app-layout-DDyQa72H.js → app-layout-Zc0v-hYh.js} +1 -1
  34. data/public/railwatch/assets/{app-wordmark-o9CODKP0.js → app-wordmark-BA_60AVb.js} +1 -1
  35. data/public/railwatch/assets/{appearance-BwuCXabr.js → appearance-CcfP9tZ7.js} +1 -1
  36. data/public/railwatch/assets/application-BrN3Sz94.css +1 -0
  37. data/public/railwatch/assets/{arrow-up-C6PxDiY3.js → arrow-up-CLQ-7heQ.js} +1 -1
  38. data/public/railwatch/assets/{auth-layout-BRt8MGFD.js → auth-layout-C05gEBIQ.js} +1 -1
  39. data/public/railwatch/assets/{badge-CAxXV8za.js → badge-DRae8XwK.js} +1 -1
  40. data/public/railwatch/assets/{braces-DgomTCNf.js → braces-rSYydpLY.js} +1 -1
  41. data/public/railwatch/assets/{card-cAtqCxWl.js → card-DPjFKfen.js} +1 -1
  42. data/public/railwatch/assets/{chart-BBeBkkNa.js → chart-DWh7l8yM.js} +1 -1
  43. data/public/railwatch/assets/{chart-hover-B1M9jc0y.js → chart-hover-CfoZUY4J.js} +1 -1
  44. data/public/railwatch/assets/{chart-panel-DUQTz_C8.js → chart-panel-CC49WTQL.js} +1 -1
  45. data/public/railwatch/assets/{checkbox-CmhMHWZO.js → checkbox-DPkLUiwM.js} +1 -1
  46. data/public/railwatch/assets/{code-DESvxyTj.js → code-CLmYS6FU.js} +1 -1
  47. data/public/railwatch/assets/{copy-block-BkSU5832.js → copy-block-CGcXxp8J.js} +1 -1
  48. data/public/railwatch/assets/{copy-id-D03GhN9F.js → copy-id-vBYHQwxg.js} +1 -1
  49. data/public/railwatch/assets/{cursor-load-more-CRyuMeQb.js → cursor-load-more-Ddwxe5dZ.js} +1 -1
  50. data/public/railwatch/assets/{data-table-BIlt7Rtm.js → data-table-CzKTEE-O.js} +1 -1
  51. data/public/railwatch/assets/{edit-O0NSBWxo.js → edit-B1kmWkzd.js} +1 -1
  52. data/public/railwatch/assets/{edit-DJ0D0wHN.js → edit-D9cx4pbG.js} +1 -1
  53. data/public/railwatch/assets/{edit-Bb6MKoe4.js → edit-yv6j9p-V.js} +1 -1
  54. data/public/railwatch/assets/{empty-state-C38il627.js → empty-state-CW4wclK_.js} +1 -1
  55. data/public/railwatch/assets/{env-layout-REF7OM4q.js → env-layout-Kz7wks1x.js} +1 -1
  56. data/public/railwatch/assets/{execution-path-FYLq1TwC.js → execution-path-FAuIzOXB.js} +1 -1
  57. data/public/railwatch/assets/{filter-bar-CYog9Alp.js → filter-bar-CJDjWFib.js} +1 -1
  58. data/public/railwatch/assets/{flamegraph-DSs69foN.js → flamegraph-EaGkP2NT.js} +1 -1
  59. data/public/railwatch/assets/{frames-BUi2J5Mk.js → frames-zZuIaNH9.js} +1 -1
  60. data/public/railwatch/assets/{google-sign-in-button-BQiIKFdd.js → google-sign-in-button-2_zgbgVy.js} +1 -1
  61. data/public/railwatch/assets/{index-ZOGOB8SA.js → index-B49SWz7K.js} +1 -1
  62. data/public/railwatch/assets/{index-DqTFTP8p.js → index-BHlY4wKe.js} +1 -1
  63. data/public/railwatch/assets/{index-DrcKVG2f.js → index-BZpPtyFY.js} +1 -1
  64. data/public/railwatch/assets/{index-Dh4IRLFI.js → index-BeVK6jCL.js} +1 -1
  65. data/public/railwatch/assets/{index-umIAl-pL.js → index-BfbSo01U.js} +1 -1
  66. data/public/railwatch/assets/{index-BoUBioBP.js → index-BfgncAv6.js} +1 -1
  67. data/public/railwatch/assets/{index-C3A_9imx.js → index-BhNszK1k.js} +1 -1
  68. data/public/railwatch/assets/{index-tpz-OGUP.js → index-Bu01uWvw.js} +1 -1
  69. data/public/railwatch/assets/{index-CGs4m_fa.js → index-C1s_hK3p.js} +1 -1
  70. data/public/railwatch/assets/{index-ZSZg9rtq.js → index-C8Cggnbw.js} +1 -1
  71. data/public/railwatch/assets/{index-so4lRrRq.js → index-CBip6V4z.js} +1 -1
  72. data/public/railwatch/assets/{index-DvjY3dPD.js → index-CMJGss5R.js} +1 -1
  73. data/public/railwatch/assets/{index-DtHmuB9Q.js → index-CRo3yK20.js} +1 -1
  74. data/public/railwatch/assets/{index-r0tSIplE.js → index-CWB_p2J8.js} +1 -1
  75. data/public/railwatch/assets/{index-CICUIFHL.js → index-CWHpndGc.js} +1 -1
  76. data/public/railwatch/assets/{index-CFFpnzIS.js → index-Ca_S4Sc3.js} +1 -1
  77. data/public/railwatch/assets/{index-DSvlZVWG.js → index-CanPDDOa.js} +1 -1
  78. data/public/railwatch/assets/{index-CrZ3vHDL.js → index-Cie90Yat.js} +1 -1
  79. data/public/railwatch/assets/{index-FhUaPPab.js → index-CiepQ_pR.js} +1 -1
  80. data/public/railwatch/assets/{index-C_upSl_k.js → index-Cm1uCIGN.js} +1 -1
  81. data/public/railwatch/assets/{index-sTYvcbkh.js → index-CzitnnSC.js} +1 -1
  82. data/public/railwatch/assets/{index-C7OtLq_3.js → index-DEUfClv3.js} +1 -1
  83. data/public/railwatch/assets/index-DJKwo-mI.js +1 -0
  84. data/public/railwatch/assets/{index-DDI_Zx5V.js → index-DK6y0YHp.js} +1 -1
  85. data/public/railwatch/assets/{index-C-PmdhXA.js → index-DMNPpLH9.js} +1 -1
  86. data/public/railwatch/assets/{index-BaR1U9An.js → index-DZO1mSfX.js} +1 -1
  87. data/public/railwatch/assets/{index-CsoN51vW.js → index-Db5wj3M5.js} +1 -1
  88. data/public/railwatch/assets/{index-BiiyMcA0.js → index-Dcy5WktB.js} +1 -1
  89. data/public/railwatch/assets/{index-8-hnAhOD.js → index-DvG0-7Lx.js} +1 -1
  90. data/public/railwatch/assets/{index-QpTtwFwu.js → index-Dvj1wuka.js} +1 -1
  91. data/public/railwatch/assets/{index-DW2CBbxU.js → index-KyZX46qX.js} +1 -1
  92. data/public/railwatch/assets/{index-BeOh2t_S.js → index-Ze-KP-sl.js} +1 -1
  93. data/public/railwatch/assets/{index-CiPo4Gob.js → index-mhRbWLBM.js} +1 -1
  94. data/public/railwatch/assets/{index-CpkI015n.js → index-x099JL5f.js} +1 -1
  95. data/public/railwatch/assets/{index-JdCVBrw8.js → index-x28zb_nf.js} +1 -1
  96. data/public/railwatch/assets/{inertia-DLew8ZNx.js → inertia-Cuyz2ZHO.js} +2 -2
  97. data/public/railwatch/assets/{input-error-cvM6_Jht.js → input-error-hog6gGxg.js} +1 -1
  98. data/public/railwatch/assets/{json-viewer-D922McGi.js → json-viewer-DH2W9HXf.js} +1 -1
  99. data/public/railwatch/assets/{klass-CrwICqN8.js → klass-DHbDelLk.js} +1 -1
  100. data/public/railwatch/assets/{label-GWl7I6sf.js → label-DdCBgiUn.js} +1 -1
  101. data/public/railwatch/assets/{layout-0ZAnD3zl.js → layout-Cueyl7c5.js} +1 -1
  102. data/public/railwatch/assets/{live-dot-D1n_BreY.js → live-dot-BZgYTYdt.js} +1 -1
  103. data/public/railwatch/assets/{nav-DPxr1NNC.js → nav-BSSGObDZ.js} +1 -1
  104. data/public/railwatch/assets/{new-D-ZzUK9a.js → new-B8FSb8Bl.js} +1 -1
  105. data/public/railwatch/assets/{new-DEVkYv-z.js → new-C39_v2Ll.js} +1 -1
  106. data/public/railwatch/assets/{new-Cdl6pqST.js → new-DVOPwaC5.js} +1 -1
  107. data/public/railwatch/assets/{new-DHAHDrN7.js → new-Dd40nvJR.js} +1 -1
  108. data/public/railwatch/assets/{new-Dz4lZf1L.js → new-SxnYe1SE.js} +1 -1
  109. data/public/railwatch/assets/{new-GMrRFurX.js → new-eP5vKD3Y.js} +1 -1
  110. data/public/railwatch/assets/onboarding-CUpZl5KB.js +1 -0
  111. data/public/railwatch/assets/{origin-identity-Bk9yHWZ1.js → origin-identity-BYt2uiuo.js} +1 -1
  112. data/public/railwatch/assets/{percentile-picker-DfSx9yJO.js → percentile-picker-CeQgllxD.js} +1 -1
  113. data/public/railwatch/assets/{relative-time-CjIjb8Lg.js → relative-time-D5UbF4oO.js} +1 -1
  114. data/public/railwatch/assets/{release-health-4b3tivEf.js → release-health-uP-GF3_V.js} +1 -1
  115. data/public/railwatch/assets/{route-C_5BUtHK.js → route-DQAY8JEr.js} +1 -1
  116. data/public/railwatch/assets/{segmented-BgbT3wZa.js → segmented-BeIe4uqk.js} +1 -1
  117. data/public/railwatch/assets/{select-DmunxCKE.js → select-2R16457Z.js} +1 -1
  118. data/public/railwatch/assets/{separator-BXzEdZ_8.js → separator-D5I0UCB5.js} +1 -1
  119. data/public/railwatch/assets/series-chart-gnrzhmM6.js +1 -0
  120. data/public/railwatch/assets/show-2BkeNRUC.js +2 -0
  121. data/public/railwatch/assets/{show-mU38uGTg.js → show-B0X1hRQH.js} +1 -1
  122. data/public/railwatch/assets/{show-CAl7xcex.js → show-BKUV5l5q.js} +1 -1
  123. data/public/railwatch/assets/{show-DnR1Dnjd.js → show-BQJD_CsF.js} +1 -1
  124. data/public/railwatch/assets/{show-Dn-GwFZL.js → show-BhmD6Sxp.js} +1 -1
  125. data/public/railwatch/assets/{show-Y74rM0VT.js → show-C3KcnVo2.js} +1 -1
  126. data/public/railwatch/assets/{show-Dily73Xk.js → show-C95cHd06.js} +1 -1
  127. data/public/railwatch/assets/{show-DXs4deaC.js → show-CdB4uVQS.js} +1 -1
  128. data/public/railwatch/assets/{show-DI8IhNUH.js → show-CoCqcVIp.js} +1 -1
  129. data/public/railwatch/assets/{show-vQ4bndYD.js → show-D2LqjEsU.js} +1 -1
  130. data/public/railwatch/assets/{show-DcpTFiLi.js → show-DNpnKyTh.js} +1 -1
  131. data/public/railwatch/assets/{show-B2zLAW83.js → show-DOlTxbig.js} +1 -1
  132. data/public/railwatch/assets/{show-SvLOcPrx.js → show-DQCkttL8.js} +1 -1
  133. data/public/railwatch/assets/{show-BLpWUHWD.js → show-DcxesipC.js} +1 -1
  134. data/public/railwatch/assets/{show-DYskfl3-.js → show-IGJoc_X0.js} +1 -1
  135. data/public/railwatch/assets/{show-DSP9Cq_C.js → show-YsNpqbcI.js} +1 -1
  136. data/public/railwatch/assets/{show-DlRVS18-.js → show-qNV6H8SH.js} +1 -1
  137. data/public/railwatch/assets/{sort-header-Dcq9bzmo.js → sort-header-mEJUM3Av.js} +1 -1
  138. data/public/railwatch/assets/{sparkline-cell-BON3qQUB.js → sparkline-cell-Dwg7awwh.js} +1 -1
  139. data/public/railwatch/assets/{stat-DFEyFxkO.js → stat-ZtxGU8lE.js} +1 -1
  140. data/public/railwatch/assets/{status-badge-BaUKP7Yo.js → status-badge-CH5P-Xkj.js} +1 -1
  141. data/public/railwatch/assets/{tenant-path-DPZPc985.js → tenant-path-CQoP-BeF.js} +1 -1
  142. data/public/railwatch/assets/{text-link-BO77t9Xk.js → text-link-DHJ8BXx5.js} +1 -1
  143. data/public/railwatch/assets/{textarea-DTqrCiV0.js → textarea-BeQtQyl5.js} +1 -1
  144. data/public/railwatch/assets/{timeline-D5rJ0es2.js → timeline-CaKQVu48.js} +1 -1
  145. data/public/railwatch/assets/{transition-DMIrZVth.js → transition-ksDpqhKJ.js} +1 -1
  146. data/public/railwatch/assets/{use-clipboard-ByoUGQqA.js → use-clipboard-DNefo-ky.js} +1 -1
  147. data/public/railwatch/assets/{use-live-D7xKz2ma.js → use-live-DMTuhKfB.js} +1 -1
  148. data/public/railwatch/manifest.json +1286 -1286
  149. metadata +116 -116
  150. data/public/railwatch/assets/application-B7h1MIhi.css +0 -1
  151. data/public/railwatch/assets/index-CFRLPs4J.js +0 -1
  152. data/public/railwatch/assets/onboarding-D1vwaHYT.js +0 -1
  153. data/public/railwatch/assets/series-chart-Xf49v9cv.js +0 -1
  154. data/public/railwatch/assets/show-SHwZjXb7.js +0 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f70fd1ff6ba5db7cfb11bff9fd076b480d6ac899c0a65694b9fbb7ce4753a10
4
- data.tar.gz: da7db6bc23129be7120f38405d522b1be212b76dd7d30435088fbd462e795028
3
+ metadata.gz: 293f64188856fdbadb54fb91bc640441a0996ba3d914d320b778c72536cbb3a8
4
+ data.tar.gz: b10e36a4cbb054e5170d1984003be84d2ea62acee2c1a3bda1c3b560a5f54bab
5
5
  SHA512:
6
- metadata.gz: 7c042d82399b79a4aeefaefd0b6a9d9af3d6868af5b4656d215115b46d84c233bbbe185c6e4692170c36c057431328f66d01ad2b4885d8b470e3cc534dbc17b1
7
- data.tar.gz: 8eb054b07c1e559362647bd125e030add13ee7b4d84d03bdd72dd1bfe3b89cdb26d98f961d01c20c682cf320570700abd6d61baca1c7263c5b3b44004d71926b
6
+ metadata.gz: 7e45fc461237f0d9aceccbd99f359b8a81a404d6620600361d3d4fd973f986baf32779ea3921f581a81527c4764687aa8771897c466498860cd8f30c53cd5e4b
7
+ data.tar.gz: 5152d7d1b250d98299e2f675a951ec6f544e0a91405365877b434e8fa6fb1cbfddf061f84fe06aca00cb9202de0092f0016c024779707e69f9d400d33117a007
data/AGENTS.md CHANGED
@@ -4,35 +4,51 @@ About *using* the `railwatch` gem from a Rails application; copy
4
4
  this into that application's repository. Index of everything else:
5
5
  [`llms.txt`](llms.txt).
6
6
 
7
- Railwatch instruments a Rails app end to end and ships linked telemetry to
7
+ Railwatch instruments a Rails app end to end and stores linked telemetry in
8
+ two SQLite databases of the app's own (embedded, the default) or sends it to
8
9
  Railwatch Cloud. Every request, job attempt, scheduled task run, and command is
9
- an **execution**; every query, cache read, log line, outgoing HTTP call, view
10
- render, exception, and span is a child of one, linked by
11
- `execution_id`/`trace_id`. It never writes to the app's database.
10
+ an **execution**; every query, cache read, log line, outgoing HTTP call, LLM
11
+ call, view render, exception, and span is a child of one, linked by
12
+ `execution_id`/`trace_id`. It never writes to the app's primary database.
12
13
 
13
14
  ## Install
14
15
 
15
16
  ```sh
16
17
  bundle add railwatch
17
- bin/rails generate railwatch:install --prompt-token --kamal-secrets
18
+ bin/rails generate railwatch:install # embedded: dashboard at /railwatch
19
+ bin/rails generate railwatch:install --prompt-token --kamal-secrets # or: Railwatch Cloud
18
20
  ```
19
21
 
20
- The generator writes `config/initializers/railwatch.rb`, mounts `Railwatch::Engine`
21
- at `/railwatch`, adds the Kamal `post-deploy` hook and the Inertia browser client
22
- where the app has them, requires `railwatch/rspec` (or `railwatch/minitest`) in the
23
- test helper, and then runs `railwatch:doctor`. A prompted/stdin/environment token
24
- goes into `.env` only when Git confirms that file is ignored; token values are
25
- never printed. Configuration lives only in that
26
- initializer; every option also has a `RAILWATCH_*` environment variable.
22
+ With no flags the install is embedded (see [Embedded mode](docs/embedded.md)):
23
+ telemetry stays in two SQLite databases the app owns (`railwatch`,
24
+ `railwatch_telemetry`, added to `config/database.yml`), Puma forks one writer
25
+ for them (`plugin :railwatch` in `config/puma.rb`), and the dashboard is served
26
+ at `/railwatch`. It is open in development and answers 401 elsewhere until
27
+ `RAILS_ENV=production bin/rails railwatch:authentication:configure` sets a
28
+ password. An exported `RAILWATCH_TOKEN` alone does not change the mode.
29
+ `--cloud`, or any of `--prompt-token`, `--token-stdin`, `--url`,
30
+ `--kamal-secrets`, sends telemetry to Railwatch Cloud instead.
31
+
32
+ Both modes write `config/initializers/railwatch.rb`, mount `Railwatch::Engine`
33
+ at `/railwatch`, add the Kamal `post-deploy` hook and the Inertia browser client
34
+ where the app has them, and require `railwatch/rspec` (or `railwatch/minitest`)
35
+ in the test helper; the cloud install then runs `railwatch:doctor`. A
36
+ prompted/stdin/environment token goes into `.env` only when Git confirms that
37
+ file is ignored; token values are never printed. Configuration lives only in
38
+ that initializer; every option also has a `RAILWATCH_*` environment variable.
39
+ An embedded install mirrors to Railwatch Cloud as well with a token and
40
+ `c.export_enabled = true`.
27
41
 
28
42
  ## Rake tasks
29
43
 
30
44
  | Task | Does |
31
45
  |---|---|
32
- | `bin/rails railwatch:doctor` | ✓/✗ per check: token, ingest URL, reachability, middleware, engine mount, deploy marker, sample rates, ignored types, Kamal hook, browser client and whether an entrypoint calls it, profiler backend, test matchers. Exits non-zero if the token is missing or the host is unreachable. **Run this first when telemetry is missing.** |
33
- | `bin/rails railwatch:token` | Where to create an ingest token for this app's platform. |
34
- | `bin/rails railwatch:mcp` | Paste-ready MCP client configuration for this app's platform. |
35
- | `bin/rails railwatch:deploy[ref,name,url]` | Records a deploy marker. Use as a release step when not deploying with Kamal. |
46
+ | `bin/rails railwatch:doctor` | ✓/✗ per check. Embedded: databases, migrations, writer process, last write, maintenance, dashboard access, export. Cloud: token, token storage, ingest URL, reachability. Both: middleware, engine mount, deploy, sample rates, ignored types, Kamal hook, browser client and whether an entrypoint calls it, profiler backend, test matchers. Exits non-zero on a missing database, pending migrations, broken export, a missing or tracked token, or an unreachable host. **Run this first when telemetry is missing.** |
47
+ | `bin/rails railwatch:authentication:configure` | Sets the embedded dashboard's HTTP Basic credentials for the current `RAILS_ENV`. |
48
+ | `bin/rails railwatch:export:status` | What the export queue holds and whether it can send. |
49
+ | `bin/rails railwatch:token` | Where to create a Railwatch Cloud ingest token for this app. |
50
+ | `bin/rails railwatch:mcp` | Paste-ready MCP client configuration for this app's Railwatch Cloud. |
51
+ | `bin/rails railwatch:deploy[ref,name,url]` | Records a deploy marker (in the embedded database, or on the cloud). Use as a release step when not deploying with Kamal. |
36
52
  | `bin/rails 'railwatch:sourcemaps[public,true]'` | Uploads Vite source maps for the configured deploy, then deletes acknowledged files. Run after building and before publishing assets. Omit `true` to retain files. See [Source maps](docs/source-maps.md). |
37
53
 
38
54
  ## Facade
@@ -73,9 +89,11 @@ Per action, in a controller class body: `railwatch_sample 0.01, only: :index`,
73
89
  ## Specs
74
90
 
75
91
  `require "railwatch/rspec"` in `spec/rails_helper.rb` (or `"railwatch/minitest"` in
76
- `test/test_helper.rb`). Railwatch must be enabled in the test env — set any
77
- non-blank `RAILWATCH_TOKEN`; records go to an in-memory transport, never over the
78
- wire. All matchers are block matchers.
92
+ `test/test_helper.rb`). Railwatch must be enabled in the test env: an embedded
93
+ install is, and needs `RAILS_ENV=test bin/rails db:prepare` once so its test
94
+ databases exist; a cloud install needs any non-blank `RAILWATCH_TOKEN`. Records
95
+ go to an in-memory transport, never over the wire. All matchers are block
96
+ matchers.
79
97
 
80
98
  ```ruby
81
99
  expect { get "/widgets" }.to have_railwatch_queries(at_most: 6) # or exactly:/at_least:
@@ -98,8 +116,8 @@ melt in production.
98
116
 
99
117
  ## MCP
100
118
 
101
- The platform is an MCP server at `<ingest host>/mcp`. Generate a personal
102
- token at Settings → Profile → "API & MCP token", then:
119
+ Railwatch Cloud (not embedded mode) is an MCP server at `<ingest host>/mcp`.
120
+ Generate a personal token at Settings → Profile → "API & MCP token", then:
103
121
 
104
122
  ```sh
105
123
  claude mcp add railwatch --transport http https://railwatch.rebulk.com/mcp \
@@ -108,10 +126,12 @@ claude mcp add railwatch --transport http https://railwatch.rebulk.com/mcp \
108
126
 
109
127
  `bin/rails railwatch:mcp` prints this and the Claude Desktop, Cursor, VS Code, and
110
128
  Zed equivalents for whichever platform the app points at. Call
111
- `list_applications` first; then `list_issues`, `get_issue`, `get_route`,
112
- `search_requests`, `get_execution`, `explain_query`, `get_profile`,
113
- `search_logs`, `release_health`, `recent_deploys`, `list_alerts`, and the
114
- `triage_issue` / `slow_route` / `daily_summary` prompts. All durations are
129
+ `list_applications` first; then `list_issues`, `get_issue` (stack frames,
130
+ cause, locals, breadcrumbs), `get_route`, `search_requests`, `get_execution`,
131
+ `explain_query`, `get_profile`, `search_logs`, `search_telemetry`,
132
+ `release_health`, `recent_deploys`, `list_alerts`, and the `triage_issue` /
133
+ `slow_route` / `daily_summary` prompts. `update_issue` and `add_comment` write,
134
+ attributed to the token's user and your agent name. All durations are
115
135
  milliseconds; these docs are served at `railwatch://docs/<name>`.
116
136
 
117
137
  ## Gotchas
data/CHANGELOG.md CHANGED
@@ -1,5 +1,152 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ <!-- Pull requests add their entry here. The version number and the date are
6
+ filled in by the release commit, which is also the only commit that
7
+ touches lib/railwatch/version.rb and Gemfile.lock. See CONTRIBUTING.md. -->
8
+
9
+ ## 0.6.1 (2026-09-23)
10
+
11
+ - The docs describe embedded as the default install throughout: two
12
+ commands, then optional Railwatch Cloud. They correct what 0.6.0 left
13
+ stale: an embedded install needs no token; the PostgreSQL and MySQL
14
+ steps; export needs a token first; a `dashboard_user` resolver names
15
+ the operator but does not gate pages; the Puma plugin line must be
16
+ unconditional; an embedded app's test databases need one
17
+ `RAILS_ENV=test bin/rails db:prepare`. MCP is marked as a Railwatch
18
+ Cloud feature. LLM calls are listed.
19
+ - The installer's embedded next steps say to get a token before turning
20
+ on export.
21
+ - Draw the Trend sparklines. Recharts pads every side of a chart by 5px,
22
+ which left the 32x10 table-cell sparkline a plot area 0px tall, so every
23
+ Trend column on every page was blank.
24
+ - LLM calls: a failed call no longer counts as "unpriced". It used no
25
+ tokens and has nothing to price, yet any failure made its model read
26
+ "+N unpriced" beside a complete spend total; the Recent table shows "–"
27
+ for it instead. The model and tool call charts label their bars ok /
28
+ failed instead of HTTP status classes, and the Recent table's Call
29
+ column truncates instead of pushing Detail off the right edge.
30
+ - Issues: the daily chart marks each deploy day with a line and names the
31
+ deploys in the tooltip, instead of printing refs that overprint when
32
+ several ship in a week.
33
+ - Jobs: the recent runs card keeps the job name readable. The exception
34
+ truncates, and the origin user and tenant columns appear from 2xl up
35
+ (the job class page shows them at every width).
36
+
37
+ ## 0.6.0 (2026-09-22)
38
+
39
+ - Embedded is now the installer's default. `bin/rails generate
40
+ railwatch:install` with no flags writes what `--local` used to: the
41
+ `railwatch` and `railwatch_telemetry` SQLite databases, `plugin
42
+ :railwatch` in `config/puma.rb`, and `c.transport = :local`, so two
43
+ commands give a working dashboard at `/railwatch` with no account and no
44
+ token. The cloud install is `--cloud`, and any option that only means
45
+ something there (`--prompt-token`, `--token-stdin`, `--url`,
46
+ `--kamal-secrets`) implies it, so existing cloud instructions keep
47
+ working. An exported `RAILWATCH_TOKEN` on its own does not pick the
48
+ cloud: a token in the shell is not a decision about where data goes.
49
+ `--local` is gone (it is the default); the gem's runtime default when no
50
+ initializer sets a transport is unchanged. The embedded next steps now
51
+ end with how to mirror to Railwatch Cloud (`c.export_enabled`).
52
+
53
+ - The embedded dashboard is open in development when HTTP Basic has no
54
+ credentials, so the first run needs no password step. Every other
55
+ environment is unchanged: closed, 401, until credentials exist. That
56
+ case now also logs a boot warning outside development and test (it was
57
+ only in the doctor), since a 401 on a deployed dashboard otherwise looks
58
+ like a broken install. Configured credentials apply in development too.
59
+
60
+ - Bound how long Puma waits for the embedded writer to stop. The plugin sent
61
+ the writer TERM and then called `Process.wait` on it, which has no timeout:
62
+ a writer that did not exit -- stuck in a SQLite write, on a full disk --
63
+ held Puma's shutdown open for as long as it stayed stuck. Measured against
64
+ a child that ignores TERM, the stop never returned (the harness gave up at
65
+ 10s with the child still alive). Puma now waits `shutdown_timeout` (2s),
66
+ the same allowance it gives its own reporter, then KILLs the writer and
67
+ reaps it, so a wedged writer costs a bounded 2s and never leaves a zombie.
68
+ A writer killed mid-batch loses nothing -- the batch is retried by id
69
+ against the next writer -- which is why the bound is not the writer's own
70
+ worst-case drain (13s at the defaults): waiting for it would buy no data,
71
+ only exit time, and that time counts against the container's stop grace.
72
+ A writer that exits on TERM is let go the moment it does (measured
73
+ ~50ms), and a pid the cluster has already reaped is still treated as gone.
74
+
75
+ - Make one HTTP attempt per delivery. `Transport::Http#deliver` retried a
76
+ raised error or a 5xx once on its own, inside a reporter that already owns
77
+ a retry ladder of eight attempts, so each rung cost two socket timeouts
78
+ and the effective attempt count was about sixteen. Against a receiver that
79
+ accepts connections and never answers, one delivery cost 6.0s; it now
80
+ costs 3.0s (one `read_timeout`), and one attempt, like `deliver_encoded`.
81
+ Response classification and `Retry-After` are unchanged: a 5xx or a raised
82
+ error still comes back retryable and the reporter still schedules it.
83
+
84
+ - Correct `docs/configuration.md` and `docs/troubleshooting.md`, which told
85
+ users to raise `buffer_size` under pressure. At the defaults the byte
86
+ ceiling (`buffer_bytes`, 16 MiB) fills at roughly 5,000 records on a
87
+ realistic mix, so the 10,000-record count is never reached and raising it
88
+ changes nothing. The setting stays; the advice now points at `buffer_bytes`.
89
+
90
+ - Say so when records are lost. `Railwatch.on_unrecoverable` fell back to
91
+ the debug log, so with no callback registered and `RAILWATCH_DEBUG` unset
92
+ a batch dropped after its retry ladder, one the receiver permanently
93
+ refused, or the records still unsent when `at_exit`'s bounded shutdown
94
+ ran out of time all vanished without a word. Since 0.3.7 that shutdown is
95
+ the only delivery a rake task or `rails runner` gets, so a cron job whose
96
+ exception never reached the platform looked exactly like one that had
97
+ nothing to report. Confirmed against a receiver that accepts and never
98
+ answers: the process left inside `shutdown_timeout` carrying three unsent
99
+ records and printed nothing.
100
+
101
+ A `Reporter::DeliveryError` -- raised only once the records are already
102
+ gone -- now prints one `[railwatch]` stderr line, and `warn_on_data_loss`
103
+ (`RAILWATCH_WARN_ON_DATA_LOSS`) defaults to **on**. Silence was the wrong
104
+ default: telemetry that disappears without a word looks exactly like
105
+ having nothing to report, which is the one failure an operator cannot
106
+ diagnose from the platform side, because the evidence is what went
107
+ missing. One line a deploy is the whole cost, and it only ever appears
108
+ when something was actually lost.
109
+
110
+ Both ways out are named in the line itself, so nobody has to find this
111
+ entry to stop it: a registered `on_unrecoverable` always wins, which is
112
+ how an app routes the loss somewhere better (`Rails.error.report`), and
113
+ `warn_on_data_loss = false` restores silence. Recovered internal errors (a
114
+ subscriber that raised, a flush that will be retried) stay debug-only
115
+ either way: the gem carried on and there is nothing for an operator to do.
116
+
117
+ - Report a lost batch outside the flush lock too. The fix above moved the
118
+ callback out of `@mutex`, the inner lock -- but `Reporter#flush` holds
119
+ `@flush_mutex` around the whole of `deliver_buffer`, and the give-up path,
120
+ the permanent-rejection path and the rescue all report from inside it. A
121
+ callback that asks this same reporter to flush (`Railwatch.flush` is public
122
+ and documented) hit the same non-reentrant `Mutex` one level out: the same
123
+ `ThreadError: deadlock; recursive locking`, rescued and hidden by
124
+ `notify_unrecoverable`, so the callback ran halfway and reported nothing.
125
+ The locked path now collects what it needs to report and `flush` hands it
126
+ over once the lock is released. Found by CodeRabbit on this pull request.
127
+
128
+ - Report a given-up batch after releasing the reporter lock, not under it.
129
+ `Reporter#retain` called `on_unrecoverable` inside `@mutex.synchronize`.
130
+ The documented callback is `Rails.error.report`, whose subscriber records
131
+ the error as an exception -- a write back into the same reporter, which
132
+ takes `@mutex` to arm its thread or request a flush. That was
133
+ `ThreadError: deadlock; recursive locking`, rescued and hidden by
134
+ `notify_unrecoverable`, so the callback died halfway and the loss it was
135
+ reporting was never seen. A callback that merely blocked held every
136
+ request thread's `write_now` and `shutdown` itself behind it for the
137
+ duration. `notify_unsent` and `delivery_rejected` already called out
138
+ unlocked; this was the one that did not.
139
+
140
+ - Investigated and left alone: `Reporter#shutdown` after `thread.join`.
141
+ The bookkeeping that follows the join (`pending_delivery`, the
142
+ once-only notify latch) takes `@mutex` for microseconds and never does
143
+ I/O -- measured 0.2ms over `shutdown_timeout` against a transport wedged
144
+ forever. The only thing that can extend it is the operator's own
145
+ callback, which runs once and is theirs to bound, as any `at_exit`
146
+ handler is. Wrapping it in `Timeout` would trade a visible cost for a
147
+ killed thread.
148
+
149
+
3
150
  ## 0.5.1 (2026-09-21)
4
151
 
5
152
  Three small seams for a host that runs these models on its own routes and
data/README.md CHANGED
@@ -6,31 +6,41 @@ outgoing HTTP, storage, views, and logs, and links them into one trace
6
6
  per execution, for about half a millisecond per request plus tens of
7
7
  microseconds per query, with zero writes to your database.
8
8
 
9
- Send that to Railwatch Cloud, or keep all of it inside the app:
10
- [embedded mode](docs/embedded.md) serves the same dashboard at
9
+ By default all of it stays inside the app:
10
+ [embedded mode](docs/embedded.md) serves the full dashboard at
11
11
  `/railwatch` out of two SQLite files your app owns, with no token, no
12
- Node, no Redis and no job worker.
12
+ Node, no Redis and no job worker. Railwatch Cloud is optional: it
13
+ delivers alerts to Slack, email, webhooks or Linear (including when the
14
+ app is down), serves MCP to your AI assistant, and puts many apps and
15
+ servers in one place.
13
16
 
14
17
  ## Install
15
18
 
16
19
  ```sh
17
- bundle add railwatch # 1. add the public gem
18
- bin/rails generate railwatch:install --prompt-token # 2. hidden token input plus app wiring
19
- bin/rails railwatch:doctor # 3. check every piece is wired up after restart
20
+ bundle add railwatch # 1. add the public gem
21
+ bin/rails generate railwatch:install # 2. embedded: databases, Puma writer, dashboard at /railwatch
20
22
  ```
21
23
 
22
- Or keep everything inside your app, with the full dashboard at
23
- `/railwatch` and no token ([Embedded mode](docs/embedded.md)):
24
+ Restart and open `/railwatch`. It is open in development; before you
25
+ deploy, give it a password with
26
+ `RAILS_ENV=production bin/rails railwatch:authentication:configure`
27
+ (it answers 401 in production until you do). On a PostgreSQL or MySQL
28
+ app without the `sqlite3` gem, the generator adds it to the Gemfile;
29
+ run `bundle install` and `bin/rails db:prepare` to finish.
30
+
31
+ To send everything to Railwatch Cloud instead, pass `--cloud` or a token
32
+ option:
24
33
 
25
34
  ```sh
26
35
  bundle add railwatch
27
- bin/rails generate railwatch:install --local
36
+ bin/rails generate railwatch:install --prompt-token # hidden token input plus app wiring
37
+ bin/rails railwatch:doctor # check every piece is wired up after restart
28
38
  ```
29
39
 
30
- Getting the token, the generator's flags, and deploying with Kamal,
40
+ The generator's flags, getting a token, and deploying with Kamal,
31
41
  Docker, Heroku, or Render are covered in
32
- [Getting started](docs/getting-started.md). For a self-hosted deployment
33
- or an unreleased revision, use the Git source instead:
42
+ [Getting started](docs/getting-started.md). For an unreleased revision,
43
+ use the Git source instead:
34
44
 
35
45
  ```ruby
36
46
  gem "railwatch", github: "Rebulk/railwatch"
@@ -46,12 +56,16 @@ gem "railwatch", github: "Rebulk/railwatch"
46
56
  ([Configuration](docs/configuration.md)).
47
57
  - An optional stack profiler through `vernier` or `stackprof`, off by
48
58
  default ([Configuration](docs/configuration.md)).
59
+ - LLM calls made through RubyLLM: tokens, cost, cut-off answers, tool
60
+ calls and workflows, each tied to the request or job that made it
61
+ ([Record types](docs/records.md#llm_call)).
49
62
  - A browser client for Inertia apps: page-visit timing, Core Web Vitals,
50
63
  and browser errors ([Getting started](docs/getting-started.md)).
51
64
  - RSpec and Minitest matchers that turn a query budget into a CI gate
52
65
  ([Testing](docs/testing.md)).
53
- - An MCP server so Claude Code, Cursor, VS Code, or Zed can read your
54
- production data ([AI assistants and MCP](docs/ai-and-mcp.md)).
66
+ - With Railwatch Cloud, an MCP server so Claude Code, Cursor, VS Code, or
67
+ Zed can read your production data
68
+ ([AI assistants and MCP](docs/ai-and-mcp.md)).
55
69
 
56
70
  Configuration lives in `config/initializers/railwatch.rb`; most options
57
71
  also have a `RAILWATCH_*` environment variable.
@@ -73,11 +87,11 @@ expect { get "/widgets" }.not_to have_railwatch_n_plus_one
73
87
 
74
88
  ## Embedded mode
75
89
 
76
- `--local` keeps everything inside the application. Telemetry goes to two
77
- SQLite files it owns -- `railwatch` for issues, comments and saved views,
78
- `railwatch_telemetry` for what the app reports -- and the dashboard is
79
- served at `/railwatch` from a bundle shipped inside the gem. Nothing
80
- leaves the machine, and there is nothing else to run.
90
+ Telemetry goes to two SQLite files the app owns -- `railwatch` for
91
+ issues, comments and saved views, `railwatch_telemetry` for what the app
92
+ reports -- and the dashboard is served at `/railwatch` from a bundle
93
+ shipped inside the gem. Nothing leaves the machine unless you turn on
94
+ export to Railwatch Cloud, and there is nothing else to run.
81
95
 
82
96
  Puma forks a single writer process (`plugin :railwatch`, which the
83
97
  installer adds) that owns both files. The web workers hand it batches
@@ -86,18 +100,21 @@ runs the maintenance clock too, so exception grouping, rollups, retention
86
100
  and threshold scans happen without a queue.
87
101
 
88
102
  That dashboard reads every query, log line and exception the app
89
- produced, so it is closed by default the way Mission Control Jobs is:
90
- HTTP Basic is on with no credentials, and every request is 401 until you
91
- set them with `bin/rails railwatch:authentication:configure`. Apps that
92
- would rather use their own session hand it a `dashboard_user` resolver
93
- instead. [Embedded mode](docs/embedded.md) covers all of it, including
94
- upgrades and what it costs to store.
103
+ produced, so it is gated the way Mission Control Jobs is: HTTP Basic is
104
+ on, and with no credentials every request is 401 until you set them
105
+ with `bin/rails railwatch:authentication:configure`. Development is the
106
+ exception: with no credentials set there, it is open. Apps that would
107
+ rather use their own authentication turn Basic off and gate the pages
108
+ with `base_controller_class` or a routes constraint around the mount; a
109
+ `dashboard_user` resolver then names the operator and authorizes live
110
+ updates, but does not gate pages. [Embedded mode](docs/embedded.md)
111
+ covers all of it, including upgrades and what it costs to store.
95
112
 
96
113
  ## Documentation
97
114
 
98
- - [Getting started](docs/getting-started.md) — five-minute install for a
99
- Rails 8 app, the three optional lines, and deploying with Kamal, Docker,
100
- Heroku, Render, or none of them.
115
+ - [Getting started](docs/getting-started.md) — the embedded install, the
116
+ Railwatch Cloud install and its token, the three optional lines, and
117
+ deploying with Kamal, Docker, Heroku, Render, or none of them.
101
118
  - [Configuration](docs/configuration.md) — every option and `RAILWATCH_*`
102
119
  variable, field by field.
103
120
  - [Record types](docs/records.md) — every record Railwatch ships and every
@@ -105,13 +122,15 @@ upgrades and what it costs to store.
105
122
  - [Testing](docs/testing.md) — the RSpec and Minitest matchers, and a CI
106
123
  performance gate.
107
124
  - [AI assistants and MCP](docs/ai-and-mcp.md) — connecting Claude Code,
108
- Cursor, VS Code, or Zed to your production data.
125
+ Cursor, VS Code, or Zed to your production data through Railwatch
126
+ Cloud.
109
127
  - [Replacing Sentry](docs/replacing-sentry.md) — a step-by-step migration,
110
128
  option by option and call site by call site.
111
129
  - [Coming from Laravel Nightwatch](docs/replacing-nightwatch.md) — the
112
130
  record-type mapping and the sampling model, for Laravel people.
113
- - [Embedded mode](docs/embedded.md) — the whole dashboard inside your
114
- app, telemetry in your own SQLite files, no cloud.
131
+ - [Embedded mode](docs/embedded.md) — the default: the whole dashboard
132
+ inside your app, telemetry in your own SQLite files, and optional
133
+ export to Railwatch Cloud.
115
134
  - [Self-hosting](docs/self-hosting.md) — pointing the gem at your own
116
135
  Railwatch Cloud.
117
136
  - [Troubleshooting](docs/troubleshooting.md) — every failure mode, paired
@@ -47,6 +47,7 @@ module Railwatch
47
47
  def authenticate_by_http_basic
48
48
  config = Railwatch.config
49
49
  return unless config.http_basic_auth_enabled
50
+ return if config.http_basic_auth_waived?
50
51
 
51
52
  if config.http_basic_auth_configured?
52
53
  http_basic_authenticate_or_request_with(name: config.http_basic_auth_user, password: config.http_basic_auth_password,
@@ -118,7 +118,9 @@ module Railwatch
118
118
  cache_read_tokens: group.sum { |r| r.cache_read_tokens.to_i }, cache_write_tokens: group.sum { |r| r.cache_write_tokens.to_i },
119
119
  cost_nanos: group.sum { |r| r.cost_nanos.to_i },
120
120
  priced: group.count { |r| r.cost_nanos },
121
- unpriced: group.count { |r| r.cost_nanos.nil? },
121
+ # A call that failed before the provider answered used no tokens,
122
+ # so there is nothing to price; only an answered call can be unpriced.
123
+ unpriced: group.count { |r| r.cost_nanos.nil? && r.status != "failed" },
122
124
  # A cut-off answer is not an error and will never show in the error
123
125
  # rate, so it needs counting on its own or it stays invisible.
124
126
  truncated: group.count { |r| r.finish_reason == "max_tokens" },
@@ -15,7 +15,7 @@ module Railwatch
15
15
  # No `railwatch` entry in this environment's database.yml, or an entry
16
16
  # whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
17
17
  # app has none and still eager-loads this class in production, and so
18
- # does the --local installer's own boot, before it has written the
18
+ # does the installer's own boot, before it has written the
19
19
  # entry -- so loading must not raise. Using it must, though: without
20
20
  # connects_to this class would inherit ActiveRecord::Base's PRIMARY
21
21
  # connection, and its tables are unprefixed, so a query would read and
@@ -25,7 +25,7 @@ module Railwatch
25
25
  def self.connection_pool
26
26
  raise Railwatch::DatabaseNotConfigured,
27
27
  "the `railwatch` database (issues, comments, saved views and deploys) is not configured for the " \
28
- "#{Rails.env} environment; run `bin/rails generate railwatch:install --local` " \
28
+ "#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
29
29
  "or add it to config/database.yml (docs/embedded.md)"
30
30
  end
31
31
  end
@@ -160,7 +160,7 @@ module Railwatch
160
160
  extra["cache_write_tokens"] += row[:cache_write_tokens].to_i
161
161
  extra["cost_nanos"] += row[:cost_nanos].to_i
162
162
  extra["priced"] += 1 if row[:cost_nanos]
163
- extra["unpriced"] += 1 if row[:cost_nanos].nil?
163
+ extra["unpriced"] += 1 if row[:cost_nanos].nil? && row[:status] != "failed"
164
164
  extra["truncated"] += 1 if row[:finish_reason] == "max_tokens"
165
165
  extra["with_attachments"] += 1 if row[:attachments].to_i.positive?
166
166
  end
@@ -18,7 +18,7 @@ module Railwatch
18
18
  # No `railwatch_telemetry` entry in this environment's database.yml, or an entry
19
19
  # whose adapter gem is not in the bundle yet (LoadError). A cloud-transport
20
20
  # app has none and still eager-loads this class in production, and so
21
- # does the --local installer's own boot, before it has written the
21
+ # does the installer's own boot, before it has written the
22
22
  # entry -- so loading must not raise. Using it must, though: without
23
23
  # connects_to this class would inherit ActiveRecord::Base's PRIMARY
24
24
  # connection, and its tables are unprefixed, so a query would read and
@@ -28,7 +28,7 @@ module Railwatch
28
28
  def self.connection_pool
29
29
  raise Railwatch::DatabaseNotConfigured,
30
30
  "the `railwatch_telemetry` database (everything the app reports) is not configured for the " \
31
- "#{Rails.env} environment; run `bin/rails generate railwatch:install --local` " \
31
+ "#{Rails.env} environment; run `bin/rails generate railwatch:install` " \
32
32
  "or add it to config/database.yml (docs/embedded.md)"
33
33
  end
34
34
  end
data/docs/ai-and-mcp.md CHANGED
@@ -9,6 +9,10 @@ crash-free rates per release. It can also write — resolve an issue, set a
9
9
  priority, leave a comment — and every write is signed with your user and the
10
10
  agent's name in the issue's activity feed.
11
11
 
12
+ MCP is a Railwatch Cloud feature. The embedded dashboard at `/railwatch`
13
+ does not serve it; an embedded install gets it by
14
+ [exporting to the cloud](embedded.md#three-ways-to-run-it).
15
+
12
16
  The endpoint is `<your ingest host>/mcp`. For the hosted platform that's
13
17
  `https://railwatch.rebulk.com/mcp`; if you self-host, it is your own host (see
14
18
  [`self-hosting.md`](self-hosting.md)). The gem knows which one you're on:
@@ -145,9 +149,9 @@ durations are milliseconds.** Every `window` argument takes `1h`, `6h`,
145
149
 
146
150
  | Tool | Arguments | Returns |
147
151
  |---|---|---|
148
- | `list_applications` | — | applications, their account, and their environments. Start here — everything else takes the `application_slug` and `environment` this returns. |
152
+ | `list_applications` | — | applications, their account, and their environments. Start here — everything else takes the `application_slug` and `environment` this returns (plus `account_slug`/`application_id` when two accounts share a slug). |
149
153
  | `list_issues` | `application_slug?`, `environment?`, `status?` (`open`/`resolved`/`ignored`, default `open`), `limit?` | issues with key, title, kind, status, priority, occurrence and affected-user counts, culprit, first/last seen. |
150
- | `get_issue` | `key` | one issue plus the sample occurrence: exception class, message, stack frames, and the execution it happened inside. |
154
+ | `get_issue` | `key` or `issue_id` | one issue. For an exception, the sample and latest occurrences with stored stack frames, cause, locals, parsed context, browser breadcrumbs, and the execution it happened inside. For a performance or anomaly issue, the metric, limit or baseline, representative records, trend, and deploy comparison. |
151
155
  | `update_issue` | `key`, `status?`, `priority?`, `assignee_email?`, `agent?` | the updated issue. Writes an activity entry attributed to your user and `agent`. |
152
156
  | `add_comment` | `key`, `body`, `agent?` | the created comment, attributed the same way. |
153
157
  | `list_slow_routes` | `application_slug`, `environment`, `window?` | the 20 slowest routes by p95, each with `group_hash`, count, errors, `p95_ms`. |
@@ -157,6 +161,7 @@ durations are milliseconds.** Every `window` argument takes `1h`, `6h`,
157
161
  | `explain_query` | `application_slug`, `environment`, `group_hash`, `window?` | the stored query plan for a query group, with the SQL and the sample's duration. `explain` is null unless the app sets `RAILWATCH_CAPTURE_QUERY_EXPLAIN`. `sql` is the normalized shape unless the app also sets `RAILWATCH_CAPTURE_SQL_VALUES`. |
158
162
  | `get_profile` | `application_slug`, `environment`, `profile_id?`, `execution_id?`, `limit?` | the hottest frames of a stack profile — self and total samples, each with a percentage. |
159
163
  | `search_logs` | `application_slug`, `environment`, `q`, `level?`, `limit?` | matching log lines, each with the `execution_id` to expand with `get_execution`. |
164
+ | `search_telemetry` | `application_slug`, `environment`, `resource` (`jobs`/`exceptions`/`logs`/`queries`), `q?`, `window?`, `cursor?`, `limit?` | raw rows of that resource with cursor pagination (`meta.next_cursor`, `meta.has_more`). `q` takes `after:`, `before:`, `user:`, `tenant:`, `deploy:`, plus per-resource keys such as `class:`, `queue:`, `level:`. |
160
165
  | `list_tenants` | `application_slug`, `environment`, `window?`, `q?` | your app's own tenants (whatever it passes to `Railwatch.context(tenant:)`) with request, error, job, exception, and user counts. |
161
166
  | `recent_deploys` | `application_slug`, `environment` | the 20 most recent deploys with ref, name, time, and link. |
162
167
  | `release_health` | `application_slug`, `environment`, `window?` | crash-free session rate, crash-free user rate, and adoption per release. |
@@ -220,7 +225,8 @@ Copy either into your own app's repo to give its agent the same context.
220
225
 
221
226
  ## See also
222
227
 
223
- - [`getting-started.md`](getting-started.md) — install, token, first request.
228
+ - [`getting-started.md`](getting-started.md) — install, the cloud token,
229
+ first request.
224
230
  - [`self-hosting.md`](self-hosting.md) — pointing the gem, and this endpoint,
225
231
  at your own platform.
226
232
  - [`troubleshooting.md`](troubleshooting.md) — when something isn't