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/records.md
ADDED
|
@@ -0,0 +1,834 @@
|
|
|
1
|
+
# Record types
|
|
2
|
+
|
|
3
|
+
Every record Railwatch ships is a flat hash (`lib/railwatch/record.rb`). This
|
|
4
|
+
lists all 26, field by field, sourced from the subscriber or patch that
|
|
5
|
+
builds each one. Field names below are the hash keys as sent over the
|
|
6
|
+
wire (symbols in Ruby, strings in the gzip NDJSON payload).
|
|
7
|
+
|
|
8
|
+
## Shared envelope
|
|
9
|
+
|
|
10
|
+
Every record carries these (`Record.build`, `lib/railwatch/record.rb`):
|
|
11
|
+
|
|
12
|
+
| Field | Meaning |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `v` | Record schema version for this type (`Record::VERSIONS`). |
|
|
15
|
+
| `t` | Type string, e.g. `"query"`. |
|
|
16
|
+
| `timestamp` | Unix seconds (float) when the event started. |
|
|
17
|
+
| `deploy` | `Railwatch.config.deploy` — auto-detected from the deploy environment, `REVISION`, or Git checkout as documented in [`configuration.md`](configuration.md#core). |
|
|
18
|
+
| `server` | `Railwatch.config.server` — hostname by default. |
|
|
19
|
+
| `_group` | 128-bit grouping hash (MD5 of type-specific parts, `Record.group_hash`) the platform uses to bucket occurrences into one issue/row. |
|
|
20
|
+
|
|
21
|
+
Each gzip NDJSON batch also has a small HTTP-header envelope. Drop accounting
|
|
22
|
+
rides as `X-Railwatch-Dropped` and `X-Railwatch-Dropped-Bytes` when non-zero.
|
|
23
|
+
`X-Railwatch-Backpressure-Factor` is present when adaptive backpressure has
|
|
24
|
+
reduced sampling; for example, `4.0` means each configured execution sample
|
|
25
|
+
rate was divided by four when the batch was sent. The reporter doubles the
|
|
26
|
+
factor on each pressured tick up to 8, then halves it toward 1 as pressure
|
|
27
|
+
clears. This makes buffer loss visible alongside the sampling response that
|
|
28
|
+
was active at delivery time.
|
|
29
|
+
|
|
30
|
+
Records created inside an execution (everything except the five parent
|
|
31
|
+
types, plus `user`/`process`/`visit`, which stand alone) also merge in the
|
|
32
|
+
execution's envelope (`Execution#envelope`, `lib/railwatch/execution.rb`):
|
|
33
|
+
|
|
34
|
+
| Field | Meaning |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `trace_id` | Shared by a request and every job it enqueues (`JobTracing`), so a chain of async work traces back to the request that started it. |
|
|
37
|
+
| `execution_source` | `"request"`, `"job"`, `"scheduled_task"`, `"command"`, or `"channel_action"`. |
|
|
38
|
+
| `execution_id` | UUID of the parent execution this child belongs to. |
|
|
39
|
+
| `parent_id` | UUID of the execution that enqueued this one (e.g. the request that enqueued a job), or nil. |
|
|
40
|
+
| `execution_preview` | Human label for the parent, e.g. `"GET /posts"` or `"PostsController#index"`. |
|
|
41
|
+
| `execution_stage` | Lifecycle stage active when the record was created (`middleware_before`, `action`, `render`, `middleware_after`, ...). |
|
|
42
|
+
| `user` | Resolved user id (`Subscribers::Users`), or nil. On a job, the id propagated from whatever enqueued it (see `job_attempt` below). |
|
|
43
|
+
| `tenant` | `Railwatch.context(tenant: ...)` / `Context.current_tenant`, or nil. On a job, the tenant propagated from whatever enqueued it. |
|
|
44
|
+
|
|
45
|
+
## Parent records
|
|
46
|
+
|
|
47
|
+
`request`, `job_attempt`, `scheduled_task`, and `command` are the four
|
|
48
|
+
parent types (`Railwatch::PARENT_TYPES`). Each opens an `Execution` and, in
|
|
49
|
+
addition to its own fields below, always carries (`Railwatch.build_parent`,
|
|
50
|
+
`lib/railwatch.rb`):
|
|
51
|
+
|
|
52
|
+
| Field | Meaning |
|
|
53
|
+
|---|---|
|
|
54
|
+
| `duration` | Wall time in microseconds, execution start to finish. |
|
|
55
|
+
| `stages` | Hash of stage name → microseconds spent in it (e.g. `{"middleware_before" => 120, "action" => 4300, "render" => 900}`). |
|
|
56
|
+
| `counters` | Hash of child-record counts for this execution — `queries`, `cached_queries`, `exceptions`, `logs`, `cache_events`, `jobs_enqueued`, `mail`, `broadcasts`, `notifications`, `outgoing_requests`, `storage_ops`, `view_renders`, `transactions`, `hydrated_models`, `lazy_loads`, `deprecations`, `spans` (`Execution::COUNTERS`). Counted even when the execution is sampled out, so aggregate rates don't depend on the sample rate. |
|
|
57
|
+
| `peak_memory` | RSS in bytes, sampled at most once per second process-wide (`Execution.sampled_memory`) — cheap enough to read but not per-execution-accurate to the microsecond. |
|
|
58
|
+
| `allocations` | Objects allocated during the execution (`GC.stat(:total_allocated_objects)` delta). |
|
|
59
|
+
| `gc_time` | GC time in the execution's window, when the Ruby build exposes `GC.stat(:time)`. |
|
|
60
|
+
| `exception_preview` | First unhandled exception's `"Class: message"`, truncated to 255 chars, or nil. |
|
|
61
|
+
| `context` | Serialized `Railwatch.context(...)` key/values active for this execution, parameter-filtered like request params. `"_railwatch_truncated": true` when it did not fit in 64KB. |
|
|
62
|
+
|
|
63
|
+
A sampled-out execution still ships its parent record if it raised an
|
|
64
|
+
unhandled exception (`Railwatch.finish_execution`) — sampling controls
|
|
65
|
+
whether child records ship, not whether an error is visible.
|
|
66
|
+
|
|
67
|
+
### `request`
|
|
68
|
+
|
|
69
|
+
Built by the outermost Rack middleware (`lib/railwatch/middleware/request.rb`),
|
|
70
|
+
which also owns the `middleware_before`/`action`/`render`/`middleware_after`
|
|
71
|
+
stage boundaries (the `action`/`render` boundaries come from
|
|
72
|
+
`start_processing.action_controller` and `render_template.action_view` in
|
|
73
|
+
`lib/railwatch/subscribers/requests.rb`).
|
|
74
|
+
|
|
75
|
+
| Field | Meaning |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `group` | Hash of `method` + route `pattern`. |
|
|
78
|
+
| `method` | HTTP verb. |
|
|
79
|
+
| `url` | Request origin + path, with authority credentials, the entire query string, and fragment removed, truncated to 2048 chars. |
|
|
80
|
+
| `path` | Request path. |
|
|
81
|
+
| `route` | Matched route pattern (`request.route_uri_pattern`), or `"unmatched"` for a 404. |
|
|
82
|
+
| `route_methods` | Array with the route's declared verb, if known. |
|
|
83
|
+
| `route_domain` | `request.host`. |
|
|
84
|
+
| `controller` | Controller name, `Controller` suffix stripped, underscored. |
|
|
85
|
+
| `action` | Action name. |
|
|
86
|
+
| `format` | Negotiated response format (`"html"`, `"json"`, ...). Empty when the request never reached a controller, since resolving it would mean parsing the request body at teardown. |
|
|
87
|
+
| `ip` | `request.remote_ip`. |
|
|
88
|
+
| `status_code` | Response status. |
|
|
89
|
+
| `request_size` / `response_size` | Bytes, from `Content-Length`. |
|
|
90
|
+
| `queue_time` | Microseconds the request waited in the proxy/web-server queue before the execution started, parsed from `X-Request-Start` (or `X-Queue-Start`): `t=1700000000.123` (seconds), `t=1700000000123` (ms), `t=1700000000123456` (µs), or the same values bare — the unit is decided by magnitude. A proxy clock running ahead clamps to `0`; anything over 60 seconds is treated as clock skew and dropped. nil when the header is absent or unparseable. |
|
|
91
|
+
| `view_runtime` / `db_runtime` | Milliseconds, from Action Controller's own `process_action.action_controller` payload. |
|
|
92
|
+
| `redirect_to` | Redirect target with authority credentials, the entire query string, and fragment removed, truncated to 512 chars, if `redirect_to` was called. This sanitizes telemetry only; it does not change the response's `Location` header. |
|
|
93
|
+
| `halted_callback` | Filter that halted the callback chain (`throw :abort`), if any. |
|
|
94
|
+
| `unpermitted_parameters` | Array of param keys strong parameters rejected. |
|
|
95
|
+
| `rate_limited` | `{name:, count:, to:}` if `ActionController::RateLimiting` fired, else nil. |
|
|
96
|
+
| `inertia` | Present only on an Inertia request (`X-Inertia` header or an Inertia render happened): `{component, version, partial_component, partial_only, partial_except, props_bytes, ssr_ms}`. `ssr_ms` is only set when `inertia_rails` SSR actually rendered this request (`lib/railwatch/patches/inertia.rb`). |
|
|
97
|
+
| `headers` | Request headers as a hash, header names Title-Cased; values matching a redacted pattern replaced with `Redactor::FILTERED` (`lib/railwatch/redactor.rb`). |
|
|
98
|
+
| `payload` | Filtered request params — only captured when `config.capture_request_payload` is on **and** the request raised an exception (never for successful requests). |
|
|
99
|
+
| `user_agent` | Truncated to 256 chars. |
|
|
100
|
+
| `files` | Array of `{name, size, content_type, error}` for each uploaded file in a multipart request (metadata only, never contents). A non-multipart request body is never parsed to fill this in. |
|
|
101
|
+
| `profiled` | `true` when a `profile` record shipped for this request; absent otherwise. |
|
|
102
|
+
|
|
103
|
+
### `job_attempt`
|
|
104
|
+
|
|
105
|
+
One per Active Job `perform` (`perform.active_job`,
|
|
106
|
+
`lib/railwatch/subscribers/jobs.rb`), for jobs Solid Queue's own recurring
|
|
107
|
+
scheduler didn't originate (see `scheduled_task` below for the ones it did).
|
|
108
|
+
|
|
109
|
+
| Field | Meaning |
|
|
110
|
+
|---|---|
|
|
111
|
+
| `group` | Hash of the job class name. |
|
|
112
|
+
| `job_id` | Active Job's `job_id`. |
|
|
113
|
+
| `provider_job_id` | Queue adapter's own id (e.g. Solid Queue job row id). |
|
|
114
|
+
| `attempt_id` | This execution's id (same as `execution_id`). |
|
|
115
|
+
| `attempt` | `job.executions` — the retry count. |
|
|
116
|
+
| `name` | Job class name. |
|
|
117
|
+
| `queue` | Queue name. |
|
|
118
|
+
| `adapter` / `connection` | Queue adapter class, demodulized, `Adapter` suffix stripped (both fields carry the same value). |
|
|
119
|
+
| `concurrency_key` | If the job responds to `concurrency_key` (e.g. `good_job`/custom concurrency controls). |
|
|
120
|
+
| `priority` | Job priority. |
|
|
121
|
+
| `status` | `"processed"`, `"failed"`, `"aborted"`, or `"released"` (released = a `retry_on` caught the error internally — see `enqueue_retry.active_job` below). |
|
|
122
|
+
| `queue_latency` | Microseconds between `scheduled_at`/`enqueued_at` and this attempt starting. |
|
|
123
|
+
| `db_runtime` | Milliseconds of DB time during the attempt, from Active Job's own payload. |
|
|
124
|
+
| `arguments_preview` | Up to 10 arguments — GlobalID string for AR objects/GlobalID-capable arguments, class name otherwise (never raw argument values). Always on. |
|
|
125
|
+
| `arguments` | The job's real arguments (`job.serialize["arguments"]`, Active Job's own JSON-safe form, so an Active Record argument is already a GlobalID). Only present when `config.capture_job_arguments` is on — off by default, because arguments routinely carry PII. Hash arguments (including hashes nested in an array argument) go through the same parameter filter as request params, so a `password:` keyword ships as `[FILTERED]`. |
|
|
126
|
+
| `arguments_truncated` | `true` when trailing arguments had to be dropped to fit `arguments` into 8 KiB of JSON. Absent otherwise, and absent entirely when `capture_job_arguments` is off. |
|
|
127
|
+
| `profiled` | `true` when a `profile` record shipped for this attempt; absent otherwise. |
|
|
128
|
+
|
|
129
|
+
`user` and `tenant` on a job attempt (and therefore on every child record
|
|
130
|
+
under it) come from the execution that enqueued the job, not from the
|
|
131
|
+
worker process, which usually has no signed-in user to resolve.
|
|
132
|
+
`JobTracing#serialize` puts the enqueuing execution's resolved user id and
|
|
133
|
+
tenant into the Active Job payload as `railwatch_user`/`railwatch_tenant`,
|
|
134
|
+
alongside `railwatch_trace_id`/`railwatch_parent_id`; `perform_start` restores
|
|
135
|
+
them onto the job's execution before its first record is built. Details
|
|
136
|
+
worth knowing:
|
|
137
|
+
|
|
138
|
+
- **Identifiers only.** Two strings — the same tenant-prefixed id the
|
|
139
|
+
`user` record carries, and the tenant name. No user or tenant model is
|
|
140
|
+
serialized, hydrated, or looked up, on either side. When the enqueuing
|
|
141
|
+
request had not bound its tenant yet, the raw id travels and the worker
|
|
142
|
+
qualifies it with the propagated tenant on restore.
|
|
143
|
+
- **Jobs enqueuing jobs.** A job serializes the values it was given, so a
|
|
144
|
+
chain of jobs keeps the identity of the request that started it.
|
|
145
|
+
- **Retries and scheduled jobs.** A retry re-enqueues the same job object,
|
|
146
|
+
and Active Job re-serializes it, so every attempt keeps the original
|
|
147
|
+
identity. `perform_later(wait:)`/`set(wait_until:)` serialize at enqueue
|
|
148
|
+
time like any other job — a job scheduled for next week is attributed to
|
|
149
|
+
whoever scheduled it. Solid Queue's recurring scheduler enqueues nothing
|
|
150
|
+
on anyone's behalf, so a `scheduled_task` has no propagated user and
|
|
151
|
+
falls back to local resolution (normally nil).
|
|
152
|
+
- **Nothing to propagate.** The keys are omitted from the payload when
|
|
153
|
+
there is no user or tenant, and a payload without them (one enqueued by
|
|
154
|
+
an older version of the gem, still sitting in a queue through a deploy)
|
|
155
|
+
deserializes to nil and falls back to `Users.resolve_from_current`,
|
|
156
|
+
exactly as before. Inline `perform_now` never serializes, so it resolves
|
|
157
|
+
locally too.
|
|
158
|
+
- **A propagated user does not emit a `user` record.** The worker skips
|
|
159
|
+
local resolution, and it is resolution that emits the name/email record.
|
|
160
|
+
The enqueuing process already emitted it for that id.
|
|
161
|
+
- **Cardinality.** The user id is one more high-cardinality dimension on
|
|
162
|
+
every job record. Apps that do not want a user attached to jobs at all
|
|
163
|
+
can return nil from `config.user` for the cases they care about — the
|
|
164
|
+
propagation only ever carries what that resolver already produced.
|
|
165
|
+
|
|
166
|
+
Also has a special case with no `Execution`: **Solid Queue pruned jobs**
|
|
167
|
+
(`fail_many_claimed.solid_queue`) never reach `perform.active_job` because
|
|
168
|
+
their worker was killed or reaped. Each gets its own throwaway execution
|
|
169
|
+
and reports `job_attempt` with `job_id: nil`, `name: "(pruned)"`,
|
|
170
|
+
`status: "failed"`, `duration: 0`, and `exception_preview` set to the
|
|
171
|
+
pruning error, truncated to 255 chars.
|
|
172
|
+
|
|
173
|
+
### `scheduled_task`
|
|
174
|
+
|
|
175
|
+
Same `perform.active_job` subscriber as `job_attempt`, but for a job
|
|
176
|
+
Solid Queue's `RecurringExecution` table shows was triggered by
|
|
177
|
+
`config/recurring.yml` rather than an ad hoc enqueue (`recurring_task_key`,
|
|
178
|
+
`lib/railwatch/subscribers/jobs.rb`). Carries every `job_attempt` field
|
|
179
|
+
above, plus:
|
|
180
|
+
|
|
181
|
+
| Field | Meaning |
|
|
182
|
+
|---|---|
|
|
183
|
+
| `task_key` | The `config/recurring.yml` key. |
|
|
184
|
+
| `group` | Hash of the task key (not the class name). |
|
|
185
|
+
| `schedule` | The task's configured schedule string (e.g. `"every day at 3am"`), looked up from `SolidQueue::RecurringTask`, refreshed at most once per 60s. |
|
|
186
|
+
| `drift` | Microseconds between the task's scheduled `run_at` and when this attempt actually started. |
|
|
187
|
+
|
|
188
|
+
### `command`
|
|
189
|
+
|
|
190
|
+
One per top-level `bin/rails runner` invocation or Rake task invocation
|
|
191
|
+
(prerequisites nest inside the same command instead of opening their own —
|
|
192
|
+
see `lib/railwatch/patches/rake_task.rb`'s comment on `Rake::Task#invoke`
|
|
193
|
+
vs `#execute`). `db:migrate` and other tasks in
|
|
194
|
+
`Configuration::DEFAULT_VENDOR_COMMANDS` are skipped unless
|
|
195
|
+
`config.capture_default_vendor_commands` is on.
|
|
196
|
+
|
|
197
|
+
| Field | Meaning |
|
|
198
|
+
|---|---|
|
|
199
|
+
| `group` | Hash of the task/command name. |
|
|
200
|
+
| `class` | `"Rake::Task"` or `"Rails::Command::RunnerCommand"`. |
|
|
201
|
+
| `name` | Task name, or `"runner"`. |
|
|
202
|
+
| `command` | Full invocation, e.g. `"rake db:seed[foo]"` or `"rails runner SomeScript.run"`. |
|
|
203
|
+
| `exit_code` | 0 on success, `SystemExit`'s status, or 1 on an unhandled exception, clamped to 0-255. |
|
|
204
|
+
| `interactive` | `true` on a `bin/rails runner` an engineer typed or piped (`-`, inline code, or a `.rb` file under `config.interactive_runner_paths`); absent otherwise. Such a run ships this record — with its `exit_code` and `exception_preview` — but its exception is not reported. A deployed script (`rails runner script/nightly.rb`), a rake task, and a job are never interactive. See [Console and runner sessions](replacing-sentry.md#console-and-runner-sessions). |
|
|
205
|
+
|
|
206
|
+
### `channel_action`
|
|
207
|
+
|
|
208
|
+
One parent per Action Cable channel action. Railwatch opens it before
|
|
209
|
+
`perform_action.action_cable` invokes application code and closes it after the
|
|
210
|
+
action returns or raises, so the SQL, logs, broadcasts, transmits, and
|
|
211
|
+
exceptions inside share one trace — an Action Cable action has no HTTP request
|
|
212
|
+
and no Rack middleware around it, so without this they had no parent at all.
|
|
213
|
+
|
|
214
|
+
| Field | Meaning |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `group` | Hash of channel class + action. |
|
|
217
|
+
| `channel` | Channel class name, e.g. `ChatChannel`. |
|
|
218
|
+
| `action` | Invoked channel action name. |
|
|
219
|
+
| `status` | `"processed"` or `"failed"`. |
|
|
220
|
+
| `failed` | Whether the action raised. |
|
|
221
|
+
|
|
222
|
+
Sampling uses `sample[:channels]` / `RAILWATCH_CHANNEL_SAMPLE_RATE`. An
|
|
223
|
+
unhandled channel exception is still eligible for exception sampling and ships
|
|
224
|
+
with this parent even when the channel sample rate is zero.
|
|
225
|
+
|
|
226
|
+
## Child records
|
|
227
|
+
|
|
228
|
+
### `query`
|
|
229
|
+
|
|
230
|
+
Every non-cached `sql.active_record` notification except `SCHEMA`,
|
|
231
|
+
`TRANSACTION`, and `EXPLAIN` statements (`lib/railwatch/subscribers/queries.rb`).
|
|
232
|
+
The hottest record type in the gem — built as one hash literal rather than
|
|
233
|
+
going through `Railwatch.record`.
|
|
234
|
+
|
|
235
|
+
| Field | Meaning |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `_group` | Hash of the normalized SQL shape + connection (`SqlNormalizer`); adapter is its own field. |
|
|
238
|
+
| `sql` | Normalized SQL shape, truncated to 16,384 chars — literals and comments removed. Set `capture_sql_values` / `RAILWATCH_CAPTURE_SQL_VALUES` to send the raw adapter SQL instead. Active Record's separate structured binds are never sent either way. |
|
|
239
|
+
| `name` | ActiveRecord's own query name (e.g. `"User Load"`). |
|
|
240
|
+
| `duration` | Microseconds. |
|
|
241
|
+
| `connection` | Database config name (e.g. `"primary"`). |
|
|
242
|
+
| `role` | Multi-DB role the connection was checked out for: `"writing"` or `"reading"`. |
|
|
243
|
+
| `adapter` | `"sqlite"`, `"postgresql"`, etc. |
|
|
244
|
+
| `async` | Whether this was an async query (`load_async`). |
|
|
245
|
+
| `row_count` | Rows returned, when the adapter reports it. |
|
|
246
|
+
| `affected_rows` | Rows affected (writes), when the adapter reports it. |
|
|
247
|
+
| `in_transaction` | Whether an open transaction wrapped this statement. |
|
|
248
|
+
| `source` | App-code call site that issued the query (`Backtrace.caller_location`) — resolved once per query shape per process, not per query, except when the query is slow (`config.slow_query_threshold_ms`), where it's always resolved fresh. |
|
|
249
|
+
| `allocations` | Ruby object allocations for this query (`event.allocations`). |
|
|
250
|
+
| `explain` | The adapter's own query plan (Postgres `EXPLAIN`, SQLite `EXPLAIN QUERY PLAN`, ...), truncated to 4000 chars, or nil. Only when `config.capture_query_explain` is on (its own privacy decision — the plan is produced from the raw statement and can echo literal predicates even though `sql` above is normalized), the statement is a `SELECT`, and it took at least `config.explain_threshold_ms`; then at most once per query shape per process per 10 minutes. The EXPLAIN runs on the same connection the query used, with Railwatch paused, so it never becomes a `query` record of its own. |
|
|
251
|
+
|
|
252
|
+
A cached query (`payload[:cached]`) only increments the execution's
|
|
253
|
+
`cached_queries` counter — it never becomes a `query` record.
|
|
254
|
+
|
|
255
|
+
### `n_plus_one`
|
|
256
|
+
|
|
257
|
+
Fired once per query group when its count within the current execution
|
|
258
|
+
crosses `config.n_plus_one_threshold` (`lib/railwatch/subscribers/queries.rb`)
|
|
259
|
+
— not on every repeat, just the crossing.
|
|
260
|
+
|
|
261
|
+
| Field | Meaning |
|
|
262
|
+
|---|---|
|
|
263
|
+
| `group` | Same group hash as the triggering `query` record. |
|
|
264
|
+
| `sql` | Normalized (parameter-stripped) SQL shape, truncated to 2048 chars. |
|
|
265
|
+
| `count` | How many times this group had run in the execution when the threshold was crossed. |
|
|
266
|
+
| `source` | App-code call site. |
|
|
267
|
+
|
|
268
|
+
### `transaction`
|
|
269
|
+
|
|
270
|
+
One per `transaction.active_record` (`lib/railwatch/subscribers/queries.rb`).
|
|
271
|
+
|
|
272
|
+
| Field | Meaning |
|
|
273
|
+
|---|---|
|
|
274
|
+
| `group` | Hash of connection name + outcome. |
|
|
275
|
+
| `duration` | Microseconds. |
|
|
276
|
+
| `outcome` | `"commit"`, `"rollback"`, etc. (`payload[:outcome]`). |
|
|
277
|
+
| `connection` | Database config name. |
|
|
278
|
+
| `statement_count` | Number of `sql.active_record` statements counted against this transaction object while it was open. |
|
|
279
|
+
|
|
280
|
+
### `exception`
|
|
281
|
+
|
|
282
|
+
Every error that reaches `Rails.error` (handled or not), plus anything
|
|
283
|
+
the request middleware or command patches catch directly, plus anything a
|
|
284
|
+
controller swallows with `rescue_from`
|
|
285
|
+
(`lib/railwatch/subscribers/exceptions.rb`). Standalone-capable — reports
|
|
286
|
+
even with no execution open (console, boot). Deduplicated per error
|
|
287
|
+
object, execution, and handled/unhandled disposition, so Rails.error plus
|
|
288
|
+
outer middleware report a re-raised error only once without suppressing the
|
|
289
|
+
same object when it is reused in another execution. A capture discarded by
|
|
290
|
+
sampling or `Railwatch.pause` does not mark the object as seen. Unhandled
|
|
291
|
+
exceptions bypass the execution buffer: `Railwatch.record_now` enqueues the
|
|
292
|
+
record and wakes the in-memory reporter immediately, without network I/O on
|
|
293
|
+
the application thread. This improves the chance of delivery before a normal
|
|
294
|
+
exit but is not a durable crash spool; a hard kill, OOM, or exit after the
|
|
295
|
+
shutdown deadline can lose the record.
|
|
296
|
+
|
|
297
|
+
| Field | Meaning |
|
|
298
|
+
|---|---|
|
|
299
|
+
| `group` | Hash of this record's `fingerprint` parts. |
|
|
300
|
+
| `fingerprint` | The parts that were hashed, up to 10 strings of 200 chars each — by default class + top in-app frame's file/line + normalized message. Always present, so the platform can show *why* an occurrence grouped where it did. |
|
|
301
|
+
| `fingerprint_source` | Where the fingerprint came from: `"default"`, `"report"` (`Railwatch.report(error, fingerprint: [...])`), `"error"` (the exception's own `#railwatch_fingerprint`), or `"resolver"` (a `Railwatch.fingerprint { }` block). |
|
|
302
|
+
| `class` | Exception class name. |
|
|
303
|
+
| `message` | Truncated to 4096 chars. |
|
|
304
|
+
| `handled` | Whether the error was rescued (`Rails.error.handle`) vs. unhandled (`Rails.error.report`/escaped). |
|
|
305
|
+
| `severity` | `:error`/`:warning`/etc., as a string. |
|
|
306
|
+
| `source` | Free-text source tag the raiser passed, e.g. `"application.active_job"`, `"application.action_cable"` (a channel action that raised), `"railwatch.middleware"`, `"action_controller.rescue_from"`, or `"browser"` for a JavaScript error (see below). |
|
|
307
|
+
| `file` / `line` | Top in-app backtrace frame. |
|
|
308
|
+
| `frames` | Full backtrace (`Backtrace.frames`), each frame optionally with source snippet lines if `config.capture_exception_source` is on. Read from `backtrace_locations`, or parsed from the String backtrace when that is nil (an exception whose backtrace was assigned with `set_backtrace` or delegated to a wrapped error, as `ActiveRecord::StatementInvalid` and `Faraday::Error` do). |
|
|
309
|
+
| `cause` | `{class, message}` of `error.cause`, truncated, or nil. |
|
|
310
|
+
| `context` | Serialized `Railwatch.context(...)` active when the error was captured, merged with capture-specific context. An Active Job retry captured by `capture_job_retry_errors` adds `attempt` and `wait` (seconds). |
|
|
311
|
+
| `code` | `Errno` constant, or `error.errno`/`error.code` if the error exposes one. |
|
|
312
|
+
| `sql_state` | Postgres SQLSTATE, for `ActiveRecord::StatementInvalid` wrapping a driver error that exposes one (not populated for SQLite). |
|
|
313
|
+
| `ruby_version` / `rails_version` | Process versions. |
|
|
314
|
+
|
|
315
|
+
A handled exception on a sampled-out execution is dropped entirely
|
|
316
|
+
(matching everything else); an *unhandled* one still ships, governed by
|
|
317
|
+
its own `exceptions` sample rate rolled once per execution
|
|
318
|
+
(`exception_sampled?`).
|
|
319
|
+
|
|
320
|
+
The default fingerprint normalizes the message before hashing it, so one
|
|
321
|
+
issue doesn't shatter into thousands: URLs, email addresses, UUIDs, ISO
|
|
322
|
+
timestamps, IPv4 addresses, quoted strings, hex runs of six characters or
|
|
323
|
+
more, and plain integers all become `?`, whitespace collapses, and the
|
|
324
|
+
result is cut at 200 chars. For classes whose message is mostly the data
|
|
325
|
+
that varied, only the message *prefix* is kept — up to the first `:` for
|
|
326
|
+
`ActiveRecord::RecordNotFound`, `ActiveRecord::RecordInvalid`, `KeyError`,
|
|
327
|
+
`ArgumentError`, and `TypeError`, up to the first `for ` for
|
|
328
|
+
`NoMethodError` and `NameError` — so `key not found: :order_id` and `key
|
|
329
|
+
not found: :user_id` are one issue rather than two. Override any of it
|
|
330
|
+
with `Railwatch.fingerprint`, `#railwatch_fingerprint`, or
|
|
331
|
+
`Railwatch.report(error, fingerprint: [...])`; see
|
|
332
|
+
[`docs/configuration.md`](configuration.md).
|
|
333
|
+
|
|
334
|
+
An error whose class — or any named ancestor of it — appears in
|
|
335
|
+
`config.ignored_exceptions` is never captured at all, handled or not.
|
|
336
|
+
An error a controller rescues with `rescue_from` is captured as
|
|
337
|
+
`handled: true`, `severity: "warning"`, `source:
|
|
338
|
+
"action_controller.rescue_from"`, from Rails'
|
|
339
|
+
`rescue_from_callback.action_controller` notification; set
|
|
340
|
+
`config.capture_rescued_exceptions = false` to turn that off. Active Job's
|
|
341
|
+
equivalents (`retry_on` exhausted, `discard_on`) are already covered by
|
|
342
|
+
the `retry_stopped`/`discard` subscriptions in
|
|
343
|
+
`lib/railwatch/subscribers/jobs.rb`. A retry that has not exhausted its
|
|
344
|
+
attempts is logged but is not an exception by default; set
|
|
345
|
+
`config.capture_job_retry_errors = true` to capture it as handled with
|
|
346
|
+
severity `warning` and source `application.active_job.enqueue_retry`. This is
|
|
347
|
+
off by default because retries are usually expected and can flood the issues
|
|
348
|
+
list. See
|
|
349
|
+
[`docs/configuration.md`](configuration.md) for both settings.
|
|
350
|
+
|
|
351
|
+
#### Browser errors (`source: "browser"`)
|
|
352
|
+
|
|
353
|
+
Every JavaScript error the browser client catches — `window.onerror`,
|
|
354
|
+
unhandled promise rejections, Inertia's failed-request events (`exception`
|
|
355
|
+
and `invalid` on Inertia 2, `networkError` and `httpException` on 3; a
|
|
356
|
+
dropped connection is reported only while the user is waiting on a visit
|
|
357
|
+
that shows the progress bar or loads deferred props, not for a background
|
|
358
|
+
poll, `router.reload`, or prefetch),
|
|
359
|
+
and anything the app reports itself with `reportError` — arrives on the
|
|
360
|
+
same beacon as visits (`POST /railwatch/beacon`, 50 errors per beacon at
|
|
361
|
+
most) and is recorded as an ordinary `exception`: `source: "browser"`,
|
|
362
|
+
`handled: false`, `severity: "error"`, `class` set to the JavaScript
|
|
363
|
+
error's `name`, `message` truncated to 1024 chars. It carries the same
|
|
364
|
+
envelope every other record does, including `deploy`, so a browser issue
|
|
365
|
+
regresses with a release exactly like a Ruby one.
|
|
366
|
+
|
|
367
|
+
The browser's stack (8192 chars at most) is parsed into the same frame
|
|
368
|
+
shape a Ruby backtrace produces — V8's `at fn (url:line:col)` and
|
|
369
|
+
SpiderMonkey/JavaScriptCore's `fn@url:line:col` are both understood, and a
|
|
370
|
+
line with no location on it is dropped:
|
|
371
|
+
|
|
372
|
+
| Frame key | Meaning |
|
|
373
|
+
|---|---|
|
|
374
|
+
| `file` | Path relative to the app's own origin (`assets/index-Bq1x9K.js`, `app/frontend/pages/orders/index.tsx`), or the whole URL for a script served from anywhere else. Any query string is cut. |
|
|
375
|
+
| `line` | Line number. Columns are parsed but not stored. |
|
|
376
|
+
| `function` | The function name the engine gave, or `"(anonymous)"`. |
|
|
377
|
+
| `in_app` | True when the script came from the app's own origin and is not under `node_modules/` or `vendor/`. |
|
|
378
|
+
|
|
379
|
+
No source snippets: the file is on the client, not on the server. Frames
|
|
380
|
+
are fingerprinted exactly like Ruby ones — class, top in-app frame, and
|
|
381
|
+
the normalized message — so browser errors group, split, merge, resolve,
|
|
382
|
+
and regress through the same Issue machinery.
|
|
383
|
+
|
|
384
|
+
`context` carries a `browser` key with the page `url`, the Inertia
|
|
385
|
+
`component`, the `visit` the error happened in (if any), the tab's
|
|
386
|
+
`session` id, the `user_agent`, and up to 20 `breadcrumbs`
|
|
387
|
+
(`{at, kind, text}`, `kind` being `console`, `click`, or `navigate`) — the
|
|
388
|
+
trail the client recorded before the crash. Anything the app passed as
|
|
389
|
+
`reportError(error, context)` is merged in alongside it, flattened to
|
|
390
|
+
strings, 20 keys at most.
|
|
391
|
+
|
|
392
|
+
### `cache_event`
|
|
393
|
+
|
|
394
|
+
Every `cache_*.active_support` notification except the inner read inside
|
|
395
|
+
a `fetch` (`lib/railwatch/subscribers/cache.rb`). Vendor cache key prefixes
|
|
396
|
+
(rack-attack, flipper, solid_cable, by default) are skipped unless
|
|
397
|
+
`config.capture_default_vendor_cache_keys` is on; keys matching
|
|
398
|
+
`config.ignored_cache_key_prefixes` are always skipped.
|
|
399
|
+
|
|
400
|
+
| Field | Meaning |
|
|
401
|
+
|---|---|
|
|
402
|
+
| `_group` | Hash of store class + key shape (digits/long-hex stripped so `"users/123"` and `"users/456"` share a group). |
|
|
403
|
+
| `store` | Cache store class, demodulized. |
|
|
404
|
+
| `key` | Truncated to 255 chars. |
|
|
405
|
+
| `type` | `"hit"`, `"miss"`, `"read_multi"`, `"generate"`, `"write"`, `"write_multi"`, `"delete"`, `"delete_multi"`, `"delete_matched"`, `"increment"`, `"decrement"`, `"exist"`, or `"fail"` when the store raised. |
|
|
406
|
+
| `duration` | Microseconds. |
|
|
407
|
+
| `ttl` | Seconds, from `expires_in`, or 0. |
|
|
408
|
+
| `hits` | Count of hits, for a `read_multi`. |
|
|
409
|
+
|
|
410
|
+
### `mail`
|
|
411
|
+
|
|
412
|
+
`deliver.action_mailer` (`lib/railwatch/subscribers/mail.rb`).
|
|
413
|
+
|
|
414
|
+
| Field | Meaning |
|
|
415
|
+
|---|---|
|
|
416
|
+
| `group` | Hash of the mailer class name. |
|
|
417
|
+
| `mailer` | Mailer class name. |
|
|
418
|
+
| `subject` | Truncated to 255 chars. |
|
|
419
|
+
| `to` / `cc` / `bcc` | Recipient **counts**, not addresses. |
|
|
420
|
+
| `attachments` | Attachment count. |
|
|
421
|
+
| `delivery_method` | E.g. `"SMTP"`, `"Test"`. |
|
|
422
|
+
| `perform_deliveries` | Whether delivery actually ran (`perform_deliveries` wasn't disabled). |
|
|
423
|
+
| `duration` | Microseconds. |
|
|
424
|
+
| `failed` | Whether an exception occurred during delivery. |
|
|
425
|
+
| `message_id` | Truncated to 255 chars. |
|
|
426
|
+
|
|
427
|
+
A mailer's own template render is a separate `view_render` record (see
|
|
428
|
+
below) via `process.action_mailer`, `kind: "mailer"`.
|
|
429
|
+
|
|
430
|
+
### `broadcast`
|
|
431
|
+
|
|
432
|
+
Action Cable broadcast/transmit/perform, which also covers Turbo Streams
|
|
433
|
+
and `inertia_cable` since both go through `broadcast.action_cable`
|
|
434
|
+
(`lib/railwatch/subscribers/broadcasts.rb`). Three sub-shapes share the type:
|
|
435
|
+
|
|
436
|
+
| Field | Present for | Meaning |
|
|
437
|
+
|---|---|---|
|
|
438
|
+
| `kind` | all | `"broadcast"`, `"transmit"`, or `"perform_action"`. |
|
|
439
|
+
| `group` | all | Hash of stream shape (broadcast) or channel class (+ action). |
|
|
440
|
+
| `stream` | broadcast | Broadcasting name, ids stripped, truncated to 255 chars. |
|
|
441
|
+
| `bytes` | broadcast, transmit | Payload size. |
|
|
442
|
+
| `coder` | broadcast | Serializer class name. |
|
|
443
|
+
| `channel` | transmit, perform_action | Channel class name. |
|
|
444
|
+
| `via` | transmit | How the transmit happened (`payload[:via]`), truncated to 255 chars. |
|
|
445
|
+
| `action` | perform_action | Channel action name. |
|
|
446
|
+
| `failed` | perform_action | `true` when the action raised; the exception itself is reported separately with source `"application.action_cable"`. |
|
|
447
|
+
| `duration` | all | Microseconds. |
|
|
448
|
+
|
|
449
|
+
### `notification`
|
|
450
|
+
|
|
451
|
+
Noticed gem deliveries only (`lib/railwatch/subscribers/notifications.rb`)
|
|
452
|
+
— tagged by hooking the same `perform.active_job` event the `job_attempt`
|
|
453
|
+
subscriber uses, filtered to jobs whose class starts with `Noticed::`.
|
|
454
|
+
No-ops entirely if the `noticed` gem isn't loaded.
|
|
455
|
+
|
|
456
|
+
| Field | Meaning |
|
|
457
|
+
|---|---|
|
|
458
|
+
| `group` | Hash of the Noticed delivery job's class name. |
|
|
459
|
+
| `notifier` | The `notification_class` from the job's first argument, if present. |
|
|
460
|
+
| `channel` | Delivery class with `Delivery` stripped and lowercased, e.g. `"email"`, `"slack"`. |
|
|
461
|
+
| `delivery_method` | Delivery job class, demodulized (e.g. `"EmailDelivery"`). |
|
|
462
|
+
| `duration` | Microseconds. |
|
|
463
|
+
| `failed` | Whether the delivery job raised. |
|
|
464
|
+
|
|
465
|
+
### `outgoing_request`
|
|
466
|
+
|
|
467
|
+
Any `Net::HTTP#request` call (covers Faraday's default adapter, HTTParty,
|
|
468
|
+
RestClient, most of the HTTP ecosystem — `lib/railwatch/patches/net_http.rb`),
|
|
469
|
+
plus Faraday connections that explicitly add `Railwatch::Faraday` middleware
|
|
470
|
+
(`lib/railwatch/faraday.rb`, for apps using a non-default Faraday adapter).
|
|
471
|
+
Requests to Railwatch's own ingest URL are always skipped so shipping
|
|
472
|
+
telemetry never generates telemetry about itself. A Faraday connection
|
|
473
|
+
using the default (Net::HTTP) adapter defers to the Net::HTTP patch via a
|
|
474
|
+
thread-local reentry flag, so it's never double-recorded.
|
|
475
|
+
|
|
476
|
+
| Field | Meaning |
|
|
477
|
+
|---|---|
|
|
478
|
+
| `group` | Hash of host + method. |
|
|
479
|
+
| `host` | Request host. |
|
|
480
|
+
| `method` | HTTP verb. |
|
|
481
|
+
| `url` | Scheme + host + path, with authority credentials, the entire query string, and fragment removed, truncated to 2048 chars. |
|
|
482
|
+
| `duration` | Microseconds. |
|
|
483
|
+
| `status_code` | Response status, 0 if the request errored before a response. |
|
|
484
|
+
| `request_size` | Bytes (Net::HTTP path only). |
|
|
485
|
+
| `response_size` | Bytes, from `Content-Length` or body size. |
|
|
486
|
+
| `error` | `"Class: message"`, truncated to 255 chars, if the request raised. |
|
|
487
|
+
| `response_body` | First 4 KiB of the response body, but only when `config.capture_response_body_on_error` is on (off by default) *and* the response was an error. A body that parses as a JSON object is run through the same parameter filter as request params and re-serialized; anything else is stored as it arrived. nil in every other case — including a connection failure, where there is no response (on the Net::HTTP path a body is read only if Net::HTTP already buffered it, so a response being streamed through `read_body` is never consumed; on the Faraday path the body is taken only once a status came back, so an outgoing request payload can never be filed as a response). |
|
|
488
|
+
| `source` | App-code call site (Net::HTTP path only). |
|
|
489
|
+
|
|
490
|
+
### `storage_op`
|
|
491
|
+
|
|
492
|
+
Every Active Storage service operation
|
|
493
|
+
(`lib/railwatch/subscribers/storage.rb`): upload, download, streaming
|
|
494
|
+
download, delete, delete_prefixed, exist, url, update_metadata, analyze,
|
|
495
|
+
transform, preview.
|
|
496
|
+
|
|
497
|
+
| Field | Meaning |
|
|
498
|
+
|---|---|
|
|
499
|
+
| `group` | Hash of service name + op. |
|
|
500
|
+
| `service` | Active Storage service name. |
|
|
501
|
+
| `op` | Operation, `service_` prefix stripped (e.g. `"upload"`, `"analyze"`). |
|
|
502
|
+
| `key` | Blob key, truncated to 255 chars. |
|
|
503
|
+
| `duration` | Microseconds. |
|
|
504
|
+
| `exist` | For `exist` ops, whether the blob existed. |
|
|
505
|
+
|
|
506
|
+
### `view_render`
|
|
507
|
+
|
|
508
|
+
Template, partial, layout, and collection renders
|
|
509
|
+
(`lib/railwatch/subscribers/views.rb`), plus mailer template renders
|
|
510
|
+
(`process.action_mailer`, `lib/railwatch/subscribers/mail.rb`, `kind:
|
|
511
|
+
"mailer"`). Only the first `config.max_view_renders_per_execution` per
|
|
512
|
+
execution are stored as records — all are still counted toward the
|
|
513
|
+
parent's `view_renders` counter regardless of the cap.
|
|
514
|
+
|
|
515
|
+
| Field | Meaning |
|
|
516
|
+
|---|---|
|
|
517
|
+
| `group` | Hash of the template identifier. |
|
|
518
|
+
| `identifier` | Template path, app-root prefix stripped, truncated to 255 chars. Mailer renders use `"Mailer#action"` instead. |
|
|
519
|
+
| `kind` | `"template"`, `"partial"`, `"layout"`, `"collection"`, or `"mailer"`. |
|
|
520
|
+
| `layout` | Layout name, for a `render_layout` event. |
|
|
521
|
+
| `count` | Item count, for a `render_collection` event. |
|
|
522
|
+
| `cache_hits` | Fragment cache hits, for a `render_collection` event. |
|
|
523
|
+
| `duration` | Microseconds. |
|
|
524
|
+
|
|
525
|
+
### `span`
|
|
526
|
+
|
|
527
|
+
Custom timing around any block of app code
|
|
528
|
+
(`Railwatch.span(name, **attributes) { ... }`, `lib/railwatch.rb`). Returns
|
|
529
|
+
the block's value untouched and is a no-op wrapper — it still yields —
|
|
530
|
+
when Railwatch is disabled, nothing is executing, or the execution isn't
|
|
531
|
+
recording. Every span also increments the parent's `spans` counter.
|
|
532
|
+
|
|
533
|
+
```ruby
|
|
534
|
+
Railwatch.span("pdf.render", template: "invoice", pages: 12) { renderer.call }
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
| Field | Meaning |
|
|
538
|
+
|---|---|
|
|
539
|
+
| `group` | Hash of the span name. |
|
|
540
|
+
| `name` | Span name, truncated to 255 chars. |
|
|
541
|
+
| `duration` | Microseconds. |
|
|
542
|
+
| `attributes` | Up to 25 keys; values stringified (`inspect` for anything that isn't already a String), truncated to 200 chars, and run through the same parameter filter as request params and exception locals — so a `password:` attribute ships as `[FILTERED]`. nil when the call passed no attributes. |
|
|
543
|
+
| `status` | `"ok"`, or `"failed"` if the block raised — the exception is recorded and then re-raised untouched. |
|
|
544
|
+
|
|
545
|
+
### `attachment`
|
|
546
|
+
|
|
547
|
+
An arbitrary blob filed against an execution and, optionally, an exception
|
|
548
|
+
(`Railwatch.attach(name, data, content_type:, exception:)`,
|
|
549
|
+
`lib/railwatch/attachments.rb`) — the payload that failed to parse, a
|
|
550
|
+
rendered PDF, the webhook body a customer swears they sent. Sentry's
|
|
551
|
+
`Sentry.add_attachment` equivalent.
|
|
552
|
+
|
|
553
|
+
```ruby
|
|
554
|
+
Railwatch.attach("payload.json", request.raw_post)
|
|
555
|
+
Railwatch.attach("invoice.pdf", Rails.root.join("tmp/invoice.pdf"))
|
|
556
|
+
Railwatch.attach("payload.json", body, exception: error)
|
|
557
|
+
Railwatch.report(error, attachments: { "payload.json" => body })
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
`data` may be a String (the bytes themselves), a `Pathname` (the file is
|
|
561
|
+
read), or any IO. This is one of the standalone types
|
|
562
|
+
(`Railwatch::STANDALONE_TYPES`): inside a recording execution it ships as a
|
|
563
|
+
child of it, and with nothing executing — a boot hook, a console, a rescue
|
|
564
|
+
outside any request — it ships on its own. Returns nil and records nothing
|
|
565
|
+
when Railwatch is disabled or the payload is empty.
|
|
566
|
+
|
|
567
|
+
| Field | Meaning |
|
|
568
|
+
|---|---|
|
|
569
|
+
| `group` | Hash of the attachment name, so the same name across occurrences buckets together. |
|
|
570
|
+
| `name` | Attachment name, truncated to 255 chars. |
|
|
571
|
+
| `content_type` | Passed explicitly, else guessed from the name's extension via Marcel (which Rails already ships for Active Storage), else `application/octet-stream`. Truncated to 128 chars. |
|
|
572
|
+
| `bytes` | Size of the payload **as stored**, i.e. after any truncation — not the size of the original. |
|
|
573
|
+
| `data` | `Base64.strict_encode64(Zlib.gzip(bytes))`, so a text payload costs a fraction of its size in the batch. |
|
|
574
|
+
| `truncated` | `true` when the payload was longer than `config.max_attachment_bytes` (default 1 MiB) and was cut to the cap. Absent otherwise. |
|
|
575
|
+
| `exception_group_hash` | The `_group` of the `exception` record this attachment belongs to, when one was passed as `exception:` — the same hash `Subscribers::Exceptions` files that error under, so the platform can show the attachment on the issue. nil otherwise. |
|
|
576
|
+
|
|
577
|
+
### `log`
|
|
578
|
+
|
|
579
|
+
Two independent sources feed this type (`lib/railwatch/subscribers/logs.rb`):
|
|
580
|
+
`Rails.logger` lines, captured by broadcasting to a `Logger` subclass
|
|
581
|
+
that intercepts every `add` call, and Rails 8.1's structured
|
|
582
|
+
`Rails.event` framework events. Lines matching Rails' own per-request/job
|
|
583
|
+
noise (`"Started GET"`, `"Processing by"`, `"Rendered"`, etc. — already
|
|
584
|
+
covered by the `request`/`job_attempt` records) are dropped, as are lines
|
|
585
|
+
below `config.log_level` and Railwatch's own `[railwatch]`-prefixed debug
|
|
586
|
+
output. Framework structured events (`action_controller.*`,
|
|
587
|
+
`active_record.*`, etc.) are dropped unless `config.capture_framework_events`
|
|
588
|
+
is on, for the same reason.
|
|
589
|
+
|
|
590
|
+
| Field | Meaning |
|
|
591
|
+
|---|---|
|
|
592
|
+
| `level` | `"debug"`/`"info"`/`"warn"`/`"error"`/`"fatal"`/`"unknown"` for a logger line, `"event"` for a structured event. |
|
|
593
|
+
| `message` | Logger line text (ANSI color codes stripped), or the event name, truncated to 8192 chars. Message text is otherwise sent as written; parameter redaction does not parse secrets embedded in a line. |
|
|
594
|
+
| `tags` | Active `Rails.logger.tagged` tags, for a logger line; the event's own tags, for a structured event. |
|
|
595
|
+
| `context` | Serialized `Railwatch.context(...)`, for a logger line; the event payload as JSON (truncated to 8192 chars), for a structured event. |
|
|
596
|
+
| `source` | File:line the structured event fired from, when available (structured events only). |
|
|
597
|
+
|
|
598
|
+
### `enqueued_job`
|
|
599
|
+
|
|
600
|
+
`enqueue`/`enqueue_at`/`enqueue_all.active_job`
|
|
601
|
+
(`lib/railwatch/subscribers/jobs.rb`) — one record per job enqueued, distinct
|
|
602
|
+
from `job_attempt`/`scheduled_task` which record the later `perform`.
|
|
603
|
+
|
|
604
|
+
| Field | Meaning |
|
|
605
|
+
|---|---|
|
|
606
|
+
| `group` | Hash of the job class name. |
|
|
607
|
+
| `job_id` | Active Job's `job_id`. |
|
|
608
|
+
| `name` | Job class name. |
|
|
609
|
+
| `queue` | Queue name. |
|
|
610
|
+
| `adapter` | Queue adapter class, demodulized. |
|
|
611
|
+
| `priority` | Job priority. |
|
|
612
|
+
| `scheduled_at` | Unix timestamp, for a delayed enqueue. |
|
|
613
|
+
| `duration` | Microseconds spent in the enqueue call itself. |
|
|
614
|
+
| `failed` | Whether enqueuing itself failed (an exception during enqueue, or `successfully_enqueued?` returning false). |
|
|
615
|
+
|
|
616
|
+
### `user`
|
|
617
|
+
|
|
618
|
+
Standalone — emitted once per distinct user id per process-hour
|
|
619
|
+
(`lib/railwatch/subscribers/users.rb`), not per request, so the platform
|
|
620
|
+
can show names/emails without every other record carrying them. Resolved
|
|
621
|
+
via `config.user` block if set, else `Current.user` (authentication-zero
|
|
622
|
+
/ Rails 8 auth generator), else Warden (Devise).
|
|
623
|
+
The process-hour cache entry is written only once the execution carrying
|
|
624
|
+
the entity has been handed to the reporter, so a sighting that was sampled
|
|
625
|
+
out or paused does not suppress the next sighting that would ship. Forked
|
|
626
|
+
workers start with an empty cache.
|
|
627
|
+
|
|
628
|
+
| Field | Meaning |
|
|
629
|
+
|---|---|
|
|
630
|
+
| `id` | Resolved user id, tenant-prefixed (`"tenant:id"`) when a tenant is bound — including when it binds *after* the user was resolved. |
|
|
631
|
+
| `name` | Truncated to 255 chars. |
|
|
632
|
+
| `email` | Truncated to 255 chars. |
|
|
633
|
+
| `tenant` | Current tenant context, if any. |
|
|
634
|
+
|
|
635
|
+
### `deprecation`
|
|
636
|
+
|
|
637
|
+
`deprecation.rails` (`lib/railwatch/subscribers/deprecations.rb`).
|
|
638
|
+
|
|
639
|
+
| Field | Meaning |
|
|
640
|
+
|---|---|
|
|
641
|
+
| `group` | Hash of gem name + first 120 chars of the message. |
|
|
642
|
+
| `message` | Truncated to 2048 chars. |
|
|
643
|
+
| `gem_name` | Gem the deprecation came from. |
|
|
644
|
+
| `horizon` | Deprecation horizon version string. |
|
|
645
|
+
| `source` | First app-code frame in the deprecation's callstack, app-root prefix stripped. |
|
|
646
|
+
|
|
647
|
+
### `visit`
|
|
648
|
+
|
|
649
|
+
Standalone — Inertia page-visit timing reported by the browser client
|
|
650
|
+
(`app/frontend/lib/railwatch.ts`, generated by `railwatch:install`), POSTed
|
|
651
|
+
to `POST /railwatch/beacon` and recorded server-side by
|
|
652
|
+
`Railwatch::BeaconController` (`app/controllers/railwatch/beacon_controller.rb`).
|
|
653
|
+
Batched client-side (flushed every 5s, on `pagehide`, or once 20 visits
|
|
654
|
+
queue up) and capped at 50 visits per beacon request, and at
|
|
655
|
+
`config.beacon_rate_limit` requests per client IP per minute (default 120;
|
|
656
|
+
`0` turns the limit off). No-ops entirely if `config.beacon_enabled` is off.
|
|
657
|
+
|
|
658
|
+
| Field | Meaning |
|
|
659
|
+
|---|---|
|
|
660
|
+
| `group` | Hash of the Inertia component name. |
|
|
661
|
+
| `component` | Truncated to 255 chars. |
|
|
662
|
+
| `url` | Truncated to 2048 chars. |
|
|
663
|
+
| `method` | Truncated to 10 chars. |
|
|
664
|
+
| `duration` | Microseconds, client-measured (`Date.now()` start to `finish`). |
|
|
665
|
+
| `status` | `"success"`, `"error"`, or `"cancelled"` (visit was superseded before finishing). |
|
|
666
|
+
| `partial` | Whether this was a partial Inertia reload (`only`/`except` present). |
|
|
667
|
+
| `only` | Up to 50 prop keys, for a partial reload. |
|
|
668
|
+
| `props_bytes` | Serialized prop payload size, client-measured. |
|
|
669
|
+
| `lcp` | Largest Contentful Paint, integer ms, clamped to 0–120000. Initial load only. |
|
|
670
|
+
| `cls` | Cumulative Layout Shift — the largest session window, float rounded to 4 decimals, clamped to 0–100. Initial load only. |
|
|
671
|
+
| `inp` | Interaction to Next Paint, integer ms, clamped to 0–120000. The slowest interaction, not the spec's high percentile. Initial load only. |
|
|
672
|
+
| `ttfb` | Time to First Byte from navigation timing's `responseStart`, integer ms, clamped to 0–120000. Initial load only. |
|
|
673
|
+
| `user` | Resolved server-side from the beacon request's session/cookies, same resolver as every other record. |
|
|
674
|
+
| `tenant` | Current tenant context. |
|
|
675
|
+
| `user_agent` | Truncated to 256 chars. |
|
|
676
|
+
|
|
677
|
+
The **initial page load** is reported as a visit too, even though Inertia
|
|
678
|
+
never routed it: `method` `"GET"`, `status` `"success"`, `component` read
|
|
679
|
+
from the Inertia root's `#app[data-page]` JSON, and `duration` taken from
|
|
680
|
+
navigation timing (`loadEventEnd` or `responseEnd`, minus `startTime`).
|
|
681
|
+
It is the only visit that carries the four Core Web Vitals, and it is held
|
|
682
|
+
back until the page is first hidden (`visibilitychange`/`pagehide`) so
|
|
683
|
+
those numbers are final when it ships. Every vital is nil on a browser
|
|
684
|
+
that doesn't support the `PerformanceObserver` entry type behind it.
|
|
685
|
+
|
|
686
|
+
### `session`
|
|
687
|
+
|
|
688
|
+
Standalone — one session of the monitored app, for release health. The
|
|
689
|
+
`deploy` on the envelope *is* the release; the platform counts sessions per
|
|
690
|
+
deploy and reports crash-free rates from them. Two sources produce the same
|
|
691
|
+
record:
|
|
692
|
+
|
|
693
|
+
- **Browser** (`source: "browser"`). The client (`app/frontend/lib/railwatch.ts`)
|
|
694
|
+
mints a 16-hex id per tab in `sessionStorage` (key `railwatch.session`, so it
|
|
695
|
+
dies with the tab), mirrors it into a `railwatch_session` cookie, and sends it
|
|
696
|
+
with every beacon flush. `Railwatch::BeaconController` writes at most one
|
|
697
|
+
`session` record per flush: the first (no `duration_ms` yet) opens the
|
|
698
|
+
session, later ones beat it along, and the `pagehide`/`visibilitychange`
|
|
699
|
+
flush closes it with `ended`.
|
|
700
|
+
- **Server** (`source: "server"`). `lib/railwatch/sessions.rb` aggregates, per
|
|
701
|
+
process, every request that resolves a user or carries that cookie (or an
|
|
702
|
+
`X-Railwatch-Session` header), and a background thread ships one record per
|
|
703
|
+
session every `config.session_flush_interval` (default 60s). A session idle
|
|
704
|
+
for `config.session_timeout` (default 30 minutes) ships with `ended` and is
|
|
705
|
+
dropped. At most 10,000 keys are tracked per process; past that the oldest
|
|
706
|
+
is dropped and counted in `Railwatch::Sessions.dropped`.
|
|
707
|
+
|
|
708
|
+
Both are off when `config.track_sessions` is false, and both key on the same
|
|
709
|
+
id when the browser cookie is present, so the platform dedupes the two halves
|
|
710
|
+
of one session rather than counting it twice.
|
|
711
|
+
|
|
712
|
+
| Field | Meaning |
|
|
713
|
+
|---|---|
|
|
714
|
+
| `group` | Hash of the session key. |
|
|
715
|
+
| `id` | Session key, truncated to 64 chars: the browser client's id, else `"user:<user_id>"`. |
|
|
716
|
+
| `source` | `"browser"` or `"server"`. |
|
|
717
|
+
| `status` | `"started"` (opened, no duration yet), `"ok"`, `"errored"` (a 5xx or a handled exception), or `"crashed"` (an unhandled exception). Server sessions only escalate. |
|
|
718
|
+
| `started_at` | Unix seconds (float) the session began — `timestamp` is when the record was flushed, not when the session started. |
|
|
719
|
+
| `duration` | Microseconds from `started_at` to the last request/visit, nil on the record that opens the session. |
|
|
720
|
+
| `requests` | Requests in this session so far (server only). |
|
|
721
|
+
| `visits` | Inertia visits in this beacon flush (browser only). |
|
|
722
|
+
| `errors` | Requests that 5xx'd or raised (server), or visits with status `"error"` in this flush (browser). |
|
|
723
|
+
| `ended` | Whether this is the session's last record. |
|
|
724
|
+
| `user` | Resolved user id, when there is one. |
|
|
725
|
+
|
|
726
|
+
### `process`
|
|
727
|
+
|
|
728
|
+
Standalone — one per process boot (`lib/railwatch/subscribers/process_info.rb`),
|
|
729
|
+
fired unconditionally during subscriber installation, not gated on
|
|
730
|
+
sampling. Gives the platform a server/deploy inventory for free.
|
|
731
|
+
|
|
732
|
+
| Field | Meaning |
|
|
733
|
+
|---|---|
|
|
734
|
+
| `pid` | Process id. |
|
|
735
|
+
| `role` | `"web"` (Puma present), `"worker"` (Solid Queue supervisor, `$PROGRAM_NAME` includes `"jobs"`), `"console"`, `"command"` (`$PROGRAM_NAME` ends in `rake`), or `"process"`. |
|
|
736
|
+
| `ruby_version` / `rails_version` / `railwatch_version` | Versions. |
|
|
737
|
+
| `app` | Top-level module name of the Rails app. |
|
|
738
|
+
| `environment` | `config.environment_name` (defaults to `Rails.env`). |
|
|
739
|
+
| `boot_seconds` | Monotonic time from `Railwatch::BOOTED_AT` (the gem's load time, as early in boot as it can observe) to `config.after_initialize`, when the record is written -- so it covers the app's own initializers. |
|
|
740
|
+
| `database_adapter` | Primary DB adapter name. |
|
|
741
|
+
| `queue_adapter` | Active Job queue adapter name. |
|
|
742
|
+
| `cache_store` | `Rails.cache` class name. |
|
|
743
|
+
|
|
744
|
+
### `health`
|
|
745
|
+
|
|
746
|
+
Standalone — one every `config.health_interval` seconds (default 15) from
|
|
747
|
+
a single background thread per process (`lib/railwatch/health.rb`), started
|
|
748
|
+
by the engine's `railwatch.health` initializer only when Railwatch is enabled,
|
|
749
|
+
the process `role` is `"web"` or `"worker"`, and the Rails env isn't
|
|
750
|
+
`test`. This is the gem's only *sampled gauge*: everything else is an
|
|
751
|
+
event, this is a periodic snapshot of how loaded the process is.
|
|
752
|
+
|
|
753
|
+
The whole sample runs inside `Railwatch.ignore` and rescues everything, so a
|
|
754
|
+
missing constant, an unmigrated queue database, or a checkout timeout
|
|
755
|
+
degrades each field to nil instead of raising on a thread nobody watches —
|
|
756
|
+
the record still ships with whatever it did manage to read.
|
|
757
|
+
|
|
758
|
+
| Field | Meaning |
|
|
759
|
+
|---|---|
|
|
760
|
+
| `pid` | Process id. |
|
|
761
|
+
| `role` | Same detection as `process` above: `"web"` or `"worker"`. |
|
|
762
|
+
| `memory` | RSS in bytes (`Execution.sampled_memory`). |
|
|
763
|
+
| `threads_max` | Puma's configured max threads, or nil when Puma isn't running. |
|
|
764
|
+
| `threads_busy` | Puma threads currently serving a request (`busy_threads`). |
|
|
765
|
+
| `backlog` | Requests queued inside Puma waiting for a thread. |
|
|
766
|
+
| `pool_size` | Active Record connection pool size (`connection_pool.stat[:size]`). |
|
|
767
|
+
| `pool_busy` | Connections checked out. |
|
|
768
|
+
| `pool_waiting` | Threads blocked waiting for a connection — sustained non-zero means the pool is undersized for the thread count. |
|
|
769
|
+
| `queue_depth` | `SolidQueue::ReadyExecution.count` — jobs ready to run right now. |
|
|
770
|
+
| `queue_latency` | Microseconds since the oldest ready job was created, i.e. the backlog's head-of-line wait. nil when the queue is empty. |
|
|
771
|
+
| `detail` | JSON string: `queues` (ready count per queue name), `workers` (`SolidQueue::Process` rows of kind `Worker`), `requests_count` (Puma's lifetime request count for this process), `running` (threads Puma has spawned), `max_threads_reached` (true when Puma's `pool_capacity` was 0 at sample time, i.e. no spare thread), `recurring_tasks` (Solid Queue recurring task key => schedule, from `SolidQueue::RecurringTask`; left out when there are none or the table could not be read, so the platform can tell a task removed from `config/recurring.yml` apart from one that stopped running). |
|
|
772
|
+
|
|
773
|
+
Every Puma field is nil when no `Puma::Server` exists in the process, and
|
|
774
|
+
every Solid Queue field is nil when `SolidQueue` isn't loaded.
|
|
775
|
+
|
|
776
|
+
The sampler re-arms itself after `fork` (Rails' `ActiveSupport::ForkTracker`
|
|
777
|
+
callback), so
|
|
778
|
+
clustered Puma workers and forked Solid Queue workers each report without
|
|
779
|
+
any `on_worker_boot` configuration.
|
|
780
|
+
|
|
781
|
+
`Railwatch::Health.start!` is idempotent, and `stop!` (registered by the
|
|
782
|
+
engine's `at_exit`, ahead of the reporter's final flush) wakes the thread
|
|
783
|
+
off its `ConditionVariable` immediately rather than waiting out the
|
|
784
|
+
interval.
|
|
785
|
+
|
|
786
|
+
### `profile`
|
|
787
|
+
|
|
788
|
+
A sampling profile of one execution (`lib/railwatch/profiler.rb`,
|
|
789
|
+
`Railwatch.start_profile`/`ship_profile` in `lib/railwatch.rb`). Off by
|
|
790
|
+
default; see `docs/configuration.md`'s **Profiling** section for how an
|
|
791
|
+
execution is picked and which backend gem the app has to install. Exactly
|
|
792
|
+
one `profile` per execution, buffered as a child of that execution and
|
|
793
|
+
shipped with it, and the execution's parent record then carries
|
|
794
|
+
`profiled: true`.
|
|
795
|
+
|
|
796
|
+
| Field | Meaning |
|
|
797
|
+
|---|---|
|
|
798
|
+
| `profiler` | `"vernier"` or `"stackprof"` — the backend that collected it. |
|
|
799
|
+
| `mode` | `"wall"`. |
|
|
800
|
+
| `interval` | Sampling interval in microseconds (`config.profile_interval_us`). |
|
|
801
|
+
| `duration` | Microseconds actually profiled, start to stop. |
|
|
802
|
+
| `samples` | Total samples collected. When `stacks` was truncated (below), the counts inside it sum to less than this. |
|
|
803
|
+
| `stacks` | Base64 of gzip of the collapsed-stack text, described below. |
|
|
804
|
+
| `stacks_bytes` | Uncompressed size of that text, in bytes. |
|
|
805
|
+
|
|
806
|
+
`stacks` decodes to *folded stacks*, the same shape Brendan Gregg's
|
|
807
|
+
`stackcollapse` produces: one line per unique stack, outermost frame
|
|
808
|
+
first, semicolon-separated, then a space and the number of samples that
|
|
809
|
+
landed on it.
|
|
810
|
+
|
|
811
|
+
```
|
|
812
|
+
<main> (config.ru:3);WidgetsController#index (app/controllers/widgets_controller.rb:4);ActiveRecord::Relation#each (activerecord-8.1.0/lib/active_record/relation/delegation.rb:89) 37
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
Each frame is `Class#method (path:line)`. The Rails root is stripped from
|
|
816
|
+
app paths, an installed gem's path becomes `<gem>/relative/path` (the
|
|
817
|
+
version is dropped; the deploy already records it), Ruby's own library
|
|
818
|
+
becomes `ruby/...`, and a C function — which has no Ruby file of its own —
|
|
819
|
+
reads `<cfunc>:0`.
|
|
820
|
+
Lines are ordered by sample count descending, ties broken by the stack
|
|
821
|
+
text, so the same profile always serialises to the same bytes.
|
|
822
|
+
|
|
823
|
+
The text is capped at **4 MiB uncompressed**
|
|
824
|
+
(`Railwatch::Profiler::MAX_COLLAPSED_BYTES`); past that the least frequent
|
|
825
|
+
stacks are dropped, since the shape of a profile lives in its frequent
|
|
826
|
+
ones. Rails stacks are deep enough that a busy request can reach the cap,
|
|
827
|
+
which is why `samples` is reported separately from the counts in `stacks`.
|
|
828
|
+
|
|
829
|
+
Both backends are process-global — there is one profiler per process, not
|
|
830
|
+
one per thread — so an execution that starts while another is being
|
|
831
|
+
profiled simply isn't profiled (counted in `Railwatch::Profiler.skipped`).
|
|
832
|
+
Vernier samples every thread in the process, so only the thread that
|
|
833
|
+
started the profile is folded in; StackProf samples wherever its `SIGPROF`
|
|
834
|
+
lands.
|