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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3c0518c86cb66b33fcb2f673b39aa4f58036369f5cae2412b93d5df3b0cac00c
|
|
4
|
+
data.tar.gz: def95a2ab24ed33f2b1264c59b0b749edf5dac98901816990775f8b53ad4ffbc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e3113af0a26ac031fab975b465b6f701a302de6df1e23c896cb883fe77c75fd66748ddcb2b4e6b865fc3adf3dc5c26576eb327220cdc5c32284d0a7a44e42535
|
|
7
|
+
data.tar.gz: 15e04ae53599352b214a39fab1bc004cdbd264f88596385c1d84393ba0d3357ecd9be43b61f980d5e554e89cafc3f7909a23aa1740427936865b7b6109ae2597
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Railwatch, for coding agents
|
|
2
|
+
|
|
3
|
+
About *using* the `railwatch` gem from a Rails application; copy
|
|
4
|
+
this into that application's repository. Index of everything else:
|
|
5
|
+
[`llms.txt`](llms.txt).
|
|
6
|
+
|
|
7
|
+
Railwatch instruments a Rails app end to end and ships linked telemetry to
|
|
8
|
+
Railwatch Cloud. Every request, job attempt, scheduled task run, and command is
|
|
9
|
+
an **execution**; every query, cache read, log line, outgoing HTTP call, view
|
|
10
|
+
render, exception, and span is a child of one, linked by
|
|
11
|
+
`execution_id`/`trace_id`. It never writes to the app's database.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bundle add railwatch
|
|
17
|
+
bin/rails generate railwatch:install --prompt-token --kamal-secrets
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
The generator writes `config/initializers/railwatch.rb`, mounts `Railwatch::Engine`
|
|
21
|
+
at `/railwatch`, adds the Kamal `post-deploy` hook and the Inertia browser client
|
|
22
|
+
where the app has them, requires `railwatch/rspec` (or `railwatch/minitest`) in the
|
|
23
|
+
test helper, and then runs `railwatch:doctor`. A prompted/stdin/environment token
|
|
24
|
+
goes into `.env` only when Git confirms that file is ignored; token values are
|
|
25
|
+
never printed. Configuration lives only in that
|
|
26
|
+
initializer; every option also has a `RAILWATCH_*` environment variable.
|
|
27
|
+
|
|
28
|
+
## Rake tasks
|
|
29
|
+
|
|
30
|
+
| Task | Does |
|
|
31
|
+
|---|---|
|
|
32
|
+
| `bin/rails railwatch:doctor` | ✓/✗ per check: token, ingest URL, reachability, middleware, engine mount, deploy marker, sample rates, ignored types, Kamal hook, browser client and whether an entrypoint calls it, profiler backend, test matchers. Exits non-zero if the token is missing or the host is unreachable. **Run this first when telemetry is missing.** |
|
|
33
|
+
| `bin/rails railwatch:token` | Where to create an ingest token for this app's platform. |
|
|
34
|
+
| `bin/rails railwatch:mcp` | Paste-ready MCP client configuration for this app's platform. |
|
|
35
|
+
| `bin/rails railwatch:deploy[ref,name,url]` | Records a deploy marker. Use as a release step when not deploying with Kamal. |
|
|
36
|
+
| `bin/rails 'railwatch:sourcemaps[public,true]'` | Uploads Vite source maps for the configured deploy, then deletes acknowledged files. Run after building and before publishing assets. Omit `true` to retain files. See [Source maps](docs/source-maps.md). |
|
|
37
|
+
|
|
38
|
+
## Facade
|
|
39
|
+
|
|
40
|
+
```ruby
|
|
41
|
+
Railwatch.report(error, handled: true, severity: :warning,
|
|
42
|
+
context: {order_id: order.id}, fingerprint: ["billing", "stripe"],
|
|
43
|
+
attachments: {"payload.json" => body})
|
|
44
|
+
Railwatch.attach("payload.json", data, exception: error)
|
|
45
|
+
|
|
46
|
+
Railwatch.context(tenant: org.slug, plan: org.plan) # onto the parent record and every exception
|
|
47
|
+
Railwatch.user { |user| {id: user.id, name: user.name, email: user.email} }
|
|
48
|
+
Railwatch.fingerprint { |error, default| error.is_a?(Timeout::Error) ? ["timeout"] : default }
|
|
49
|
+
|
|
50
|
+
Railwatch.span("pdf.render", template: "invoice", pages: 12) { renderer.call } # returns the block's value
|
|
51
|
+
Railwatch.instrument_outgoing(:get, url) { client.get(url) } # clients with no built-in patch
|
|
52
|
+
Faraday.new(url) { |f| f.use Railwatch::Faraday } # Net::HTTP needs nothing
|
|
53
|
+
|
|
54
|
+
Railwatch.ignore { ExpensiveSync.run } # also Railwatch.pause / Railwatch.resume
|
|
55
|
+
Railwatch.sample(0.01) # re-decide this execution; Railwatch.dont_sample to drop it
|
|
56
|
+
Railwatch.keep! # keep this execution whatever the head decision was
|
|
57
|
+
Railwatch.flush # ship what's buffered now
|
|
58
|
+
|
|
59
|
+
Railwatch.redact_queries { |q| q[:sql] = q[:sql].gsub(/'[^']*'/, "'?'") }
|
|
60
|
+
Railwatch.reject_outgoing_requests { |r| r[:host] == "127.0.0.1" }
|
|
61
|
+
Railwatch.reject_cache_keys %w[session: rack::attack*]
|
|
62
|
+
Railwatch.before_ingest { |batch| batch.reject { |r| r[:t] == "log" } }
|
|
63
|
+
Railwatch.on_unrecoverable { |error| Rails.error.report(error, handled: true) }
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
`redact_*`: requests, queries, exceptions, cache_events, commands, mail,
|
|
67
|
+
outgoing_requests, logs. `reject_*`: queries, cache_events, mail,
|
|
68
|
+
notifications, broadcasts, outgoing_requests, enqueued_jobs, logs.
|
|
69
|
+
|
|
70
|
+
Per action, in a controller class body: `railwatch_sample 0.01, only: :index`,
|
|
71
|
+
`railwatch_never_sample only: :ping`.
|
|
72
|
+
|
|
73
|
+
## Specs
|
|
74
|
+
|
|
75
|
+
`require "railwatch/rspec"` in `spec/rails_helper.rb` (or `"railwatch/minitest"` in
|
|
76
|
+
`test/test_helper.rb`). Railwatch must be enabled in the test env — set any
|
|
77
|
+
non-blank `RAILWATCH_TOKEN`; records go to an in-memory transport, never over the
|
|
78
|
+
wire. All matchers are block matchers.
|
|
79
|
+
|
|
80
|
+
```ruby
|
|
81
|
+
expect { get "/widgets" }.to have_railwatch_queries(at_most: 6) # or exactly:/at_least:
|
|
82
|
+
expect { get "/widgets" }.not_to have_railwatch_n_plus_one
|
|
83
|
+
expect { Checkout.new(cart).total }.to record_railwatch_span("checkout.total")
|
|
84
|
+
expect { importer.run }.to record_railwatch_exception(ArgumentError)
|
|
85
|
+
expect { importer.run }.not_to record_railwatch_exceptions
|
|
86
|
+
expect { SyncCustomers.run }.to have_railwatch_outgoing_requests(at_most: 1)
|
|
87
|
+
|
|
88
|
+
records = railwatch_capture { get "/widgets" } # everything the block produced
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Minitest: `assert_railwatch_queries(at_most: 5) { }`,
|
|
92
|
+
`refute_railwatch_n_plus_one { }`, `assert_railwatch_span("name") { }`.
|
|
93
|
+
|
|
94
|
+
Use these to hold a hot path to a query budget in CI; failure messages list the
|
|
95
|
+
offending SQL. Seed enough rows that an N+1 actually crosses
|
|
96
|
+
`config.n_plus_one_threshold` (default 5), or the gate passes on code that would
|
|
97
|
+
melt in production.
|
|
98
|
+
|
|
99
|
+
## MCP
|
|
100
|
+
|
|
101
|
+
The platform is an MCP server at `<ingest host>/mcp`. Generate a personal
|
|
102
|
+
token at Settings → Profile → "API & MCP token", then:
|
|
103
|
+
|
|
104
|
+
```sh
|
|
105
|
+
claude mcp add railwatch --transport http https://railwatch.rebulk.com/mcp \
|
|
106
|
+
--header "Authorization: Bearer lnt_your_token_here"
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`bin/rails railwatch:mcp` prints this and the Claude Desktop, Cursor, VS Code, and
|
|
110
|
+
Zed equivalents for whichever platform the app points at. Call
|
|
111
|
+
`list_applications` first; then `list_issues`, `get_issue`, `get_route`,
|
|
112
|
+
`search_requests`, `get_execution`, `explain_query`, `get_profile`,
|
|
113
|
+
`search_logs`, `release_health`, `recent_deploys`, `list_alerts`, and the
|
|
114
|
+
`triage_issue` / `slow_route` / `daily_summary` prompts. All durations are
|
|
115
|
+
milliseconds; these docs are served at `railwatch://docs/<name>`.
|
|
116
|
+
|
|
117
|
+
## Gotchas
|
|
118
|
+
|
|
119
|
+
- Sampling is decided **once per execution**; a sampled-out request ships nothing but its unhandled exception.
|
|
120
|
+
- If the app uses WebMock, re-prepend the patch after WebMock loads or outgoing
|
|
121
|
+
requests are invisible in specs: `Net::HTTP.prepend(Railwatch::Patches::NetHttp)`.
|
|
122
|
+
- Profiling needs `vernier` or `stackprof` in the Gemfile; without one, `profile_sample` does nothing.
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 (2026-09-14)
|
|
4
|
+
|
|
5
|
+
First public release.
|
|
6
|
+
|
|
7
|
+
- Renamed the gem from Lantern to Railwatch: constants, file paths,
|
|
8
|
+
`X-Railwatch-*` headers, `RAILWATCH_*` environment variables, rake tasks,
|
|
9
|
+
the generator, the `/railwatch` mount, and the `railwatch` distribution name.
|
|
10
|
+
There is no compatibility layer.
|
|
11
|
+
- Prepared the gem for its public RubyGems distribution. Added strict package
|
|
12
|
+
verification, Trusted Publishing release automation, public
|
|
13
|
+
security/contribution guidance, TLS verification assertions, an
|
|
14
|
+
HTTPS-by-default ingest policy, and browser beacon payload hardening.
|
|
15
|
+
|
|
16
|
+
- Deploy detection follows a git worktree's `.git` file to its gitdir and
|
|
17
|
+
resolves the branch through the repository's refs, so a development app
|
|
18
|
+
checked out as a worktree gets a deploy value like a plain clone.
|
|
19
|
+
|
|
20
|
+
- Reporter backpressure now smooths sustained bursts before the bounded queue
|
|
21
|
+
starts losing whole executions. On each existing reporter tick, a buffer at
|
|
22
|
+
80% of either its record or byte ceiling, or an active ingest retry ladder,
|
|
23
|
+
doubles every execution kind's effective sample divisor up to 8; clear ticks
|
|
24
|
+
halve it back to 1. It is enabled by default and configurable with
|
|
25
|
+
`backpressure` / `RAILWATCH_BACKPRESSURE` and `backpressure_high_water` /
|
|
26
|
+
`RAILWATCH_BACKPRESSURE_HIGH_WATER`. The current divisor rides on deliveries
|
|
27
|
+
as `X-Railwatch-Backpressure-Factor`, and resets after fork.
|
|
28
|
+
|
|
29
|
+
- Active Job retries can optionally capture the exception that caused the
|
|
30
|
+
retry as handled, warning-level telemetry with its attempt and wait in
|
|
31
|
+
context (`capture_job_retry_errors`,
|
|
32
|
+
`RAILWATCH_CAPTURE_JOB_RETRY_ERRORS`). It is off by default because retries
|
|
33
|
+
are usually expected and enabling it can flood the issues list. The
|
|
34
|
+
existing retry log is unchanged.
|
|
35
|
+
|
|
36
|
+
- Deploy identifiers are auto-detected without spawning Git: explicit Railwatch
|
|
37
|
+
and Kamal values first, then common Heroku, Render, Fly, Vercel, GitLab,
|
|
38
|
+
GitHub, and build environment variables, a Capistrano `REVISION`, and the
|
|
39
|
+
checkout's loose or packed Git ref. Full SHAs are consistently shortened to
|
|
40
|
+
12 characters. `RAILWATCH_DETECT_DEPLOY=false` opts out of inferred values,
|
|
41
|
+
and `railwatch:doctor` reports the selected source.
|
|
42
|
+
|
|
43
|
+
- `health` records carry the recurring task schedule Solid Queue is
|
|
44
|
+
running (`detail.recurring_tasks`, key => schedule), so the platform can
|
|
45
|
+
tell a task that was removed from `config/recurring.yml` apart from one
|
|
46
|
+
that stopped running. Railwatch Cloud used to flag a removed task as
|
|
47
|
+
missed every ten minutes for thirty days after its last run. Left out,
|
|
48
|
+
not sent empty, when there are no tasks or the table could not be read.
|
|
49
|
+
|
|
50
|
+
- The browser client reports a dropped Inertia request (`networkError` on
|
|
51
|
+
Inertia 3, `exception` on 2) only while the user is waiting on a visit,
|
|
52
|
+
meaning one that shows Inertia's progress bar. A visit the page started by
|
|
53
|
+
itself — a poll, a refresh when the tab comes back, `router.reload`, a
|
|
54
|
+
prefetch on hover — runs without it, and when one drops its connection
|
|
55
|
+
nothing the user did has failed: the page keeps what it has and the next
|
|
56
|
+
tick refreshes it. A laptop waking on a new network used to open an issue
|
|
57
|
+
that way, and regress it every morning. The visit still lands in timing
|
|
58
|
+
data with `status: "cancelled"`. A click or a form submit is reported as
|
|
59
|
+
before, including a click Inertia serves from a prefetch already in
|
|
60
|
+
flight (which never gets a `start` of its own), and so is the load of a
|
|
61
|
+
page's deferred props, which the user watches as a skeleton; an app that
|
|
62
|
+
wants a particular background refresh reported passes `showProgress:
|
|
63
|
+
true`. Inertia re-rejects a failed request's error after firing the
|
|
64
|
+
event, and the client now drops that unhandled-rejection copy by identity
|
|
65
|
+
rather than relying on the reported record to dedupe it.
|
|
66
|
+
|
|
67
|
+
- Boot with the gem enabled is now within noise of boot without it; it was
|
|
68
|
+
about 300 ms and 10 MB slower.
|
|
69
|
+
The `process` record read its adapter names through `ActiveRecord::Base`
|
|
70
|
+
and `ActiveJob::Base`, which autoloaded both frameworks before anything
|
|
71
|
+
else asked for them; it now reads the app's configuration. The Rake and
|
|
72
|
+
`bin/rails runner` patches are installed from the engine's `rake_tasks`
|
|
73
|
+
and `runner` hooks instead of every boot, which stops a web or worker
|
|
74
|
+
process requiring rake and railties' runner command.
|
|
75
|
+
- The `process` record is written from an `after_initialize` hook that
|
|
76
|
+
the engine registers from inside an initializer, so it runs after every
|
|
77
|
+
`after_initialize` block the app itself registers; `boot_seconds` covers
|
|
78
|
+
the app's own initializers and those blocks, and an app that reconfigures
|
|
79
|
+
Railwatch late is respected.
|
|
80
|
+
- Fork handling is one `ActiveSupport::ForkTracker` callback -- Rails' own
|
|
81
|
+
`Process._fork` hook -- instead of three separate prepends on `Process`.
|
|
82
|
+
`Railwatch::Reporter::ForkHook`, `Railwatch::Health::ForkHook`, and
|
|
83
|
+
`Railwatch::Sessions::ForkHook` are gone; `Railwatch.restart_after_fork!`
|
|
84
|
+
resets everything in order.
|
|
85
|
+
- The gemspec declares `base64` (a bundled gem since Ruby 3.4, previously
|
|
86
|
+
reached only through Active Support's own dependency) and bounds the
|
|
87
|
+
Rails dependency to `>= 8.1, < 9`.
|
|
88
|
+
|
|
89
|
+
- An unhandled exception's urgent flush is coalesced over a quarter-second
|
|
90
|
+
window (`Reporter::URGENT_FLUSH_DELAY`) instead of waking the reporter
|
|
91
|
+
per record. During an exception storm every request used to trigger its
|
|
92
|
+
own POST carrying the handful of records written since the last one: a
|
|
93
|
+
ten-second burst that produced 4,000 records went out as 400 POSTs of
|
|
94
|
+
ten, at about double the gzip bytes per record of a full batch. A lone
|
|
95
|
+
exception still ships within the window; a buffer that crosses
|
|
96
|
+
`flush_threshold` flushes at once as before.
|
|
97
|
+
|
|
98
|
+
- `POST /railwatch/beacon` is rate limited per client IP: 120 requests a
|
|
99
|
+
minute by default (`beacon_rate_limit`, `RAILWATCH_BEACON_RATE_LIMIT`; `0`
|
|
100
|
+
disables), answered with 429 and `Retry-After` past that. The beacon takes
|
|
101
|
+
no credential and keeps every browser error it is sent, so until now a
|
|
102
|
+
script could spend an app's event quota and open junk browser issues from
|
|
103
|
+
a shell. The counter lives in the app's cache store; a store that cannot
|
|
104
|
+
count fails open.
|
|
105
|
+
- **Behaviour change for every app: `query` records now carry normalized SQL,
|
|
106
|
+
not the raw statement.** String, numeric, hex, Postgres dollar-quoted and
|
|
107
|
+
adapter-specific literals, plus SQL comments, are replaced with `?` while
|
|
108
|
+
the statement shape and placeholders remain. SQL literals routinely contain
|
|
109
|
+
email addresses, tokens, and other customer data, and until now every one of
|
|
110
|
+
them was shipped. Set `capture_sql_values` (`RAILWATCH_CAPTURE_SQL_VALUES`) to
|
|
111
|
+
restore the old behaviour. Active Record's separate structured binds are
|
|
112
|
+
never sent either way, and `capture_query_explain` is unaffected -- the
|
|
113
|
+
EXPLAIN still runs on the raw statement, only what is stored in `sql`
|
|
114
|
+
changed. Note that a plan can echo literal predicates, so
|
|
115
|
+
`capture_query_explain` remains a privacy decision of its own.
|
|
116
|
+
- The SQL normalizer is a byte-oriented lexical scanner rather than a set of
|
|
117
|
+
regexes: adapter-aware quoting (MySQL backticks and double-quoted strings,
|
|
118
|
+
SQLite `[ident]` and its double-quoted-string fallback, Postgres
|
|
119
|
+
dollar-quoting and `E''`), nested block comments, and bounded input. It
|
|
120
|
+
scans each dialect's *default* backslash-escaping rule. Session modes that
|
|
121
|
+
change that rule (`NO_BACKSLASH_ESCAPES`, `standard_conforming_strings =
|
|
122
|
+
off`) are not carried in the notification; the previous approach of
|
|
123
|
+
abandoning the rest of the statement whenever a backslash-quote appeared
|
|
124
|
+
was worse, because on MySQL -- where backslash escaping is the quoting
|
|
125
|
+
Active Record emits -- it truncated every statement containing an
|
|
126
|
+
apostrophe and collapsed distinct queries into one group.
|
|
127
|
+
|
|
128
|
+
- Telemetry memory is now bounded by bytes as well as by record count. A
|
|
129
|
+
record count alone does not bound memory: 10,000 records is a few megabytes
|
|
130
|
+
of ordinary telemetry, or a gigabyte of captured attachments and
|
|
131
|
+
multi-megabyte SQL strings. Three new ceilings, all configurable:
|
|
132
|
+
`buffer_bytes` (16 MiB, reporter queue), `execution_buffer_bytes` (8 MiB,
|
|
133
|
+
one execution's buffered tree), and `batch_bytes` (8 MiB uncompressed
|
|
134
|
+
NDJSON per ingest request). Each record is weighed once, when it is
|
|
135
|
+
buffered, and the weight travels with it, so nothing is measured twice.
|
|
136
|
+
Byte loss is counted and reported alongside record loss
|
|
137
|
+
(`X-Railwatch-Dropped-Bytes`).
|
|
138
|
+
- A queue holding more than one batch is delivered as several batches; the
|
|
139
|
+
tail is kept for the next flush rather than dropped. A record that still
|
|
140
|
+
does not fit one delivery is dropped and counted rather than raising -- a
|
|
141
|
+
batch that is too large is exactly as large on the next attempt, so
|
|
142
|
+
retrying it would burn all eight attempts and drop it anyway.
|
|
143
|
+
- `Railwatch.attach` reads a file or IO with a bounded `cap + 1` read. A 2GB
|
|
144
|
+
log file used to be read whole and then sliced to 1 MiB.
|
|
145
|
+
|
|
146
|
+
- User references survive a tenant that binds after the user is resolved.
|
|
147
|
+
An app that resolves its user in one `before_action` and its tenant in the
|
|
148
|
+
next used to emit a bare `"1"` for every tenant's user 1 -- two tenants
|
|
149
|
+
collapsed onto one person on the platform. The execution now keeps the raw
|
|
150
|
+
id and requalifies it (plus the records already buffered, and the pending
|
|
151
|
+
`user` entity) the moment the tenant binds, so the final reference is
|
|
152
|
+
`"acme:1"` and the entity is deduplicated against that final reference
|
|
153
|
+
rather than the provisional one. Jobs enqueued from such a request carry
|
|
154
|
+
the raw id and the tenant, and the worker qualifies it on restore.
|
|
155
|
+
- `Railwatch.context(tenant: ...)` now actually sets the tenant on records, as
|
|
156
|
+
documented. It binds onto the running execution at `Railwatch.context` time;
|
|
157
|
+
`Context.current_tenant` reads that before falling back to `TenantRecord` /
|
|
158
|
+
`ActiveRecord::Tenanted`, so nothing on the per-record path pays for it.
|
|
159
|
+
|
|
160
|
+
- A `user` entity is now cached (one per id per process-hour) only after the
|
|
161
|
+
execution that carried it was actually handed to the reporter. Previously
|
|
162
|
+
the first sighting wrote the cache entry unconditionally, so if that
|
|
163
|
+
sighting happened inside a sampled-out or `Railwatch.pause`d execution -- or
|
|
164
|
+
an execution whose sampling flipped afterwards -- no `user` record was ever
|
|
165
|
+
written, and every sampled-in sighting for the next hour was suppressed:
|
|
166
|
+
the platform had records attributed to a user it had no name or email for.
|
|
167
|
+
Forked workers also reset the cache, since a child's reporter buffer starts
|
|
168
|
+
empty and has to emit its own entities.
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
- Action Cable channel actions now open a real `channel_action` execution
|
|
173
|
+
before application code runs, so the queries, logs, broadcasts, transmits,
|
|
174
|
+
and unhandled exceptions inside an action share one execution and one trace.
|
|
175
|
+
A channel action has no HTTP request and no Rack middleware around it, so
|
|
176
|
+
until now those records had no parent at all. Head sampling is its own knob,
|
|
177
|
+
`sample[:channels]` / `RAILWATCH_CHANNEL_SAMPLE_RATE`; an unhandled channel
|
|
178
|
+
exception still ships with its parent when the channel rate is zero.
|
|
179
|
+
|
|
180
|
+
- `Railwatch.context` is now redacted with the same `ActiveSupport::ParameterFilter`
|
|
181
|
+
that redacts request params. Context is application data and gets copied
|
|
182
|
+
onto every record built while it is set, so an app that put an API token or
|
|
183
|
+
a password there was writing it verbatim into telemetry.
|
|
184
|
+
- An oversized context is now rebuilt rather than sliced. The 64KB cap used
|
|
185
|
+
to `byteslice` the encoded JSON, which cut it mid-string or mid-object and
|
|
186
|
+
left the platform with an unparseable fragment -- the whole context was
|
|
187
|
+
lost rather than most of it. Whole values are kept while they fit, an
|
|
188
|
+
oversized string value ends with `[TRUNCATED]`, anything that still does
|
|
189
|
+
not fit is dropped, and `"_railwatch_truncated": true` says it happened. The
|
|
190
|
+
empty-context fast path (which runs on every log record) is unchanged.
|
|
191
|
+
|
|
192
|
+
- Request teardown no longer parses a non-multipart request body. Emitting
|
|
193
|
+
the `request` record read `request.params` (for `files`) and
|
|
194
|
+
`request.format`, and Rack parses the body the first time either is asked
|
|
195
|
+
for. When a controller ran, that parse had already happened and was
|
|
196
|
+
memoized; when nothing ran -- a routing 404, a rack-attack block, an
|
|
197
|
+
upstream rejection -- Railwatch was the only component that ever read the
|
|
198
|
+
body, at teardown, after the response was decided. Both reads are now
|
|
199
|
+
guarded: `files` falls back to walking params only for a multipart request
|
|
200
|
+
(nothing else can carry an upload), and `format` is reported only when it
|
|
201
|
+
is already resolved or the request is multipart, otherwise `""`.
|
|
202
|
+
|
|
203
|
+
- Ingest acknowledgements are validated before a batch is considered
|
|
204
|
+
delivered: a 2xx whose body is not a JSON object, is malformed JSON, omits
|
|
205
|
+
`accepted`/`rejected`, or reports counts that do not cover the submitted
|
|
206
|
+
batch now retains the batch (under the same idempotency key) for retry
|
|
207
|
+
instead of silently dropping it. HTML sign-in pages from an intercepting
|
|
208
|
+
proxy were the motivating case. Two shapes are explicitly a successful
|
|
209
|
+
drain rather than a failure: an acknowledgement carrying a `reason`, and an
|
|
210
|
+
all-zero `{"accepted":0,"rejected":0}` -- that is how the platform answers
|
|
211
|
+
for a paused or over-quota environment, and retrying it would burn eight
|
|
212
|
+
attempts and drop the records anyway. Per-record rejections
|
|
213
|
+
(`rejected > 0`) stay routine: they are logged under `RAILWATCH_DEBUG` and
|
|
214
|
+
are not reported to `on_unrecoverable`, since the platform's ingest batch
|
|
215
|
+
is the authoritative accounting for them.
|
|
216
|
+
- Per-request overhead roughly halved, and per-query overhead cut by about
|
|
217
|
+
two thirds, measured against a baseline with the gem's subscribers
|
|
218
|
+
genuinely unsubscribed. The log capture reported itself at DEBUG, which
|
|
219
|
+
made `Rails.logger.debug?` true for the whole app and had every framework
|
|
220
|
+
`LogSubscriber` format its SQL, render, and cache lines for nobody; it
|
|
221
|
+
now reports `config.log_level`. A head-sampled-out request no longer
|
|
222
|
+
builds the request record it was about to discard. The transaction
|
|
223
|
+
statement counter shares the query subscriber's event instead of taking
|
|
224
|
+
a second one per query, and the model-hydration counter subscribes
|
|
225
|
+
without an `Event` object. Cache-event and view-render group hashes,
|
|
226
|
+
the vendor cache-key check, and the `GC.stat(:time)` probe are computed
|
|
227
|
+
once instead of per event.
|
|
228
|
+
|
|
229
|
+
- `bench/overhead.rb` now measures against a real baseline: every
|
|
230
|
+
subscriber the gem installed is unsubscribed and its log capture detached
|
|
231
|
+
for the "off" batches, rather than flipping `config.enabled` with
|
|
232
|
+
everything still wired in, which had hidden most of the cost. It gates
|
|
233
|
+
three request shapes on SQLite and fails if `Rails.logger.debug?` is on.
|
|
234
|
+
The scripts that produced the numbers in the docs are committed alongside
|
|
235
|
+
it and indexed in `bench/README.md`: per-shape cost, the request, query,
|
|
236
|
+
and exception paths piece by piece, reporter-thread and wire cost,
|
|
237
|
+
allocation and CPU attribution, and an end-to-end Puma load harness.
|
|
238
|
+
|
|
239
|
+
- The installer now prefers a hidden prompt, stdin, or `RAILWATCH_TOKEN`, never
|
|
240
|
+
prints token values, and refuses to put a token in a tracked or non-ignored
|
|
241
|
+
`.env`. `railwatch:doctor` also fails when it finds a plaintext `lt_...` token
|
|
242
|
+
in likely secret-bearing files tracked by Git. The legacy `--token=` option
|
|
243
|
+
remains compatible but warns about shell-history and process-list exposure.
|
|
244
|
+
|
|
245
|
+
- A forked worker profiles again. `Process._fork` now resets the profiler's
|
|
246
|
+
process-global state in the child: it used to inherit `@running` holding
|
|
247
|
+
the handle of a profile the parent was taking, which nothing in the child
|
|
248
|
+
ever stopped, so every execution in that worker was counted as skipped and
|
|
249
|
+
never profiled for the life of the process.
|
|
250
|
+
|
|
251
|
+
- Numeric `RAILWATCH_*` environment variables are parsed with `Integer()`/
|
|
252
|
+
`Float()` and fall back to the documented default when the value is not a
|
|
253
|
+
number. `RAILWATCH_BUFFER_SIZE=12px` used to become `0` via `String#to_i`,
|
|
254
|
+
silently turning off buffering; the same applied to timeouts, intervals
|
|
255
|
+
and sample rates.
|
|
256
|
+
|
|
257
|
+
- Inbound `traceparent` parsing follows the W3C trace-context validity
|
|
258
|
+
rules: version `ff`, an all-zero trace id, and an all-zero parent id are
|
|
259
|
+
rejected instead of being adopted as a trace, and trailing data after the
|
|
260
|
+
flags is rejected on version `00`. A future version that appends
|
|
261
|
+
dash-delimited fields after the flags is now accepted (its extra fields
|
|
262
|
+
are never interpreted) rather than dropped, so a newer upstream still
|
|
263
|
+
links to this service.
|
|
264
|
+
|
|
265
|
+
- The request record's `url` and `redirect_to` URL fields now retain only
|
|
266
|
+
origin (without authority credentials) and path. Query strings and
|
|
267
|
+
fragments are always removed from these fields, preventing reset tokens,
|
|
268
|
+
OAuth codes, signed-URL credentials, and other parameter values from being
|
|
269
|
+
exported even when request-payload capture is disabled. The Net::HTTP and
|
|
270
|
+
Faraday patches sanitize an `outgoing_request` URL through the same
|
|
271
|
+
helper, so authority credentials and fragments are now dropped there too.
|
|
272
|
+
|
|
273
|
+
- Exception deduplication is now scoped to the current execution and to the
|
|
274
|
+
handled/unhandled disposition, and only marks an error after sampling and
|
|
275
|
+
pause checks pass. A sampled-out or paused handled report can no longer
|
|
276
|
+
suppress a later unhandled raise of the same object, while Rails.error and
|
|
277
|
+
middleware still collapse duplicate observations in one execution.
|
|
278
|
+
|
|
279
|
+
- Header masking no longer depends on an app enumerating every vendor
|
|
280
|
+
header name. A header whose name has a credential-shaped segment
|
|
281
|
+
(`api-key`/`apikey`, `access-key`/`accesstoken`, `private-key`,
|
|
282
|
+
`auth`/`authentication`/`authorization`, `bearer`, `credential`, `hmac`,
|
|
283
|
+
`jwt`, `token`, `secret`, `signature`) is masked as `[FILTERED]` on top of
|
|
284
|
+
the exact `redact_headers` denylist. Concatenated Rack aliases such as
|
|
285
|
+
`X-AuthToken`, `X-ApiToken`, `X-AccessToken`, `X-ClientToken`,
|
|
286
|
+
`X-SessionToken`, `X-RefreshToken`, `X-SecretKey`, `X-HmacSignature`, and
|
|
287
|
+
`X-CSRFToken` are covered alongside `X-Api-Key`, `Stripe-Signature`, and
|
|
288
|
+
`X-Hub-Signature-256`. Ordinary diagnostic headers are untouched.
|
|
289
|
+
|
|
290
|
+
- `buffer_size` defaults to 10,000 (was 5,000), matching
|
|
291
|
+
`Execution::MAX_RECORDS`. A job whose tree was larger than the queue lost
|
|
292
|
+
its first records when the tree was written at the end of the execution;
|
|
293
|
+
on rebulk-system that was every outgoing HTTP request of a 30-second sync.
|
|
294
|
+
|
|
295
|
+
- Active Job payloads now carry the enqueuing execution's user and tenant
|
|
296
|
+
(`railwatch_user`/`railwatch_tenant`) next to the trace and parent ids, and
|
|
297
|
+
the worker restores them before the attempt opens. A `job_attempt` and
|
|
298
|
+
every child record under it are attributed to the person whose request
|
|
299
|
+
enqueued the job instead of to a worker process with no signed-in user,
|
|
300
|
+
and jobs that enqueue jobs pass the same identity along. Identifier
|
|
301
|
+
strings only — no user or tenant model is serialized or hydrated.
|
|
302
|
+
Payloads without the keys (enqueued before this change) deserialize to
|
|
303
|
+
nil and fall back to local resolution as before.
|
|
304
|
+
|
|
305
|
+
- `c.failure_context = 200` (`RAILWATCH_FAILURE_CONTEXT`) keeps a
|
|
306
|
+
head-sampled-out execution's last 200 child records in a ring and ships
|
|
307
|
+
them only if that execution reports an unhandled exception, so an
|
|
308
|
+
unsampled failure is diagnosable without enabling slow-request tail
|
|
309
|
+
sampling globally. Off (0) by default, which leaves the sampled-out path
|
|
310
|
+
building and buffering nothing exactly as before. `exceptions: 0`,
|
|
311
|
+
ignored and handled exceptions, `Railwatch.pause`/`ignore`, and an
|
|
312
|
+
interactive `rails runner` never promote a ring; overflow is counted onto
|
|
313
|
+
the batch's dropped-record count; with `tail_sample_slow_ms` also set,
|
|
314
|
+
tail sampling's larger buffer wins.
|
|
315
|
+
|
|
316
|
+
- A retained delivery batch gives up after `Reporter::MAX_RETRY_ATTEMPTS`
|
|
317
|
+
(8) and is dropped and counted, so a batch that keeps failing cannot pin
|
|
318
|
+
itself in memory while every newer record is discarded around it.
|
|
319
|
+
|
|
320
|
+
- `c.beacon_user { |request| ... }`: who is behind a browser beacon, for
|
|
321
|
+
apps that authenticate in a `before_action` the gem's beacon controller
|
|
322
|
+
never runs. Before this, every visit, browser session and JavaScript
|
|
323
|
+
error from such an app shipped with no user.
|
|
324
|
+
- Console breadcrumbs render objects as JSON instead of `[object Object]`.
|
|
325
|
+
|
|
326
|
+
- `bin/rails runner` from a shell never got its command execution: the
|
|
327
|
+
runner patch is prepended during `boot_application!`, which the
|
|
328
|
+
already-running `#perform` calls, so the `#perform` override only ever
|
|
329
|
+
ran in this gem's own specs. The patch now also wraps
|
|
330
|
+
`conditional_executor`, which that `#perform` reaches after boot, so a
|
|
331
|
+
real runner ships its `command` record and an interactive one (`-`,
|
|
332
|
+
inline code, a script under `/tmp`) withholds its exception as documented.
|
|
333
|
+
|
|
334
|
+
- Forked Puma and Active Job workers now replace inherited reporter buffers,
|
|
335
|
+
drop accounting, transport policy state, and synchronization primitives
|
|
336
|
+
before recording anything. Parent telemetry is delivered only by the
|
|
337
|
+
parent; each child emits its own process/health records, and a mutex held
|
|
338
|
+
by another thread at fork can no longer deadlock the child reporter.
|
|
339
|
+
|
|
340
|
+
- Exceptions whose backtrace was assigned rather than raised
|
|
341
|
+
(`ActiveRecord::StatementInvalid` via `set_backtrace`, `Faraday::Error`
|
|
342
|
+
delegating to its wrapped exception) shipped with no frames, no
|
|
343
|
+
culprit, and a fingerprint of class and message only, because
|
|
344
|
+
`backtrace_locations` is nil for them. `Backtrace.frames` now parses the
|
|
345
|
+
String backtrace in that case.
|
|
346
|
+
|
|
347
|
+
- Request exclusions now bypass instrumentation correctly. `/up` and
|
|
348
|
+
`/railwatch/beacon` are excluded by default, apps can configure exact paths
|
|
349
|
+
or regexps through `ignored_request_paths`, and a same-origin authenticated
|
|
350
|
+
reporter POST to `/ingest` is recognized behind reverse proxies. The last
|
|
351
|
+
case prevents Railwatch Cloud's self-monitoring from creating an endless
|
|
352
|
+
flush -> ingest-request -> flush feedback loop without hiding unrelated
|
|
353
|
+
application routes also named `/ingest`.
|
|
354
|
+
|
|
355
|
+
- Retryable ingest failures no longer discard a drained batch. Network
|
|
356
|
+
failures plus HTTP 402, 408, 429, and 5xx responses restore records and
|
|
357
|
+
their drop accounting to the bounded buffer, then retry on the reporter
|
|
358
|
+
thread with jittered exponential backoff. Permanent client rejections and
|
|
359
|
+
shutdown deadlines with unsent records remain observable through
|
|
360
|
+
`on_unrecoverable`; urgent exception reporting now wakes the background
|
|
361
|
+
thread instead of waiting through network timeouts on the request thread.
|
|
362
|
+
|
|
363
|
+
- Browser JavaScript errors. The Inertia browser client now captures
|
|
364
|
+
uncaught errors, unhandled promise rejections, and Inertia's failed-request
|
|
365
|
+
events (`exception`/`invalid` on Inertia 2, `networkError`/`httpException`
|
|
366
|
+
on Inertia 3), batches them onto the existing beacon, and the
|
|
367
|
+
beacon controller records each as an `exception` with `source: "browser"`
|
|
368
|
+
— stack parsed into the same `{file, line, function, in_app}` frames a
|
|
369
|
+
Ruby backtrace produces, and the same default fingerprint, so a browser
|
|
370
|
+
error groups, regresses and resolves like any other issue. Each error
|
|
371
|
+
carries the page URL, Inertia component, visit, tab session, user agent,
|
|
372
|
+
and the last 20 breadcrumbs (console errors, clicks, navigations).
|
|
373
|
+
`startRailwatch({ ignoreErrors, denyUrls, tenant })`, plus
|
|
374
|
+
`railwatchRootOptions()` for React 19's `createRoot` and
|
|
375
|
+
`reportError(error, context)` for a React 18 boundary — outside a
|
|
376
|
+
development build React never hands a boundary-caught error to
|
|
377
|
+
`window.onerror`. Replaces `@sentry/react`; see
|
|
378
|
+
`docs/replacing-sentry.md`.
|
|
379
|
+
|
|
380
|
+
- Interactive sessions are no longer treated as application failures.
|
|
381
|
+
`bin/rails console` captures nothing, starts no background thread, and
|
|
382
|
+
sends no `process`/`health` record (`config.capture_console` /
|
|
383
|
+
`RAILWATCH_CAPTURE_CONSOLE=1` re-enables everything). A `bin/rails runner`
|
|
384
|
+
the operator typed (`-`, inline code, or a `.rb` file under
|
|
385
|
+
`config.interactive_runner_paths`, default `/tmp/` and `/var/tmp/`) ships
|
|
386
|
+
its `command` record with `interactive: true` and its exit code, but does
|
|
387
|
+
not report the exception; a deployed script (`rails runner
|
|
388
|
+
script/nightly.rb`), a rake task, and a job report exactly as before.
|
|
389
|
+
|
|
390
|
+
- Release health: a new `session` record type, from the browser client (one
|
|
391
|
+
session per tab, riding along on the visit beacon) and from the request
|
|
392
|
+
middleware (`Railwatch::Sessions`, one flusher thread per web process).
|
|
393
|
+
`config.track_sessions`, `config.session_flush_interval`,
|
|
394
|
+
`config.session_timeout`; `c.ignore = [:sessions]` turns off shipping.
|
|
395
|
+
|
|
396
|
+
- Added `docs/records.md` (every wire record type, field by field) and
|
|
397
|
+
`docs/configuration.md` (every `Configuration` attribute, the public
|
|
398
|
+
facade, sampling, transport, Inertia beacon/SSR, the overhead gate, the
|
|
399
|
+
Kamal hook, and the rake tasks); README tightened to point at both,
|
|
400
|
+
plus a new "Replacing Sentry" section.
|
|
401
|
+
- Initial release: requests, jobs, scheduled tasks, commands, queries (with
|
|
402
|
+
N+1 detection), transactions, exceptions, cache, mail, Action Cable
|
|
403
|
+
broadcasts, Noticed notifications, outgoing HTTP, Active Storage, view
|
|
404
|
+
renders, logs and Rails 8.1 structured events, deprecations, users,
|
|
405
|
+
processes, and Inertia visits. Sampling, ignore/pause, redaction,
|
|
406
|
+
rejection, before-ingest rate limiting, deploy tracking, Kamal hook.
|
|
407
|
+
- Job attempts report `status` (`released` when `retry_on` re-enqueues instead
|
|
408
|
+
of failing), `queue_latency` measured at perform-start rather than after the
|
|
409
|
+
job runs, `connection`, and `concurrency_key`; a pruned job attempt gets a
|
|
410
|
+
fresh `execution_id`/`trace_id` instead of reusing a stale one.
|
|
411
|
+
- `process` records measure `boot_seconds` from `Railwatch::BOOTED_AT`, a clock
|
|
412
|
+
reading taken as early in process boot as Railwatch can observe, instead of an
|
|
413
|
+
unset global.
|
|
414
|
+
- `bin/rails runner` invocations are instrumented as a `command` execution.
|
|
415
|
+
- Inertia SSR renders are timed automatically (`inertia.ssr_ms`) wherever
|
|
416
|
+
`inertia_rails` SSR is enabled, including full-page (non-XHR) visits.
|
|
417
|
+
- Exceptions report `code` (errno, for a `SystemCallError`) and `sql_state`.
|
|
418
|
+
- Transactions report `statement_count` (writes made inside the transaction)
|
|
419
|
+
and a `group` hash for grouping in the UI.
|
|
420
|
+
- Default vendor rake tasks (`db:migrate`, `assets:precompile`, ...) and
|
|
421
|
+
default vendor cache-key prefixes (`rack::attack`, `flipper`, ...) are
|
|
422
|
+
excluded by default; opt back in with `capture_default_vendor_commands` /
|
|
423
|
+
`capture_default_vendor_cache_keys`.
|
|
424
|
+
- `Railwatch.reject_cache_keys` drops your own noisy cache keys the same way as
|
|
425
|
+
the default vendor list, with trailing-`*` prefix matching and regex support.
|
|
426
|
+
- A request sampled out together with `sample[:exceptions] = 0` now ships
|
|
427
|
+
nothing for an unhandled exception, instead of always shipping one.
|
|
428
|
+
- Requests report `route_methods`, `route_domain`, and uploaded `files`
|
|
429
|
+
(name/size/content_type only, never file contents).
|
|
430
|
+
- `Railwatch.on_unrecoverable` registers a callback for Railwatch's own internal
|
|
431
|
+
errors (a subscriber raising, or delivery failing after its retry).
|
|
432
|
+
- `Railwatch::Faraday` middleware instruments outgoing HTTP made through
|
|
433
|
+
Faraday (`f.use Railwatch::Faraday`); `Railwatch.instrument_outgoing(method,
|
|
434
|
+
url) { }` covers any other HTTP client.
|
|
435
|
+
- Fixed `Backtrace.caller_location` excluding legitimate app/spec frames that
|
|
436
|
+
happened to live under a path containing "/railwatch/" (this gem's own
|
|
437
|
+
`spec/dummy`, for one); it now only skips Railwatch's own `lib/` and frames
|
|
438
|
+
inside an installed gem, so query and outgoing-request source locations
|
|
439
|
+
resolve correctly again.
|
|
440
|
+
- Fixed `railwatch:status` and `railwatch:deploy` rake tasks running twice per
|
|
441
|
+
invocation: the engine no longer manually `load`s `lib/tasks/railwatch_tasks.rake`
|
|
442
|
+
on top of Rails' automatic `lib/tasks/*.rake` loading.
|
|
443
|
+
- `Transport::Http` is now HTTP-status-aware: a 5xx response is retried once,
|
|
444
|
+
a 4xx is not retried, a 401 marks the reporter unauthorized and stops
|
|
445
|
+
flushing (logged once via `Railwatch.debug` and `Railwatch.on_unrecoverable`),
|
|
446
|
+
and a 402 (quota) backs off for 60 seconds, dropping and counting records
|
|
447
|
+
as dropped during the backoff window. Delivery still never raises.
|
|
448
|
+
- Fixed `Patches::RakeTask` shipping a separate command record per
|
|
449
|
+
prerequisite instead of nesting the whole dependency chain under one
|
|
450
|
+
command execution; nested calls made from inside an already-excluded
|
|
451
|
+
vendor task (e.g. `db:_dump`, invoked internally by `db:migrate`) are also
|
|
452
|
+
left untracked instead of starting their own execution.
|
|
453
|
+
- `job_attempt` records now include `parent_id` (the enqueuing execution's
|
|
454
|
+
id), previously captured but never emitted in the shipped hash.
|
|
455
|
+
- Fixed `Patches.install_runner_command!` requiring the wrong path for
|
|
456
|
+
`Rails::Command::RunnerCommand` (`rails/command/runner_command` instead of
|
|
457
|
+
`rails/commands/runner/runner_command`), which silently no-oped and left
|
|
458
|
+
`bin/rails runner` uninstrumented.
|
|
459
|
+
- `Configuration#ignore=` now raises `ArgumentError` for a record type
|
|
460
|
+
outside `RECORD_TYPES` instead of silently accepting it.
|
|
461
|
+
- `scheduled_task` records report `drift` (microseconds between the
|
|
462
|
+
`RecurringExecution#run_at` and the actual perform start).
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2026 Cole Robertson and Rebulk
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|