railwatch 0.5.1 → 0.6.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.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/AGENTS.md +6 -2
  3. data/CHANGELOG.md +119 -0
  4. data/README.md +21 -13
  5. data/app/controllers/railwatch/dashboard_controller.rb +1 -0
  6. data/app/models/railwatch/application_record.rb +2 -2
  7. data/app/models/railwatch/telemetry_record.rb +2 -2
  8. data/docs/configuration.md +29 -13
  9. data/docs/embedded.md +32 -12
  10. data/docs/faq.md +6 -5
  11. data/docs/getting-started.md +9 -5
  12. data/docs/troubleshooting.md +10 -6
  13. data/lib/generators/railwatch/install/install_generator.rb +31 -16
  14. data/lib/generators/railwatch/install/templates/initializer.rb.tt +4 -4
  15. data/lib/puma/plugin/railwatch.rb +48 -3
  16. data/lib/railwatch/configuration.rb +16 -1
  17. data/lib/railwatch/engine.rb +14 -3
  18. data/lib/railwatch/reporter.rb +52 -16
  19. data/lib/railwatch/transport/http.rb +4 -9
  20. data/lib/railwatch/version.rb +1 -1
  21. data/lib/railwatch.rb +23 -1
  22. data/lib/tasks/railwatch_tasks.rake +11 -4
  23. data/llms.txt +8 -4
  24. data/public/railwatch/assets/{app-layout-DDyQa72H.js → app-layout-2xAKndVD.js} +1 -1
  25. data/public/railwatch/assets/{app-wordmark-o9CODKP0.js → app-wordmark-Bt6XqURo.js} +1 -1
  26. data/public/railwatch/assets/{appearance-BwuCXabr.js → appearance-DyCfeh9o.js} +1 -1
  27. data/public/railwatch/assets/{arrow-up-C6PxDiY3.js → arrow-up-EmePsTKP.js} +1 -1
  28. data/public/railwatch/assets/{auth-layout-BRt8MGFD.js → auth-layout-BuwogWMZ.js} +1 -1
  29. data/public/railwatch/assets/{badge-CAxXV8za.js → badge-Dv7I-ebP.js} +1 -1
  30. data/public/railwatch/assets/{braces-DgomTCNf.js → braces-iZ7MEfYP.js} +1 -1
  31. data/public/railwatch/assets/{card-cAtqCxWl.js → card-DqdEEKcJ.js} +1 -1
  32. data/public/railwatch/assets/{chart-BBeBkkNa.js → chart-C4k7Ub9W.js} +1 -1
  33. data/public/railwatch/assets/{chart-hover-B1M9jc0y.js → chart-hover-2yZG3w2u.js} +1 -1
  34. data/public/railwatch/assets/{chart-panel-DUQTz_C8.js → chart-panel-CONkZcn9.js} +1 -1
  35. data/public/railwatch/assets/{checkbox-CmhMHWZO.js → checkbox-DKbThbxr.js} +1 -1
  36. data/public/railwatch/assets/{code-DESvxyTj.js → code-CYahYQHZ.js} +1 -1
  37. data/public/railwatch/assets/{copy-block-BkSU5832.js → copy-block-CbJum9s8.js} +1 -1
  38. data/public/railwatch/assets/{copy-id-D03GhN9F.js → copy-id-JmvLfkmx.js} +1 -1
  39. data/public/railwatch/assets/{cursor-load-more-CRyuMeQb.js → cursor-load-more-C1ylbqqL.js} +1 -1
  40. data/public/railwatch/assets/{data-table-BIlt7Rtm.js → data-table-CVKR2wkq.js} +1 -1
  41. data/public/railwatch/assets/{edit-DJ0D0wHN.js → edit-BXF5oNYA.js} +1 -1
  42. data/public/railwatch/assets/{edit-O0NSBWxo.js → edit-BwSEVrTG.js} +1 -1
  43. data/public/railwatch/assets/{edit-Bb6MKoe4.js → edit-D5BRDwLT.js} +1 -1
  44. data/public/railwatch/assets/{empty-state-C38il627.js → empty-state-Bn48Gplm.js} +1 -1
  45. data/public/railwatch/assets/{env-layout-REF7OM4q.js → env-layout-CKLL8Ds7.js} +1 -1
  46. data/public/railwatch/assets/{execution-path-FYLq1TwC.js → execution-path-DWovhgLi.js} +1 -1
  47. data/public/railwatch/assets/{filter-bar-CYog9Alp.js → filter-bar-xcbJC93z.js} +1 -1
  48. data/public/railwatch/assets/{flamegraph-DSs69foN.js → flamegraph-BNbNIuKT.js} +1 -1
  49. data/public/railwatch/assets/{frames-BUi2J5Mk.js → frames-Bk39xfJG.js} +1 -1
  50. data/public/railwatch/assets/{google-sign-in-button-BQiIKFdd.js → google-sign-in-button-B5as2ps1.js} +1 -1
  51. data/public/railwatch/assets/{index-QpTtwFwu.js → index-Atp1FTgX.js} +1 -1
  52. data/public/railwatch/assets/{index-C-PmdhXA.js → index-B-FXAo1w.js} +1 -1
  53. data/public/railwatch/assets/{index-DqTFTP8p.js → index-B0xgzHuN.js} +1 -1
  54. data/public/railwatch/assets/{index-so4lRrRq.js → index-BDVShWMs.js} +1 -1
  55. data/public/railwatch/assets/{index-C_upSl_k.js → index-BRvRkfob.js} +1 -1
  56. data/public/railwatch/assets/{index-CsoN51vW.js → index-BVWapDeB.js} +1 -1
  57. data/public/railwatch/assets/{index-CrZ3vHDL.js → index-Bd4YpOIO.js} +1 -1
  58. data/public/railwatch/assets/{index-BaR1U9An.js → index-BeQw69GM.js} +1 -1
  59. data/public/railwatch/assets/{index-Dh4IRLFI.js → index-BfitxVBv.js} +1 -1
  60. data/public/railwatch/assets/{index-umIAl-pL.js → index-C-EwH4lO.js} +1 -1
  61. data/public/railwatch/assets/{index-CICUIFHL.js → index-C8JIdb3_.js} +1 -1
  62. data/public/railwatch/assets/{index-CFFpnzIS.js → index-CBFCtJAR.js} +1 -1
  63. data/public/railwatch/assets/{index-tpz-OGUP.js → index-CDXlITyG.js} +1 -1
  64. data/public/railwatch/assets/{index-DW2CBbxU.js → index-COd9lT84.js} +1 -1
  65. data/public/railwatch/assets/{index-FhUaPPab.js → index-C_hQmVTP.js} +1 -1
  66. data/public/railwatch/assets/{index-JdCVBrw8.js → index-CcennT28.js} +1 -1
  67. data/public/railwatch/assets/{index-DrcKVG2f.js → index-CiaeS-n7.js} +1 -1
  68. data/public/railwatch/assets/{index-BoUBioBP.js → index-CrA7jBTK.js} +1 -1
  69. data/public/railwatch/assets/{index-C3A_9imx.js → index-D0mKj3Hg.js} +1 -1
  70. data/public/railwatch/assets/{index-BiiyMcA0.js → index-DV5MdmpY.js} +1 -1
  71. data/public/railwatch/assets/{index-CiPo4Gob.js → index-DaE-1xhx.js} +1 -1
  72. data/public/railwatch/assets/{index-CpkI015n.js → index-DdVg9LKX.js} +1 -1
  73. data/public/railwatch/assets/{index-DvjY3dPD.js → index-DeLtMWou.js} +1 -1
  74. data/public/railwatch/assets/{index-CFRLPs4J.js → index-DiQ4mqT_.js} +1 -1
  75. data/public/railwatch/assets/{index-DtHmuB9Q.js → index-LKgpCzoR.js} +1 -1
  76. data/public/railwatch/assets/{index-BeOh2t_S.js → index-S0NVaLj9.js} +1 -1
  77. data/public/railwatch/assets/{index-C7OtLq_3.js → index-Vj93BTR5.js} +1 -1
  78. data/public/railwatch/assets/{index-ZOGOB8SA.js → index-XjXcV-EO.js} +1 -1
  79. data/public/railwatch/assets/{index-ZSZg9rtq.js → index-Zkt4W_CM.js} +1 -1
  80. data/public/railwatch/assets/{index-8-hnAhOD.js → index-aPCZX-pW.js} +1 -1
  81. data/public/railwatch/assets/{index-DDI_Zx5V.js → index-cTzBycCg.js} +1 -1
  82. data/public/railwatch/assets/{index-sTYvcbkh.js → index-dpG-19ZN.js} +1 -1
  83. data/public/railwatch/assets/{index-r0tSIplE.js → index-gCvuyG1l.js} +1 -1
  84. data/public/railwatch/assets/{index-DSvlZVWG.js → index-o1xqGvLo.js} +1 -1
  85. data/public/railwatch/assets/{index-CGs4m_fa.js → index-w_skgMmK.js} +1 -1
  86. data/public/railwatch/assets/{inertia-DLew8ZNx.js → inertia-CqnzqPVD.js} +2 -2
  87. data/public/railwatch/assets/{input-error-cvM6_Jht.js → input-error-ByT28UdP.js} +1 -1
  88. data/public/railwatch/assets/{json-viewer-D922McGi.js → json-viewer-Bnu9RsJL.js} +1 -1
  89. data/public/railwatch/assets/{klass-CrwICqN8.js → klass-nLiBV-Az.js} +1 -1
  90. data/public/railwatch/assets/{label-GWl7I6sf.js → label-1nSzpSB6.js} +1 -1
  91. data/public/railwatch/assets/{layout-0ZAnD3zl.js → layout-D7lE1Doo.js} +1 -1
  92. data/public/railwatch/assets/{live-dot-D1n_BreY.js → live-dot-UdzOfzJb.js} +1 -1
  93. data/public/railwatch/assets/{nav-DPxr1NNC.js → nav-BRCgp2w3.js} +1 -1
  94. data/public/railwatch/assets/{new-DHAHDrN7.js → new-C60G72DR.js} +1 -1
  95. data/public/railwatch/assets/{new-D-ZzUK9a.js → new-COyDKOOb.js} +1 -1
  96. data/public/railwatch/assets/{new-DEVkYv-z.js → new-Cd9zPNJ8.js} +1 -1
  97. data/public/railwatch/assets/{new-Dz4lZf1L.js → new-DMJGKjmH.js} +1 -1
  98. data/public/railwatch/assets/{new-GMrRFurX.js → new-Omvf-c9s.js} +1 -1
  99. data/public/railwatch/assets/{new-Cdl6pqST.js → new-vjRzERL3.js} +1 -1
  100. data/public/railwatch/assets/onboarding-gUjJQc-6.js +1 -0
  101. data/public/railwatch/assets/{origin-identity-Bk9yHWZ1.js → origin-identity-M1YUPYS0.js} +1 -1
  102. data/public/railwatch/assets/{percentile-picker-DfSx9yJO.js → percentile-picker-BxIVblBw.js} +1 -1
  103. data/public/railwatch/assets/{relative-time-CjIjb8Lg.js → relative-time-DQ4kjowZ.js} +1 -1
  104. data/public/railwatch/assets/{release-health-4b3tivEf.js → release-health-Cb1tMOOO.js} +1 -1
  105. data/public/railwatch/assets/{route-C_5BUtHK.js → route-BJkoEm7r.js} +1 -1
  106. data/public/railwatch/assets/{segmented-BgbT3wZa.js → segmented-C4oWE73o.js} +1 -1
  107. data/public/railwatch/assets/{select-DmunxCKE.js → select-ocdQJdQo.js} +1 -1
  108. data/public/railwatch/assets/{separator-BXzEdZ_8.js → separator-Ct2UcZ2x.js} +1 -1
  109. data/public/railwatch/assets/{series-chart-Xf49v9cv.js → series-chart-D6upsrSn.js} +1 -1
  110. data/public/railwatch/assets/{show-DlRVS18-.js → show-B4bwI9x2.js} +1 -1
  111. data/public/railwatch/assets/{show-DnR1Dnjd.js → show-BAjfddEn.js} +1 -1
  112. data/public/railwatch/assets/{show-mU38uGTg.js → show-BLJOyLb_.js} +1 -1
  113. data/public/railwatch/assets/{show-DYskfl3-.js → show-BoVyMQS6.js} +1 -1
  114. data/public/railwatch/assets/{show-DXs4deaC.js → show-Bsu1rvnK.js} +1 -1
  115. data/public/railwatch/assets/{show-Dn-GwFZL.js → show-C6OiJ6Hr.js} +1 -1
  116. data/public/railwatch/assets/{show-CAl7xcex.js → show-C8TkpEgm.js} +1 -1
  117. data/public/railwatch/assets/{show-BLpWUHWD.js → show-CBFBiBY2.js} +1 -1
  118. data/public/railwatch/assets/{show-Y74rM0VT.js → show-CEhPVMMT.js} +1 -1
  119. data/public/railwatch/assets/{show-SHwZjXb7.js → show-CV-lG2b6.js} +1 -1
  120. data/public/railwatch/assets/{show-Dily73Xk.js → show-CX_pab9S.js} +1 -1
  121. data/public/railwatch/assets/{show-DcpTFiLi.js → show-D6lgzL6n.js} +1 -1
  122. data/public/railwatch/assets/{show-vQ4bndYD.js → show-DDVsv-uS.js} +1 -1
  123. data/public/railwatch/assets/{show-SvLOcPrx.js → show-Dwiq-8hA.js} +1 -1
  124. data/public/railwatch/assets/{show-B2zLAW83.js → show-eKazflRI.js} +1 -1
  125. data/public/railwatch/assets/{show-DSP9Cq_C.js → show-gtwKsTsl.js} +1 -1
  126. data/public/railwatch/assets/{show-DI8IhNUH.js → show-yEOrk6Pt.js} +1 -1
  127. data/public/railwatch/assets/{sort-header-Dcq9bzmo.js → sort-header-CPM6fhXv.js} +1 -1
  128. data/public/railwatch/assets/{sparkline-cell-BON3qQUB.js → sparkline-cell-CXCxXPWt.js} +1 -1
  129. data/public/railwatch/assets/{stat-DFEyFxkO.js → stat-BNEfE8X1.js} +1 -1
  130. data/public/railwatch/assets/{status-badge-BaUKP7Yo.js → status-badge-D0aylrM5.js} +1 -1
  131. data/public/railwatch/assets/{tenant-path-DPZPc985.js → tenant-path-C4aM_cO0.js} +1 -1
  132. data/public/railwatch/assets/{text-link-BO77t9Xk.js → text-link-CP4lqUi6.js} +1 -1
  133. data/public/railwatch/assets/{textarea-DTqrCiV0.js → textarea-DzJo2ds4.js} +1 -1
  134. data/public/railwatch/assets/{timeline-D5rJ0es2.js → timeline-BNcJ9PMc.js} +1 -1
  135. data/public/railwatch/assets/{transition-DMIrZVth.js → transition-BX2M1iaq.js} +1 -1
  136. data/public/railwatch/assets/{use-clipboard-ByoUGQqA.js → use-clipboard-C1ApSwzA.js} +1 -1
  137. data/public/railwatch/assets/{use-live-D7xKz2ma.js → use-live-DWwLg1Nj.js} +1 -1
  138. data/public/railwatch/manifest.json +1283 -1283
  139. metadata +115 -115
  140. data/public/railwatch/assets/onboarding-D1vwaHYT.js +0 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6f70fd1ff6ba5db7cfb11bff9fd076b480d6ac899c0a65694b9fbb7ce4753a10
4
- data.tar.gz: da7db6bc23129be7120f38405d522b1be212b76dd7d30435088fbd462e795028
3
+ metadata.gz: 6da9b1e629335b1623cfc24e8b18c5a5d17bed076630768b9d947ff2d0b1a5c2
4
+ data.tar.gz: 827e8cc13fa4e1373bd6656c851e7fabbdfe2af60c37432c9feeb10801972367
5
5
  SHA512:
6
- metadata.gz: 7c042d82399b79a4aeefaefd0b6a9d9af3d6868af5b4656d215115b46d84c233bbbe185c6e4692170c36c057431328f66d01ad2b4885d8b470e3cc534dbc17b1
7
- data.tar.gz: 8eb054b07c1e559362647bd125e030add13ee7b4d84d03bdd72dd1bfe3b89cdb26d98f961d01c20c682cf320570700abd6d61baca1c7263c5b3b44004d71926b
6
+ metadata.gz: 741964c31992572e63aeabb9a601482d4a4facb6f895ac1c3fe362c9e0f493f7e7b7636b070bc0909b764e38514495f838e8dbda9de5fef8423cd978fc833281
7
+ data.tar.gz: 2eb78e494a27b97451023876f6e72e1cb73bf4aebacb002af148d674841a3f62348c2992cd131d16f7cc75a6308203d68da2738b1431791c4a15fd134b7d3fc4
data/AGENTS.md CHANGED
@@ -14,10 +14,14 @@ render, exception, and span is a child of one, linked by
14
14
 
15
15
  ```sh
16
16
  bundle add railwatch
17
- bin/rails generate railwatch:install --prompt-token --kamal-secrets
17
+ bin/rails generate railwatch:install # embedded: dashboard at /railwatch
18
+ bin/rails generate railwatch:install --prompt-token --kamal-secrets # or: Railwatch Cloud
18
19
  ```
19
20
 
20
- The generator writes `config/initializers/railwatch.rb`, mounts `Railwatch::Engine`
21
+ With no flags the install is embedded (see [Embedded mode](docs/embedded.md)):
22
+ telemetry stays in two SQLite databases the app owns and the dashboard is served
23
+ at `/railwatch`. `--cloud`, or any of `--prompt-token`, `--token-stdin`, `--url`,
24
+ `--kamal-secrets`, sends it to Railwatch Cloud instead. For the cloud install the generator writes `config/initializers/railwatch.rb`, mounts `Railwatch::Engine`
21
25
  at `/railwatch`, adds the Kamal `post-deploy` hook and the Inertia browser client
22
26
  where the app has them, requires `railwatch/rspec` (or `railwatch/minitest`) in the
23
27
  test helper, and then runs `railwatch:doctor`. A prompted/stdin/environment token
data/CHANGELOG.md CHANGED
@@ -1,5 +1,124 @@
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.0 (2026-09-22)
10
+
11
+ - Embedded is now the installer's default. `bin/rails generate
12
+ railwatch:install` with no flags writes what `--local` used to: the
13
+ `railwatch` and `railwatch_telemetry` SQLite databases, `plugin
14
+ :railwatch` in `config/puma.rb`, and `c.transport = :local`, so two
15
+ commands give a working dashboard at `/railwatch` with no account and no
16
+ token. The cloud install is `--cloud`, and any option that only means
17
+ something there (`--prompt-token`, `--token-stdin`, `--url`,
18
+ `--kamal-secrets`) implies it, so existing cloud instructions keep
19
+ working. An exported `RAILWATCH_TOKEN` on its own does not pick the
20
+ cloud: a token in the shell is not a decision about where data goes.
21
+ `--local` is gone (it is the default); the gem's runtime default when no
22
+ initializer sets a transport is unchanged. The embedded next steps now
23
+ end with how to mirror to Railwatch Cloud (`c.export_enabled`).
24
+
25
+ - The embedded dashboard is open in development when HTTP Basic has no
26
+ credentials, so the first run needs no password step. Every other
27
+ environment is unchanged: closed, 401, until credentials exist. That
28
+ case now also logs a boot warning outside development and test (it was
29
+ only in the doctor), since a 401 on a deployed dashboard otherwise looks
30
+ like a broken install. Configured credentials apply in development too.
31
+
32
+ - Bound how long Puma waits for the embedded writer to stop. The plugin sent
33
+ the writer TERM and then called `Process.wait` on it, which has no timeout:
34
+ a writer that did not exit -- stuck in a SQLite write, on a full disk --
35
+ held Puma's shutdown open for as long as it stayed stuck. Measured against
36
+ a child that ignores TERM, the stop never returned (the harness gave up at
37
+ 10s with the child still alive). Puma now waits `shutdown_timeout` (2s),
38
+ the same allowance it gives its own reporter, then KILLs the writer and
39
+ reaps it, so a wedged writer costs a bounded 2s and never leaves a zombie.
40
+ A writer killed mid-batch loses nothing -- the batch is retried by id
41
+ against the next writer -- which is why the bound is not the writer's own
42
+ worst-case drain (13s at the defaults): waiting for it would buy no data,
43
+ only exit time, and that time counts against the container's stop grace.
44
+ A writer that exits on TERM is let go the moment it does (measured
45
+ ~50ms), and a pid the cluster has already reaped is still treated as gone.
46
+
47
+ - Make one HTTP attempt per delivery. `Transport::Http#deliver` retried a
48
+ raised error or a 5xx once on its own, inside a reporter that already owns
49
+ a retry ladder of eight attempts, so each rung cost two socket timeouts
50
+ and the effective attempt count was about sixteen. Against a receiver that
51
+ accepts connections and never answers, one delivery cost 6.0s; it now
52
+ costs 3.0s (one `read_timeout`), and one attempt, like `deliver_encoded`.
53
+ Response classification and `Retry-After` are unchanged: a 5xx or a raised
54
+ error still comes back retryable and the reporter still schedules it.
55
+
56
+ - Correct `docs/configuration.md` and `docs/troubleshooting.md`, which told
57
+ users to raise `buffer_size` under pressure. At the defaults the byte
58
+ ceiling (`buffer_bytes`, 16 MiB) fills at roughly 5,000 records on a
59
+ realistic mix, so the 10,000-record count is never reached and raising it
60
+ changes nothing. The setting stays; the advice now points at `buffer_bytes`.
61
+
62
+ - Say so when records are lost. `Railwatch.on_unrecoverable` fell back to
63
+ the debug log, so with no callback registered and `RAILWATCH_DEBUG` unset
64
+ a batch dropped after its retry ladder, one the receiver permanently
65
+ refused, or the records still unsent when `at_exit`'s bounded shutdown
66
+ ran out of time all vanished without a word. Since 0.3.7 that shutdown is
67
+ the only delivery a rake task or `rails runner` gets, so a cron job whose
68
+ exception never reached the platform looked exactly like one that had
69
+ nothing to report. Confirmed against a receiver that accepts and never
70
+ answers: the process left inside `shutdown_timeout` carrying three unsent
71
+ records and printed nothing.
72
+
73
+ A `Reporter::DeliveryError` -- raised only once the records are already
74
+ gone -- now prints one `[railwatch]` stderr line, and `warn_on_data_loss`
75
+ (`RAILWATCH_WARN_ON_DATA_LOSS`) defaults to **on**. Silence was the wrong
76
+ default: telemetry that disappears without a word looks exactly like
77
+ having nothing to report, which is the one failure an operator cannot
78
+ diagnose from the platform side, because the evidence is what went
79
+ missing. One line a deploy is the whole cost, and it only ever appears
80
+ when something was actually lost.
81
+
82
+ Both ways out are named in the line itself, so nobody has to find this
83
+ entry to stop it: a registered `on_unrecoverable` always wins, which is
84
+ how an app routes the loss somewhere better (`Rails.error.report`), and
85
+ `warn_on_data_loss = false` restores silence. Recovered internal errors (a
86
+ subscriber that raised, a flush that will be retried) stay debug-only
87
+ either way: the gem carried on and there is nothing for an operator to do.
88
+
89
+ - Report a lost batch outside the flush lock too. The fix above moved the
90
+ callback out of `@mutex`, the inner lock -- but `Reporter#flush` holds
91
+ `@flush_mutex` around the whole of `deliver_buffer`, and the give-up path,
92
+ the permanent-rejection path and the rescue all report from inside it. A
93
+ callback that asks this same reporter to flush (`Railwatch.flush` is public
94
+ and documented) hit the same non-reentrant `Mutex` one level out: the same
95
+ `ThreadError: deadlock; recursive locking`, rescued and hidden by
96
+ `notify_unrecoverable`, so the callback ran halfway and reported nothing.
97
+ The locked path now collects what it needs to report and `flush` hands it
98
+ over once the lock is released. Found by CodeRabbit on this pull request.
99
+
100
+ - Report a given-up batch after releasing the reporter lock, not under it.
101
+ `Reporter#retain` called `on_unrecoverable` inside `@mutex.synchronize`.
102
+ The documented callback is `Rails.error.report`, whose subscriber records
103
+ the error as an exception -- a write back into the same reporter, which
104
+ takes `@mutex` to arm its thread or request a flush. That was
105
+ `ThreadError: deadlock; recursive locking`, rescued and hidden by
106
+ `notify_unrecoverable`, so the callback died halfway and the loss it was
107
+ reporting was never seen. A callback that merely blocked held every
108
+ request thread's `write_now` and `shutdown` itself behind it for the
109
+ duration. `notify_unsent` and `delivery_rejected` already called out
110
+ unlocked; this was the one that did not.
111
+
112
+ - Investigated and left alone: `Reporter#shutdown` after `thread.join`.
113
+ The bookkeeping that follows the join (`pending_delivery`, the
114
+ once-only notify latch) takes `@mutex` for microseconds and never does
115
+ I/O -- measured 0.2ms over `shutdown_timeout` against a transport wedged
116
+ forever. The only thing that can extend it is the operator's own
117
+ callback, which runs once and is theirs to bound, as any `at_exit`
118
+ handler is. Wrapping it in `Timeout` would trade a visible cost for a
119
+ killed thread.
120
+
121
+
3
122
  ## 0.5.1 (2026-09-21)
4
123
 
5
124
  Three small seams for a host that runs these models on its own routes and
data/README.md CHANGED
@@ -6,25 +6,31 @@ 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, for
13
+ alerts that arrive when the app is down, MCP for your AI assistant, and
14
+ many apps and servers in one place.
13
15
 
14
16
  ## Install
15
17
 
16
18
  ```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
19
+ bundle add railwatch # 1. add the public gem
20
+ bin/rails generate railwatch:install # 2. embedded: databases, Puma writer, dashboard at /railwatch
20
21
  ```
21
22
 
22
- Or keep everything inside your app, with the full dashboard at
23
- `/railwatch` and no token ([Embedded mode](docs/embedded.md)):
23
+ Restart and open `/railwatch`. It is open in development; before you
24
+ deploy, give it a password with
25
+ `RAILS_ENV=production bin/rails railwatch:authentication:configure`
26
+ (it answers 401 in production until you do).
27
+
28
+ Or send everything to Railwatch Cloud instead:
24
29
 
25
30
  ```sh
26
31
  bundle add railwatch
27
- bin/rails generate railwatch:install --local
32
+ bin/rails generate railwatch:install --prompt-token # hidden token input plus app wiring
33
+ bin/rails railwatch:doctor # check every piece is wired up after restart
28
34
  ```
29
35
 
30
36
  Getting the token, the generator's flags, and deploying with Kamal,
@@ -73,7 +79,7 @@ expect { get "/widgets" }.not_to have_railwatch_n_plus_one
73
79
 
74
80
  ## Embedded mode
75
81
 
76
- `--local` keeps everything inside the application. Telemetry goes to two
82
+ The default install keeps everything inside the application. Telemetry goes to two
77
83
  SQLite files it owns -- `railwatch` for issues, comments and saved views,
78
84
  `railwatch_telemetry` for what the app reports -- and the dashboard is
79
85
  served at `/railwatch` from a bundle shipped inside the gem. Nothing
@@ -86,9 +92,11 @@ runs the maintenance clock too, so exception grouping, rollups, retention
86
92
  and threshold scans happen without a queue.
87
93
 
88
94
  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
95
+ produced, so outside development it is closed by default the way
96
+ Mission Control Jobs is: HTTP Basic is on with no credentials, and every
97
+ request is 401 until you set them with
98
+ `bin/rails railwatch:authentication:configure`. In development, with no
99
+ credentials set, it is open. Apps that
92
100
  would rather use their own session hand it a `dashboard_user` resolver
93
101
  instead. [Embedded mode](docs/embedded.md) covers all of it, including
94
102
  upgrades and what it costs to store.
@@ -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,
@@ -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
@@ -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
@@ -385,7 +385,7 @@ the app database.
385
385
 
386
386
  | Attribute | Env var | Default | Meaning |
387
387
  |---|---|---|---|
388
- | `buffer_size` | `RAILWATCH_BUFFER_SIZE` | `10000` | Max buffered records (`Railwatch::Buffer`). Oldest is dropped (and counted) when full — never blocks the request thread. Keep it at or above `Execution::MAX_RECORDS` (10,000): a kept execution's whole tree is written here at once when it ends, and a queue smaller than the tree drops the tree's own oldest records first. |
388
+ | `buffer_size` | `RAILWATCH_BUFFER_SIZE` | `10000` | Max buffered records (`Railwatch::Buffer`). Oldest is dropped (and counted) when full — never blocks the request thread. In practice `buffer_bytes` fills first: on a realistic mix of records, 16 MiB holds about 5,000 of them, so this count is never reached and raising it changes nothing. Tune `buffer_bytes` instead. Do not lower this below `Execution::MAX_RECORDS` (10,000): a kept execution's whole tree is written here at once when it ends, and a queue smaller than the tree drops the tree's own oldest records first. |
389
389
  | `buffer_bytes` | `RAILWATCH_BUFFER_BYTES` | `16777216` (16 MiB) | Estimated payload memory the reporter queue may hold. A record count alone does not bound memory: 10,000 records is a few megabytes of ordinary telemetry, or a gigabyte of captured attachments. Oldest records are dropped (and counted) under byte pressure, same as under count pressure. |
390
390
  | `execution_buffer_bytes` | `RAILWATCH_EXECUTION_BUFFER_BYTES` | `8388608` (8 MiB) | The same ceiling for one execution's buffered tree, before it finishes. A normal execution keeps its earliest records; a failure-context ring keeps its latest. |
391
391
  | `batch_bytes` | `RAILWATCH_BATCH_BYTES` | `8388608` (8 MiB) | Uncompressed NDJSON bytes in one ingest request. A queue holding more than this is delivered as several batches — the tail is kept for the next flush, not dropped. |
@@ -399,13 +399,13 @@ the app database.
399
399
 
400
400
  Delivery is `Railwatch::Transport::Http`, in
401
401
  `lib/railwatch/transport/http.rb`: a gzip NDJSON POST to
402
- `{ingest_url}/ingest`, with one retry on a raised error or a 5xx within
403
- each delivery attempt. If that still fails, or ingest returns 402, 408,
404
- or 429, the immutable batch and its prior drop count are retained for
405
- retry. Every newly formed batch gets an `X-Railwatch-Batch-Id` UUID. It is
406
- reused for the immediate HTTP retry and every later reporter retry, so
407
- the platform can return the first committed result without inserting the
408
- payload twice. Records written while a request is in flight collect in a
402
+ `{ingest_url}/ingest`, one attempt per delivery. If it raises, or ingest
403
+ returns 402, 408, 429, or a 5xx, the immutable batch and its prior drop
404
+ count are retained for retry; the transport does not retry on its own,
405
+ so each rung of the reporter's ladder costs one timeout, not two. Every
406
+ newly formed batch gets an `X-Railwatch-Batch-Id` UUID. It is reused for
407
+ every reporter retry, so the platform can return the first committed
408
+ result without inserting the payload twice. Records written while a request is in flight collect in a
409
409
  separate bounded buffer, so they never change the retained request's
410
410
  identity. At most one retained batch plus one live buffer are held in
411
411
  memory. The reporter retries with jittered exponential backoff, from one
@@ -827,9 +827,25 @@ Railwatch.on_unrecoverable { |error| Rails.error.report(error, handled: true) }
827
827
  Called whenever Railwatch rescues one of its own internal errors, ingest
828
828
  permanently rejects a batch, or shutdown expires with retained records
829
829
  that could not be sent. Retryable delivery failures stay buffered and do
830
- not fire the callback on every attempt. With no callback registered, this
831
- falls back to `Railwatch.debug`. That goes to stderr, gated on
832
- `RAILWATCH_DEBUG`, never `Rails.logger`, so gem-internal failures can
830
+ not fire the callback on every attempt. With no callback registered, a
831
+ recovered internal error falls back to `Railwatch.debug`, gated on
832
+ `RAILWATCH_DEBUG` — the gem carried on and there is nothing to do about it.
833
+
834
+ **Lost records are different, and are reported by default.** A batch
835
+ dropped after its retry ladder, one the receiver permanently refused, or
836
+ records still unsent when the bounded shutdown ran out of time each print
837
+ one `[railwatch]` stderr line. Telemetry that vanishes silently looks
838
+ exactly like having nothing to report, and a short-lived process — a rake
839
+ task, a `rails runner`, a cron job — gets one bounded shutdown and no
840
+ second chance to mention it.
841
+
842
+ Two ways out, and the line names both. A registered `on_unrecoverable`
843
+ always wins, which is how an app routes the loss somewhere better
844
+ (`Rails.error.report`). Or set `warn_on_data_loss = false`
845
+ (`RAILWATCH_WARN_ON_DATA_LOSS=false`) and the gem goes back to saying
846
+ nothing.
847
+
848
+ Either way it is stderr, never `Rails.logger`, so gem-internal failures can
833
849
  never themselves become `log` records.
834
850
 
835
851
  ## Faraday
@@ -872,7 +888,7 @@ c.transport = :local # RAILWATCH_TRANSPORT; default "http"
872
888
  c.issue_prefix = "SHOP" # RAILWATCH_ISSUE_PREFIX; default from the app name
873
889
  c.repository_url = "..." # RAILWATCH_REPOSITORY_URL
874
890
  c.retention_days = 7 # RAILWATCH_RETENTION_DAYS
875
- c.http_basic_auth_enabled = true # RAILWATCH_HTTP_BASIC_AUTH_ENABLED; on and closed until credentials exist
891
+ c.http_basic_auth_enabled = true # RAILWATCH_HTTP_BASIC_AUTH_ENABLED; on, and closed outside development until credentials exist
876
892
  c.http_basic_auth_user = "ops" # RAILWATCH_HTTP_BASIC_AUTH_USER, or credentials railwatch.http_basic_auth_user
877
893
  c.http_basic_auth_password = "..." # RAILWATCH_HTTP_BASIC_AUTH_PASSWORD, or credentials railwatch.http_basic_auth_password
878
894
  c.base_controller_class = "AdminController" # RAILWATCH_BASE_CONTROLLER_CLASS; default ActionController::Base
@@ -884,7 +900,7 @@ With `transport = :local` the reporter writes each batch into the app's
884
900
  own `railwatch_telemetry` database instead of POSTing it, and the engine
885
901
  serves the dashboard at its mount. `enabled?` no longer needs a token.
886
902
  The others only matter in that mode. The dashboard is behind HTTP Basic
887
- by default and answers 401 until `bin/rails
903
+ by default and, outside development, answers 401 until `bin/rails
888
904
  railwatch:authentication:configure` has written credentials; a host with
889
905
  its own admin auth turns Basic off and sets `base_controller_class` or a
890
906
  routes constraint. Full walkthrough: [Embedded mode](embedded.md).
data/docs/embedded.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Embedded mode: the dashboard inside your app
2
2
 
3
- Railwatch can keep every record in your own application and serve the
4
- full dashboard at `/railwatch`, with no token and no cloud. The gem's
3
+ This is the default. Railwatch keeps every record in your own application
4
+ and serves the full dashboard at `/railwatch`, with no token and no cloud. The gem's
5
5
  reporter, buffer and sampling are the same; the only difference is
6
6
  where a batch ends up. In embedded mode it is written straight into a
7
7
  SQLite database your app owns, and the dashboard reads it back from
@@ -25,8 +25,9 @@ up:
25
25
  | **Cloud** | Railwatch Cloud | the hosted one |
26
26
  | **Both** | your app's files, *and* Railwatch Cloud | either |
27
27
 
28
- Embedded is `c.transport = :local`, which is what `--local` writes.
29
- Cloud is the default. "Both" is embedded plus one more line:
28
+ Embedded is `c.transport = :local`, which is what the installer writes
29
+ unless you ask it for the cloud (`--cloud`, or any token or URL option).
30
+ Cloud is the gem's default when no initializer says otherwise. "Both" is embedded plus one more line:
30
31
 
31
32
  ```ruby
32
33
  c.export_enabled = true # or RAILWATCH_EXPORT_ENABLED=true
@@ -59,10 +60,11 @@ documents: `id`, `slug`, `name`, `with_telemetry`, and the display attributes.
59
60
 
60
61
  ```sh
61
62
  bundle add railwatch
62
- bin/rails generate railwatch:install --local
63
+ bin/rails generate railwatch:install
63
64
  ```
64
65
 
65
- Restart the app and open `/railwatch`. Then `bin/rails railwatch:doctor`
66
+ Restart the app and open `/railwatch`; in development it is open with no
67
+ password (see [Authentication](#authentication) for production). Then `bin/rails railwatch:doctor`
66
68
  checks the wiring. The generator creates and migrates both databases
67
69
  itself; `bin/rails db:prepare`, which a deploy already runs, migrates
68
70
  them after every gem update.
@@ -83,7 +85,7 @@ On a PostgreSQL or MySQL app that means the install is two commands
83
85
  rather than one, because SQLite's adapter gem will not be in your bundle:
84
86
 
85
87
  ```sh
86
- bin/rails generate railwatch:install --local # adds gem "sqlite3", writes the config
88
+ bin/rails generate railwatch:install # adds gem "sqlite3", writes the config
87
89
  bundle install
88
90
  bin/rails db:prepare # creates the two SQLite files
89
91
  ```
@@ -92,7 +94,7 @@ Verified end to end on both. On a PostgreSQL app and on a MySQL app, the
92
94
  application's own four databases stay where they were, Railwatch's two are
93
95
  files under `storage/`, and neither server gains a single Railwatch table.
94
96
 
95
- What `--local` writes, on top of the usual install:
97
+ What the embedded install writes, on top of what every install writes:
96
98
 
97
99
  - `config/initializers/railwatch.rb` with `c.transport = :local` and the
98
100
  dashboard's own paths excluded from request capture.
@@ -140,15 +142,22 @@ and the MCP server.
140
142
  The dashboard shows every query, log line and exception your app
141
143
  produced, so it works the way Mission Control Jobs does: **HTTP Basic
142
144
  authentication is on and closed by default**. With no credentials
143
- configured every dashboard request is 401, and `railwatch:doctor` says
144
- so. Set them with
145
+ configured every dashboard request is 401, the app logs a warning at
146
+ boot, and `railwatch:doctor` says so. Set them with
145
147
 
146
148
  ```sh
147
149
  bin/rails railwatch:authentication:configure
148
150
  RAILS_ENV=production bin/rails railwatch:authentication:configure
149
151
  ```
150
152
 
151
- which writes them to that environment's Rails credentials:
153
+ The one exception is development. There, with Basic on and no
154
+ credentials set, the dashboard is open, so a first run is the install and
155
+ a page rather than a password step first; Rails already shows full error
156
+ pages in development for the same reason. Set credentials there too and
157
+ development asks for them like everywhere else. Test, staging and
158
+ production are closed until you do.
159
+
160
+ `railwatch:authentication:configure` writes them to that environment's Rails credentials:
152
161
 
153
162
  ```yml
154
163
  railwatch:
@@ -262,7 +271,7 @@ authorisation rule as well as a label.
262
271
  ## The writer process
263
272
 
264
273
  Puma forks one Railwatch writer from its master when `config/puma.rb`
265
- carries the plugin (`--local` adds it):
274
+ carries the plugin (the embedded install adds it):
266
275
 
267
276
  ```ruby
268
277
  plugin :railwatch if defined?(Railwatch)
@@ -319,6 +328,17 @@ through `Railwatch.on_unrecoverable`, so loss is never silent. A Puma
319
328
  phased restart stops the writer and starts a fresh one once the new
320
329
  workers are up.
321
330
 
331
+ Stopping the writer is bounded. Puma sends it TERM and waits up to
332
+ `c.shutdown_timeout` (2 seconds) for it to exit, the same allowance it
333
+ gives its own reporter, then kills it. A writer killed mid-batch loses
334
+ nothing: the transaction rolls back and the worker retries that batch by
335
+ id against the next writer, so waiting longer for its drain would buy no
336
+ data. The bound is what keeps Puma's exit short when the writer is wedged
337
+ in a SQLite write or on a full disk. It counts against the container's
338
+ stop grace (Docker's default is 10 seconds; Kamal's `stop_timeout` sets
339
+ it), and `RAILWATCH_SHUTDOWN_TIMEOUT` raises it for an app whose grace
340
+ allows more.
341
+
322
342
  ## Maintenance
323
343
 
324
344
  Railwatch needs no job worker and nothing in `config/recurring.yml`.
data/docs/faq.md CHANGED
@@ -182,10 +182,10 @@ being silent.
182
182
  A queue holding more than one batch is delivered as several batches: the
183
183
  tail is put back for the next flush rather than dropped.
184
184
 
185
- A background thread drains the buffer and POSTs. Each POST retries one
186
- raised network error or 5xx immediately. If delivery still fails, the batch
187
- and its drop counter go back into the bounded buffer; **402**, **408**,
188
- **429**, and all **5xx** responses are retained the same way. So is a **2xx
185
+ A background thread drains the buffer and POSTs, one attempt per delivery.
186
+ If it fails, the batch and its drop counter go back into the bounded
187
+ buffer; a raised network error, **402**, **408**, **429**, and all **5xx**
188
+ responses are retained the same way. So is a **2xx
189
189
  that cannot acknowledge the batch** — a proxy's HTML sign-in page, malformed
190
190
  JSON, or `accepted`/`rejected` counts that do not cover what was sent — which
191
191
  would otherwise be a silent drop. The reporter
@@ -217,7 +217,8 @@ already appear on its own ingest batch. Those are visible under
217
217
  On shutdown, `at_exit` gives the thread `c.shutdown_timeout` (2 seconds) to
218
218
  attempt retained records immediately and retry within the remaining time.
219
219
  If the deadline expires, the records stay retained and their count is sent
220
- to `Railwatch.on_unrecoverable` (or stderr under `RAILWATCH_DEBUG=1`). This is an
220
+ to `Railwatch.on_unrecoverable` (or, with no callback, to `Railwatch.debug`, and
221
+ to stderr by default, which `warn_on_data_loss = false` turns off). This is an
221
222
  in-memory buffer, not an on-disk spool: a hard kill, or exiting after that
222
223
  deadline, cannot carry those records into the next process. Railwatch never
223
224
  uses `Rails.logger` for its own failures, which would turn them into `log`
@@ -4,10 +4,10 @@ Five minutes from `bundle add` to a request on the dashboard, on a
4
4
  Rails 8 app. Everything below is the gem's own generator and rake tasks;
5
5
  nothing else has to be wired by hand.
6
6
 
7
- Running one server and want the dashboard inside the app itself, with
8
- no token and no cloud? That is `bin/rails generate railwatch:install
9
- --local`; see [Embedded mode](embedded.md). The rest of this page is
10
- the cloud install.
7
+ The installer's default is embedded: the dashboard inside the app at
8
+ `/railwatch`, no token and no cloud; see [Embedded mode](embedded.md).
9
+ This page is the cloud install, which is the same generator with
10
+ `--cloud` or any of the token options below.
11
11
 
12
12
  ## 1. Add the gem
13
13
 
@@ -21,7 +21,7 @@ The gem, its Ruby namespace, and its require path share one name:
21
21
  ## 2. Run the installer
22
22
 
23
23
  ```sh
24
- bin/rails generate railwatch:install
24
+ bin/rails generate railwatch:install --cloud
25
25
  ```
26
26
 
27
27
  With the token already in hand, let the generator read it without placing the
@@ -36,6 +36,10 @@ bin/rails generate railwatch:install \
36
36
 
37
37
  - `--prompt-token` reads without echo. `--token-stdin` is available for a
38
38
  secret-manager pipe; an already exported `RAILWATCH_TOKEN` is also detected.
39
+ - Any of `--prompt-token`, `--token-stdin`, `--url=` and `--kamal-secrets`
40
+ means the cloud, so none of them needs `--cloud` as well. An exported
41
+ `RAILWATCH_TOKEN` on its own does not: without one of these flags the
42
+ install is embedded.
39
43
  - A token is written to `.env` only when Git confirms that `.env` is ignored.
40
44
  A tracked or non-ignored dotenv file is refused; use Rails credentials, a
41
45
  deployment secret manager, or add `.env` to `.gitignore` first. Token values
@@ -215,14 +215,18 @@ can't be made until it ends.
215
215
  (10,000). Past that, records are dropped and counted. The count is
216
216
  added to the reporter's drop counter so the loss is visible on the
217
217
  platform rather than silent.
218
- - `c.buffer_size` (default 10,000, the same as `MAX_RECORDS`) caps the
219
- process-wide queue between the app and the reporter thread.
220
- Oldest-dropped-first, also counted. Do not set it below `MAX_RECORDS`.
221
- An execution's tree is written to the queue in one go when it ends, so
222
- a tree larger than the queue loses its own first records. Typically
218
+ - The process-wide queue between the app and the reporter thread is
219
+ bounded by `c.buffer_bytes` (default 16 MiB) and by `c.buffer_size`
220
+ (default 10,000, the same as `MAX_RECORDS`). Oldest-dropped-first, also
221
+ counted. The byte ceiling is the one that fills: on a realistic mix of
222
+ records, 16 MiB holds about 5,000 of them, so raising `buffer_size`
223
+ changes nothing. Do not set it below `MAX_RECORDS`, though: an
224
+ execution's tree is written to the queue in one go when it ends, so a
225
+ tree larger than the queue loses its own first records. Typically
223
226
  those are the outgoing requests a long job made before it started
224
227
  writing. Keeping far more executions than before means far more
225
- records arriving at this queue. Raise it, or lower what you keep.
228
+ records arriving at this queue. Raise `buffer_bytes`, or lower what
229
+ you keep.
226
230
  - `c.profile_slow_ms` compounds it. It profiles every tail-buffering
227
231
  execution from its first line and throws away the fast ones, so the
228
232
  profiler's stack table is held alongside the record buffer.