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
@@ -0,0 +1,216 @@
1
+ # Coming from Laravel Nightwatch
2
+
3
+ Railwatch is the same product shape for Rails: one package instruments the
4
+ framework end to end, records are grouped under the execution that
5
+ produced them, and a hosted platform turns them into routes, jobs,
6
+ queries, issues, and alerts. If you know Nightwatch, you already know
7
+ how to read Railwatch — this page maps the vocabulary and points out the
8
+ three places the Rails answer is genuinely different.
9
+
10
+ Railwatch is an independent product and is not affiliated with Laravel or
11
+ Laravel Nightwatch.
12
+
13
+ ## The architecture difference: no agent
14
+
15
+ Nightwatch needs `nightwatch:agent` because PHP-FPM has no long-lived
16
+ process to batch from: the app writes records over a socket and a
17
+ separate daemon POSTs them.
18
+
19
+ Puma and Solid Queue workers *are* long-lived, so Railwatch skips that
20
+ tier. A single reporter thread per process holds a bounded buffer
21
+ (default 5,000 records, oldest dropped and counted), and gzip-NDJSON
22
+ POSTs batches to the platform every 2 seconds or every 500 records. There
23
+ is no daemon to install, supervise, or forget to restart, and nothing
24
+ between the app and the ingest URL.
25
+
26
+ The thread is re-armed after `fork`, so clustered Puma workers and
27
+ forked Solid Queue workers each get their own with no `on_worker_boot`
28
+ hook to write.
29
+
30
+ ## Records
31
+
32
+ Nightwatch's types, and what they're called here:
33
+
34
+ | Nightwatch | Railwatch | Notes |
35
+ |---|---|---|
36
+ | `request` | `request` | Plus `controller`/`action`, `format`, `queue_time` from `X-Request-Start`, `view_runtime`/`db_runtime`, `redirect_to`, `halted_callback`, `unpermitted_parameters`, `rate_limited`, and an `inertia` block (component, version, partial reload, prop bytes, SSR ms). |
37
+ | `command` | `command` | Rake tasks and `bin/rails runner`, not Artisan — Rails has no command bus. Task prerequisites nest inside the top-level command rather than opening their own. |
38
+ | `job-attempt` | `job_attempt` | Active Job level, so the adapter (Solid Queue, Sidekiq, ...) doesn't matter. Adds `attempt`, `queue_latency`, `concurrency_key`, `priority`, and a `"released"` status for a `retry_on` that caught internally. |
39
+ | `scheduled-task` | `scheduled_task` | Solid Queue recurring tasks from `config/recurring.yml`, detected from `SolidQueue::RecurringExecution`. Carries `task_key`, `schedule`, and `drift`. |
40
+ | — | `channel_action` | One Action Cable action parent with its SQL, logs, broadcasts/transmits, and exception in the same trace. |
41
+ | `query` | `query` | Normalized per adapter, with source `file:line`. Cached queries are counted, not stored. |
42
+ | — | `n_plus_one` | Derived in-process: the same query group repeating `n_plus_one_threshold` times (default 5) in one execution. |
43
+ | — | `transaction` | Duration, outcome, statement count. |
44
+ | `exception` | `exception` | Grouped by class + top app frame + normalized message, overridable per class, per call, or globally. |
45
+ | `cache-event` | `cache_event` | Same hit/miss/write/delete shape, over Active Support::Cache. |
46
+ | `mail` | `mail` | Render ms vs deliver ms, recipient counts, delivery method. |
47
+ | `notification` | `notification` | The `noticed` gem, if loaded. Laravel's channel system has no Rails equivalent. |
48
+ | — | `broadcast` | Action Cable `broadcast`/`transmit` — the closest thing Rails has to Laravel's push channels. |
49
+ | `outgoing-request` | `outgoing_request` | One `Net::HTTP` prepend covers Faraday's default adapter, HTTParty, RestClient, and `ruby-llm`; `Railwatch::Faraday` is the Guzzle-middleware equivalent for other adapters. |
50
+ | `queued-job` | `enqueued_job` | The enqueue side, in the execution that enqueued it. |
51
+ | `log` | `log` | Lines at or above `log_level`, plus Rails 8.1 structured `Rails.event` events. |
52
+ | `user` | `user` | Resolved once per user per process-hour, not once per request. |
53
+ | deployment (`nightwatch:deploy`) | `Deploy` on the platform | Posted by `railwatch:deploy` or the Kamal `post-deploy` hook, with up to 50 commits so the platform can diff what shipped. |
54
+ | request `stages` | parent `stages` | Same idea, Rails boundaries: `middleware_before`, `action`, `render`, `middleware_after`, `body`. Laravel's `bootstrap` has no equivalent in a warm process — Railwatch reports boot time once per process as a `process` record instead. |
55
+
56
+ And the types with no Nightwatch counterpart at all: `storage_op` (Active
57
+ Storage), `view_render`, `span` (your own timed blocks), `attachment`,
58
+ `deprecation`, `visit` (Inertia page visits from the browser),
59
+ `session` (release health), `process`, `health` (Puma pool, Active Record
60
+ pool, Solid Queue backlog), and `profile` (sampled stack profiles).
61
+
62
+ Field-by-field detail for all 26 is in [`records.md`](records.md).
63
+
64
+ ## What "execution" means
65
+
66
+ Exactly what it means in Nightwatch, with one more parent type. An
67
+ execution is a **request**, a **job attempt**, a **scheduled task run**,
68
+ or a **command**. Every other record is a child of one: it carries
69
+ `execution_id`, `execution_source`, `execution_preview` (the human
70
+ label, e.g. `"GET /posts"`), and `execution_stage` (which lifecycle stage
71
+ it happened in).
72
+
73
+ `trace_id` is the wider unit. It is shared by a request and every job
74
+ that request enqueued, so async work traces back to what started it, and
75
+ it is propagated across services as a W3C `traceparent` on outgoing HTTP
76
+ — an inbound `traceparent` is adopted, so a trace spans services rather
77
+ than stopping at the process boundary.
78
+
79
+ Practically: on the platform you never look at a query in isolation. You
80
+ open the request, and the query is in its waterfall with everything else
81
+ that execution did.
82
+
83
+ ## Sampling parity
84
+
85
+ Nightwatch samples per kind (`sampling.requests`, `.commands`,
86
+ `.exceptions`, `.scheduled_tasks`), decided once per execution. Railwatch
87
+ is the same hash with `jobs` added:
88
+
89
+ ```ruby
90
+ c.sample = { requests: 0.1, jobs: 1.0, commands: 1.0,
91
+ scheduled_tasks: 1.0, channels: 1.0, exceptions: 1.0 }
92
+ ```
93
+
94
+ Same semantics: sampled in means the whole tree ships, sampled out means
95
+ nothing ships — except an unhandled exception, which is governed by the
96
+ `exceptions` rate and flushed immediately. Counters on the parent
97
+ (queries, cache events, mail, ...) are incremented even when the
98
+ execution is sampled out, so aggregate rates don't depend on the sample
99
+ rate.
100
+
101
+ Nightwatch's `Sample::rate(0.5) / always() / never()` route middleware
102
+ becomes a controller macro:
103
+
104
+ ```ruby
105
+ class ReportsController < ApplicationController
106
+ railwatch_sample 0.01, only: :index
107
+ railwatch_never_sample only: :health
108
+ end
109
+ ```
110
+
111
+ Both take the same options as `before_action`. Programmatically:
112
+ `Railwatch.sample(rate)`, `Railwatch.dont_sample`, `Railwatch.sampling?`.
113
+
114
+ **What Railwatch adds: tail sampling.** Head sampling throws away exactly
115
+ the slow requests you wanted. Set `c.tail_sample_slow_ms = 500` and a
116
+ head-sampled-out execution keeps buffering its children and is kept at
117
+ the end if it ran that long, raised, or called `Railwatch.keep!`. Its
118
+ parent record carries `tail_sampled: true` so it stays distinguishable
119
+ from a head-sampled one.
120
+
121
+ ```ruby
122
+ c.sample = { requests: 0.05 } # 5% of requests...
123
+ c.tail_sample_slow_ms = 500 # ...plus every one over 500ms
124
+ Railwatch.keep! # ...plus this one, whatever the roll said
125
+ ```
126
+
127
+ The trade-off is memory: with tail sampling on, every sampled-out
128
+ execution buffers its child records for its lifetime (capped at 10,000
129
+ per execution) instead of discarding them as they happen. With it off —
130
+ the default — nothing is built or buffered for a sampled-out execution
131
+ at all.
132
+
133
+ **And failure context.** Nightwatch, like Railwatch before this, ships an
134
+ unsampled execution's unhandled exception with its parent record and
135
+ nothing else: no queries, logs or outgoing requests from the moments
136
+ before it. Set `c.failure_context = 200` and a head-sampled-out
137
+ execution keeps its last 200 child records in a ring, shipping them only
138
+ if it reports an unhandled exception. It is the failure half of tail
139
+ sampling without the memory bill of keeping every sampled-out execution
140
+ alive; the two are independent, and tail sampling's larger buffer wins if
141
+ both are set. See `docs/configuration.md`.
142
+
143
+ ## Facade parity
144
+
145
+ Same facade, Ruby names:
146
+
147
+ | Nightwatch | Railwatch |
148
+ |---|---|
149
+ | `Nightwatch::user($cb)` | `Railwatch.user { \|user\| ... }` (or `c.user { }`) |
150
+ | `sample($rate)` | `Railwatch.sample(rate)` |
151
+ | `dontSample()` | `Railwatch.dont_sample` |
152
+ | `sampling()` | `Railwatch.sampling?` |
153
+ | `ignore($cb)` | `Railwatch.ignore { }` |
154
+ | `pause()` / `resume()` / `paused()` | `Railwatch.pause` / `Railwatch.resume` / `Railwatch.paused?` |
155
+ | `report($e, $handled)` | `Railwatch.report(error, handled: true)` |
156
+ | `redactRequests` / `redactQueries` / `redactExceptions` / `redactCacheEvents` / `redactCommands` / `redactMail` / `redactOutgoingRequests` | `Railwatch.redact_requests`, `redact_queries`, `redact_exceptions`, `redact_cache_events`, `redact_commands`, `redact_mail`, `redact_outgoing_requests`, plus `redact_logs` |
157
+ | `rejectQueries` / `rejectCacheEvents` / `rejectMail` / `rejectNotifications` / `rejectOutgoingRequests` / `rejectQueuedJobs` | `Railwatch.reject_queries`, `reject_cache_events`, `reject_mail`, `reject_notifications`, `reject_outgoing_requests`, `reject_enqueued_jobs`, plus `reject_broadcasts` and `reject_logs` |
158
+ | `rejectCacheKeys([...])` | `Railwatch.reject_cache_keys(%w[session: rack::attack*])` |
159
+ | `captureDefaultVendorCommands` / `CacheKeys` | `c.capture_default_vendor_commands` / `c.capture_default_vendor_cache_keys` |
160
+ | `guzzleMiddleware()` | `Railwatch::Faraday` (`Faraday.new(url) { \|f\| f.use Railwatch::Faraday }`); `Net::HTTP` is covered globally with no setup |
161
+ | `IngestingEvents` listener returning `false` | `Railwatch.before_ingest { \|batch\| ... }` |
162
+
163
+ Beyond the facade: `Railwatch.context(**attrs)` (Laravel Context's
164
+ counterpart, writing through to all three of Rails' own context stores),
165
+ `Railwatch.span(name, **attrs) { }`, `Railwatch.keep!`,
166
+ `Railwatch.attach(name, data)`, `Railwatch.fingerprint { }`,
167
+ `Railwatch.instrument_outgoing(method, url) { }`,
168
+ `Railwatch.on_unrecoverable { }`, and `Railwatch.flush`.
169
+
170
+ ## Config and commands
171
+
172
+ `config/nightwatch.php` becomes `config/initializers/railwatch.rb`, and
173
+ every setting still has an env var — `NIGHTWATCH_*` becomes `RAILWATCH_*`.
174
+ `filtering.ignore_*` becomes one list, `c.ignore = [:cache_events,
175
+ :queries, ...]`, validated at assignment. `filtering.log_level` becomes
176
+ `c.log_level`. `ingest.uri`, `.timeout`, `.connection_timeout`, and
177
+ `.event_buffer` become `c.ingest_url`, `c.timeout`,
178
+ `c.connect_timeout`, and `c.buffer_size`.
179
+
180
+ | Nightwatch | Railwatch |
181
+ |---|---|
182
+ | `nightwatch:agent` | Nothing — the reporter thread lives in the app process. |
183
+ | `nightwatch:status` | `bin/rails railwatch:status` |
184
+ | `nightwatch:deploy {deploy} --ref --name --url` | `bin/rails railwatch:deploy[ref,name,url]`, or the generated `.kamal/hooks/post-deploy` |
185
+ | — | `bin/rails railwatch:doctor`, which checks the whole install and exits non-zero if the token or the ingest host is wrong |
186
+
187
+ ## The three things worth knowing about Rails
188
+
189
+ **Tenancy is first class.** Nightwatch tells you to prefix user ids by
190
+ hand. Railwatch reads `ActiveRecord::Base.current_tenant` /
191
+ `TenantRecord.current_tenant` (`activerecord-tenanted`) with zero config,
192
+ stamps `tenant` on every record, and gives you a Tenants page.
193
+ `Railwatch.context(tenant: org.slug)` sets it explicitly for apps that
194
+ roll their own.
195
+
196
+ **Jobs are instrumented at Active Job**, not per adapter, so Solid Queue,
197
+ Sidekiq, and anything else with an Active Job adapter all report the same
198
+ `job_attempt` fields. Scheduled tasks are Solid Queue recurring tasks,
199
+ so cron monitoring needs no check-in calls.
200
+
201
+ **Your test suite is a performance gate.** The same instrumentation runs
202
+ under RSpec and Minitest, so a query budget can be checked in and CI can
203
+ fail the pull request that regresses it:
204
+
205
+ ```ruby
206
+ expect { get "/widgets" }.to have_railwatch_queries(at_most: 6)
207
+ expect { get "/widgets" }.not_to have_railwatch_n_plus_one
208
+ ```
209
+
210
+ See [`testing.md`](testing.md).
211
+
212
+ ## Next
213
+
214
+ - [`getting-started.md`](getting-started.md) — install, in five minutes.
215
+ - [`configuration.md`](configuration.md) — every option and env var.
216
+ - [`records.md`](records.md) — all 26 record types, field by field.