railwatch 0.3.5 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -2
- data/app/channels/railwatch/environment_channel.rb +17 -4
- data/app/models/railwatch/ingest/batch.rb +1 -1
- data/app/models/railwatch/ingest/mapper.rb +1 -3
- data/docs/embedded.md +17 -6
- data/docs/getting-started.md +0 -2
- data/docs/source-maps.md +1 -3
- data/lib/generators/railwatch/install/install_generator.rb +0 -8
- data/lib/railwatch/backtrace.rb +12 -2
- data/lib/railwatch/configuration.rb +6 -5
- data/lib/railwatch/engine.rb +2 -2
- data/lib/railwatch/export/client.rb +10 -3
- data/lib/railwatch/reporter.rb +2 -12
- data/lib/railwatch/source_maps.rb +13 -1
- data/lib/railwatch/spec_helper.rb +1 -1
- data/lib/railwatch/transport/http.rb +14 -5
- data/lib/railwatch/version.rb +1 -1
- data/public/railwatch/assets/{app-layout-CdJ5Qjms.js → app-layout-B5Fcdwfz.js} +1 -1
- data/public/railwatch/assets/{app-wordmark-B-0yZiDb.js → app-wordmark-1SRllgHv.js} +1 -1
- data/public/railwatch/assets/{appearance-CU28GBf1.js → appearance-DbwahDrc.js} +1 -1
- data/public/railwatch/assets/{arrow-up-DY1fS0tD.js → arrow-up-DwKQcab4.js} +1 -1
- data/public/railwatch/assets/{auth-layout-C3FKuUJz.js → auth-layout-DXfxXAN_.js} +1 -1
- data/public/railwatch/assets/{badge-DKvItdlb.js → badge-CApjyWcg.js} +1 -1
- data/public/railwatch/assets/{braces-Bse5Gh3K.js → braces-kGSgxWnS.js} +1 -1
- data/public/railwatch/assets/{card-C9j_ffR9.js → card-BaJ8kNkp.js} +1 -1
- data/public/railwatch/assets/{chart-Cmh6JEww.js → chart-Xnrz1Nqw.js} +1 -1
- data/public/railwatch/assets/{chart-hover-B0SjyWFy.js → chart-hover-XYPNE2CX.js} +1 -1
- data/public/railwatch/assets/{chart-panel-DKNxoauI.js → chart-panel-BYk68mJJ.js} +1 -1
- data/public/railwatch/assets/{checkbox-BsBkRg5n.js → checkbox-Bf41R8W6.js} +1 -1
- data/public/railwatch/assets/{code-C879Qwgg.js → code-a-9sh36A.js} +1 -1
- data/public/railwatch/assets/{copy-block-DtO_3esq.js → copy-block-zykZWQym.js} +1 -1
- data/public/railwatch/assets/{copy-id-SeLCKsW2.js → copy-id-b6CW5ESL.js} +1 -1
- data/public/railwatch/assets/{cursor-load-more-OpHbe_kE.js → cursor-load-more-0cHzbTPT.js} +1 -1
- data/public/railwatch/assets/{data-table-Bc3ZRnpb.js → data-table-IvA7xvrK.js} +1 -1
- data/public/railwatch/assets/{edit-DDvf9mkq.js → edit-7lmf3k80.js} +1 -1
- data/public/railwatch/assets/{edit-BMpgcQhU.js → edit-DTtRV1fO.js} +1 -1
- data/public/railwatch/assets/{edit-CTD1jSWh.js → edit-ZKE15r-F.js} +1 -1
- data/public/railwatch/assets/{empty-state-CkPROZB0.js → empty-state-BFTnQ2oT.js} +1 -1
- data/public/railwatch/assets/{env-layout-aGQgTZRg.js → env-layout-erSDpIXg.js} +1 -1
- data/public/railwatch/assets/{execution-path-BX5NcXUZ.js → execution-path-Di5uUHi-.js} +1 -1
- data/public/railwatch/assets/{filter-bar-C0O2rJVr.js → filter-bar-Btm__gQi.js} +1 -1
- data/public/railwatch/assets/{flamegraph-Vj4QSAlT.js → flamegraph-C-mZdqa7.js} +1 -1
- data/public/railwatch/assets/{frames-CTiGwm4E.js → frames-KZBsjg6-.js} +1 -1
- data/public/railwatch/assets/{google-sign-in-button-BzogBGeF.js → google-sign-in-button-DJRoD72g.js} +1 -1
- data/public/railwatch/assets/{index-chS28dvS.js → index-9LL_C3eG.js} +1 -1
- data/public/railwatch/assets/{index-C__S10ze.js → index-B-bSm1jB.js} +1 -1
- data/public/railwatch/assets/{index-Dy7Wg880.js → index-B0VXzXYG.js} +1 -1
- data/public/railwatch/assets/{index-B-kuWp-m.js → index-BAkZNNEh.js} +1 -1
- data/public/railwatch/assets/{index-CPQKOQxV.js → index-BWSyQRZI.js} +1 -1
- data/public/railwatch/assets/{index-FgpFDA1Z.js → index-Bjw4sTXU.js} +1 -1
- data/public/railwatch/assets/{index-D9cV-IQr.js → index-Bo8hrjbs.js} +1 -1
- data/public/railwatch/assets/{index-BCLvKVhP.js → index-BoTOVM0v.js} +1 -1
- data/public/railwatch/assets/{index-y2c8xKzI.js → index-Bot7kHMA.js} +1 -1
- data/public/railwatch/assets/{index-DbyRgbzQ.js → index-BsrCnQqg.js} +1 -1
- data/public/railwatch/assets/{index-BKgAm-w2.js → index-BtSlLmkg.js} +1 -1
- data/public/railwatch/assets/{index-DigUOHQ8.js → index-CAzQkw-h.js} +1 -1
- data/public/railwatch/assets/{index-ChQ1_u8u.js → index-CBJZQgHA.js} +1 -1
- data/public/railwatch/assets/{index-uuKg6Zqn.js → index-CIUl9POV.js} +1 -1
- data/public/railwatch/assets/{index-DepurF6s.js → index-CP6jrrPT.js} +1 -1
- data/public/railwatch/assets/{index-Bvy021Vv.js → index-CU6yhEyt.js} +1 -1
- data/public/railwatch/assets/{index-B2_QluAw.js → index-CX8Br7m9.js} +1 -1
- data/public/railwatch/assets/{index-Dcxd_H-U.js → index-CYYmZpZu.js} +1 -1
- data/public/railwatch/assets/{index-DxuDLoTc.js → index-CeqMDByC.js} +1 -1
- data/public/railwatch/assets/{index-D3Axxkk2.js → index-Cj3_jD1E.js} +1 -1
- data/public/railwatch/assets/{index-D5YVcrqR.js → index-D4968-TC.js} +1 -1
- data/public/railwatch/assets/{index-DkgImCOg.js → index-DAEzO_vq.js} +1 -1
- data/public/railwatch/assets/{index-CtT4T6kp.js → index-DBTxQw77.js} +1 -1
- data/public/railwatch/assets/{index-CCLMaY9k.js → index-DCAho97K.js} +1 -1
- data/public/railwatch/assets/{index-jSEU_Yp7.js → index-DEfu_YvU.js} +1 -1
- data/public/railwatch/assets/{index-CY6lbwAC.js → index-DXUwhSCQ.js} +1 -1
- data/public/railwatch/assets/{index-Wb_U2_hz.js → index-DcItJxa5.js} +1 -1
- data/public/railwatch/assets/{index-CD4HIHq0.js → index-EGU__DOB.js} +1 -1
- data/public/railwatch/assets/{index-LaTKDe0-.js → index-HCJmFcZu.js} +1 -1
- data/public/railwatch/assets/{index-BRN_bXV_.js → index-P_e_cI0O.js} +1 -1
- data/public/railwatch/assets/{index-Ble4RoA3.js → index-QyHE9WsU.js} +1 -1
- data/public/railwatch/assets/{index-B69Iad2U.js → index-Z1c4nFL0.js} +1 -1
- data/public/railwatch/assets/{index-z2YfKBJl.js → index-kNhZPLVj.js} +1 -1
- data/public/railwatch/assets/{index-CxU1Jj35.js → index-uHBVO8Lh.js} +1 -1
- data/public/railwatch/assets/{index-Dep2XomH.js → index-wOj5j0KC.js} +1 -1
- data/public/railwatch/assets/{inertia-CgBdJDaE.js → inertia-Degv_G9N.js} +2 -2
- data/public/railwatch/assets/{input-error-DfpIawjl.js → input-error-C6SqO_iU.js} +1 -1
- data/public/railwatch/assets/{json-viewer-CH7nEIUs.js → json-viewer-DcQIxT86.js} +1 -1
- data/public/railwatch/assets/{klass-BTg1l2AO.js → klass-WgQEfWpo.js} +1 -1
- data/public/railwatch/assets/{label-BWgQzHCO.js → label-B_vK19tR.js} +1 -1
- data/public/railwatch/assets/{layout-neknbxn-.js → layout-C8q7GUlZ.js} +1 -1
- data/public/railwatch/assets/{live-dot-Bejk0a5o.js → live-dot-DaXEbkXo.js} +1 -1
- data/public/railwatch/assets/{nav-C1M8941-.js → nav-BnD5XArZ.js} +1 -1
- data/public/railwatch/assets/{new-BnGIYM37.js → new-BL9OqXHe.js} +1 -1
- data/public/railwatch/assets/{new-DGYPcz7w.js → new-BhRnQn4w.js} +1 -1
- data/public/railwatch/assets/{new-DElC-JCS.js → new-BqP0NhOF.js} +1 -1
- data/public/railwatch/assets/{new-bBwawsEg.js → new-CWP3wtwt.js} +1 -1
- data/public/railwatch/assets/{new-B8CzyoMu.js → new-DIjwu2jF.js} +1 -1
- data/public/railwatch/assets/{new-DLgCLBiW.js → new-uxMnBtoY.js} +1 -1
- data/public/railwatch/assets/{onboarding-CW0jUhHT.js → onboarding-C_Y3xTsH.js} +1 -1
- data/public/railwatch/assets/{origin-identity-bJMxeEHB.js → origin-identity-CNhOnfBa.js} +1 -1
- data/public/railwatch/assets/{percentile-picker-DLAnTKY3.js → percentile-picker-BujVEhP6.js} +1 -1
- data/public/railwatch/assets/{relative-time-BHbJh8HW.js → relative-time-6CoKr1lN.js} +1 -1
- data/public/railwatch/assets/{release-health-2O1GRclB.js → release-health-1Qzc_XhT.js} +1 -1
- data/public/railwatch/assets/{route-DSw8aIZa.js → route-xqgYjOwJ.js} +1 -1
- data/public/railwatch/assets/{segmented-Dg8CES94.js → segmented-CVKq2Xpa.js} +1 -1
- data/public/railwatch/assets/{select-CqKpvI98.js → select-CsmSXsJ4.js} +1 -1
- data/public/railwatch/assets/{separator-BOp2LHw3.js → separator-CrM10Oot.js} +1 -1
- data/public/railwatch/assets/{series-chart-COEtj3PY.js → series-chart-D33UhdG2.js} +1 -1
- data/public/railwatch/assets/{show-DrhEDGPn.js → show-8H9WAGav.js} +1 -1
- data/public/railwatch/assets/{show-dRDz8OVu.js → show-9jDeBNpc.js} +1 -1
- data/public/railwatch/assets/{show-C-xpDkoj.js → show-B0nHy31L.js} +1 -1
- data/public/railwatch/assets/{show-B7Zqjlqc.js → show-B3izvRdU.js} +1 -1
- data/public/railwatch/assets/{show-CGv85sRo.js → show-B7NhcWgp.js} +1 -1
- data/public/railwatch/assets/{show-Zr2xV4OD.js → show-BM-E2iYc.js} +1 -1
- data/public/railwatch/assets/{show-B40560ZT.js → show-BlNR3ZOZ.js} +1 -1
- data/public/railwatch/assets/{show-C9Y1To9_.js → show-CJMeX75E.js} +1 -1
- data/public/railwatch/assets/{show-BAxMoNvw.js → show-CMisHNAI.js} +1 -1
- data/public/railwatch/assets/{show-HjjzcKRq.js → show-CQ4Ze-Un.js} +1 -1
- data/public/railwatch/assets/{show-CPh-7VGL.js → show-CvNV23gH.js} +1 -1
- data/public/railwatch/assets/{show-DqtE0LI6.js → show-DMZlwzL_.js} +1 -1
- data/public/railwatch/assets/{show-C_9M2nLy.js → show-DUwQlohF.js} +1 -1
- data/public/railwatch/assets/{show-DFlP1ktM.js → show-DZwcZZrz.js} +1 -1
- data/public/railwatch/assets/{show-B_QVoqKE.js → show-ixK2r2f1.js} +1 -1
- data/public/railwatch/assets/{show-pbkvGRER.js → show-rysBbvt-.js} +1 -1
- data/public/railwatch/assets/{show-Bbs1i46N.js → show-zxVxhUel.js} +1 -1
- data/public/railwatch/assets/{sort-header-DBxQ_KFX.js → sort-header-CKj_7G3U.js} +1 -1
- data/public/railwatch/assets/{sparkline-cell-BbfXw3kA.js → sparkline-cell-Ckg29yVq.js} +1 -1
- data/public/railwatch/assets/{stat-Cd8QaTGw.js → stat-TxvoBiC8.js} +1 -1
- data/public/railwatch/assets/{status-badge-Ys4b0fcG.js → status-badge-5NCVJHjo.js} +1 -1
- data/public/railwatch/assets/{tenant-path-CQWzaNYR.js → tenant-path-B5ZRK4x3.js} +1 -1
- data/public/railwatch/assets/{text-link-BY8FY-CJ.js → text-link-BPllRj0F.js} +1 -1
- data/public/railwatch/assets/{textarea-Bq2jjOMC.js → textarea-DtD5yZp_.js} +1 -1
- data/public/railwatch/assets/{timeline-SxStCFO2.js → timeline-DZz5ZnHN.js} +1 -1
- data/public/railwatch/assets/{transition-CQ9yvfPa.js → transition-Ds6Cakuc.js} +1 -1
- data/public/railwatch/assets/{use-clipboard-3hTXU91g.js → use-clipboard-BLjjYH8V.js} +1 -1
- data/public/railwatch/assets/{use-live-LwJStJvD.js → use-live-GKze1mNC.js} +1 -1
- data/public/railwatch/manifest.json +1285 -1285
- metadata +115 -115
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7670085bcbae37c58def37c8e23ffee964be9277a57462869346017016c33a3c
|
|
4
|
+
data.tar.gz: fb68d72c0863778eb607cfd2bded53714651caaecffefbff20994e25422a1411
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 429bb5789a56059dce9848403622511df0f5094c2efe4a6ed8c51e5086fb11eaf56b8c762ee9ae68d1ae393a5fae7af65d628df019f01b00466644e207aa6b1d
|
|
7
|
+
data.tar.gz: 0c38280c362f3529ca2af1e93a6982d77ed15a872351072727dd1152396ea2c3f1a2e634f1132a6cac83bf0ee152f818fda15db76e2251a3f0f1a7a24db5bfb1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.6 (2026-09-20)
|
|
4
|
+
|
|
5
|
+
- Harden embedded live-stream authorization and telemetry delivery. Bound
|
|
6
|
+
source-file access and HTTP response reads.
|
|
7
|
+
- Simplify prelaunch interfaces: use prompt, stdin, or environment token input;
|
|
8
|
+
pass the complete transport delivery metadata; use the current process-version
|
|
9
|
+
field. Update public install examples and remove obsolete upgrade wording.
|
|
10
|
+
|
|
3
11
|
## 0.3.5 (2026-09-20)
|
|
4
12
|
|
|
5
13
|
- Make the embedded telemetry database give disk back. `PruneTelemetryJob`
|
|
@@ -676,8 +684,7 @@ First public release.
|
|
|
676
684
|
- The installer now prefers a hidden prompt, stdin, or `RAILWATCH_TOKEN`, never
|
|
677
685
|
prints token values, and refuses to put a token in a tracked or non-ignored
|
|
678
686
|
`.env`. `railwatch:doctor` also fails when it finds a plaintext `lt_...` token
|
|
679
|
-
in likely secret-bearing files tracked by Git.
|
|
680
|
-
remains compatible but warns about shell-history and process-list exposure.
|
|
687
|
+
in likely secret-bearing files tracked by Git.
|
|
681
688
|
|
|
682
689
|
- A forked worker profiles again. `Process._fork` now resets the profiler's
|
|
683
690
|
process-global state in the child: it used to inherit `@running` holding
|
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
module Railwatch
|
|
4
4
|
# Live dashboard updates: Ingest::Batch broadcasts "ingested" on
|
|
5
|
-
#
|
|
5
|
+
# railwatch:environment:<id> after each write (throttled to one per 2 s), and the
|
|
6
6
|
# page reloads its props. Under HTTP Basic the browser sends the same
|
|
7
7
|
# Authorization header on the WebSocket handshake, so the channel checks
|
|
8
8
|
# the same credentials as the pages.
|
|
9
9
|
#
|
|
10
10
|
# With Basic off, Configuration#dashboard_channel_allowed? asks whichever
|
|
11
11
|
# gate the host declared: a dashboard_user resolver can refuse this
|
|
12
|
-
# request,
|
|
13
|
-
#
|
|
12
|
+
# request, or dashboard_open explicitly allows public live updates.
|
|
13
|
+
# A named base controller alone cannot authorize a channel. That last case matters
|
|
14
14
|
# because Action Cable runs on the host's own /cable endpoint: a routes
|
|
15
15
|
# constraint around the engine's mount does not cover it and a base
|
|
16
16
|
# controller cannot reach it, so "something in front of /railwatch" is not
|
|
@@ -19,7 +19,9 @@ module Railwatch
|
|
|
19
19
|
class EnvironmentChannel < ActionCable::Channel::Base
|
|
20
20
|
def subscribed
|
|
21
21
|
if params[:id].to_i == Environment::ID && Railwatch.config.dashboard_channel_allowed?(connection_request)
|
|
22
|
-
stream_from "
|
|
22
|
+
stream_from "railwatch:environment:#{Environment::ID}", coder: ActiveSupport::JSON do |message|
|
|
23
|
+
transmit_authorized(message)
|
|
24
|
+
end
|
|
23
25
|
else
|
|
24
26
|
reject
|
|
25
27
|
end
|
|
@@ -27,6 +29,17 @@ module Railwatch
|
|
|
27
29
|
|
|
28
30
|
private
|
|
29
31
|
|
|
32
|
+
# A host's resolver can revoke an operator while their socket is open.
|
|
33
|
+
def transmit_authorized(message)
|
|
34
|
+
if Railwatch.config.dashboard_channel_allowed?(connection_request)
|
|
35
|
+
transmit message
|
|
36
|
+
else
|
|
37
|
+
stop_all_streams
|
|
38
|
+
reject
|
|
39
|
+
reject_subscription
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
30
43
|
# Built from the connection's env rather than asking it for its request:
|
|
31
44
|
# ActionCable::Connection::Base#request is private (Rails documents it for
|
|
32
45
|
# use inside a Connection subclass, not from a channel), so the obvious
|
|
@@ -340,7 +340,7 @@ module Railwatch
|
|
|
340
340
|
return if last && now - last < THROTTLE_WINDOW
|
|
341
341
|
|
|
342
342
|
LAST_BROADCAST_AT[@environment.id] = now
|
|
343
|
-
ActionCable.server.broadcast("
|
|
343
|
+
ActionCable.server.broadcast("railwatch:environment:#{@environment.id}", { event: "ingested", at: now.iso8601, counts: @counts })
|
|
344
344
|
rescue StandardError, LoadError => e
|
|
345
345
|
# A live refresh ping is not worth failing a written batch over.
|
|
346
346
|
# LoadError too: a host whose production cable adapter is redis with
|
|
@@ -417,9 +417,7 @@ module Railwatch
|
|
|
417
417
|
[ Telemetry::Process, {
|
|
418
418
|
booted_at: timestamp_string(rec["timestamp"]), pid: rec["pid"], role: rec["role"], server: rec["server"],
|
|
419
419
|
deploy: rec["deploy"], ruby_version: rec["ruby_version"], rails_version: rec["rails_version"],
|
|
420
|
-
|
|
421
|
-
# by one are still readable.
|
|
422
|
-
railwatch_version: rec["railwatch_version"] || rec["lantern_version"], boot_seconds: rec["boot_seconds"],
|
|
420
|
+
railwatch_version: rec["railwatch_version"], boot_seconds: rec["boot_seconds"],
|
|
423
421
|
detail: rec.slice("app", "environment", "database_adapter", "queue_adapter", "cache_store")
|
|
424
422
|
} ]
|
|
425
423
|
end
|
data/docs/embedded.md
CHANGED
|
@@ -165,7 +165,11 @@ c.base_controller_class = "AdminController" # requires an admin, or redirects
|
|
|
165
165
|
```
|
|
166
166
|
|
|
167
167
|
Your controller's code runs inside the engine, whose route helpers take
|
|
168
|
-
precedence; reach your app's with `main_app.root_path`.
|
|
168
|
+
precedence; reach your app's with `main_app.root_path`. The controller does
|
|
169
|
+
not run for Action Cable subscriptions: live updates stay closed unless
|
|
170
|
+
`dashboard_user` also authorizes the connection (as below). Return `nil` or
|
|
171
|
+
`false` for unauthorized users. The resolver names the operator on pages;
|
|
172
|
+
keep the controller or mount constraint as the page authorization gate.
|
|
169
173
|
|
|
170
174
|
Or a routes constraint, which keeps the engine out of it entirely (for
|
|
171
175
|
example with the sessions Rails' authentication generator creates):
|
|
@@ -183,7 +187,11 @@ purpose:
|
|
|
183
187
|
|
|
184
188
|
```ruby
|
|
185
189
|
c.http_basic_auth_enabled = false
|
|
186
|
-
|
|
190
|
+
# The mount constraint does not cover /cable. Authorize live updates too:
|
|
191
|
+
c.dashboard_user = ->(request) {
|
|
192
|
+
user = Session.find_by(id: request.cookie_jar.signed[:session_id])&.user
|
|
193
|
+
{ id: user.id, name: user.name } if user&.admin?
|
|
194
|
+
}
|
|
187
195
|
```
|
|
188
196
|
|
|
189
197
|
### Deliberately public
|
|
@@ -201,7 +209,8 @@ With Basic off and none of `base_controller_class`, `dashboard_user` or
|
|
|
201
209
|
forgotten one. It serves the dashboard (a routes constraint it cannot see
|
|
202
210
|
is a legitimate answer) but logs a warning at every boot outside
|
|
203
211
|
development, `railwatch:doctor` reports the gate as undeclared, and live
|
|
204
|
-
updates are refused.
|
|
212
|
+
updates are refused. A custom base controller declares the page gate only;
|
|
213
|
+
live updates still require their own authorization as described below.
|
|
205
214
|
|
|
206
215
|
### Live updates and `/cable`
|
|
207
216
|
|
|
@@ -209,9 +218,11 @@ Action Cable runs on your application's own `/cable` endpoint, not under
|
|
|
209
218
|
the engine's mount, so a routes constraint around `/railwatch` does not
|
|
210
219
|
cover it and a base controller cannot reach it. The live-update channel
|
|
211
220
|
therefore follows what you declared: HTTP Basic credentials are checked
|
|
212
|
-
|
|
213
|
-
`dashboard_open`
|
|
214
|
-
|
|
221
|
+
from the WebSocket handshake, a `dashboard_user` resolver is consulted,
|
|
222
|
+
or `dashboard_open` explicitly permits public live updates. A custom
|
|
223
|
+
`base_controller_class` authorizes pages only and never permits a Cable
|
|
224
|
+
subscription by itself. The gate is rechecked before each live update;
|
|
225
|
+
revocation removes the subscription and notifies the client. The channel carries an ingest ping (a
|
|
215
226
|
timestamp and per-type counts) and never telemetry records.
|
|
216
227
|
|
|
217
228
|
### Naming the operator, and what an id may be
|
data/docs/getting-started.md
CHANGED
|
@@ -36,8 +36,6 @@ 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
|
-
The legacy `--token=` flag warns because command arguments can be visible in
|
|
40
|
-
shell history and process listings.
|
|
41
39
|
- A token is written to `.env` only when Git confirms that `.env` is ignored.
|
|
42
40
|
A tracked or non-ignored dotenv file is refused; use Rails credentials, a
|
|
43
41
|
deployment secret manager, or add `.env` to `.gitignore` first. Token values
|
data/docs/source-maps.md
CHANGED
|
@@ -48,9 +48,7 @@ New default browser issue fingerprints use the resolved original location
|
|
|
48
48
|
when a map is already available. Custom fingerprints are preserved. Late
|
|
49
49
|
uploads improve existing stack displays, Copy for AI, API issue details and
|
|
50
50
|
MCP `get_issue`, while existing occurrences keep their original grouping.
|
|
51
|
-
Missing maps or columns fall back to the raw stack.
|
|
52
|
-
new browser occurrences to include columns; older gem versions discarded
|
|
53
|
-
them and cannot resolve minified locations accurately.
|
|
51
|
+
Missing maps or columns fall back to the raw stack.
|
|
54
52
|
|
|
55
53
|
For a custom release uploader, POST the raw `.map` bytes to
|
|
56
54
|
`/ingest/sourcemaps` with `Content-Type: application/octet-stream`,
|
|
@@ -13,8 +13,6 @@ module Railwatch
|
|
|
13
13
|
|
|
14
14
|
class_option :local, type: :boolean, default: false,
|
|
15
15
|
desc: "Keep telemetry in this app and serve the dashboard at /railwatch: no token, no cloud."
|
|
16
|
-
class_option :token, type: :string,
|
|
17
|
-
desc: "Deprecated: token in process arguments. Prefer --prompt-token, --token-stdin, or RAILWATCH_TOKEN."
|
|
18
16
|
class_option :prompt_token, type: :boolean, default: false,
|
|
19
17
|
desc: "Prompt for the ingest token without echoing it."
|
|
20
18
|
class_option :token_stdin, type: :boolean, default: false,
|
|
@@ -167,10 +165,6 @@ module Railwatch
|
|
|
167
165
|
return if options[:local]
|
|
168
166
|
|
|
169
167
|
token = resolved_token
|
|
170
|
-
if options[:token]
|
|
171
|
-
say("--token exposes #{Railwatch::SecretSafety.token_preview(options[:token])} in process arguments; " \
|
|
172
|
-
"use --prompt-token or --token-stdin next time.", :yellow)
|
|
173
|
-
end
|
|
174
168
|
vars = { TOKEN_VAR => token, URL_VAR => options[:url] }.compact
|
|
175
169
|
return if vars.empty?
|
|
176
170
|
return say(env_instructions(vars), :yellow) unless dotenv_app?
|
|
@@ -463,8 +457,6 @@ module Railwatch
|
|
|
463
457
|
ask("Railwatch ingest token (input hidden):", echo: false)
|
|
464
458
|
elsif options[:token_stdin]
|
|
465
459
|
$stdin.gets
|
|
466
|
-
elsif options[:token]
|
|
467
|
-
options[:token]
|
|
468
460
|
elsif !ENV[TOKEN_VAR].to_s.empty?
|
|
469
461
|
ENV[TOKEN_VAR]
|
|
470
462
|
end
|
data/lib/railwatch/backtrace.rb
CHANGED
|
@@ -145,9 +145,19 @@ module Railwatch
|
|
|
145
145
|
url.start_with?("#{origin}/")
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
MAX_SOURCE_BYTES = 1024 * 1024
|
|
149
|
+
|
|
148
150
|
def source_snippet(path, line, context: 5)
|
|
149
|
-
return nil unless path
|
|
150
|
-
|
|
151
|
+
return nil unless path
|
|
152
|
+
# String backtraces can be assigned by libraries. A lexical Rails.root
|
|
153
|
+
# prefix alone allows ../ and symlinks to disclose files outside it.
|
|
154
|
+
root = File.realpath(app_root) + File::SEPARATOR
|
|
155
|
+
source = File.realpath(path)
|
|
156
|
+
return nil unless source.start_with?(root) && File.file?(source)
|
|
157
|
+
|
|
158
|
+
data = File.binread(source, MAX_SOURCE_BYTES + 1)
|
|
159
|
+
return nil if data.bytesize > MAX_SOURCE_BYTES
|
|
160
|
+
lines = data.force_encoding(Encoding::UTF_8).scrub.lines(chomp: true)
|
|
151
161
|
from = [ line - context - 1, 0 ].max
|
|
152
162
|
to = [ line + context - 1, lines.size - 1 ].min
|
|
153
163
|
(from..to).to_h { |i| [ i + 1, lines[i].to_s[0, 200] ] }
|
|
@@ -407,12 +407,13 @@ module Railwatch
|
|
|
407
407
|
# engine's mount does not cover and a base controller cannot reach, so an
|
|
408
408
|
# undeclared gate refuses rather than assuming.
|
|
409
409
|
def dashboard_channel_allowed?(request)
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
when :open, :controller then true
|
|
414
|
-
else false
|
|
410
|
+
return http_basic_auth_ok?(request) if http_basic_auth_enabled
|
|
411
|
+
if dashboard_user
|
|
412
|
+
return !!(dashboard_user.respond_to?(:call) ? dashboard_user.call(request) : dashboard_user)
|
|
415
413
|
end
|
|
414
|
+
|
|
415
|
+
# A controller name is not evidence of authorization on /cable.
|
|
416
|
+
!!dashboard_open
|
|
416
417
|
end
|
|
417
418
|
|
|
418
419
|
def resolve_dashboard_user(request)
|
data/lib/railwatch/engine.rb
CHANGED
|
@@ -105,8 +105,8 @@ module Railwatch
|
|
|
105
105
|
Rails.logger.warn(
|
|
106
106
|
"[railwatch] the dashboard at the engine's mount has no gate this gem can see: HTTP Basic is off and " \
|
|
107
107
|
"no base_controller_class, dashboard_user or dashboard_open is set. If a routes constraint or your " \
|
|
108
|
-
"network already gates it, set `c.dashboard_open = true`
|
|
109
|
-
"
|
|
108
|
+
"network already gates it, authorize /cable separately with `c.dashboard_user`, or set `c.dashboard_open = true` " \
|
|
109
|
+
"only when the network protects both endpoints. Otherwise anyone who can reach the dashboard can read every query, log line and exception this app records."
|
|
110
110
|
)
|
|
111
111
|
end
|
|
112
112
|
end
|
|
@@ -22,7 +22,14 @@ module Railwatch
|
|
|
22
22
|
# Not `send`: shadowing Object#send on a class makes it impossible to
|
|
23
23
|
# reach the real one, and reads as a coincidence rather than a verb.
|
|
24
24
|
def deliver(claim, producer_id:)
|
|
25
|
-
|
|
25
|
+
# Snapshot once, then verify and use those exact bytes. Configuration
|
|
26
|
+
# can change after Outbox#claim! checked the destination binding.
|
|
27
|
+
token = @config.resolved_export_token.to_s.dup.freeze
|
|
28
|
+
unless Digest::SHA256.hexdigest(token) == claim.token_digest
|
|
29
|
+
return Outcome.new(disposition: :deferred, status: 401, reason: "export credential changed after claim")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
result = transport_for(claim, token: token).deliver_encoded(
|
|
26
33
|
body: claim.body, expected_count: claim.record_count, batch_id: claim.delivery_id,
|
|
27
34
|
dropped: claim.metadata.fetch("dropped", 0).to_i,
|
|
28
35
|
dropped_bytes: claim.metadata.fetch("dropped_bytes", 0).to_i,
|
|
@@ -54,11 +61,11 @@ module Railwatch
|
|
|
54
61
|
# otherwise post bytes admitted for one receiver to another one -- and,
|
|
55
62
|
# if they belong to different tenants, hand a customer's telemetry to
|
|
56
63
|
# somebody else while marking it delivered.
|
|
57
|
-
def transport_for(claim)
|
|
64
|
+
def transport_for(claim, token:)
|
|
58
65
|
key = [ claim.url, claim.token_digest ]
|
|
59
66
|
@transports[key] ||= begin
|
|
60
67
|
credentialed = @config.dup
|
|
61
|
-
credentialed.token =
|
|
68
|
+
credentialed.token = token
|
|
62
69
|
Transport::Http.new(credentialed, endpoint: claim.url)
|
|
63
70
|
end
|
|
64
71
|
end
|
data/lib/railwatch/reporter.rb
CHANGED
|
@@ -481,19 +481,9 @@ module Railwatch
|
|
|
481
481
|
dropped_bytes: batch.dropped_bytes + dropped_bytes, prepared: true)
|
|
482
482
|
end
|
|
483
483
|
|
|
484
|
-
# Third-party/test transports written before batch idempotency only accept
|
|
485
|
-
# `dropped:`. Keep those working while the HTTP transport receives the
|
|
486
|
-
# stable identity required to replay a request safely.
|
|
487
484
|
def deliver(batch)
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
accepts_dropped_bytes = parameters.any? { |kind, name| kind == :keyrest || name == :dropped_bytes }
|
|
491
|
-
accepts_backpressure = parameters.any? { |kind, name| kind == :keyrest || name == :backpressure_factor }
|
|
492
|
-
keywords = { dropped: batch.dropped }
|
|
493
|
-
keywords[:batch_id] = batch.id if accepts_batch_id
|
|
494
|
-
keywords[:dropped_bytes] = batch.dropped_bytes if accepts_dropped_bytes
|
|
495
|
-
keywords[:backpressure_factor] = @backpressure_factor if accepts_backpressure
|
|
496
|
-
@transport.deliver(batch.records, **keywords)
|
|
485
|
+
@transport.deliver(batch.records, dropped: batch.dropped, batch_id: batch.id,
|
|
486
|
+
dropped_bytes: batch.dropped_bytes, backpressure_factor: @backpressure_factor)
|
|
497
487
|
end
|
|
498
488
|
|
|
499
489
|
def update_backpressure
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
|
+
require "openssl"
|
|
4
5
|
require "uri"
|
|
5
6
|
require "pathname"
|
|
6
7
|
require "json"
|
|
@@ -10,6 +11,7 @@ module Railwatch
|
|
|
10
11
|
# during release preparation; it never follows map URLs or HTTP redirects.
|
|
11
12
|
class SourceMaps
|
|
12
13
|
MAX_BYTES = 10 * 1024 * 1024
|
|
14
|
+
MAX_RESPONSE_BYTES = 64 * 1024
|
|
13
15
|
|
|
14
16
|
def initialize(config)
|
|
15
17
|
@config = config
|
|
@@ -46,7 +48,17 @@ module Railwatch
|
|
|
46
48
|
request["X-Railwatch-Deploy"] = @config.deploy
|
|
47
49
|
request["X-Railwatch-Filename"] = filename
|
|
48
50
|
request.body = data
|
|
49
|
-
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https",
|
|
51
|
+
response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", verify_mode: OpenSSL::SSL::VERIFY_PEER,
|
|
52
|
+
open_timeout: 5, read_timeout: 30, write_timeout: 30) do |http|
|
|
53
|
+
http.request(request) do |result|
|
|
54
|
+
body = String.new(encoding: Encoding::BINARY)
|
|
55
|
+
result.read_body do |chunk|
|
|
56
|
+
raise "Source map response exceeds 64 KiB" if body.bytesize + chunk.bytesize > MAX_RESPONSE_BYTES
|
|
57
|
+
body << chunk
|
|
58
|
+
end
|
|
59
|
+
result.body = body
|
|
60
|
+
end
|
|
61
|
+
end
|
|
50
62
|
unless response.is_a?(Net::HTTPSuccess)
|
|
51
63
|
raise "Source map upload failed (HTTP #{response.code}) for #{filename}"
|
|
52
64
|
end
|
|
@@ -15,7 +15,7 @@ module Railwatch
|
|
|
15
15
|
class MemoryTransport
|
|
16
16
|
attr_reader :batches
|
|
17
17
|
def initialize = @batches = []
|
|
18
|
-
def deliver(records, dropped: 0, batch_id: nil)
|
|
18
|
+
def deliver(records, dropped: 0, batch_id: nil, **)
|
|
19
19
|
@batches << records
|
|
20
20
|
Transport::Http::Result.new(ok: true, status: 200, accepted: records.size, rejected: 0)
|
|
21
21
|
end
|
|
@@ -174,12 +174,22 @@ module Railwatch
|
|
|
174
174
|
# explicitly so a Ruby default change cannot silently weaken ingest.
|
|
175
175
|
options[:verify_mode] = OpenSSL::SSL::VERIFY_PEER if options[:use_ssl]
|
|
176
176
|
Net::HTTP.start(@uri.host, @uri.port, **options) do |http|
|
|
177
|
-
http.request(req)
|
|
177
|
+
http.request(req) do |response|
|
|
178
|
+
body = String.new(encoding: Encoding::BINARY)
|
|
179
|
+
response.read_body do |chunk|
|
|
180
|
+
if body.bytesize + chunk.bytesize > MAX_RESPONSE_BYTES
|
|
181
|
+
raise ResponseTooLarge, "ingest response is larger than #{MAX_RESPONSE_BYTES} bytes"
|
|
182
|
+
end
|
|
183
|
+
body << chunk
|
|
184
|
+
end
|
|
185
|
+
response.body = body
|
|
186
|
+
end
|
|
178
187
|
end
|
|
179
188
|
end
|
|
180
189
|
|
|
181
|
-
#
|
|
182
|
-
# error page
|
|
190
|
+
# Enforced while streaming, before Net::HTTP can buffer an unbounded
|
|
191
|
+
# proxy error page or decompressed response inside the monitored app.
|
|
192
|
+
class ResponseTooLarge < StandardError; end
|
|
183
193
|
MAX_RESPONSE_BYTES = 64 * 1024
|
|
184
194
|
# The longest delay we will take from a receiver, in either spelling.
|
|
185
195
|
MAX_RETRY_AFTER = 86_400
|
|
@@ -193,8 +203,7 @@ module Railwatch
|
|
|
193
203
|
end
|
|
194
204
|
end
|
|
195
205
|
|
|
196
|
-
#
|
|
197
|
-
# socket -- it buffers the whole response before we ever see it.
|
|
206
|
+
# Keep diagnostics smaller still than the bounded response body.
|
|
198
207
|
def summarize(body) = body.to_s.byteslice(0, 200).to_s.scrub
|
|
199
208
|
|
|
200
209
|
# Seconds, or an HTTP date. Anything else is not a delay we can trust,
|
data/lib/railwatch/version.rb
CHANGED