railwatch 0.1.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 +7 -0
- data/AGENTS.md +122 -0
- data/CHANGELOG.md +462 -0
- data/MIT-LICENSE +20 -0
- data/README.md +226 -0
- data/app/controllers/railwatch/beacon_controller.rb +254 -0
- data/config/routes.rb +5 -0
- data/docs/ai-and-mcp.md +227 -0
- data/docs/configuration.md +931 -0
- data/docs/faq.md +230 -0
- data/docs/getting-started.md +279 -0
- data/docs/records.md +834 -0
- data/docs/replacing-nightwatch.md +216 -0
- data/docs/replacing-sentry.md +573 -0
- data/docs/security.md +94 -0
- data/docs/self-hosting.md +60 -0
- data/docs/source-maps.md +60 -0
- data/docs/testing.md +175 -0
- data/docs/troubleshooting.md +319 -0
- data/lib/generators/railwatch/install/install_generator.rb +280 -0
- data/lib/generators/railwatch/install/templates/initializer.rb +54 -0
- data/lib/generators/railwatch/install/templates/post-deploy +98 -0
- data/lib/generators/railwatch/install/templates/railwatch.ts +658 -0
- data/lib/railwatch/attachments.rb +83 -0
- data/lib/railwatch/backtrace.rb +158 -0
- data/lib/railwatch/buffer.rb +122 -0
- data/lib/railwatch/clock.rb +25 -0
- data/lib/railwatch/configuration.rb +334 -0
- data/lib/railwatch/console.rb +48 -0
- data/lib/railwatch/context.rb +125 -0
- data/lib/railwatch/controller_helpers.rb +21 -0
- data/lib/railwatch/current.rb +32 -0
- data/lib/railwatch/engine.rb +144 -0
- data/lib/railwatch/execution.rb +367 -0
- data/lib/railwatch/faraday.rb +73 -0
- data/lib/railwatch/health.rb +188 -0
- data/lib/railwatch/job_tracing.rb +49 -0
- data/lib/railwatch/middleware/request.rb +289 -0
- data/lib/railwatch/minitest.rb +43 -0
- data/lib/railwatch/patches/inertia.rb +34 -0
- data/lib/railwatch/patches/net_http.rb +102 -0
- data/lib/railwatch/patches/rake_task.rb +88 -0
- data/lib/railwatch/patches/runner_command.rb +120 -0
- data/lib/railwatch/patches.rb +43 -0
- data/lib/railwatch/profiler.rb +270 -0
- data/lib/railwatch/record.rb +119 -0
- data/lib/railwatch/redactor.rb +67 -0
- data/lib/railwatch/release_detector.rb +97 -0
- data/lib/railwatch/reporter.rb +539 -0
- data/lib/railwatch/rspec.rb +139 -0
- data/lib/railwatch/sampler.rb +17 -0
- data/lib/railwatch/secret_safety.rb +62 -0
- data/lib/railwatch/sessions.rb +162 -0
- data/lib/railwatch/source_maps.rb +59 -0
- data/lib/railwatch/spec_helper.rb +147 -0
- data/lib/railwatch/sql_normalizer.rb +398 -0
- data/lib/railwatch/subscribers/base.rb +54 -0
- data/lib/railwatch/subscribers/broadcasts.rb +107 -0
- data/lib/railwatch/subscribers/cache.rb +107 -0
- data/lib/railwatch/subscribers/deprecations.rb +26 -0
- data/lib/railwatch/subscribers/exceptions.rb +304 -0
- data/lib/railwatch/subscribers/jobs.rb +282 -0
- data/lib/railwatch/subscribers/logs.rb +137 -0
- data/lib/railwatch/subscribers/mail.rb +42 -0
- data/lib/railwatch/subscribers/notifications.rb +36 -0
- data/lib/railwatch/subscribers/process_info.rb +98 -0
- data/lib/railwatch/subscribers/queries.rb +183 -0
- data/lib/railwatch/subscribers/requests.rb +94 -0
- data/lib/railwatch/subscribers/storage.rb +35 -0
- data/lib/railwatch/subscribers/users.rb +159 -0
- data/lib/railwatch/subscribers/views.rb +54 -0
- data/lib/railwatch/subscribers.rb +34 -0
- data/lib/railwatch/transport/http.rb +208 -0
- data/lib/railwatch/version.rb +5 -0
- data/lib/railwatch.rb +550 -0
- data/lib/tasks/railwatch_tasks.rake +289 -0
- data/llms.txt +38 -0
- metadata +157 -0
data/docs/faq.md
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
# FAQ
|
|
2
|
+
|
|
3
|
+
## What does it cost per request?
|
|
4
|
+
|
|
5
|
+
The gem ships with an overhead gate that CI runs on every change
|
|
6
|
+
(`bench/overhead.rb`, `bundle exec ruby bench/overhead.rb`). It boots the
|
|
7
|
+
dummy app on SQLite, drives three request shapes with Railwatch genuinely out
|
|
8
|
+
of the way (its notification subscribers unsubscribed, its log capture
|
|
9
|
+
detached) and then in, alternating every batch so background load hits
|
|
10
|
+
both equally, and fails the build if instrumentation costs more than:
|
|
11
|
+
|
|
12
|
+
| Request shape | Added CPU per request | Added allocations |
|
|
13
|
+
|---|---|---|
|
|
14
|
+
| Trivial, no queries | 0.9 ms | 400 |
|
|
15
|
+
| 20 uncached SQLite queries | 3.0 ms | 1,000 |
|
|
16
|
+
| N+1 page: 7 queries, 1 log line | 2.0 ms | 500 |
|
|
17
|
+
|
|
18
|
+
Measured on a shared 8-core box at load average 4, the gem comes in at
|
|
19
|
+
roughly **0.4 ms fixed per request plus 40 to 80 µs per real query**, so
|
|
20
|
+
about 0.9 ms for the N+1 page and 1.7 to 2.3 ms for the 20-query stress
|
|
21
|
+
case. A head-sampled-out request pays about half the fixed cost and 10 to
|
|
22
|
+
20 µs per query, all of it counting. The limits are higher than the
|
|
23
|
+
measurement on purpose: they leave headroom for a loaded CI box without
|
|
24
|
+
letting a real regression through.
|
|
25
|
+
|
|
26
|
+
Most of the per-query figure is Rails' own notification dispatch (an
|
|
27
|
+
`ActiveSupport::Notifications::Event` costs about 6 µs to build and
|
|
28
|
+
deliver, and a query fires two of them); Railwatch's subscriber body is 5 to
|
|
29
|
+
15 µs of it.
|
|
30
|
+
|
|
31
|
+
Off the request thread, the reporter spends about 30 µs of CPU per record
|
|
32
|
+
serializing and gzipping, which was 2 to 4 percent of process CPU in a
|
|
33
|
+
saturated load test, and each record on the wire is about 95 bytes after
|
|
34
|
+
gzip. Boot with the gem enabled is within noise of boot without it (it
|
|
35
|
+
used to be 300 ms and 10 MB slower, until the process record stopped
|
|
36
|
+
loading Active Record and Active Job just to name their adapters, and the
|
|
37
|
+
rake and runner patches moved out of the web boot); resident memory is 1 to 2 MB higher at idle
|
|
38
|
+
plus about 2 KB per buffered record.
|
|
39
|
+
|
|
40
|
+
Two honest caveats. The budget is **CPU time on the request thread**, not
|
|
41
|
+
wall time — wall time on a shared runner swings by tens of milliseconds
|
|
42
|
+
for reasons that have nothing to do with the gem, which would make the
|
|
43
|
+
gate useless. And the 20-query request is a stress case; a normal request
|
|
44
|
+
pays mostly the fixed cost.
|
|
45
|
+
|
|
46
|
+
The scripts behind these numbers, and the ones for finding out where a
|
|
47
|
+
number comes from before changing the code, are listed in
|
|
48
|
+
[`bench/README.md`](../bench/README.md).
|
|
49
|
+
|
|
50
|
+
A second gate, `bench/no_db_writes.rb`, drives 200 requests and a job with
|
|
51
|
+
a `sql.active_record` subscriber watching for any `INSERT`/`UPDATE`/
|
|
52
|
+
`DELETE` issued from a frame inside `lib/railwatch`, and fails if it finds
|
|
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
|
|
56
|
+
single-writer SQLite app into a "database is locked" incident.
|
|
57
|
+
|
|
58
|
+
## Where does the data go, and how long is it kept?
|
|
59
|
+
|
|
60
|
+
To the platform, over one gzip-NDJSON POST to `{ingest_url}/ingest` per
|
|
61
|
+
batch. The platform stores each monitored environment's telemetry in its
|
|
62
|
+
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.
|
|
68
|
+
|
|
69
|
+
## What about PII?
|
|
70
|
+
|
|
71
|
+
Two things are redacted with no configuration:
|
|
72
|
+
|
|
73
|
+
- **Headers**, by name: `Authorization`, `Cookie`, `Set-Cookie`,
|
|
74
|
+
`Proxy-Authorization`, `X-CSRF-Token`, `X-XSRF-TOKEN`, plus any
|
|
75
|
+
credential-shaped name segment such as `api-key`, `access-key`,
|
|
76
|
+
`private-key`, `auth`, `bearer`, `credential`, `hmac`, `jwt`, `token`,
|
|
77
|
+
`secret`, or `signature` (including vendor headers such as
|
|
78
|
+
`X-Shopify-Hmac-Sha256` and concatenated Rack aliases such as `X-AuthToken`,
|
|
79
|
+
`X-ApiToken`, `X-AccessToken`, `X-ClientToken`, `X-SessionToken`,
|
|
80
|
+
`X-RefreshToken`, `X-SecretKey`, `X-HmacSignature`, and `X-CSRFToken`).
|
|
81
|
+
Values are replaced with `[FILTERED]`. Extend the exact denylist for
|
|
82
|
+
application-specific names with
|
|
83
|
+
`c.redact_headers += [...]`.
|
|
84
|
+
- **Parameters**, by name: `password`, `password_confirmation`,
|
|
85
|
+
`authenticity_token`, `_token` — merged with your app's own
|
|
86
|
+
`config.filter_parameters`, so anything already hidden from your logs
|
|
87
|
+
is hidden here too. Extend with `c.redact_params += [...]`.
|
|
88
|
+
|
|
89
|
+
Four things that could carry PII are **off by default and opt-in one at a
|
|
90
|
+
time**. There is no single "send everything" switch:
|
|
91
|
+
|
|
92
|
+
| Setting | What it adds |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `capture_request_payload` | Request params — and only for a request that raised, never a successful one. Filtered. |
|
|
95
|
+
| `capture_job_arguments` | A job's real arguments, capped at 8 KiB of JSON, hashes filtered. (Argument *shapes* — `arguments_preview` — are always on and carry no values.) |
|
|
96
|
+
| `capture_response_body_on_error` | The first 4 KiB of a failing upstream's response body. |
|
|
97
|
+
| `capture_exception_locals` | The raising frame's local variables, truncated and filtered. |
|
|
98
|
+
|
|
99
|
+
Everything else is per record type, in your initializer:
|
|
100
|
+
`Railwatch.redact_requests`, `redact_queries`, `redact_exceptions`,
|
|
101
|
+
`redact_cache_events`, `redact_commands`, `redact_mail`,
|
|
102
|
+
`redact_outgoing_requests`, `redact_logs` mutate a record in place;
|
|
103
|
+
`Railwatch.reject_queries`, `reject_cache_events`, `reject_mail`,
|
|
104
|
+
`reject_notifications`, `reject_broadcasts`, `reject_outgoing_requests`,
|
|
105
|
+
`reject_enqueued_jobs`, `reject_logs` drop it entirely.
|
|
106
|
+
`Railwatch.before_ingest` gets the last look at a whole batch.
|
|
107
|
+
|
|
108
|
+
Who the user is comes from a resolver block you write
|
|
109
|
+
(`c.user { |u| ... }`), so the fields on a `user` record are exactly the
|
|
110
|
+
ones you chose to put there. Cache keys are truncated at 255 characters
|
|
111
|
+
and can be dropped wholesale with `Railwatch.reject_cache_keys`; outgoing
|
|
112
|
+
request URLs, inbound request URLs, and redirect targets have authority
|
|
113
|
+
credentials, entire query strings, and fragments stripped; uploaded files are
|
|
114
|
+
recorded as metadata (name, size, content type) and never contents.
|
|
115
|
+
|
|
116
|
+
## Does SQLite work?
|
|
117
|
+
|
|
118
|
+
Yes, on both sides, and it's the first-class target.
|
|
119
|
+
|
|
120
|
+
**In your app:** the gem does no I/O on the request path and never writes
|
|
121
|
+
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.
|
|
124
|
+
|
|
125
|
+
**On the platform:** telemetry is stored one SQLite database per
|
|
126
|
+
monitored environment. That is what makes retention pruning, backup, and
|
|
127
|
+
restore per-environment operations rather than one enormous table, and
|
|
128
|
+
it's why log search gets FTS5 with snippet highlighting. The platform
|
|
129
|
+
runs unchanged on Postgres if that's what's configured.
|
|
130
|
+
|
|
131
|
+
## One database per environment — what does that mean for me?
|
|
132
|
+
|
|
133
|
+
Each environment you create (production, staging, ...) has its own
|
|
134
|
+
ingest token and its own telemetry store. Nothing crosses between them:
|
|
135
|
+
a staging exception storm can't fill production's retention window, and
|
|
136
|
+
deleting a staging environment deletes a file. Applications and issues
|
|
137
|
+
live in the shared database, so an issue keeps a stable id like
|
|
138
|
+
`APP-171` even after the raw rows behind it are pruned.
|
|
139
|
+
|
|
140
|
+
## Do I need Inertia?
|
|
141
|
+
|
|
142
|
+
No. The browser client is the only Inertia-specific piece, and it's
|
|
143
|
+
optional — it adds `visit` records (page-visit duration, prop bytes,
|
|
144
|
+
partial reloads, Core Web Vitals) and the browser half of release health.
|
|
145
|
+
Everything else — requests, jobs, queries, exceptions, cache, mail,
|
|
146
|
+
logs — is server-side and works on any Rails app, API-only included.
|
|
147
|
+
|
|
148
|
+
Without the client, sessions still report from the request middleware,
|
|
149
|
+
which is the source that can see an unhandled exception anyway.
|
|
150
|
+
|
|
151
|
+
## Does it work with Sidekiq? Solid Queue?
|
|
152
|
+
|
|
153
|
+
Both, and anything else with an Active Job adapter. Jobs are
|
|
154
|
+
instrumented at the Active Job level (`perform_start.active_job` /
|
|
155
|
+
`perform.active_job`), so the adapter is a field on the record rather
|
|
156
|
+
than an integration to write. `job_attempt` records carry the adapter's
|
|
157
|
+
own id (`provider_job_id`) alongside Active Job's `job_id`.
|
|
158
|
+
|
|
159
|
+
Two features are Solid Queue-specific, because they read its tables:
|
|
160
|
+
`scheduled_task` records (recurring tasks from `config/recurring.yml`,
|
|
161
|
+
with `task_key`, `schedule`, and `drift`) and the queue depth and
|
|
162
|
+
oldest-job age on `health` records.
|
|
163
|
+
|
|
164
|
+
## What happens when the platform is unreachable?
|
|
165
|
+
|
|
166
|
+
Nothing, from your app's point of view. This is the property everything
|
|
167
|
+
else is built around: **delivery never raises into application code.**
|
|
168
|
+
|
|
169
|
+
Concretely. Recording pushes onto an in-memory buffer bounded two ways:
|
|
170
|
+
by record count (`c.buffer_size`, default 10,000) and by estimated payload
|
|
171
|
+
memory (`c.buffer_bytes`, default 16 MiB). The byte ceiling is the one that
|
|
172
|
+
matters when records are large — 10,000 records is a few megabytes of
|
|
173
|
+
ordinary telemetry, or a gigabyte of captured attachments. One execution's
|
|
174
|
+
buffered tree gets the same treatment (`c.execution_buffer_bytes`, 8 MiB),
|
|
175
|
+
and one delivery is capped at `c.batch_bytes` (8 MiB uncompressed). When a
|
|
176
|
+
limit is reached the *oldest* record is dropped and a counter is
|
|
177
|
+
incremented — the app thread never blocks waiting for room. The counters
|
|
178
|
+
ride along on the next successful batch (`X-Railwatch-Dropped` and
|
|
179
|
+
`X-Railwatch-Dropped-Bytes`), so loss shows up on the platform instead of
|
|
180
|
+
being silent.
|
|
181
|
+
|
|
182
|
+
A queue holding more than one batch is delivered as several batches: the
|
|
183
|
+
tail is put back for the next flush rather than dropped.
|
|
184
|
+
|
|
185
|
+
A background thread drains the buffer and POSTs. Each POST retries one
|
|
186
|
+
raised network error or 5xx immediately. If delivery still fails, the batch
|
|
187
|
+
and its drop counter go back into the bounded buffer; **402**, **408**,
|
|
188
|
+
**429**, and all **5xx** responses are retained the same way. So is a **2xx
|
|
189
|
+
that cannot acknowledge the batch** — a proxy's HTML sign-in page, malformed
|
|
190
|
+
JSON, or `accepted`/`rejected` counts that do not cover what was sent — which
|
|
191
|
+
would otherwise be a silent drop. The reporter
|
|
192
|
+
retries with jittered exponential backoff from one second up to 60 seconds,
|
|
193
|
+
so an outage cannot create a busy loop. A retained batch is retried eight
|
|
194
|
+
times (about four minutes on that ladder), then dropped and counted so the
|
|
195
|
+
buffer's newest records win again; meanwhile newer traffic that overflows
|
|
196
|
+
the buffer drops its oldest records, and every loss stays counted.
|
|
197
|
+
|
|
198
|
+
Connect timeout is 1 second and read/write timeout 3 seconds by default,
|
|
199
|
+
both configurable, and they're always on the reporter thread — even an
|
|
200
|
+
unhandled exception only enqueues and wakes that thread. A **401** marks the
|
|
201
|
+
transport unauthorized and stops further HTTP attempts for that process's
|
|
202
|
+
lifetime (fix the token and restart). A 401 or other permanent client
|
|
203
|
+
rejection drops that rejected batch and calls `Railwatch.on_unrecoverable`
|
|
204
|
+
with its status and record count.
|
|
205
|
+
|
|
206
|
+
Two 2xx shapes are a *successful* drain rather than a failure. An
|
|
207
|
+
acknowledgement carrying a `reason`, and an all-zero
|
|
208
|
+
`{"accepted":0,"rejected":0}`, are how the platform answers for an
|
|
209
|
+
environment it is not currently ingesting for (paused, over quota) — the
|
|
210
|
+
batch is released, because retrying it would burn all eight attempts and
|
|
211
|
+
drop the records anyway. And `rejected > 0` is routine, not an incident:
|
|
212
|
+
the platform rejects individual records it cannot store, records that
|
|
213
|
+
already appear on its own ingest batch. Those are visible under
|
|
214
|
+
`RAILWATCH_DEBUG=1` and are deliberately **not** sent to
|
|
215
|
+
`Railwatch.on_unrecoverable`.
|
|
216
|
+
|
|
217
|
+
On shutdown, `at_exit` gives the thread `c.shutdown_timeout` (2 seconds) to
|
|
218
|
+
attempt retained records immediately and retry within the remaining time.
|
|
219
|
+
If the deadline expires, the records stay retained and their count is sent
|
|
220
|
+
to `Railwatch.on_unrecoverable` (or stderr under `RAILWATCH_DEBUG=1`). This is an
|
|
221
|
+
in-memory buffer, not an on-disk spool: a hard kill, or exiting after that
|
|
222
|
+
deadline, cannot carry those records into the next process. Railwatch never
|
|
223
|
+
uses `Rails.logger` for its own failures, which would turn them into `log`
|
|
224
|
+
records about Railwatch.
|
|
225
|
+
|
|
226
|
+
## See also
|
|
227
|
+
|
|
228
|
+
- [`getting-started.md`](getting-started.md) — install and first request.
|
|
229
|
+
- [`configuration.md`](configuration.md) — every option and default.
|
|
230
|
+
- [`troubleshooting.md`](troubleshooting.md) — when something is missing.
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
Five minutes from `bundle add` to a request on the dashboard, on a
|
|
4
|
+
Rails 8 app. Everything below is the gem's own generator and rake tasks;
|
|
5
|
+
nothing else has to be wired by hand.
|
|
6
|
+
|
|
7
|
+
## 1. Add the gem
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
bundle add railwatch
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
The gem, its Ruby namespace, and its require path share one name:
|
|
14
|
+
`railwatch`, `Railwatch::*`, `require "railwatch"`.
|
|
15
|
+
|
|
16
|
+
## 2. Run the installer
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
bin/rails generate railwatch:install
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
With the token already in hand, let the generator read it without placing the
|
|
23
|
+
secret in shell history or process arguments:
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bin/rails generate railwatch:install \
|
|
27
|
+
--prompt-token \
|
|
28
|
+
--url=https://railwatch.rebulk.com \
|
|
29
|
+
--kamal-secrets
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `--prompt-token` reads without echo. `--token-stdin` is available for a
|
|
33
|
+
secret-manager pipe; an already exported `RAILWATCH_TOKEN` is also detected.
|
|
34
|
+
The legacy `--token=` flag warns because command arguments can be visible in
|
|
35
|
+
shell history and process listings.
|
|
36
|
+
- A token is written to `.env` only when Git confirms that `.env` is ignored.
|
|
37
|
+
A tracked or non-ignored dotenv file is refused; use Rails credentials, a
|
|
38
|
+
deployment secret manager, or add `.env` to `.gitignore first. Token values
|
|
39
|
+
are never printed by the generator.
|
|
40
|
+
- `--url=` sets `RAILWATCH_INGEST_URL`, for a self-hosted platform. Leave
|
|
41
|
+
it off to use the default, `https://railwatch.rebulk.com`.
|
|
42
|
+
- `--kamal-secrets` appends `RAILWATCH_TOKEN=$RAILWATCH_TOKEN` to
|
|
43
|
+
`.kamal/secrets` and adds `RAILWATCH_TOKEN` under `env: secret:` in
|
|
44
|
+
`config/deploy.yml`, which is the pair of edits Kamal needs to pass a
|
|
45
|
+
secret through to the containers.
|
|
46
|
+
|
|
47
|
+
What the generator writes, in every case:
|
|
48
|
+
|
|
49
|
+
- `config/initializers/railwatch.rb`, with every option commented out at
|
|
50
|
+
its default.
|
|
51
|
+
- `mount Railwatch::Engine, at: "/railwatch"` in `config/routes.rb` (the
|
|
52
|
+
beacon endpoint the browser client posts to).
|
|
53
|
+
- `.kamal/hooks/post-deploy` — only if `config/deploy.yml` already
|
|
54
|
+
exists.
|
|
55
|
+
- `app/frontend/lib/railwatch.ts` plus the `startRailwatch()` call in your
|
|
56
|
+
Inertia entrypoint — only if `app/frontend/` exists. If it can't find
|
|
57
|
+
an entrypoint it prints the two lines to add.
|
|
58
|
+
- `require "railwatch/rspec"` in `spec/rails_helper.rb`, or
|
|
59
|
+
`require "railwatch/minitest"` in `test/test_helper.rb`.
|
|
60
|
+
|
|
61
|
+
It finishes by running `railwatch:doctor` for you, so the install either
|
|
62
|
+
ends in a clean checklist or tells you what is still missing.
|
|
63
|
+
|
|
64
|
+
## 3. Where the token comes from
|
|
65
|
+
|
|
66
|
+
In Railwatch Cloud, create an application, then an environment inside it
|
|
67
|
+
(`production`, `staging`, one token each). The token is shown once, on
|
|
68
|
+
the page you land on right after creating the environment — copy it
|
|
69
|
+
then. If you lose it, rotate it from the environment's settings and
|
|
70
|
+
update `RAILWATCH_TOKEN`.
|
|
71
|
+
|
|
72
|
+
```sh
|
|
73
|
+
bin/rails railwatch:token # prints the URL to create/copy a token
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A token looks like `lt_` followed by 40 characters. The gem is
|
|
77
|
+
completely inert without one: `Railwatch.enabled?` is `config.enabled &&
|
|
78
|
+
token.present?`, so an app with no token installs no subscribers and
|
|
79
|
+
ships nothing.
|
|
80
|
+
|
|
81
|
+
## 4. Check the wiring
|
|
82
|
+
|
|
83
|
+
```sh
|
|
84
|
+
bin/rails railwatch:doctor
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
✓ token: lt_9Qv... (43 chars)
|
|
89
|
+
✓ ingest url: https://railwatch.rebulk.com
|
|
90
|
+
✓ ingest reachable: GET https://railwatch.rebulk.com/ingest/ping
|
|
91
|
+
✓ request middleware: Railwatch::Middleware::Request at position 0
|
|
92
|
+
✓ engine mounted: POST /railwatch/beacon -> railwatch/beacon#create
|
|
93
|
+
✓ deploy: 8f31c0a42e91 (from GIT_REV)
|
|
94
|
+
✓ sample rates: requests=1.0 jobs=1.0 commands=1.0 scheduled_tasks=1.0 exceptions=1.0
|
|
95
|
+
✓ ignored record types: none
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
It exits non-zero only when the token is missing or the ingest host is
|
|
99
|
+
unreachable; the rest of the checklist is informational. Every line and
|
|
100
|
+
what to do about a `✗` is in
|
|
101
|
+
[`troubleshooting.md`](troubleshooting.md).
|
|
102
|
+
|
|
103
|
+
`bin/rails railwatch:status` is the one-line version: it pings
|
|
104
|
+
`{ingest_url}/ingest/ping` and prints the ingest URL, deploy, and server.
|
|
105
|
+
|
|
106
|
+
## 5. Make one request
|
|
107
|
+
|
|
108
|
+
```sh
|
|
109
|
+
bin/rails server
|
|
110
|
+
curl http://localhost:3000/
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Records are batched in-process and flushed every `flush_interval`
|
|
114
|
+
(2 seconds by default) or every 500 records, whichever comes first, so
|
|
115
|
+
the request shows up on the environment's **Requests** page a couple of
|
|
116
|
+
seconds after you make it — with its queries, cache reads, view renders,
|
|
117
|
+
and log lines already attached to it.
|
|
118
|
+
|
|
119
|
+
## Three optional lines
|
|
120
|
+
|
|
121
|
+
Each of these is worth adding, and none of them is required for requests,
|
|
122
|
+
jobs, queries, and exceptions to report.
|
|
123
|
+
|
|
124
|
+
**The browser client**, for Inertia visit timing, Core Web Vitals, and
|
|
125
|
+
JavaScript errors. The generator adds both lines to your entrypoint when
|
|
126
|
+
it finds one:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import { startRailwatch } from "@/lib/railwatch"
|
|
130
|
+
|
|
131
|
+
startRailwatch()
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Errors ride the same beacon as visit timing — uncaught errors, unhandled
|
|
135
|
+
promise rejections, and Inertia's own failed-request events (`exception`
|
|
136
|
+
and `invalid` on Inertia 2, `networkError` and `httpException` on 3) —
|
|
137
|
+
and land as ordinary issues next to your Ruby ones.
|
|
138
|
+
|
|
139
|
+
If you have React error boundaries, add one more line where the root is
|
|
140
|
+
created. React does not report a boundary-caught error to `window.onerror`
|
|
141
|
+
outside a development build, so this is the only thing that gets a caught
|
|
142
|
+
render error out of production:
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { createRoot } from "react-dom/client"
|
|
146
|
+
import { railwatchRootOptions } from "@/lib/railwatch"
|
|
147
|
+
|
|
148
|
+
createRoot(el, railwatchRootOptions()).render(<App {...props} />)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
On React 18, whose roots take no error options, call
|
|
152
|
+
`reportError(error, { componentStack: info.componentStack })` from the
|
|
153
|
+
boundary's `componentDidCatch` instead. `startRailwatch` also takes optional
|
|
154
|
+
`ignoreErrors`, `denyUrls`, and `tenant` settings; see
|
|
155
|
+
[`docs/configuration.md`](configuration.md) and
|
|
156
|
+
[`docs/replacing-sentry.md`](replacing-sentry.md).
|
|
157
|
+
|
|
158
|
+
**The Kamal post-deploy hook**, for deploy markers and the commit diff
|
|
159
|
+
between deploys. Generated at `.kamal/hooks/post-deploy` when
|
|
160
|
+
`config/deploy.yml` exists; see the Kamal section below.
|
|
161
|
+
|
|
162
|
+
**The test matchers**, which turn your suite into a performance gate:
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
# spec/rails_helper.rb
|
|
166
|
+
require "railwatch/rspec"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
expect { get "/widgets" }.to have_railwatch_queries(at_most: 6)
|
|
171
|
+
expect { get "/widgets" }.not_to have_railwatch_n_plus_one
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
Full matcher list and a CI recipe: [`testing.md`](testing.md).
|
|
175
|
+
|
|
176
|
+
## What you'll see
|
|
177
|
+
|
|
178
|
+
Per environment, grouped the way the sidebar groups them:
|
|
179
|
+
|
|
180
|
+
**Activity** — Overview (throughput, p95, error rate, slowest routes,
|
|
181
|
+
newest issues, deploy markers), Requests (routes table and per-request
|
|
182
|
+
waterfall of every child record), Jobs, Scheduled tasks, Commands,
|
|
183
|
+
Exceptions, Queries (slow list and N+1 list with the app line that
|
|
184
|
+
issued them), Spans, Profiles, Transactions, View renders, Cache, Mail,
|
|
185
|
+
Notifications, Broadcasts, Outgoing requests, Storage, Logs,
|
|
186
|
+
Deprecations.
|
|
187
|
+
|
|
188
|
+
**Monitoring** — Visits (Inertia page-visit timing and web vitals),
|
|
189
|
+
Users, Tenants, Deploys, Releases (crash-free session and user rates),
|
|
190
|
+
Processes (Puma pool, Active Record pool, Solid Queue backlog), Alerts.
|
|
191
|
+
|
|
192
|
+
**Settings** — Thresholds, Usage. Issues and alert rules live one level
|
|
193
|
+
up, on the account.
|
|
194
|
+
|
|
195
|
+
## Deploying
|
|
196
|
+
|
|
197
|
+
### Kamal
|
|
198
|
+
|
|
199
|
+
Two edits, both of which `--kamal-secrets` makes for you:
|
|
200
|
+
|
|
201
|
+
```sh
|
|
202
|
+
# .kamal/secrets
|
|
203
|
+
RAILWATCH_TOKEN=$RAILWATCH_TOKEN
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
```yaml
|
|
207
|
+
# config/deploy.yml
|
|
208
|
+
env:
|
|
209
|
+
secret:
|
|
210
|
+
- RAILWATCH_TOKEN
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`config.deploy` picks up `KAMAL_VERSION` on its own, so every record is
|
|
214
|
+
stamped with the version that shipped it without any further
|
|
215
|
+
configuration.
|
|
216
|
+
|
|
217
|
+
The generated `.kamal/hooks/post-deploy` adds the deploy marker itself.
|
|
218
|
+
It runs on the deployer machine — which, unlike a container, has the git
|
|
219
|
+
history and Kamal's `KAMAL_*` variables — and POSTs twice: the deploy
|
|
220
|
+
(`{deploy, ref, name, url, server, timestamp, performer, destination,
|
|
221
|
+
service, commits}`, with up to 50 commits, which is what gives the
|
|
222
|
+
Deploys page a diff of what actually shipped) and the Kamal host list
|
|
223
|
+
(`{version, hosts, roles, ...}`, so the platform knows which servers
|
|
224
|
+
should be reporting). It exits immediately when `RAILWATCH_TOKEN` isn't
|
|
225
|
+
set and never fails a deploy — every network call ends in `|| true`.
|
|
226
|
+
|
|
227
|
+
Set the optional `RAILWATCH_DEPLOY_URL` to link the deploy marker at a CI
|
|
228
|
+
run or a release page.
|
|
229
|
+
|
|
230
|
+
### Docker, Heroku, Render
|
|
231
|
+
|
|
232
|
+
Environment variables only:
|
|
233
|
+
|
|
234
|
+
```sh
|
|
235
|
+
RAILWATCH_TOKEN=lt_...
|
|
236
|
+
RAILWATCH_INGEST_URL=https://railwatch.rebulk.com # only when self-hosting
|
|
237
|
+
RAILWATCH_DEPLOY=<release identifier>
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
`RAILWATCH_DEPLOY` is the explicit override. Without it, `config.deploy` checks,
|
|
241
|
+
in order: `KAMAL_VERSION`; `GIT_REV`, `GIT_SHA`, `SOURCE_VERSION`,
|
|
242
|
+
`HEROKU_SLUG_COMMIT`, `RENDER_GIT_COMMIT`, the tag from `FLY_IMAGE_REF`,
|
|
243
|
+
`VERCEL_GIT_COMMIT_SHA`, `CI_COMMIT_SHA`, and `GITHUB_SHA`; a Capistrano
|
|
244
|
+
`REVISION` file; then `.git/HEAD` through loose or packed refs. It never runs
|
|
245
|
+
Git during boot. Full 40-character SHAs are shortened to 12 characters. Set
|
|
246
|
+
`RAILWATCH_DETECT_DEPLOY=false` to disable inferred sources while retaining
|
|
247
|
+
`RAILWATCH_DEPLOY` and `KAMAL_VERSION`.
|
|
248
|
+
|
|
249
|
+
### No Kamal
|
|
250
|
+
|
|
251
|
+
Run the deploy task as a release or post-deploy step, so charts still
|
|
252
|
+
get deploy markers:
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
bin/rails "railwatch:deploy[$GIT_SHA,v42,https://ci.example.com/runs/42]"
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
All three arguments are optional: `ref` defaults to `git rev-parse HEAD`,
|
|
259
|
+
`name` and `url` are labels for the marker. The task aborts if
|
|
260
|
+
`config.deploy` is unset. Run inside a container built from a repo with
|
|
261
|
+
no `.git`, the commit list comes back empty and the marker ships without
|
|
262
|
+
one — the deploy is still recorded.
|
|
263
|
+
|
|
264
|
+
## Next
|
|
265
|
+
|
|
266
|
+
- [`configuration.md`](configuration.md) — every option, env var, and
|
|
267
|
+
default.
|
|
268
|
+
- [`records.md`](records.md) — every record type, field by field.
|
|
269
|
+
- [`testing.md`](testing.md) — matchers and the CI performance gate.
|
|
270
|
+
- [`replacing-sentry.md`](replacing-sentry.md) — migrating off
|
|
271
|
+
`sentry-rails`.
|
|
272
|
+
- [`replacing-nightwatch.md`](replacing-nightwatch.md) — for people
|
|
273
|
+
coming from Laravel.
|
|
274
|
+
- [`self-hosting.md`](self-hosting.md) — pointing the gem at your own
|
|
275
|
+
platform install.
|
|
276
|
+
- [`troubleshooting.md`](troubleshooting.md) — every `railwatch:doctor`
|
|
277
|
+
line and what a failure means.
|
|
278
|
+
- [`faq.md`](faq.md) — overhead, retention, PII, unreachable platform.
|
|
279
|
+
- [`ai-and-mcp.md`](ai-and-mcp.md) — asking an AI assistant what broke.
|