railwatch 0.3.6 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +60 -0
- data/{lib/generators/railwatch/install/templates → app/frontend/lib}/railwatch.ts +162 -42
- data/app/models/railwatch/ingest/batch.rb +1 -0
- data/app/models/railwatch/ingest/mapper.rb +12 -0
- data/docs/configuration.md +8 -3
- data/lib/generators/railwatch/install/install_generator.rb +5 -1
- data/lib/railwatch/patches/rake_task.rb +10 -1
- data/lib/railwatch/patches/runner_command.rb +3 -1
- data/lib/railwatch/transport/http.rb +15 -5
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch/wire_fixtures.json +856 -0
- data/lib/railwatch.rb +22 -0
- data/lib/tasks/railwatch_tasks.rake +7 -0
- data/public/railwatch/assets/{app-layout-B5Fcdwfz.js → app-layout-DUk54qUM.js} +1 -1
- data/public/railwatch/assets/{app-wordmark-1SRllgHv.js → app-wordmark-DDNuuq5_.js} +1 -1
- data/public/railwatch/assets/{appearance-DbwahDrc.js → appearance-C3LMdsDy.js} +1 -1
- data/public/railwatch/assets/{arrow-up-DwKQcab4.js → arrow-up-DYTCBENO.js} +1 -1
- data/public/railwatch/assets/{auth-layout-DXfxXAN_.js → auth-layout-CzPRawuI.js} +1 -1
- data/public/railwatch/assets/{badge-CApjyWcg.js → badge-s7YDycYg.js} +1 -1
- data/public/railwatch/assets/{braces-kGSgxWnS.js → braces-BFrg-bvp.js} +1 -1
- data/public/railwatch/assets/{card-BaJ8kNkp.js → card-DiW-JhK3.js} +1 -1
- data/public/railwatch/assets/{chart-hover-XYPNE2CX.js → chart-hover-DMWx5-Rs.js} +1 -1
- data/public/railwatch/assets/{chart-Xnrz1Nqw.js → chart-nsdZV0Dv.js} +1 -1
- data/public/railwatch/assets/{chart-panel-BYk68mJJ.js → chart-panel-BlirnMrQ.js} +1 -1
- data/public/railwatch/assets/{checkbox-Bf41R8W6.js → checkbox-C2AqEZXX.js} +1 -1
- data/public/railwatch/assets/{code-a-9sh36A.js → code-DVJtYP7h.js} +1 -1
- data/public/railwatch/assets/{copy-block-zykZWQym.js → copy-block-D1myf7is.js} +1 -1
- data/public/railwatch/assets/{copy-id-b6CW5ESL.js → copy-id-CyRCMfdH.js} +1 -1
- data/public/railwatch/assets/{cursor-load-more-0cHzbTPT.js → cursor-load-more-JfSVBn9C.js} +1 -1
- data/public/railwatch/assets/{data-table-IvA7xvrK.js → data-table-CwtPkw_i.js} +1 -1
- data/public/railwatch/assets/{edit-7lmf3k80.js → edit-BvTugIpj.js} +1 -1
- data/public/railwatch/assets/{edit-ZKE15r-F.js → edit-DvpDUM-t.js} +1 -1
- data/public/railwatch/assets/{edit-DTtRV1fO.js → edit-fB83T9Yh.js} +1 -1
- data/public/railwatch/assets/{empty-state-BFTnQ2oT.js → empty-state-CshhuRUU.js} +1 -1
- data/public/railwatch/assets/{env-layout-erSDpIXg.js → env-layout-Dkp_4NvM.js} +1 -1
- data/public/railwatch/assets/{execution-path-Di5uUHi-.js → execution-path-CdEerUGH.js} +1 -1
- data/public/railwatch/assets/{filter-bar-Btm__gQi.js → filter-bar-BMHf8xBb.js} +1 -1
- data/public/railwatch/assets/{flamegraph-C-mZdqa7.js → flamegraph-uYWk1Yov.js} +1 -1
- data/public/railwatch/assets/{frames-KZBsjg6-.js → frames-B5ZECkvt.js} +1 -1
- data/public/railwatch/assets/{google-sign-in-button-DJRoD72g.js → google-sign-in-button-CAvsXYb4.js} +1 -1
- data/public/railwatch/assets/{index-CeqMDByC.js → index-1-78ZewP.js} +1 -1
- data/public/railwatch/assets/{index-P_e_cI0O.js → index-B-qRsj0W.js} +1 -1
- data/public/railwatch/assets/{index-kNhZPLVj.js → index-B4IRsZXK.js} +1 -1
- data/public/railwatch/assets/{index-QyHE9WsU.js → index-BIZsdfdM.js} +1 -1
- data/public/railwatch/assets/{index-EGU__DOB.js → index-BIom4V2g.js} +1 -1
- data/public/railwatch/assets/{index-Bot7kHMA.js → index-BMgA9NNc.js} +1 -1
- data/public/railwatch/assets/{index-CX8Br7m9.js → index-BpnfuXi-.js} +1 -1
- data/public/railwatch/assets/{index-D4968-TC.js → index-Bw_ByIps.js} +1 -1
- data/public/railwatch/assets/{index-CU6yhEyt.js → index-C7bU7oUF.js} +1 -1
- data/public/railwatch/assets/{index-Z1c4nFL0.js → index-CAkCcAty.js} +1 -1
- data/public/railwatch/assets/{index-B-bSm1jB.js → index-CRShH-fR.js} +1 -1
- data/public/railwatch/assets/{index-DBTxQw77.js → index-CZWzIE8F.js} +1 -1
- data/public/railwatch/assets/{index-Cj3_jD1E.js → index-CaJvo0Oc.js} +1 -1
- data/public/railwatch/assets/{index-HCJmFcZu.js → index-CdM3F3jz.js} +1 -1
- data/public/railwatch/assets/{index-DcItJxa5.js → index-ClG89uia.js} +1 -1
- data/public/railwatch/assets/{index-CAzQkw-h.js → index-ClJtZh5Q.js} +1 -1
- data/public/railwatch/assets/{index-Bjw4sTXU.js → index-Czq2bi1U.js} +1 -1
- data/public/railwatch/assets/{index-CP6jrrPT.js → index-D-O1LUPq.js} +1 -1
- data/public/railwatch/assets/{index-CYYmZpZu.js → index-DJIXGkfB.js} +1 -1
- data/public/railwatch/assets/{index-BAkZNNEh.js → index-DMcsT2ES.js} +1 -1
- data/public/railwatch/assets/{index-wOj5j0KC.js → index-DVUwgfqP.js} +1 -1
- data/public/railwatch/assets/{index-DAEzO_vq.js → index-DWMKU6mG.js} +1 -1
- data/public/railwatch/assets/{index-BWSyQRZI.js → index-DYrF-A96.js} +1 -1
- data/public/railwatch/assets/{index-BsrCnQqg.js → index-DcuyMiuG.js} +1 -1
- data/public/railwatch/assets/{index-BoTOVM0v.js → index-DpWQMikK.js} +1 -1
- data/public/railwatch/assets/{index-CIUl9POV.js → index-DuGGTdIJ.js} +1 -1
- data/public/railwatch/assets/{index-uHBVO8Lh.js → index-DzoUj38Q.js} +1 -1
- data/public/railwatch/assets/{index-CBJZQgHA.js → index-QxAKxnHK.js} +1 -1
- data/public/railwatch/assets/{index-9LL_C3eG.js → index-X13K9BbO.js} +1 -1
- data/public/railwatch/assets/{index-DEfu_YvU.js → index-Zf9xXwq7.js} +1 -1
- data/public/railwatch/assets/{index-Bo8hrjbs.js → index-dKn1tKSX.js} +1 -1
- data/public/railwatch/assets/{index-DCAho97K.js → index-fA7h22qS.js} +1 -1
- data/public/railwatch/assets/{index-BtSlLmkg.js → index-iVNm3uzI.js} +1 -1
- data/public/railwatch/assets/{index-B0VXzXYG.js → index-n16aviHx.js} +1 -1
- data/public/railwatch/assets/{index-DXUwhSCQ.js → index-vC9KU2bt.js} +1 -1
- data/public/railwatch/assets/{inertia-Degv_G9N.js → inertia-CUTKXjxg.js} +3 -3
- data/public/railwatch/assets/{input-error-C6SqO_iU.js → input-error-C8ycyeD0.js} +1 -1
- data/public/railwatch/assets/{json-viewer-DcQIxT86.js → json-viewer-CJECqZ0l.js} +1 -1
- data/public/railwatch/assets/{klass-WgQEfWpo.js → klass-Br9dng39.js} +1 -1
- data/public/railwatch/assets/{label-B_vK19tR.js → label-B32Ks8n7.js} +1 -1
- data/public/railwatch/assets/{layout-C8q7GUlZ.js → layout-5ZhRtywp.js} +1 -1
- data/public/railwatch/assets/{live-dot-DaXEbkXo.js → live-dot-Bb1UYT9F.js} +1 -1
- data/public/railwatch/assets/{nav-BnD5XArZ.js → nav-XUcwnBso.js} +1 -1
- data/public/railwatch/assets/{new-BL9OqXHe.js → new-0xyEF6NL.js} +1 -1
- data/public/railwatch/assets/{new-DIjwu2jF.js → new-CV011uLi.js} +1 -1
- data/public/railwatch/assets/{new-BqP0NhOF.js → new-CdvlEBjJ.js} +1 -1
- data/public/railwatch/assets/{new-BhRnQn4w.js → new-D9-ORiNW.js} +1 -1
- data/public/railwatch/assets/{new-uxMnBtoY.js → new-GIkDYLt8.js} +1 -1
- data/public/railwatch/assets/{new-CWP3wtwt.js → new-YBWOo9_V.js} +1 -1
- data/public/railwatch/assets/{onboarding-C_Y3xTsH.js → onboarding-BqEuP-Ml.js} +1 -1
- data/public/railwatch/assets/{origin-identity-CNhOnfBa.js → origin-identity-DrJ4D2TX.js} +1 -1
- data/public/railwatch/assets/{percentile-picker-BujVEhP6.js → percentile-picker-_wllqYoJ.js} +1 -1
- data/public/railwatch/assets/{relative-time-6CoKr1lN.js → relative-time-CV7V8aSP.js} +1 -1
- data/public/railwatch/assets/{release-health-1Qzc_XhT.js → release-health-DRNLZtT7.js} +1 -1
- data/public/railwatch/assets/{route-xqgYjOwJ.js → route-D9slUz_a.js} +1 -1
- data/public/railwatch/assets/{segmented-CVKq2Xpa.js → segmented-Bj70Htrs.js} +1 -1
- data/public/railwatch/assets/{select-CsmSXsJ4.js → select-DJBZX2ky.js} +1 -1
- data/public/railwatch/assets/{separator-CrM10Oot.js → separator-UUo0bkyc.js} +1 -1
- data/public/railwatch/assets/{series-chart-D33UhdG2.js → series-chart-C1YrNlID.js} +1 -1
- data/public/railwatch/assets/{show-zxVxhUel.js → show-BQtEcTeD.js} +1 -1
- data/public/railwatch/assets/{show-B0nHy31L.js → show-BT3tKCgP.js} +1 -1
- data/public/railwatch/assets/{show-DZwcZZrz.js → show-BoG7jcwk.js} +1 -1
- data/public/railwatch/assets/{show-ixK2r2f1.js → show-BpyCRL-I.js} +1 -1
- data/public/railwatch/assets/{show-rysBbvt-.js → show-C0uC3gnA.js} +1 -1
- data/public/railwatch/assets/{show-B3izvRdU.js → show-COfP8yWt.js} +1 -1
- data/public/railwatch/assets/{show-B7NhcWgp.js → show-CefJwag7.js} +1 -1
- data/public/railwatch/assets/{show-8H9WAGav.js → show-ChDJZjoj.js} +1 -1
- data/public/railwatch/assets/{show-CJMeX75E.js → show-D7IL22Hy.js} +1 -1
- data/public/railwatch/assets/{show-DUwQlohF.js → show-D7tP881O.js} +1 -1
- data/public/railwatch/assets/{show-BM-E2iYc.js → show-DR6u4CR7.js} +1 -1
- data/public/railwatch/assets/{show-CMisHNAI.js → show-DnBCvlnd.js} +1 -1
- data/public/railwatch/assets/{show-BlNR3ZOZ.js → show-DoCg0ifP.js} +1 -1
- data/public/railwatch/assets/{show-9jDeBNpc.js → show-GruxlCov.js} +1 -1
- data/public/railwatch/assets/{show-CvNV23gH.js → show-Pu3Iehw5.js} +1 -1
- data/public/railwatch/assets/{show-DMZlwzL_.js → show-TTCk2Met.js} +1 -1
- data/public/railwatch/assets/{show-CQ4Ze-Un.js → show-yQipmite.js} +1 -1
- data/public/railwatch/assets/{sort-header-CKj_7G3U.js → sort-header-CqQQhaxg.js} +1 -1
- data/public/railwatch/assets/{sparkline-cell-Ckg29yVq.js → sparkline-cell-BwdBz0yv.js} +1 -1
- data/public/railwatch/assets/{stat-TxvoBiC8.js → stat-IghK3WjD.js} +1 -1
- data/public/railwatch/assets/{status-badge-5NCVJHjo.js → status-badge-XYUN9gUA.js} +1 -1
- data/public/railwatch/assets/{tenant-path-B5ZRK4x3.js → tenant-path-SxnZbfU1.js} +1 -1
- data/public/railwatch/assets/{text-link-BPllRj0F.js → text-link-NCL9N5D5.js} +1 -1
- data/public/railwatch/assets/{textarea-DtD5yZp_.js → textarea-DhI1dmQa.js} +1 -1
- data/public/railwatch/assets/{timeline-DZz5ZnHN.js → timeline-CLAwsAlo.js} +1 -1
- data/public/railwatch/assets/{transition-Ds6Cakuc.js → transition-DpTb9Yiv.js} +1 -1
- data/public/railwatch/assets/{use-clipboard-BLjjYH8V.js → use-clipboard-D5i6e5kf.js} +1 -1
- data/public/railwatch/assets/{use-live-GKze1mNC.js → use-live-V-slSdFJ.js} +1 -1
- data/public/railwatch/manifest.json +1287 -1287
- metadata +117 -116
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e3cbe26df2ef93b1488b1cfb67d2cb15bb9c2930f63607651af4e5a9d4f526cb
|
|
4
|
+
data.tar.gz: b62edbbb492f16ef449e4bc7ccc57ad90206f67c0e538c24232b30d931290339
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc2578bf58e017c5f311d9642c9b51f52c7c203a84328d8bbe7845568c0836afe5bc7cbec5327aa286cd9e4bb378e930111cedce72b27337f2608e36a6305484
|
|
7
|
+
data.tar.gz: 2e4f1467282564b4ef5d6d3f1446acb80f6cbfdf116588df899bb5a0cb3172330b817282d7d797d29d0cced2b7ab6aa1688d6fa266324d1634fcdafbb3cfe0df
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0 (2026-09-21)
|
|
4
|
+
|
|
5
|
+
The gem is the one author of what goes over the wire and what runs in the
|
|
6
|
+
browser. Anything that receives from it, Railwatch Cloud first, reads these
|
|
7
|
+
from the gem instead of keeping a copy.
|
|
8
|
+
|
|
9
|
+
- **One browser client.** The gem carried two: the install template wrote
|
|
10
|
+
the `railwatch_session` cookie that `Railwatch::Sessions` reads, while the
|
|
11
|
+
dashboard's own copy still wrote `lantern_session`, so every embedded
|
|
12
|
+
dashboard shipped a beacon whose sessions never stitched to their server
|
|
13
|
+
requests. There is one file now, `app/frontend/lib/railwatch.ts`, shipped
|
|
14
|
+
in the gem (`Railwatch.browser_client_path`): the dashboard bundle is built
|
|
15
|
+
from it, `railwatch:install` copies it, and a host with its own frontend
|
|
16
|
+
build imports it. A spec holds the cookie name to the regex.
|
|
17
|
+
- **Shipped wire fixtures.** `lib/railwatch/wire_fixtures.json` holds one
|
|
18
|
+
record per type in `Railwatch::Record::VERSIONS`, produced by the gem's own
|
|
19
|
+
subscribers and middleware, stabilized, and checked in.
|
|
20
|
+
`Railwatch.wire_fixtures` loads it. A receiver tests its mapper against
|
|
21
|
+
these rather than hand-writing what it remembers the gem sending. The spec
|
|
22
|
+
that generates them fails when the file is stale; `rake
|
|
23
|
+
railwatch:wire_fixtures` regenerates it, and that diff is the review
|
|
24
|
+
surface for a wire change.
|
|
25
|
+
- **Record versions are enforced.** Every record has carried a `v` since the
|
|
26
|
+
first release and no receiver read it. `Railwatch::Ingest::Mapper` now
|
|
27
|
+
refuses a record whose version is not the one this gem emits for its type,
|
|
28
|
+
by name (`log v2 is not v1`), on both the trusted embedded path and the
|
|
29
|
+
untrusted one. A shape change without a version bump is caught by the
|
|
30
|
+
fixture spec; a version bump the receiver has not shipped is refused
|
|
31
|
+
instead of misread.
|
|
32
|
+
- `Railwatch::Transport::Http::HEADERS` names the delivery headers in one
|
|
33
|
+
place for the receiver to read.
|
|
34
|
+
|
|
35
|
+
## 0.3.7 (2026-09-20)
|
|
36
|
+
|
|
37
|
+
- Stop a rake task or `rails runner` waiting on the network as it finishes.
|
|
38
|
+
The command patches called `Railwatch.flush` when the execution closed --
|
|
39
|
+
on the application's own thread, with no time limit. The engine's `at_exit`
|
|
40
|
+
already calls `Reporter#shutdown`, which wakes the reporter thread and joins
|
|
41
|
+
it for `shutdown_timeout`, so the same records were already being delivered,
|
|
42
|
+
already bounded, and a failed task's exception already went with them. The
|
|
43
|
+
flush was a second, unbounded way to do it.
|
|
44
|
+
|
|
45
|
+
Against a receiver that accepts connections and never answers, that second
|
|
46
|
+
way cost a full timeout ladder per process -- measured at ~6s, and ~12s when
|
|
47
|
+
it queued behind a delivery the reporter thread was already stuck in. An app
|
|
48
|
+
whose container entrypoint boots Rails eleven times before starting Puma
|
|
49
|
+
spent that on every boot and lost the deploy to its proxy's timeout. With
|
|
50
|
+
the call removed the task returns immediately and the process still leaves
|
|
51
|
+
within `shutdown_timeout`, carrying the same records.
|
|
52
|
+
|
|
53
|
+
Only short-lived processes were affected: web and worker processes flush on
|
|
54
|
+
the reporter's own thread and never blocked. `Railwatch.flush` is unchanged
|
|
55
|
+
and still public, for a caller who wants to wait on purpose.
|
|
56
|
+
|
|
57
|
+
- Add a process-level regression that runs a failing rake task against a
|
|
58
|
+
socket that accepts and never answers, and asserts the task returns without
|
|
59
|
+
waiting on it and the process exits inside the shutdown bound. In-process
|
|
60
|
+
examples cannot see this: the cost lives in a socket read and the bound in
|
|
61
|
+
an `at_exit` join.
|
|
62
|
+
|
|
3
63
|
## 0.3.6 (2026-09-20)
|
|
4
64
|
|
|
5
65
|
- Harden embedded live-stream authorization and telemetry delivery. Bound
|
|
@@ -77,7 +77,11 @@ const DEFAULT_IGNORE_ERRORS = [
|
|
|
77
77
|
"ResizeObserver loop limit exceeded",
|
|
78
78
|
"ResizeObserver loop completed with undelivered notifications",
|
|
79
79
|
]
|
|
80
|
-
const DEFAULT_DENY_URLS = [
|
|
80
|
+
const DEFAULT_DENY_URLS = [
|
|
81
|
+
/extensions\//i,
|
|
82
|
+
/^chrome:\/\//i,
|
|
83
|
+
/^moz-extension:\/\//i,
|
|
84
|
+
]
|
|
81
85
|
|
|
82
86
|
const SESSION_ID_KEY = "railwatch.session"
|
|
83
87
|
const SESSION_STARTED_KEY = "railwatch.session.at"
|
|
@@ -128,10 +132,18 @@ function endpoint() {
|
|
|
128
132
|
}
|
|
129
133
|
|
|
130
134
|
function csrf() {
|
|
131
|
-
return
|
|
135
|
+
return (
|
|
136
|
+
document.querySelector<HTMLMetaElement>('meta[name="csrf-token"]')
|
|
137
|
+
?.content ?? ""
|
|
138
|
+
)
|
|
132
139
|
}
|
|
133
140
|
|
|
134
|
-
function post(payload: {
|
|
141
|
+
function post(payload: {
|
|
142
|
+
visits: Visit[]
|
|
143
|
+
errors: JsError[]
|
|
144
|
+
session?: Session
|
|
145
|
+
tenant?: string
|
|
146
|
+
}) {
|
|
135
147
|
const body = JSON.stringify(payload)
|
|
136
148
|
const blob = new Blob([body], { type: "application/json" })
|
|
137
149
|
if (navigator.sendBeacon?.(endpoint(), blob)) return
|
|
@@ -145,7 +157,12 @@ function post(payload: { visits: Visit[]; errors: JsError[]; session?: Session;
|
|
|
145
157
|
|
|
146
158
|
function flush(ended = false) {
|
|
147
159
|
if (queue.length === 0 && errors.length === 0 && !ended) return
|
|
148
|
-
post({
|
|
160
|
+
post({
|
|
161
|
+
visits: queue.splice(0, queue.length),
|
|
162
|
+
errors: errors.splice(0, errors.length),
|
|
163
|
+
session: beat(ended),
|
|
164
|
+
tenant: tenant(),
|
|
165
|
+
})
|
|
149
166
|
}
|
|
150
167
|
|
|
151
168
|
// The app's tenant resolver runs on the flush path, where a throw would cost
|
|
@@ -175,15 +192,23 @@ function startSession() {
|
|
|
175
192
|
try {
|
|
176
193
|
const existing = sessionStorage.getItem(SESSION_ID_KEY)
|
|
177
194
|
const id = existing ?? randomId()
|
|
178
|
-
const startedAt =
|
|
195
|
+
const startedAt =
|
|
196
|
+
Number(sessionStorage.getItem(SESSION_STARTED_KEY)) || Date.now()
|
|
179
197
|
if (!existing) {
|
|
180
198
|
sessionStorage.setItem(SESSION_ID_KEY, id)
|
|
181
199
|
sessionStorage.setItem(SESSION_STARTED_KEY, String(startedAt))
|
|
182
200
|
}
|
|
201
|
+
// Railwatch::Sessions::COOKIE reads this name to stitch server requests to the browser session.
|
|
183
202
|
document.cookie = `railwatch_session=${id}; path=/; SameSite=Lax`
|
|
184
203
|
session = { id, started_at: startedAt }
|
|
185
204
|
// No duration on the first beat: that is what opens the session.
|
|
186
|
-
if (!existing)
|
|
205
|
+
if (!existing)
|
|
206
|
+
post({
|
|
207
|
+
visits: [],
|
|
208
|
+
errors: [],
|
|
209
|
+
session: { ...session },
|
|
210
|
+
tenant: tenant(),
|
|
211
|
+
})
|
|
187
212
|
} catch {
|
|
188
213
|
// sessionStorage is unavailable (private mode, storage disabled).
|
|
189
214
|
// Everything else still reports; this tab just has no session.
|
|
@@ -213,7 +238,8 @@ function crumb(kind: Crumb["kind"], text: string) {
|
|
|
213
238
|
// oldest entries until it fits the byte budget.
|
|
214
239
|
function trail(): Crumb[] {
|
|
215
240
|
const taken = crumbs.slice()
|
|
216
|
-
while (taken.length > 0 && JSON.stringify(taken).length > MAX_CRUMB_BYTES)
|
|
241
|
+
while (taken.length > 0 && JSON.stringify(taken).length > MAX_CRUMB_BYTES)
|
|
242
|
+
taken.shift()
|
|
217
243
|
return taken
|
|
218
244
|
}
|
|
219
245
|
|
|
@@ -223,16 +249,26 @@ function trail(): Crumb[] {
|
|
|
223
249
|
function describeTarget(target: EventTarget | null): string {
|
|
224
250
|
if (!(target instanceof Element)) return ""
|
|
225
251
|
const id = target.id ? `#${target.id}` : ""
|
|
226
|
-
const className =
|
|
252
|
+
const className =
|
|
253
|
+
typeof target.className === "string" ? target.className.trim() : ""
|
|
227
254
|
const classes = className ? `.${className.split(/\s+/).join(".")}` : ""
|
|
228
|
-
const text =
|
|
255
|
+
const text =
|
|
256
|
+
target instanceof HTMLInputElement
|
|
257
|
+
? ""
|
|
258
|
+
: (target.textContent ?? "").trim().replace(/\s+/g, " ").slice(0, 80)
|
|
229
259
|
return `${target.tagName.toLowerCase()}${id}${classes}${text ? ` "${text}"` : ""}`
|
|
230
260
|
}
|
|
231
261
|
|
|
232
262
|
function startBreadcrumbs() {
|
|
233
|
-
document.addEventListener(
|
|
263
|
+
document.addEventListener(
|
|
264
|
+
"click",
|
|
265
|
+
(event) => crumb("click", describeTarget(event.target)),
|
|
266
|
+
true,
|
|
267
|
+
)
|
|
234
268
|
for (const level of ["error", "warn"] as const) {
|
|
235
|
-
const original = console[level].bind(console) as (
|
|
269
|
+
const original = console[level].bind(console) as (
|
|
270
|
+
...args: unknown[]
|
|
271
|
+
) => void
|
|
236
272
|
console[level] = (...args: unknown[]) => {
|
|
237
273
|
crumb("console", `${level}: ${args.map(stringify).join(" ")}`)
|
|
238
274
|
original(...args)
|
|
@@ -267,14 +303,26 @@ function topFrameUrl(stack: string): string | undefined {
|
|
|
267
303
|
// the app's own code at all -- an extension, an injected widget, a tag
|
|
268
304
|
// manager. None of those are anything this app can fix.
|
|
269
305
|
function ignored(message: string, stack?: string): boolean {
|
|
270
|
-
if (
|
|
306
|
+
if (
|
|
307
|
+
ignoreErrors.some((pattern) =>
|
|
308
|
+
typeof pattern === "string"
|
|
309
|
+
? message.includes(pattern)
|
|
310
|
+
: pattern.test(message),
|
|
311
|
+
)
|
|
312
|
+
)
|
|
313
|
+
return true
|
|
271
314
|
const url = stack ? topFrameUrl(stack) : undefined
|
|
272
315
|
if (!url) return false
|
|
273
316
|
if (denyUrls.some((pattern) => pattern.test(url))) return true
|
|
274
317
|
return !url.startsWith("/") && !url.startsWith(`${location.origin}/`)
|
|
275
318
|
}
|
|
276
319
|
|
|
277
|
-
function capture(
|
|
320
|
+
function capture(
|
|
321
|
+
name: string,
|
|
322
|
+
message: string,
|
|
323
|
+
stack?: string,
|
|
324
|
+
context?: Record<string, unknown>,
|
|
325
|
+
) {
|
|
278
326
|
if (errors.length >= MAX_ERRORS) return
|
|
279
327
|
if (ignored(message, stack)) return
|
|
280
328
|
const breadcrumbs = trail()
|
|
@@ -292,15 +340,28 @@ function capture(name: string, message: string, stack?: string, context?: Record
|
|
|
292
340
|
// Deduped within the flush, not across the page's life: a render loop
|
|
293
341
|
// throws the same error every retry. (Inertia's re-rejection of a failed
|
|
294
342
|
// request is dropped by identity in the rejection handler instead.)
|
|
295
|
-
if (
|
|
343
|
+
if (
|
|
344
|
+
errors.some(
|
|
345
|
+
(e) =>
|
|
346
|
+
e.name === error.name &&
|
|
347
|
+
e.message === error.message &&
|
|
348
|
+
e.stack === error.stack,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
return
|
|
296
352
|
errors.push(error)
|
|
297
353
|
}
|
|
298
354
|
|
|
299
355
|
// Anything at all can be thrown or rejected in JavaScript, not just an
|
|
300
356
|
// Error. A non-Error value is reported under `fallback` with whatever it
|
|
301
357
|
// stringifies to as the message.
|
|
302
|
-
function captureValue(
|
|
303
|
-
|
|
358
|
+
function captureValue(
|
|
359
|
+
value: unknown,
|
|
360
|
+
fallback: string,
|
|
361
|
+
context?: Record<string, unknown>,
|
|
362
|
+
) {
|
|
363
|
+
if (value instanceof Error)
|
|
364
|
+
capture(value.name, value.message, value.stack, context)
|
|
304
365
|
else capture(fallback, stringify(value), undefined, context)
|
|
305
366
|
}
|
|
306
367
|
|
|
@@ -336,7 +397,11 @@ export function railwatchRootOptions(): {
|
|
|
336
397
|
onUncaughtError: (error: unknown, info: ReactErrorInfo) => void
|
|
337
398
|
} {
|
|
338
399
|
const report = (error: unknown, info: ReactErrorInfo) =>
|
|
339
|
-
captureValue(
|
|
400
|
+
captureValue(
|
|
401
|
+
error,
|
|
402
|
+
"Error",
|
|
403
|
+
info.componentStack ? { componentStack: info.componentStack } : undefined,
|
|
404
|
+
)
|
|
340
405
|
return { onCaughtError: report, onUncaughtError: report }
|
|
341
406
|
}
|
|
342
407
|
|
|
@@ -346,7 +411,12 @@ export function railwatchRootOptions(): {
|
|
|
346
411
|
function stringify(value: unknown) {
|
|
347
412
|
try {
|
|
348
413
|
if (value instanceof Error) return `${value.name}: ${value.message}`
|
|
349
|
-
if (
|
|
414
|
+
if (
|
|
415
|
+
Array.isArray(value) ||
|
|
416
|
+
(typeof value === "object" &&
|
|
417
|
+
value !== null &&
|
|
418
|
+
value.toString === Object.prototype.toString)
|
|
419
|
+
) {
|
|
350
420
|
return JSON.stringify(value).slice(0, MAX_CRUMB_TEXT)
|
|
351
421
|
}
|
|
352
422
|
return String(value)
|
|
@@ -376,13 +446,20 @@ function startErrorCapture() {
|
|
|
376
446
|
})
|
|
377
447
|
window.addEventListener("unhandledrejection", (event) => {
|
|
378
448
|
const reason = event.reason as unknown
|
|
379
|
-
if (
|
|
449
|
+
if (
|
|
450
|
+
typeof reason === "object" &&
|
|
451
|
+
reason !== null &&
|
|
452
|
+
requestFailures.has(reason)
|
|
453
|
+
)
|
|
454
|
+
return
|
|
380
455
|
captureValue(reason, "UnhandledRejection")
|
|
381
456
|
})
|
|
382
457
|
onInertia("exception", (detail) => captureRequestFailure(detail.exception))
|
|
383
458
|
onInertia("networkError", (detail) => captureRequestFailure(detail.error))
|
|
384
459
|
onInertia("invalid", (detail) => captureInvalidResponse(detail.response))
|
|
385
|
-
onInertia("httpException", (detail) =>
|
|
460
|
+
onInertia("httpException", (detail) =>
|
|
461
|
+
captureInvalidResponse(detail.response),
|
|
462
|
+
)
|
|
386
463
|
}
|
|
387
464
|
|
|
388
465
|
// A visit the user is waiting on: one that shows the progress bar, or the
|
|
@@ -402,12 +479,16 @@ function waiting(visit: object): boolean {
|
|
|
402
479
|
// the next waiting `before`, a waiting `start`, or a reported failure,
|
|
403
480
|
// which can only over-report.
|
|
404
481
|
function idOf(record: object, key: string): string | undefined {
|
|
405
|
-
const value =
|
|
482
|
+
const value =
|
|
483
|
+
key in record ? (record as Record<string, unknown>)[key] : undefined
|
|
406
484
|
return typeof value === "string" ? value : undefined
|
|
407
485
|
}
|
|
408
486
|
|
|
409
487
|
function landed(detail: object) {
|
|
410
|
-
if (
|
|
488
|
+
if (
|
|
489
|
+
foregroundRequest?.id !== undefined &&
|
|
490
|
+
idOf(detail, "visitId") === foregroundRequest.id
|
|
491
|
+
) {
|
|
411
492
|
foregroundRequest = null
|
|
412
493
|
}
|
|
413
494
|
}
|
|
@@ -424,10 +505,17 @@ function captureRequestFailure(error: unknown) {
|
|
|
424
505
|
captureValue(error, "InertiaException")
|
|
425
506
|
}
|
|
426
507
|
|
|
427
|
-
function onInertia(
|
|
508
|
+
function onInertia(
|
|
509
|
+
name: string,
|
|
510
|
+
handler: (detail: Record<string, unknown>) => void,
|
|
511
|
+
) {
|
|
428
512
|
document.addEventListener(`inertia:${name}`, (event) => {
|
|
429
513
|
const detail = (event as CustomEvent<unknown>).detail
|
|
430
|
-
handler(
|
|
514
|
+
handler(
|
|
515
|
+
typeof detail === "object" && detail !== null
|
|
516
|
+
? (detail as Record<string, unknown>)
|
|
517
|
+
: {},
|
|
518
|
+
)
|
|
431
519
|
})
|
|
432
520
|
}
|
|
433
521
|
|
|
@@ -439,19 +527,31 @@ function onInertia(name: string, handler: (detail: Record<string, unknown>) => v
|
|
|
439
527
|
// predicate for the two cases is the x-inertia response header, so that is
|
|
440
528
|
// the gate here; Inertia 2's `invalid` only ever fired for the first kind.
|
|
441
529
|
function captureInvalidResponse(response: unknown) {
|
|
442
|
-
const res = (response ?? {}) as {
|
|
530
|
+
const res = (response ?? {}) as {
|
|
531
|
+
status?: unknown
|
|
532
|
+
headers?: Record<string, unknown>
|
|
533
|
+
}
|
|
443
534
|
if (inertiaResponse(res.headers)) return
|
|
444
535
|
const status = typeof res.status === "number" ? res.status : "unknown status"
|
|
445
536
|
const contentType = res.headers?.["content-type"]
|
|
446
|
-
capture(
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
537
|
+
capture(
|
|
538
|
+
"InertiaInvalidResponse",
|
|
539
|
+
`Inertia invalid response (${status})`,
|
|
540
|
+
undefined,
|
|
541
|
+
{
|
|
542
|
+
status,
|
|
543
|
+
...(typeof contentType === "string" ? { content_type: contentType } : {}),
|
|
544
|
+
},
|
|
545
|
+
)
|
|
450
546
|
}
|
|
451
547
|
|
|
452
|
-
function inertiaResponse(
|
|
548
|
+
function inertiaResponse(
|
|
549
|
+
headers: Record<string, unknown> | undefined,
|
|
550
|
+
): boolean {
|
|
453
551
|
if (!headers || typeof headers !== "object") return false
|
|
454
|
-
return Object.keys(headers).some(
|
|
552
|
+
return Object.keys(headers).some(
|
|
553
|
+
(key) => key.toLowerCase() === "x-inertia" && headers[key],
|
|
554
|
+
)
|
|
455
555
|
}
|
|
456
556
|
|
|
457
557
|
// --- Core Web Vitals ---------------------------------------------------
|
|
@@ -472,10 +572,16 @@ interface ObserveOptions {
|
|
|
472
572
|
durationThreshold?: number
|
|
473
573
|
}
|
|
474
574
|
|
|
475
|
-
function observe(
|
|
575
|
+
function observe(
|
|
576
|
+
type: string,
|
|
577
|
+
callback: (entries: VitalEntry[]) => void,
|
|
578
|
+
options: ObserveOptions = {},
|
|
579
|
+
) {
|
|
476
580
|
if (typeof PerformanceObserver === "undefined") return
|
|
477
581
|
try {
|
|
478
|
-
const observer = new PerformanceObserver((list) =>
|
|
582
|
+
const observer = new PerformanceObserver((list) =>
|
|
583
|
+
callback(list.getEntries()),
|
|
584
|
+
)
|
|
479
585
|
observer.observe({ type, buffered: true, ...options })
|
|
480
586
|
} catch {
|
|
481
587
|
// This browser does not support this entry type. Skip that metric only.
|
|
@@ -487,9 +593,12 @@ function observe(type: string, callback: (entries: VitalEntry[]) => void, option
|
|
|
487
593
|
// this file is imported into) does not define, and a ReferenceError here
|
|
488
594
|
// would take the whole client down with it.
|
|
489
595
|
function navigationEntry(): PerformanceNavigationTiming | undefined {
|
|
490
|
-
const entries: PerformanceEntry[] =
|
|
596
|
+
const entries: PerformanceEntry[] =
|
|
597
|
+
performance.getEntriesByType?.("navigation") ?? []
|
|
491
598
|
const nav = entries[0]
|
|
492
|
-
return nav?.entryType === "navigation"
|
|
599
|
+
return nav?.entryType === "navigation"
|
|
600
|
+
? (nav as PerformanceNavigationTiming)
|
|
601
|
+
: undefined
|
|
493
602
|
}
|
|
494
603
|
|
|
495
604
|
function startVitals() {
|
|
@@ -511,7 +620,11 @@ function startVitals() {
|
|
|
511
620
|
for (const entry of entries) {
|
|
512
621
|
if (entry.hadRecentInput) continue
|
|
513
622
|
const value = entry.value ?? 0
|
|
514
|
-
if (
|
|
623
|
+
if (
|
|
624
|
+
sessionValue &&
|
|
625
|
+
entry.startTime - sessionLast < 1000 &&
|
|
626
|
+
entry.startTime - sessionFirst < 5000
|
|
627
|
+
) {
|
|
515
628
|
sessionValue += value
|
|
516
629
|
sessionLast = entry.startTime
|
|
517
630
|
} else {
|
|
@@ -541,7 +654,9 @@ function startVitals() {
|
|
|
541
654
|
// serialized page object. Inertia's own events take over from here.
|
|
542
655
|
function pageComponent(): string | undefined {
|
|
543
656
|
try {
|
|
544
|
-
const page = JSON.parse(
|
|
657
|
+
const page = JSON.parse(
|
|
658
|
+
document.getElementById("app")?.dataset.page ?? "{}",
|
|
659
|
+
) as { component?: string }
|
|
545
660
|
return page.component
|
|
546
661
|
} catch {
|
|
547
662
|
// Not an Inertia-rendered page, or the payload moved. Report it anyway.
|
|
@@ -557,7 +672,10 @@ function initialVisit(): Visit | null {
|
|
|
557
672
|
|
|
558
673
|
const duration = (nav.loadEventEnd || nav.responseEnd) - nav.startTime
|
|
559
674
|
return {
|
|
560
|
-
started_at: Math.round(
|
|
675
|
+
started_at: Math.round(
|
|
676
|
+
(performance.timeOrigin ?? Date.now() - performance.now()) +
|
|
677
|
+
nav.startTime,
|
|
678
|
+
),
|
|
561
679
|
url: location.pathname + location.search,
|
|
562
680
|
method: "GET",
|
|
563
681
|
component,
|
|
@@ -587,8 +705,8 @@ function finalize() {
|
|
|
587
705
|
}
|
|
588
706
|
|
|
589
707
|
export function startRailwatch(options: RailwatchOptions = {}) {
|
|
590
|
-
ignoreErrors = [
|
|
591
|
-
denyUrls = [
|
|
708
|
+
ignoreErrors = [...DEFAULT_IGNORE_ERRORS, ...(options.ignoreErrors ?? [])]
|
|
709
|
+
denyUrls = [...DEFAULT_DENY_URLS, ...(options.denyUrls ?? [])]
|
|
592
710
|
tenantOf = options.tenant
|
|
593
711
|
startVitals()
|
|
594
712
|
startSession()
|
|
@@ -609,7 +727,8 @@ export function startRailwatch(options: RailwatchOptions = {}) {
|
|
|
609
727
|
if (!waiting(v) || event.defaultPrevented) return
|
|
610
728
|
const request = (foregroundRequest = { id: idOf(v, "id") })
|
|
611
729
|
queueMicrotask(() => {
|
|
612
|
-
if (event.defaultPrevented && foregroundRequest === request)
|
|
730
|
+
if (event.defaultPrevented && foregroundRequest === request)
|
|
731
|
+
foregroundRequest = null
|
|
613
732
|
})
|
|
614
733
|
})
|
|
615
734
|
router.on("start", (event) => {
|
|
@@ -642,7 +761,8 @@ export function startRailwatch(options: RailwatchOptions = {}) {
|
|
|
642
761
|
router.on("finish", (event) => {
|
|
643
762
|
// Fires once whether the visit completed, failed, or was cancelled.
|
|
644
763
|
// Clamped so a missed start can only over-report, never silence.
|
|
645
|
-
if (waiting(event.detail.visit))
|
|
764
|
+
if (waiting(event.detail.visit))
|
|
765
|
+
foregroundVisits = Math.max(0, foregroundVisits - 1)
|
|
646
766
|
if (!current) return
|
|
647
767
|
current.duration_ms = Date.now() - current.started_at
|
|
648
768
|
current.status ??= "cancelled"
|
|
@@ -182,6 +182,7 @@ module Railwatch
|
|
|
182
182
|
# batch does not need.
|
|
183
183
|
raise TypeError, "record must be an object" unless rec.is_a?(Hash)
|
|
184
184
|
raise TypeError, "t must be a string" unless rec["t"].is_a?(String)
|
|
185
|
+
Ingest::Mapper.validate_version!(rec)
|
|
185
186
|
else
|
|
186
187
|
Ingest::Mapper.validate_record!(rec)
|
|
187
188
|
end
|
|
@@ -111,6 +111,7 @@ module Railwatch
|
|
|
111
111
|
def validate_record!(rec, identifiers: true)
|
|
112
112
|
raise TypeError, "record must be an object" unless rec.is_a?(Hash)
|
|
113
113
|
raise TypeError, "t must be a string" unless rec["t"].is_a?(String)
|
|
114
|
+
validate_version!(rec)
|
|
114
115
|
|
|
115
116
|
if identifiers
|
|
116
117
|
validate_identifier!(rec, "_group", GROUP_HASH, "32 hexadecimal characters")
|
|
@@ -129,6 +130,17 @@ module Railwatch
|
|
|
129
130
|
validate_locals!(rec["locals"])
|
|
130
131
|
end
|
|
131
132
|
|
|
133
|
+
# A record shape is named by its type and version (Railwatch::Record::VERSIONS).
|
|
134
|
+
# This mapper only knows the versions the gem it ships with emits; a
|
|
135
|
+
# record from a newer or older gem is refused by name rather than read
|
|
136
|
+
# through columns that may no longer mean the same thing.
|
|
137
|
+
def validate_version!(rec)
|
|
138
|
+
expected = Record::VERSIONS[rec["t"].to_sym]
|
|
139
|
+
return if expected.nil? # unknown type: row_for answers nil and the batch rejects it as such
|
|
140
|
+
|
|
141
|
+
raise TypeError, "#{rec["t"]} v#{rec["v"].inspect} is not v#{expected}" unless rec["v"].is_a?(Integer) && rec["v"] == expected
|
|
142
|
+
end
|
|
143
|
+
|
|
132
144
|
def validate_identifier!(rec, field, pattern, description)
|
|
133
145
|
value = rec[field]
|
|
134
146
|
return if value.nil?
|
data/docs/configuration.md
CHANGED
|
@@ -431,9 +431,14 @@ affects one probabilistic decision. Set `backpressure` false to keep the
|
|
|
431
431
|
factor at 1. The current value is sent as
|
|
432
432
|
`X-Railwatch-Backpressure-Factor` whenever it is greater than 1.
|
|
433
433
|
|
|
434
|
-
`Railwatch.flush` forces an immediate flush
|
|
435
|
-
|
|
436
|
-
|
|
434
|
+
`Railwatch.flush` forces an immediate flush, on the calling thread and
|
|
435
|
+
without a time limit, so it is yours to call when you know you want to
|
|
436
|
+
wait. Nothing in the gem calls it for you: a short-lived process keeps its
|
|
437
|
+
last batch because the engine's `at_exit` runs `Reporter#shutdown`, which
|
|
438
|
+
wakes the reporter thread and joins it for `shutdown_timeout`. That is a
|
|
439
|
+
bounded wait, which is what a rake task, a runner or a cron job needs --
|
|
440
|
+
a receiver that accepts connections and never answers then costs
|
|
441
|
+
`shutdown_timeout`, not a timeout ladder per process. An unhandled
|
|
437
442
|
exception goes through `Railwatch.record_now` → `Reporter#write_now`,
|
|
438
443
|
which enqueues the record and asks for an urgent flush. It never performs
|
|
439
444
|
network I/O or a timeout cycle on the application thread. Urgent means
|
|
@@ -119,9 +119,13 @@ module Railwatch
|
|
|
119
119
|
chmod ".kamal/hooks/post-deploy", 0o755
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
+
# The one browser client, the same file the gem's own dashboard bundle
|
|
123
|
+
# is built from (Railwatch.browser_client_path). Copied rather than
|
|
124
|
+
# templated: there is nothing to interpolate, and a copy means the host
|
|
125
|
+
# sees exactly what the gem tests.
|
|
122
126
|
def create_browser_client
|
|
123
127
|
return unless File.directory?("app/frontend")
|
|
124
|
-
|
|
128
|
+
copy_file Railwatch.browser_client_path.to_s, "app/frontend/lib/railwatch.ts"
|
|
125
129
|
end
|
|
126
130
|
|
|
127
131
|
# Adds the import and the call to the Inertia entrypoint, so page-visit
|
|
@@ -72,7 +72,16 @@ module Railwatch
|
|
|
72
72
|
name: name,
|
|
73
73
|
command: "rake #{name}#{command_suffix}",
|
|
74
74
|
exit_code: exit_code.to_i.clamp(0, 255))
|
|
75
|
-
|
|
75
|
+
# Deliberately no flush. The engine's at_exit calls Reporter#shutdown,
|
|
76
|
+
# which wakes the reporter thread and joins it for shutdown_timeout --
|
|
77
|
+
# already a bounded last delivery, and already the one that ships a
|
|
78
|
+
# failed task's exception. Flushing here as well delivered the same
|
|
79
|
+
# records a second way, on the application's own thread, with no
|
|
80
|
+
# bound: against a receiver that accepts connections and never
|
|
81
|
+
# answers it cost a full timeout ladder per process, measured at ~6s,
|
|
82
|
+
# and ~12s when it queued behind a delivery the reporter thread was
|
|
83
|
+
# already stuck in. An app whose entrypoint boots Rails eleven times
|
|
84
|
+
# spent that eleven times over and lost the deploy.
|
|
76
85
|
end
|
|
77
86
|
end
|
|
78
87
|
|
|
@@ -91,7 +91,9 @@ module Railwatch
|
|
|
91
91
|
}
|
|
92
92
|
fields[:interactive] = true if interactive
|
|
93
93
|
Railwatch.finish_execution(:command, **fields)
|
|
94
|
-
|
|
94
|
+
# No flush here either; see the note in patches/rake_task.rb. The
|
|
95
|
+
# at_exit shutdown is the bounded delivery, and it runs for a runner
|
|
96
|
+
# exactly as it does for a task.
|
|
95
97
|
end
|
|
96
98
|
end
|
|
97
99
|
|
|
@@ -13,6 +13,16 @@ module Railwatch
|
|
|
13
13
|
# Reporter owns retention and backoff between calls. A 401 marks the
|
|
14
14
|
# transport unauthorized so no further requests are made.
|
|
15
15
|
class Http
|
|
16
|
+
# The request headers a delivery carries besides the body. The receiver
|
|
17
|
+
# reads them by these names; Railwatch Cloud does so through this table.
|
|
18
|
+
HEADERS = {
|
|
19
|
+
dropped: "X-Railwatch-Dropped",
|
|
20
|
+
dropped_bytes: "X-Railwatch-Dropped-Bytes",
|
|
21
|
+
backpressure_factor: "X-Railwatch-Backpressure-Factor",
|
|
22
|
+
version: "X-Railwatch-Version",
|
|
23
|
+
batch_id: "X-Railwatch-Batch-Id"
|
|
24
|
+
}.freeze
|
|
25
|
+
|
|
16
26
|
RETRYABLE_STATUSES = [ 402, 408, 429 ].freeze
|
|
17
27
|
UNAUTHORIZED_STATUS = 401
|
|
18
28
|
|
|
@@ -143,17 +153,17 @@ module Railwatch
|
|
|
143
153
|
headers.each { |name, value| req[name] = value.to_s }
|
|
144
154
|
req["Content-Type"] = "application/x-ndjson"
|
|
145
155
|
req["Content-Encoding"] = "gzip"
|
|
146
|
-
req[
|
|
147
|
-
req[
|
|
156
|
+
req[HEADERS[:dropped]] = dropped.to_s if dropped.positive?
|
|
157
|
+
req[HEADERS[:dropped_bytes]] = dropped_bytes.to_s if dropped_bytes.positive?
|
|
148
158
|
if backpressure_factor > 1.0
|
|
149
|
-
req[
|
|
159
|
+
req[HEADERS[:backpressure_factor]] = backpressure_factor.to_s
|
|
150
160
|
end
|
|
151
161
|
# The version this payload was built by, which for a stored delivery is
|
|
152
162
|
# not the version running now. The receiver digests this header, so
|
|
153
163
|
# sending today's value would turn an upgrade into a conflict and
|
|
154
164
|
# destroy a delivery it had already accepted.
|
|
155
|
-
req[
|
|
156
|
-
req[
|
|
165
|
+
req[HEADERS[:version]] = gem_version
|
|
166
|
+
req[HEADERS[:batch_id]] = batch_id
|
|
157
167
|
req.body = body
|
|
158
168
|
request(req)
|
|
159
169
|
end
|
data/lib/railwatch/version.rb
CHANGED