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
@@ -2,18 +2,17 @@
2
2
 
3
3
  Everything below lives on `Railwatch::Configuration`, in
4
4
  `lib/railwatch/configuration.rb`. Set it via
5
- `Railwatch.configure { |c| ... }` in `config/initializers/railwatch.rb`.
6
- That file is created by
7
- `bin/rails generate railwatch:install`. Most settings have a `RAILWATCH_*`
8
- env var default; the tables below show which. Explicit values set in the
9
- initializer always win over the env var.
5
+ `Railwatch.configure { |c| ... }` in `config/initializers/railwatch.rb`,
6
+ which `bin/rails generate railwatch:install` creates. Most settings have a
7
+ `RAILWATCH_*` env var default; the tables below show which. Explicit
8
+ values set in the initializer always win over the env var.
10
9
 
11
10
  ## Core
12
11
 
13
12
  | Attribute | Env var | Default | Meaning |
14
13
  |---|---|---|---|
15
- | `enabled` | `RAILWATCH_ENABLED` | `true` | Master switch. `Railwatch.enabled?` is also `false` whenever `token` is blank, so setting only `RAILWATCH_TOKEN` is enough to turn Railwatch on. |
16
- | `token` | `RAILWATCH_TOKEN` | nil | Bearer token for `/ingest`. Required. |
14
+ | `enabled` | `RAILWATCH_ENABLED` | `true` | Master switch. With `transport = :http`, `Railwatch.enabled?` is also `false` whenever `token` is blank, so setting only `RAILWATCH_TOKEN` is enough to turn a cloud install on. An embedded install needs no token. |
15
+ | `token` | `RAILWATCH_TOKEN` | nil | Bearer token for `/ingest`. Required with `transport = :http` and for [export](#export-to-railwatch-cloud). |
17
16
  | `ingest_url` | `RAILWATCH_INGEST_URL` | `https://railwatch.rebulk.com` | Platform base URL. Point at a self-hosted instance to override. |
18
17
  | `allow_http` | `RAILWATCH_ALLOW_HTTP` | `false` | Permit a non-loopback plain HTTP ingest URL. HTTPS is required by default; `localhost`, `127.0.0.1`, and `::1` remain available for local self-hosted development. |
19
18
  | `deploy` | `RAILWATCH_DEPLOY` | auto-detected (order below), then nil | Version tag stamped on every record and used by `railwatch:deploy`. Full 40-character SHAs are shortened to 12 characters. |
@@ -26,7 +25,8 @@ initializer always win over the env var.
26
25
  | `beacon_rate_limit` | `RAILWATCH_BEACON_RATE_LIMIT` | `120` | Beacon POSTs accepted per client IP per minute before `POST /railwatch/beacon` answers 429. The beacon is unauthenticated and keeps every browser error it is sent, so this is what stops a script from spending the app's event quota. Counted in the app's cache store; `0` turns it off. |
27
26
 
28
27
  `Railwatch.enabled?` delegates to `config.enabled?`, which is `@enabled &&
29
- token.present?`. There is no separate "is configured" check elsewhere.
28
+ (local? || token.present?)`. There is no separate "is configured" check
29
+ elsewhere.
30
30
 
31
31
  Deploy detection stops at the first value found: `RAILWATCH_DEPLOY`,
32
32
  `KAMAL_VERSION`, `GIT_REV`, `GIT_SHA`, `SOURCE_VERSION`,
@@ -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
@@ -569,7 +569,7 @@ Rake tasks and Solid Queue jobs are never interactive.
569
569
 
570
570
  | Attribute | Env var | Default | Meaning |
571
571
  |---|---|---|---|
572
- | `capture_exception_source` | `RAILWATCH_CAPTURE_EXCEPTION_SOURCE_CODE` | `true` | Send source snippet lines surrounding each in-application exception frame to Railwatch Cloud. This is on by default for crash context; disable it when source disclosure is outside the application's telemetry policy. |
572
+ | `capture_exception_source` | `RAILWATCH_CAPTURE_EXCEPTION_SOURCE_CODE` | `true` | Record source snippet lines surrounding each in-application exception frame (sent to Railwatch Cloud when the install reports there or exports). This is on by default for crash context; disable it when source disclosure is outside the application's telemetry policy. |
573
573
  | `capture_exception_locals` | `RAILWATCH_CAPTURE_EXCEPTION_LOCALS` | `false` | Snapshot the raising frame's local variables (up to 25, values truncated to 200 chars, run through the same filter as request params) onto each exception, like Sentry's locals panel. Installs a `TracePoint(:raise)`; opt in per environment. |
574
574
  | `capture_request_payload` | `RAILWATCH_CAPTURE_REQUEST_PAYLOAD` | `false` | Capture (redacted) request params — only for a request that raised, never otherwise. |
575
575
  | `capture_job_arguments` | `RAILWATCH_CAPTURE_JOB_ARGUMENTS` | `false` | Add the job's real arguments (`job.serialize["arguments"]`) to each `job_attempt`/`scheduled_task` record, capped at 8 KiB of JSON. Hash arguments run through the same filter as request params. Off by default because job arguments routinely carry PII; `arguments_preview` (argument *shapes* only) is always on regardless. |
@@ -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
@@ -868,46 +884,70 @@ ignore block's building blocks, and are nestable.
868
884
  ## Embedded mode
869
885
 
870
886
  ```ruby
871
- c.transport = :local # RAILWATCH_TRANSPORT; default "http"
887
+ c.transport = :local # RAILWATCH_TRANSPORT; runtime default "http", but the installer writes :local
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
879
- c.dashboard_open = false # RAILWATCH_DASHBOARD_OPEN; public on purpose
895
+ c.dashboard_open = false # RAILWATCH_DASHBOARD_OPEN; default false, true makes it public on purpose
880
896
  c.dashboard_user = ->(request) { { id:, name:, email: } or nil }
897
+ c.writer_socket = "tmp/sockets/railwatch-writer.sock" # RAILWATCH_WRITER_SOCKET
881
898
  ```
882
899
 
883
900
  With `transport = :local` the reporter writes each batch into the app's
884
901
  own `railwatch_telemetry` database instead of POSTing it, and the engine
885
902
  serves the dashboard at its mount. `enabled?` no longer needs a token.
886
903
  The others only matter in that mode. The dashboard is behind HTTP Basic
887
- by default and answers 401 until `bin/rails
904
+ by default and, outside development, answers 401 until `bin/rails
888
905
  railwatch:authentication:configure` has written credentials; a host with
889
906
  its own admin auth turns Basic off and sets `base_controller_class` or a
890
907
  routes constraint. Full walkthrough: [Embedded mode](embedded.md).
891
908
 
909
+ ### Export to Railwatch Cloud
910
+
911
+ An embedded install can also mirror every record to Railwatch Cloud.
912
+ Off unless `export_enabled` is set; a configured token alone sends
913
+ nothing. Only meaningful with `transport = :local`.
914
+
915
+ | Attribute | Env var | Default | Meaning |
916
+ |---|---|---|---|
917
+ | `export_enabled` | `RAILWATCH_EXPORT_ENABLED` | `false` | Mirror every record to the cloud as well as storing it locally. |
918
+ | `export_token` | `RAILWATCH_EXPORT_TOKEN` | `token` | Cloud environment token to send with. |
919
+ | `export_url` | `RAILWATCH_EXPORT_URL` | `{ingest_url}/ingest` | Where to send. |
920
+ | `export_max_bytes` | `RAILWATCH_EXPORT_MAX_BYTES` | `268435456` (256 MiB) | Queue ceiling in bytes. At capacity new work is refused and counted, not swapped for old. |
921
+ | `export_max_deliveries` | `RAILWATCH_EXPORT_MAX_DELIVERIES` | `100000` | Queue ceiling in deliveries. |
922
+ | `export_max_age` | `RAILWATCH_EXPORT_MAX_AGE_SECONDS` | `86400` | How long a queued delivery is kept. At most seven days, past which the receiver no longer recognises it. |
923
+
924
+ `railwatch:doctor` fails when export is enabled and cannot work, and
925
+ `railwatch:export:status` shows the queue. See
926
+ [Embedded mode](embedded.md#three-ways-to-run-it).
927
+
892
928
  ## Rake tasks
893
929
 
894
930
  Ship with the gem via Rails::Engine's default `lib/tasks` convention, in
895
931
  `lib/tasks/railwatch_tasks.rake`:
896
932
 
897
- - **`railwatch:status`** pings `{ingest_url}/ingest/ping` with the
898
- configured token. It aborts if `RAILWATCH_TOKEN` is unset or the ping
899
- fails.
900
- - **`railwatch:doctor`** prints a ✓/✗ checklist of the whole install:
901
- token, ingest URL, `GET /ingest/ping`, `Railwatch::Middleware::Request`
933
+ - **`railwatch:status`** embedded: prints that telemetry is stored in the
934
+ app. Cloud: pings `{ingest_url}/ingest/ping` with the configured token,
935
+ and aborts if `RAILWATCH_TOKEN` is unset or the ping fails.
936
+ - **`railwatch:doctor`** prints a ✓/✗ checklist of the whole install.
937
+ Embedded, it checks both databases and their migrations, telemetry
938
+ disk mode, the maintenance clock, the writer process, dashboard access,
939
+ and export when enabled. Cloud, it checks the token, where the token is
940
+ stored, the ingest URL and its transport security, and
941
+ `GET /ingest/ping`. Then, either way: `Railwatch::Middleware::Request`
902
942
  in the middleware stack, the mounted engine's beacon route,
903
- `config.deploy` and its environment, `REVISION`, Git, or initializer
904
- source, sample rates, ignored record types, the Kamal `post-deploy`
905
- hook, `app/frontend/lib/railwatch.ts`, and whether `railwatch/rspec` or
906
- `railwatch/minitest` is required by the test helper. The last five are
907
- informational. It exits non-zero only when the token is missing or the
908
- ping fails.
909
- - **`railwatch:deploy[ref,name,url]`** POSTs `{deploy, ref, name, url,
910
- server, timestamp, performer, destination, service, commits}` to
943
+ `config.deploy` and its source, sample rates, ignored record types, the
944
+ Kamal `post-deploy` hook, the browser client and whether an entrypoint
945
+ calls it, the profiler backend, and the test matchers. It exits
946
+ non-zero only on the lines marked fatal in
947
+ [Troubleshooting](troubleshooting.md).
948
+ - **`railwatch:deploy[ref,name,url]`** embedded: writes the deploy marker
949
+ to the app's `railwatch` database. Cloud: POSTs `{deploy, ref, name,
950
+ url, server, timestamp, performer, destination, service, commits}` to
911
951
  `{ingest_url}/ingest/deploys`. `deploy` comes from `config.deploy`. It
912
952
  aborts if that's unset. `ref` defaults to `git rev-parse HEAD` when not
913
953
  passed. `performer`/`destination`/`service` come from `KAMAL_PERFORMER`,
@@ -915,13 +955,30 @@ Ship with the gem via Rails::Engine's default `lib/tasks` convention, in
915
955
  `{sha, author, message, at}` objects, newest first, from `git log`. It
916
956
  is empty inside an app container, which has no `.git`. That is why the
917
957
  hook below posts from the deployer instead.
958
+ - **`railwatch:authentication:configure`** writes the embedded
959
+ dashboard's HTTP Basic credentials to the current environment's Rails
960
+ credentials.
961
+ - **`railwatch:export:status`**, **`railwatch:export:rebind`**,
962
+ **`railwatch:export:discard`** show the export queue, clear a
963
+ credential block (abandoning work queued under the old token), and
964
+ abandon everything queued.
965
+ - **`railwatch:vacuum:status`** and **`railwatch:vacuum`** report and
966
+ reclaim the telemetry database's disk
967
+ ([Embedded mode](embedded.md#giving-the-disk-back)).
968
+ - **`railwatch:token`** and **`railwatch:mcp`** print where to create a
969
+ cloud ingest token and paste-ready MCP client configuration.
970
+ - **`railwatch:sourcemaps[directory,delete]`** uploads browser source
971
+ maps ([Source maps](source-maps.md)).
918
972
 
919
973
  ## Kamal integration
920
974
 
921
975
  `bin/rails generate railwatch:install` writes `.kamal/hooks/post-deploy`,
922
- but only if `config/deploy.yml` already exists. It no-ops when
923
- `RAILWATCH_TOKEN` isn't set, and never fails a deploy. Every network call
924
- ends in `|| true`.
976
+ but only if `config/deploy.yml` already exists. With
977
+ `RAILWATCH_TRANSPORT=local` in the deployer's environment it runs
978
+ `bin/rails railwatch:deploy[$KAMAL_VERSION]` in the primary container,
979
+ which records the marker in the embedded database, and stops. Otherwise
980
+ it no-ops when `RAILWATCH_TOKEN` isn't set. It never fails a deploy.
981
+ Every network call ends in `|| true`.
925
982
 
926
983
  The hook runs on the **deployer machine**, not in a container, which is
927
984
  the whole point. That's where the git history lives and where Kamal
data/docs/embedded.md CHANGED
@@ -1,18 +1,17 @@
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
5
- reporter, buffer and sampling are the same; the only difference is
6
- where a batch ends up. In embedded mode it is written straight into a
7
- SQLite database your app owns, and the dashboard reads it back from
8
- there.
3
+ This is what the installer sets up by default. Railwatch keeps every
4
+ record in your own application and serves the full dashboard at
5
+ `/railwatch`, with no token and no cloud. The gem's reporter, buffer and
6
+ sampling are the same as with the cloud; the only difference is where a
7
+ batch ends up. In embedded mode it is written into a SQLite database
8
+ your app owns, and the dashboard reads it back from there.
9
9
 
10
10
  Use it when one server runs the app. Telemetry lands in a file next to
11
11
  your other SQLite databases, so several servers would each see only
12
- their own slice. For more than one server, or for a team that wants one
13
- place for many apps, point the gem at Railwatch Cloud instead
14
- ([Getting started](getting-started.md)); the two are switchable with
15
- one setting.
12
+ their own slice. For more than one server, or one place for many apps,
13
+ add Railwatch Cloud: export alongside embedded, or the cloud on its own
14
+ ([Getting started](getting-started.md#railwatch-cloud-instead)).
16
15
 
17
16
  ## Three ways to run it
18
17
 
@@ -25,24 +24,40 @@ up:
25
24
  | **Cloud** | Railwatch Cloud | the hosted one |
26
25
  | **Both** | your app's files, *and* Railwatch Cloud | either |
27
26
 
28
- Embedded is `c.transport = :local`, which is what `--local` writes.
29
- Cloud is the default. "Both" is embedded plus one more line:
27
+ Embedded is `c.transport = :local`, which is what the installer writes
28
+ unless you ask it for the cloud (`--cloud`, or any token or URL option).
29
+ With no initializer setting it, the runtime default is `:http`, the
30
+ cloud. "Both" is embedded plus export, which needs a Railwatch Cloud
31
+ environment token. The default embedded install has none, so set one
32
+ first (`bin/rails railwatch:token` prints where to create it), plus the
33
+ ingest URL if you self-host:
34
+
35
+ ```sh
36
+ RAILWATCH_TOKEN=rw_...
37
+ RAILWATCH_INGEST_URL=https://telemetry.example.com # only when self-hosting
38
+ ```
39
+
40
+ With a token in place, export is one more line:
30
41
 
31
42
  ```ruby
32
43
  c.export_enabled = true # or RAILWATCH_EXPORT_ENABLED=true
33
44
  ```
34
45
 
35
- It reuses the token and ingest URL you already have, so an install that
36
- was pointed at the cloud and moved to embedded needs nothing else to send
37
- to both. Everything captured locally is mirrored — the same records the
38
- same install would have sent had you chosen the cloud — so the hosted
39
- dashboard is as complete as it would be either way.
46
+ It reuses `RAILWATCH_TOKEN` and `RAILWATCH_INGEST_URL`
47
+ (`RAILWATCH_EXPORT_TOKEN` and `RAILWATCH_EXPORT_URL` override them), so
48
+ an install that was pointed at the cloud and moved to embedded needs
49
+ nothing else. Every record captured locally is mirrored, so the hosted
50
+ dashboard is as complete as a cloud-only install's, and `/railwatch`
51
+ keeps working.
40
52
 
41
53
  It is off unless you set that flag. A token being present is not consent:
42
54
  an embedded install that has one configured still sends nothing.
43
55
  `railwatch:doctor` says nothing about export until you ask for it, and
44
- fails loudly if you ask for it and it cannot work. `railwatch:export:status`
45
- shows what is queued.
56
+ fails if you ask for it and it cannot work. `railwatch:export:status`
57
+ shows what is queued. The queue is durable, in the telemetry database,
58
+ and bounded: 256 MiB (`RAILWATCH_EXPORT_MAX_BYTES`), 100,000 deliveries
59
+ (`RAILWATCH_EXPORT_MAX_DELIVERIES`), and a day's age
60
+ (`RAILWATCH_EXPORT_MAX_AGE_SECONDS`, at most seven days).
46
61
 
47
62
  ## Railwatch Cloud runs the same models
48
63
 
@@ -59,13 +74,16 @@ documents: `id`, `slug`, `name`, `with_telemetry`, and the display attributes.
59
74
 
60
75
  ```sh
61
76
  bundle add railwatch
62
- bin/rails generate railwatch:install --local
77
+ bin/rails generate railwatch:install
63
78
  ```
64
79
 
65
- Restart the app and open `/railwatch`. Then `bin/rails railwatch:doctor`
66
- checks the wiring. The generator creates and migrates both databases
67
- itself; `bin/rails db:prepare`, which a deploy already runs, migrates
68
- them after every gem update.
80
+ Restart the app and open `/railwatch`; in development it is open with no
81
+ password (see [Authentication](#authentication) for production).
82
+ `bin/rails railwatch:doctor` checks the wiring. When the `sqlite3` gem
83
+ is already in the bundle, the generator creates and migrates both
84
+ databases itself; when it is not, see the next section. After that,
85
+ `bin/rails db:prepare`, which a deploy already runs, migrates them after
86
+ every gem update.
69
87
 
70
88
  The engine needs Active Job (its grouping and scan jobs are Active Job
71
89
  classes even though embedded mode calls them directly) and loads it
@@ -79,20 +97,21 @@ databases it adds are SQLite files either way, so the generated entries
79
97
  name `adapter: sqlite3` themselves rather than inheriting your default
80
98
  block, and they need no `&default` anchor to exist.
81
99
 
82
- On a PostgreSQL or MySQL app that means the install is two commands
83
- rather than one, because SQLite's adapter gem will not be in your bundle:
100
+ On a PostgreSQL or MySQL app without the `sqlite3` gem, the install
101
+ takes two more commands:
84
102
 
85
103
  ```sh
86
- bin/rails generate railwatch:install --local # adds gem "sqlite3", writes the config
104
+ bin/rails generate railwatch:install # adds gem "sqlite3", writes the config
87
105
  bundle install
88
- bin/rails db:prepare # creates the two SQLite files
106
+ bin/rails db:prepare # creates the two SQLite files
89
107
  ```
90
108
 
91
109
  Verified end to end on both. On a PostgreSQL app and on a MySQL app, the
92
110
  application's own four databases stay where they were, Railwatch's two are
93
111
  files under `storage/`, and neither server gains a single Railwatch table.
94
112
 
95
- What `--local` writes, on top of the usual install:
113
+ What the embedded install writes, on top of what every install writes
114
+ (listed in [Getting started](getting-started.md#what-the-generator-writes)):
96
115
 
97
116
  - `config/initializers/railwatch.rb` with `c.transport = :local` and the
98
117
  dashboard's own paths excluded from request capture.
@@ -104,6 +123,8 @@ What `--local` writes, on top of the usual install:
104
123
  databases need that form. Each entry's `migrations_paths` points into
105
124
  the gem, so `db:prepare` builds the tables from the gem's own
106
125
  migrations and nothing is copied into `db/`.
126
+ - `plugin :railwatch` at the end of `config/puma.rb`, which forks the
127
+ [writer process](#the-writer-process).
107
128
  - `mount Railwatch::Engine, at: "/railwatch"`, as always.
108
129
 
109
130
  Nothing touches your primary database.
@@ -132,23 +153,32 @@ inside the gem, so the app needs no Node, no Vite and no asset pipeline
132
153
  integration.
133
154
 
134
155
  Not in embedded mode: accounts and members (the operator is whoever your
135
- app lets through), integrations (alerts are recorded, not delivered),
136
- and the MCP server.
156
+ app lets through), integrations (alerts are recorded, not delivered to
157
+ Slack, email, webhooks or Linear), and the
158
+ [MCP server](ai-and-mcp.md). Those are Railwatch Cloud's, and
159
+ [export](#three-ways-to-run-it) gets them without giving up the local
160
+ dashboard.
137
161
 
138
162
  ## Authentication
139
163
 
140
164
  The dashboard shows every query, log line and exception your app
141
165
  produced, so it works the way Mission Control Jobs does: **HTTP Basic
142
- 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
166
+ authentication is on by default, and closed until credentials exist**.
167
+ With none configured every dashboard request is 401, the app logs a
168
+ warning at boot, and `railwatch:doctor` says so. Set them with
145
169
 
146
170
  ```sh
147
171
  bin/rails railwatch:authentication:configure
148
172
  RAILS_ENV=production bin/rails railwatch:authentication:configure
149
173
  ```
150
174
 
151
- which writes them to that environment's Rails credentials:
175
+ The one exception is development. There, with Basic on and no
176
+ credentials set, the dashboard is open, so a first run needs no password
177
+ step; Rails shows full error pages in development for the same reason.
178
+ Set credentials there too and development asks for them like everywhere
179
+ else. Test, staging and production are closed until you do.
180
+
181
+ `railwatch:authentication:configure` writes them to that environment's Rails credentials:
152
182
 
153
183
  ```yml
154
184
  railwatch:
@@ -256,18 +286,25 @@ no user table to check it against. Comments, saved views, issue activity
256
286
  and assignment all key off whatever you return, so a person keeps their
257
287
  own views and their name on their own comments however you identify them.
258
288
 
259
- Returning `nil` refuses the request, which is what makes this an
260
- authorisation rule as well as a label.
289
+ On a page, returning `nil` does not refuse the request: the page renders
290
+ with the default "Operator". With HTTP Basic off, `nil` refuses the
291
+ live-update subscription. Pages are gated by HTTP Basic, your
292
+ `base_controller_class`, or a mount constraint, never by this resolver.
261
293
 
262
294
  ## The writer process
263
295
 
264
296
  Puma forks one Railwatch writer from its master when `config/puma.rb`
265
- carries the plugin (`--local` adds it):
297
+ carries the plugin (the embedded install adds it):
266
298
 
267
299
  ```ruby
268
- plugin :railwatch if defined?(Railwatch)
300
+ plugin :railwatch
269
301
  ```
270
302
 
303
+ Leave it unconditional: `bundle exec puma` reads this file before it
304
+ loads the app, so `if defined?(Railwatch)` would be false there and no
305
+ writer would start. The plugin does nothing when Railwatch is off or not
306
+ in embedded mode.
307
+
271
308
  Every web worker keeps its reporter thread, but instead of writing
272
309
  SQLite it hands each batch to the writer over a Unix socket
273
310
  (`tmp/sockets/railwatch-writer.sock`, `RAILWATCH_WRITER_SOCKET`). The
@@ -319,6 +356,17 @@ through `Railwatch.on_unrecoverable`, so loss is never silent. A Puma
319
356
  phased restart stops the writer and starts a fresh one once the new
320
357
  workers are up.
321
358
 
359
+ Stopping the writer is bounded. Puma sends it TERM and waits up to
360
+ `c.shutdown_timeout` (2 seconds) for it to exit, the same allowance it
361
+ gives its own reporter, then kills it. A writer killed mid-batch loses
362
+ nothing: the transaction rolls back and the worker retries that batch by
363
+ id against the next writer, so waiting longer for its drain would buy no
364
+ data. The bound is what keeps Puma's exit short when the writer is wedged
365
+ in a SQLite write or on a full disk. It counts against the container's
366
+ stop grace (Docker's default is 10 seconds; Kamal's `stop_timeout` sets
367
+ it), and `RAILWATCH_SHUTDOWN_TIMEOUT` raises it for an app whose grace
368
+ allows more.
369
+
322
370
  ## Maintenance
323
371
 
324
372
  Railwatch needs no job worker and nothing in `config/recurring.yml`.
@@ -371,7 +419,7 @@ Railwatch.configure do |c|
371
419
  c.retention_days = 7 # RAILWATCH_RETENTION_DAYS
372
420
  c.http_basic_auth_enabled = true # RAILWATCH_HTTP_BASIC_AUTH_ENABLED; credentials from Rails credentials or env
373
421
  c.base_controller_class = "ActionController::Base" # RAILWATCH_BASE_CONTROLLER_CLASS
374
- c.dashboard_open = false # RAILWATCH_DASHBOARD_OPEN; "yes, public, on purpose"
422
+ c.dashboard_open = false # RAILWATCH_DASHBOARD_OPEN; true makes it public on purpose
375
423
  c.dashboard_user = ->(request) { ... }
376
424
  end
377
425
  ```
@@ -383,8 +431,11 @@ a busy app.
383
431
  ## Deploys
384
432
 
385
433
  `bin/rails railwatch:deploy` records the marker in the app's own
386
- database instead of posting it, and the Kamal post-deploy hook does the
387
- same when `RAILWATCH_TRANSPORT=local` is set on the deployer.
434
+ database instead of posting it. The Kamal post-deploy hook runs on the
435
+ deployer, which does not read your initializer, so it only knows the
436
+ install is embedded when `RAILWATCH_TRANSPORT=local` is set in the
437
+ deployer's environment. Then it runs `railwatch:deploy` inside the
438
+ primary container.
388
439
 
389
440
  ## Storage and overhead
390
441
 
@@ -445,6 +496,8 @@ never need to run it again.
445
496
 
446
497
  ## Switching to the cloud later
447
498
 
448
- Set a token and drop `c.transport = :local` (or set
499
+ To keep the local dashboard and add the cloud, turn on
500
+ [export](#three-ways-to-run-it). To move to the cloud entirely, set a
501
+ token and drop `c.transport = :local` (or set
449
502
  `RAILWATCH_TRANSPORT=http`). The local databases can stay; the dashboard
450
503
  at `/railwatch` keeps reading what is there until it is pruned.
data/docs/faq.md CHANGED
@@ -51,20 +51,26 @@ A second gate, `bench/no_db_writes.rb`, drives 200 requests and a job with
51
51
  a `sql.active_record` subscriber watching for any `INSERT`/`UPDATE`/
52
52
  `DELETE` issued from a frame inside `lib/railwatch`, and fails if it finds
53
53
  one. **Railwatch never writes to your application's database.** Records
54
- live in memory and are shipped by a background thread. That is not a
55
- nicety: instrumentation that takes a write lock is what turns a
54
+ live in memory and are delivered by a background thread, to the cloud or
55
+ to the embedded writer process, which owns its own two SQLite files. That
56
+ is not a nicety: instrumentation that takes a write lock is what turns a
56
57
  single-writer SQLite app into a "database is locked" incident.
57
58
 
58
59
  ## Where does the data go, and how long is it kept?
59
60
 
60
- To the platform, over one gzip-NDJSON POST to `{ingest_url}/ingest` per
61
+ Embedded (the default): into the app's own `railwatch_telemetry` SQLite
62
+ database under `storage/`, written by one writer process Puma forks.
63
+ Raw rows are pruned nightly to `retention_days` (7 by default), and
64
+ hourly rollups back the charts. Nothing leaves the machine unless you
65
+ turn on export. See [Embedded mode](embedded.md).
66
+
67
+ Railwatch Cloud: one gzip-NDJSON POST to `{ingest_url}/ingest` per
61
68
  batch. The platform stores each monitored environment's telemetry in its
62
69
  own database, prunes raw rows on a retention window, and keeps hourly
63
- rollups for the charts.
64
-
65
- Retention is set by the account's plan tier, not by the gem — 7, 30, or
66
- 90 days depending on the plan. For a self-hosted install, retention,
67
- backups, and pruning are the platform operator's responsibility.
70
+ rollups for the charts. Retention there is set by the account's plan
71
+ tier, not by the gem — 7, 30, or 90 days depending on the plan. For a
72
+ self-hosted platform, retention, backups, and pruning are the operator's
73
+ responsibility.
68
74
 
69
75
  ## What about PII?
70
76
 
@@ -119,8 +125,9 @@ Yes, on both sides, and it's the first-class target.
119
125
 
120
126
  **In your app:** the gem does no I/O on the request path and never writes
121
127
  to the app database, so there is no contention with SQLite's single
122
- writer. SQL normalization is per adapter, so SQLite, Postgres, MySQL, and
123
- Trilogy all group correctly.
128
+ writer. Embedded mode's own two databases are separate SQLite files,
129
+ whatever the app runs on. SQL normalization is per adapter, so SQLite,
130
+ Postgres, MySQL, and Trilogy all group correctly.
124
131
 
125
132
  **On the platform:** telemetry is stored one SQLite database per
126
133
  monitored environment. That is what makes retention pruning, backup, and
@@ -163,6 +170,11 @@ oldest-job age on `health` records.
163
170
 
164
171
  ## What happens when the platform is unreachable?
165
172
 
173
+ This section is about the cloud transport. In embedded mode the
174
+ equivalent is the writer process being down, covered in
175
+ [Embedded mode](embedded.md#the-writer-process), and export keeps its
176
+ own durable queue.
177
+
166
178
  Nothing, from your app's point of view. This is the property everything
167
179
  else is built around: **delivery never raises into application code.**
168
180
 
@@ -182,10 +194,10 @@ being silent.
182
194
  A queue holding more than one batch is delivered as several batches: the
183
195
  tail is put back for the next flush rather than dropped.
184
196
 
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
197
+ A background thread drains the buffer and POSTs, one attempt per delivery.
198
+ If it fails, the batch and its drop counter go back into the bounded
199
+ buffer; a raised network error, **402**, **408**, **429**, and all **5xx**
200
+ responses are retained the same way. So is a **2xx
189
201
  that cannot acknowledge the batch** — a proxy's HTML sign-in page, malformed
190
202
  JSON, or `accepted`/`rejected` counts that do not cover what was sent — which
191
203
  would otherwise be a silent drop. The reporter
@@ -217,7 +229,8 @@ already appear on its own ingest batch. Those are visible under
217
229
  On shutdown, `at_exit` gives the thread `c.shutdown_timeout` (2 seconds) to
218
230
  attempt retained records immediately and retry within the remaining time.
219
231
  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
232
+ to `Railwatch.on_unrecoverable` (or, with no callback, to `Railwatch.debug`, and
233
+ to stderr by default, which `warn_on_data_loss = false` turns off). This is an
221
234
  in-memory buffer, not an on-disk spool: a hard kill, or exiting after that
222
235
  deadline, cannot carry those records into the next process. Railwatch never
223
236
  uses `Rails.logger` for its own failures, which would turn them into `log`