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.
Files changed (78) hide show
  1. checksums.yaml +7 -0
  2. data/AGENTS.md +122 -0
  3. data/CHANGELOG.md +462 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.md +226 -0
  6. data/app/controllers/railwatch/beacon_controller.rb +254 -0
  7. data/config/routes.rb +5 -0
  8. data/docs/ai-and-mcp.md +227 -0
  9. data/docs/configuration.md +931 -0
  10. data/docs/faq.md +230 -0
  11. data/docs/getting-started.md +279 -0
  12. data/docs/records.md +834 -0
  13. data/docs/replacing-nightwatch.md +216 -0
  14. data/docs/replacing-sentry.md +573 -0
  15. data/docs/security.md +94 -0
  16. data/docs/self-hosting.md +60 -0
  17. data/docs/source-maps.md +60 -0
  18. data/docs/testing.md +175 -0
  19. data/docs/troubleshooting.md +319 -0
  20. data/lib/generators/railwatch/install/install_generator.rb +280 -0
  21. data/lib/generators/railwatch/install/templates/initializer.rb +54 -0
  22. data/lib/generators/railwatch/install/templates/post-deploy +98 -0
  23. data/lib/generators/railwatch/install/templates/railwatch.ts +658 -0
  24. data/lib/railwatch/attachments.rb +83 -0
  25. data/lib/railwatch/backtrace.rb +158 -0
  26. data/lib/railwatch/buffer.rb +122 -0
  27. data/lib/railwatch/clock.rb +25 -0
  28. data/lib/railwatch/configuration.rb +334 -0
  29. data/lib/railwatch/console.rb +48 -0
  30. data/lib/railwatch/context.rb +125 -0
  31. data/lib/railwatch/controller_helpers.rb +21 -0
  32. data/lib/railwatch/current.rb +32 -0
  33. data/lib/railwatch/engine.rb +144 -0
  34. data/lib/railwatch/execution.rb +367 -0
  35. data/lib/railwatch/faraday.rb +73 -0
  36. data/lib/railwatch/health.rb +188 -0
  37. data/lib/railwatch/job_tracing.rb +49 -0
  38. data/lib/railwatch/middleware/request.rb +289 -0
  39. data/lib/railwatch/minitest.rb +43 -0
  40. data/lib/railwatch/patches/inertia.rb +34 -0
  41. data/lib/railwatch/patches/net_http.rb +102 -0
  42. data/lib/railwatch/patches/rake_task.rb +88 -0
  43. data/lib/railwatch/patches/runner_command.rb +120 -0
  44. data/lib/railwatch/patches.rb +43 -0
  45. data/lib/railwatch/profiler.rb +270 -0
  46. data/lib/railwatch/record.rb +119 -0
  47. data/lib/railwatch/redactor.rb +67 -0
  48. data/lib/railwatch/release_detector.rb +97 -0
  49. data/lib/railwatch/reporter.rb +539 -0
  50. data/lib/railwatch/rspec.rb +139 -0
  51. data/lib/railwatch/sampler.rb +17 -0
  52. data/lib/railwatch/secret_safety.rb +62 -0
  53. data/lib/railwatch/sessions.rb +162 -0
  54. data/lib/railwatch/source_maps.rb +59 -0
  55. data/lib/railwatch/spec_helper.rb +147 -0
  56. data/lib/railwatch/sql_normalizer.rb +398 -0
  57. data/lib/railwatch/subscribers/base.rb +54 -0
  58. data/lib/railwatch/subscribers/broadcasts.rb +107 -0
  59. data/lib/railwatch/subscribers/cache.rb +107 -0
  60. data/lib/railwatch/subscribers/deprecations.rb +26 -0
  61. data/lib/railwatch/subscribers/exceptions.rb +304 -0
  62. data/lib/railwatch/subscribers/jobs.rb +282 -0
  63. data/lib/railwatch/subscribers/logs.rb +137 -0
  64. data/lib/railwatch/subscribers/mail.rb +42 -0
  65. data/lib/railwatch/subscribers/notifications.rb +36 -0
  66. data/lib/railwatch/subscribers/process_info.rb +98 -0
  67. data/lib/railwatch/subscribers/queries.rb +183 -0
  68. data/lib/railwatch/subscribers/requests.rb +94 -0
  69. data/lib/railwatch/subscribers/storage.rb +35 -0
  70. data/lib/railwatch/subscribers/users.rb +159 -0
  71. data/lib/railwatch/subscribers/views.rb +54 -0
  72. data/lib/railwatch/subscribers.rb +34 -0
  73. data/lib/railwatch/transport/http.rb +208 -0
  74. data/lib/railwatch/version.rb +5 -0
  75. data/lib/railwatch.rb +550 -0
  76. data/lib/tasks/railwatch_tasks.rake +289 -0
  77. data/llms.txt +38 -0
  78. 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.