railwatch 0.8.6 → 0.9.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 +4 -4
- data/AGENTS.md +1 -0
- data/CHANGELOG.md +28 -0
- data/app/models/railwatch/ingest/mapper.rb +1 -1
- data/db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb +10 -0
- data/docs/records.md +1 -0
- data/docs/testing.md +15 -0
- data/lib/railwatch/configuration.rb +8 -1
- data/lib/railwatch/execution.rb +19 -0
- data/lib/railwatch/faraday.rb +2 -1
- data/lib/railwatch/minitest.rb +7 -0
- data/lib/railwatch/patches/migration_busy_timeout.rb +73 -0
- data/lib/railwatch/patches/net_http.rb +1 -0
- data/lib/railwatch/patches.rb +11 -0
- data/lib/railwatch/rspec.rb +23 -0
- data/lib/railwatch/spec_helper.rb +1 -1
- data/lib/railwatch/subscribers/queries.rb +8 -0
- data/lib/railwatch/version.rb +1 -1
- data/lib/railwatch/wire_fixtures.json +1 -0
- data/lib/railwatch.rb +2 -1
- data/llms.txt +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cd11714c7a290f5f6717f3b8f239b72b669fce39a2797a59fcb862595925778
|
|
4
|
+
data.tar.gz: 147d3416d6d047a6c87b8ccdb9d2b67b8e2a6058f881956241f65dff0ea239c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 774a7cbf876e5220af985a97a2cc6919ebb91ff9ea7a38e414956b1c0cbf3cedb723cbc470baaa807fa7343295117576df15485923502d855212e83698bee592
|
|
7
|
+
data.tar.gz: c0d3106cd7c800cf938daf056ce9c279b10e5013af618b39e4e31411517dc578a4e71fd711b5a4158e3b4acdb76964476ca298377f792c0b8f40319ad13c877e
|
data/AGENTS.md
CHANGED
|
@@ -102,6 +102,7 @@ expect { Checkout.new(cart).total }.to record_railwatch_span("checkout.total")
|
|
|
102
102
|
expect { importer.run }.to record_railwatch_exception(ArgumentError)
|
|
103
103
|
expect { importer.run }.not_to record_railwatch_exceptions
|
|
104
104
|
expect { SyncCustomers.run }.to have_railwatch_outgoing_requests(at_most: 1)
|
|
105
|
+
expect { post "/orders", params: }.not_to have_railwatch_http_in_transaction
|
|
105
106
|
|
|
106
107
|
records = railwatch_capture { get "/widgets" } # everything the block produced
|
|
107
108
|
```
|
data/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,34 @@
|
|
|
6
6
|
filled in by the release commit, which is also the only commit that
|
|
7
7
|
touches lib/railwatch/version.rb and Gemfile.lock. See CONTRIBUTING.md. -->
|
|
8
8
|
|
|
9
|
+
## 0.9.0 (2026-10-08)
|
|
10
|
+
|
|
11
|
+
- An `outgoing_request` now says whether it was made inside a database
|
|
12
|
+
transaction (`in_transaction`), like Isolator: the transaction and its
|
|
13
|
+
locks wait on the other service, and a rollback cannot undo the call. Only
|
|
14
|
+
transactions the execution began count, so transactional test fixtures do
|
|
15
|
+
not. New `have_railwatch_http_in_transaction` matcher and
|
|
16
|
+
`refute_railwatch_http_in_transaction` assertion, and a telemetry migration
|
|
17
|
+
adding the column to `outgoing_requests`.
|
|
18
|
+
|
|
19
|
+
## 0.8.7 (2026-10-06)
|
|
20
|
+
|
|
21
|
+
- A migration of the railwatch databases waits up to 60 s for SQLite's
|
|
22
|
+
write lock instead of the database's own `timeout` (5 s in the generated
|
|
23
|
+
`database.yml`). A deploy migrates while the previous release is still
|
|
24
|
+
serving, and on an embedded install that release's writer keeps
|
|
25
|
+
committing telemetry to the same file: about a second per transaction on
|
|
26
|
+
a warm 15 GB file, several seconds while the deploy's image build has the
|
|
27
|
+
disk. rebulk-system's container entrypoint ran `db:prepare` into that,
|
|
28
|
+
got `SQLite3::BusyException: database is locked` from the first
|
|
29
|
+
migration that needed a write, and crash-looped until the health check
|
|
30
|
+
gave up. Only the connection Active Record migrates with is changed, only
|
|
31
|
+
while it migrates, and only for a database whose `migrations_paths` are
|
|
32
|
+
this gem's; the app's requests keep failing fast. It applies with
|
|
33
|
+
Railwatch disabled, which is how an entrypoint migrates.
|
|
34
|
+
`RAILWATCH_MIGRATION_BUSY_TIMEOUT` (`c.migration_busy_timeout`, seconds)
|
|
35
|
+
sets it; keep it inside the deploy's health-check window.
|
|
36
|
+
|
|
9
37
|
## 0.8.6 (2026-10-06)
|
|
10
38
|
|
|
11
39
|
- An install upgrading from 0.5.0 or older can migrate again. 0.5.1
|
|
@@ -216,7 +216,7 @@ module Railwatch
|
|
|
216
216
|
when "mail" then child(Telemetry::Mail, rec, %w[mailer subject to cc bcc attachments delivery_method perform_deliveries duration failed message_id])
|
|
217
217
|
when "broadcast" then child(Telemetry::Broadcast, rec, %w[kind stream channel action bytes duration failed])
|
|
218
218
|
when "notification" then child(Telemetry::Notification, rec, %w[notifier delivery_method channel duration failed])
|
|
219
|
-
when "outgoing_request" then child(Telemetry::OutgoingRequest, rec, %w[host method url duration status_code request_size response_size error source response_body])
|
|
219
|
+
when "outgoing_request" then child(Telemetry::OutgoingRequest, rec, %w[host method url duration status_code request_size response_size error source response_body in_transaction])
|
|
220
220
|
when "llm_call" then child(Telemetry::LlmCall, rec, %w[operation provider model response_model tool_name duration status error
|
|
221
221
|
streaming message_count tool_count input_tokens output_tokens cache_read_tokens cache_write_tokens thinking_tokens
|
|
222
222
|
cost_nanos workflow_id workflow_name workflow_step_id workflow_step_name workflow_step_parent_id prompt completion
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# An outgoing HTTP call made while the execution held a database transaction
|
|
4
|
+
# open: the transaction (and on SQLite with BEGIN IMMEDIATE, the writer lock)
|
|
5
|
+
# stays held for the whole round trip. Same meaning as queries.in_transaction.
|
|
6
|
+
class AddInTransactionToOutgoingRequests < ActiveRecord::Migration[8.1]
|
|
7
|
+
def change
|
|
8
|
+
add_column :outgoing_requests, :in_transaction, :boolean, default: false
|
|
9
|
+
end
|
|
10
|
+
end
|
data/docs/records.md
CHANGED
|
@@ -500,6 +500,7 @@ never double-recorded.
|
|
|
500
500
|
| `response_size` | Bytes, from `Content-Length` or body size. |
|
|
501
501
|
| `error` | `"Class: message"`, truncated to 255 chars, if the request raised. |
|
|
502
502
|
| `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). |
|
|
503
|
+
| `in_transaction` | True when the execution had a database transaction (or savepoint) of its own open for this call, so the transaction and its locks were held for the whole round trip and a rollback cannot undo what the call did. Only transactions the execution began count: one already open when it started, such as a test's transactional fixture, does not. Active Record begins a transaction at its first statement, so a `transaction do` block that has not yet run one is not open. A job performed inline inherits its parent's. The `have_railwatch_http_in_transaction` matcher fails a spec on it. |
|
|
503
504
|
| `source` | App-code call site (Net::HTTP path only). |
|
|
504
505
|
|
|
505
506
|
### `llm_call`
|
data/docs/testing.md
CHANGED
|
@@ -109,11 +109,26 @@ expect { SyncCustomers.run }.to have_railwatch_outgoing_requests(at_most: 1)
|
|
|
109
109
|
Same bounds as `have_railwatch_queries`. Failures list the method and URL of
|
|
110
110
|
every request the block made.
|
|
111
111
|
|
|
112
|
+
### `have_railwatch_http_in_transaction`
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
expect { post "/orders", params: }.not_to have_railwatch_http_in_transaction
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Railwatch's version of [Isolator](https://github.com/palkan/isolator): fails
|
|
119
|
+
when the block makes an outgoing HTTP call while a database transaction it
|
|
120
|
+
opened is still open. The transaction, and every lock it holds, waits on the
|
|
121
|
+
other service, and rolling back cannot undo what the call did. The negated
|
|
122
|
+
failure message lists each such call with the app line that made it. Only
|
|
123
|
+
transactions opened inside the block count, so a suite's transactional
|
|
124
|
+
fixtures never trip it.
|
|
125
|
+
|
|
112
126
|
## Minitest assertions
|
|
113
127
|
|
|
114
128
|
```ruby
|
|
115
129
|
assert_railwatch_queries(at_most: 5) { OrderSummary.new(order).to_h }
|
|
116
130
|
refute_railwatch_n_plus_one { get widgets_url }
|
|
131
|
+
refute_railwatch_http_in_transaction { post orders_url, params: }
|
|
117
132
|
assert_railwatch_span("checkout.total") { Checkout.new(cart).total }
|
|
118
133
|
```
|
|
119
134
|
|
|
@@ -76,7 +76,7 @@ module Railwatch
|
|
|
76
76
|
:buffer_size, :buffer_bytes, :execution_buffer_bytes, :batch_bytes,
|
|
77
77
|
:backpressure,
|
|
78
78
|
:flush_interval, :flush_threshold,
|
|
79
|
-
:connect_timeout, :timeout, :shutdown_timeout,
|
|
79
|
+
:connect_timeout, :timeout, :shutdown_timeout, :migration_busy_timeout,
|
|
80
80
|
:slow_query_threshold_ms, :n_plus_one_threshold,
|
|
81
81
|
:max_view_renders_per_execution, :ignored_cache_key_prefixes,
|
|
82
82
|
:beacon_enabled, :beacon_rate_limit, :beacon_global_rate_limit, :beacon_allowed_origins,
|
|
@@ -187,6 +187,13 @@ module Railwatch
|
|
|
187
187
|
@connect_timeout = env_float("RAILWATCH_CONNECT_TIMEOUT", 1.0)
|
|
188
188
|
@timeout = env_float("RAILWATCH_TIMEOUT", 3.0)
|
|
189
189
|
@shutdown_timeout = env_float("RAILWATCH_SHUTDOWN_TIMEOUT", 2.0)
|
|
190
|
+
# Seconds a migration of the railwatch databases waits for SQLite's
|
|
191
|
+
# write lock (Patches::MigrationBusyTimeout). A deploy migrates while
|
|
192
|
+
# the old release's writer is still committing to the same file; the
|
|
193
|
+
# database's own `timeout` (5 s) is too short to get between its
|
|
194
|
+
# transactions when the disk is busy. Kept under a typical health-check
|
|
195
|
+
# window (Kamal's deploy_timeout defaults to 30 s and is often 90 s).
|
|
196
|
+
@migration_busy_timeout = env_float("RAILWATCH_MIGRATION_BUSY_TIMEOUT", 60.0)
|
|
190
197
|
@slow_query_threshold_ms = env_float("RAILWATCH_SLOW_QUERY_MS", 5.0)
|
|
191
198
|
@n_plus_one_threshold = env_int("RAILWATCH_N_PLUS_ONE_THRESHOLD", 5)
|
|
192
199
|
@max_view_renders_per_execution = 20
|
data/lib/railwatch/execution.rb
CHANGED
|
@@ -132,6 +132,7 @@ module Railwatch
|
|
|
132
132
|
@record_limit = @failure_context ? Railwatch.config.failure_context : MAX_RECORDS
|
|
133
133
|
@byte_limit = Railwatch.config.execution_buffer_bytes
|
|
134
134
|
@transaction_statement_counts = Hash.new(0)
|
|
135
|
+
@open_transactions = 0
|
|
135
136
|
@allocations_start = GC.stat(:total_allocated_objects)
|
|
136
137
|
@gc_time_start = GC.stat(:time) if GC_TIME_SUPPORTED
|
|
137
138
|
end
|
|
@@ -306,6 +307,24 @@ module Railwatch
|
|
|
306
307
|
@transaction_statement_counts.delete(transaction_object_id) || 0
|
|
307
308
|
end
|
|
308
309
|
|
|
310
|
+
# Database transactions (and savepoints) this execution has BEGUN and not
|
|
311
|
+
# yet finished, so an outgoing HTTP call can say whether it held one open.
|
|
312
|
+
# Only transactions opened during the execution count: a test's
|
|
313
|
+
# transactional fixture is already open when the execution starts, and
|
|
314
|
+
# flagging every request spec would make the signal useless. A nested
|
|
315
|
+
# execution (perform_now inside a request) inherits its parent's.
|
|
316
|
+
def transaction_opened
|
|
317
|
+
@open_transactions += 1
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
def transaction_closed
|
|
321
|
+
@open_transactions -= 1 if @open_transactions.positive?
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
def in_transaction?
|
|
325
|
+
@open_transactions.positive? || (parent_execution&.in_transaction? || false)
|
|
326
|
+
end
|
|
327
|
+
|
|
309
328
|
def duration
|
|
310
329
|
Clock.micros_since(@started_mono)
|
|
311
330
|
end
|
data/lib/railwatch/faraday.rb
CHANGED
|
@@ -53,7 +53,8 @@ module Railwatch
|
|
|
53
53
|
url: Record.url_without_sensitive_components(url, limit: 2048),
|
|
54
54
|
duration: Clock.micros_since(start), status_code: env.status.to_i,
|
|
55
55
|
error: error && "#{error.class}: #{error.message}"[0, 255],
|
|
56
|
-
response_body: response_body(env)
|
|
56
|
+
response_body: response_body(env),
|
|
57
|
+
in_transaction: Railwatch.execution&.in_transaction? || false)
|
|
57
58
|
end
|
|
58
59
|
|
|
59
60
|
# Faraday threads one Env through the whole stack, and the adapter
|
data/lib/railwatch/minitest.rb
CHANGED
|
@@ -32,6 +32,13 @@ module Railwatch
|
|
|
32
32
|
"detected:#{SpecHelper.n_plus_one_lines(n_plus_ones)}"
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
+
def refute_railwatch_http_in_transaction(&block)
|
|
36
|
+
offending = railwatch_capture(&block).select { |r| r[:t] == "outgoing_request" && r[:in_transaction] }
|
|
37
|
+
assert offending.empty?,
|
|
38
|
+
"Expected no outgoing HTTP requests inside a database transaction, but the block made " \
|
|
39
|
+
"#{offending.size}:#{SpecHelper.outgoing_lines(offending)}"
|
|
40
|
+
end
|
|
41
|
+
|
|
35
42
|
def assert_railwatch_span(name, &block)
|
|
36
43
|
spans = railwatch_capture(&block).select { |r| r[:t] == "span" }
|
|
37
44
|
assert spans.any? { |s| s[:name] == name },
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railwatch
|
|
4
|
+
module Patches
|
|
5
|
+
# Gives the migration connection to a Railwatch database a long SQLite
|
|
6
|
+
# busy timeout, for the migration only.
|
|
7
|
+
#
|
|
8
|
+
# A deploy migrates while the previous release is still serving, and on
|
|
9
|
+
# an embedded install that release's writer process is committing
|
|
10
|
+
# telemetry to the same file the whole time. Each of its transactions
|
|
11
|
+
# takes SQLite's single write lock: about a second on a warm 15 GB file
|
|
12
|
+
# (the export sender's claim scans the destination's whole delivery
|
|
13
|
+
# history), several seconds while a deploy's image build has the disk.
|
|
14
|
+
# The migration connection waited only the database's configured
|
|
15
|
+
# `timeout` -- 5 s in the generated database.yml -- and then raised
|
|
16
|
+
# SQLite3::BusyException, which db:prepare reports as a failed migration
|
|
17
|
+
# and the container entrypoint as a failed boot. That is how a deploy of
|
|
18
|
+
# rebulk-system crash-looped eight times without one migration running.
|
|
19
|
+
#
|
|
20
|
+
# The configured timeout is the right one for the app's own requests,
|
|
21
|
+
# which should fail fast rather than queue behind ingest. A migration is
|
|
22
|
+
# the opposite: it runs once, before the server binds, and waiting is the
|
|
23
|
+
# only way through. So only the connection Active Record migrates with is
|
|
24
|
+
# changed, only while it migrates, and only for a database whose
|
|
25
|
+
# migrations_paths include this gem's. The host's own databases keep
|
|
26
|
+
# their timeouts.
|
|
27
|
+
module MigrationBusyTimeout
|
|
28
|
+
def migrate(*, **)
|
|
29
|
+
pool = migration_connection_pool
|
|
30
|
+
raw = Railwatch::Patches::MigrationBusyTimeout.lengthen(pool)
|
|
31
|
+
super
|
|
32
|
+
ensure
|
|
33
|
+
Railwatch::Patches::MigrationBusyTimeout.restore(pool, raw) if raw
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class << self
|
|
37
|
+
# The raw SQLite connection whose timeout was lengthened, or nil when
|
|
38
|
+
# this is not a Railwatch database (or not SQLite).
|
|
39
|
+
def lengthen(pool)
|
|
40
|
+
return unless railwatch_database?(pool.db_config)
|
|
41
|
+
|
|
42
|
+
raw = pool.lease_connection.raw_connection
|
|
43
|
+
return unless raw.respond_to?(:busy_handler_timeout=)
|
|
44
|
+
|
|
45
|
+
raw.busy_handler_timeout = (Railwatch.config.migration_busy_timeout.to_f * 1000).to_i
|
|
46
|
+
raw
|
|
47
|
+
rescue StandardError => e
|
|
48
|
+
Railwatch.debug { "migration busy timeout not applied: #{e.class}: #{e.message}" }
|
|
49
|
+
nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Back to what database.yml says, so a connection that outlives the
|
|
53
|
+
# migration (db:migrate in a long-lived process, a spec) behaves as
|
|
54
|
+
# configured afterwards.
|
|
55
|
+
def restore(pool, raw)
|
|
56
|
+
timeout = pool.db_config.configuration_hash[:timeout]
|
|
57
|
+
if timeout
|
|
58
|
+
raw.busy_handler_timeout = Integer(timeout)
|
|
59
|
+
else
|
|
60
|
+
raw.busy_handler(nil)
|
|
61
|
+
end
|
|
62
|
+
rescue StandardError => e
|
|
63
|
+
Railwatch.debug { "migration busy timeout not restored: #{e.class}: #{e.message}" }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def railwatch_database?(db_config)
|
|
67
|
+
ours = %i[railwatch railwatch_telemetry].map { |name| File.expand_path(Railwatch.migrations_path(name)) }
|
|
68
|
+
Array(db_config.migrations_paths).any? { |path| ours.include?(File.expand_path(path.to_s, Rails.root.to_s)) }
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -68,6 +68,7 @@ module Railwatch
|
|
|
68
68
|
response_size: response ? (response["Content-Length"]&.to_i || response.body&.bytesize rescue nil) : nil,
|
|
69
69
|
error: error && "#{error.class}: #{error.message}"[0, 255],
|
|
70
70
|
response_body: response_body(response, error),
|
|
71
|
+
in_transaction: Railwatch.execution&.in_transaction? || false,
|
|
71
72
|
source: Backtrace.caller_location(skip: 4))
|
|
72
73
|
rescue StandardError => e
|
|
73
74
|
Railwatch.debug { "outgoing request record failed: #{e.message}" }
|
data/lib/railwatch/patches.rb
CHANGED
|
@@ -4,6 +4,7 @@ require "railwatch/patches/net_http"
|
|
|
4
4
|
require "railwatch/patches/rake_task"
|
|
5
5
|
require "railwatch/patches/runner_command"
|
|
6
6
|
require "railwatch/patches/inertia"
|
|
7
|
+
require "railwatch/patches/migration_busy_timeout"
|
|
7
8
|
|
|
8
9
|
module Railwatch
|
|
9
10
|
module Patches
|
|
@@ -22,6 +23,16 @@ module Railwatch
|
|
|
22
23
|
# From Rails::Engine#load_tasks, which has already required rake.
|
|
23
24
|
def install_rake_task!
|
|
24
25
|
::Rake::Task.prepend(RakeTask) unless ::Rake::Task.ancestors.include?(RakeTask)
|
|
26
|
+
install_migration_busy_timeout!
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# db:prepare and db:migrate run as rake tasks, so this goes in with the
|
|
30
|
+
# rake patch. Unlike it, this one is not gated on Railwatch.enabled?: a
|
|
31
|
+
# container entrypoint migrates with Railwatch switched off, and that is
|
|
32
|
+
# exactly the migration that has to wait out the old release's writer.
|
|
33
|
+
def install_migration_busy_timeout!
|
|
34
|
+
tasks = ::ActiveRecord::Tasks::DatabaseTasks.singleton_class
|
|
35
|
+
tasks.prepend(MigrationBusyTimeout) unless tasks.ancestors.include?(MigrationBusyTimeout)
|
|
25
36
|
end
|
|
26
37
|
|
|
27
38
|
# From Rails::Application#load_runner, which RunnerCommand#perform calls
|
data/lib/railwatch/rspec.rb
CHANGED
|
@@ -136,4 +136,27 @@ RSpec::Matchers.define :have_railwatch_outgoing_requests do |bounds|
|
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
138
|
|
|
139
|
+
# Isolator's check: an HTTP call made while a database transaction this
|
|
140
|
+
# block opened is still open holds that transaction (and its locks) for the
|
|
141
|
+
# whole round trip, and a rollback cannot undo what the call did.
|
|
142
|
+
RSpec::Matchers.define :have_railwatch_http_in_transaction do
|
|
143
|
+
supports_block_expectations
|
|
144
|
+
|
|
145
|
+
match do |block|
|
|
146
|
+
@offending = railwatch_capture(&block).select { |r| r[:t] == "outgoing_request" && r[:in_transaction] }
|
|
147
|
+
@offending.any?
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
description { "make an outgoing HTTP request inside a database transaction" }
|
|
151
|
+
|
|
152
|
+
failure_message do
|
|
153
|
+
"expected the block to make an outgoing HTTP request inside a database transaction, but it made none"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
failure_message_when_negated do
|
|
157
|
+
"expected no outgoing HTTP requests inside a database transaction, but the block made " \
|
|
158
|
+
"#{@offending.size}:#{Railwatch::SpecHelper.outgoing_lines(@offending)}"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
139
162
|
RSpec.configure { |config| config.include Railwatch::SpecHelper } if defined?(RSpec.configure)
|
|
@@ -115,7 +115,7 @@ module Railwatch
|
|
|
115
115
|
end
|
|
116
116
|
|
|
117
117
|
def outgoing_lines(records)
|
|
118
|
-
lines(records) { |r| "#{r[:method]} #{truncate(r[:url])}" }
|
|
118
|
+
lines(records) { |r| "#{r[:method]} #{truncate(r[:url])}#{" at #{r[:source]}" if r[:source]}" }
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def exception_lines(records)
|
|
@@ -148,8 +148,16 @@ module Railwatch
|
|
|
148
148
|
end
|
|
149
149
|
end
|
|
150
150
|
|
|
151
|
+
# Fired when a transaction or savepoint actually sends BEGIN (Active
|
|
152
|
+
# Record opens them lazily), paired with transaction.active_record
|
|
153
|
+
# below on commit, rollback, or restart.
|
|
154
|
+
subscribe_payload("start_transaction.active_record") do |_payload|
|
|
155
|
+
execution&.transaction_opened
|
|
156
|
+
end
|
|
157
|
+
|
|
151
158
|
subscribe("transaction.active_record") do |event|
|
|
152
159
|
exe = execution
|
|
160
|
+
exe&.transaction_closed
|
|
153
161
|
exe&.count(:transactions)
|
|
154
162
|
p = event.payload
|
|
155
163
|
# Taken (and removed) whether or not a record is built: the
|
data/lib/railwatch/version.rb
CHANGED
data/lib/railwatch.rb
CHANGED
|
@@ -389,7 +389,8 @@ module Railwatch
|
|
|
389
389
|
host = (URI(url.to_s).host rescue nil)
|
|
390
390
|
record(:outgoing_request, group: Record.group_hash(host, method.to_s.upcase),
|
|
391
391
|
timestamp: started_at, host: host, method: method.to_s.upcase,
|
|
392
|
-
url: url.to_s[0, 2048], duration: Clock.micros_since(start), status_code: result.status.to_i
|
|
392
|
+
url: url.to_s[0, 2048], duration: Clock.micros_since(start), status_code: result.status.to_i,
|
|
393
|
+
in_transaction: execution&.in_transaction? || false)
|
|
393
394
|
end
|
|
394
395
|
result
|
|
395
396
|
end
|
data/llms.txt
CHANGED
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
- [Embedded mode](docs/embedded.md): the default install — the dashboard inside the app, its two SQLite databases, dashboard authentication, the Puma writer process, maintenance without a job worker, disk reclaim, and optional export to Railwatch Cloud.
|
|
27
27
|
- [Configuration](docs/configuration.md): every configuration option and its `RAILWATCH_*` environment variable, plus the full public facade, redaction, rejection, transport and buffering behaviour, and the rake tasks.
|
|
28
28
|
- [Record types](docs/records.md): every record type the gem ships and every attribute on it, including RubyLLM calls (tokens, cost, finish reason, tool calls, workflows), sourced from the code that builds it.
|
|
29
|
-
- [Testing](docs/testing.md): the RSpec and Minitest matchers (`have_railwatch_queries`, `have_railwatch_n_plus_one`, `record_railwatch_span`, `record_railwatch_exception`, `have_railwatch_outgoing_requests`) and a CI performance-gate recipe.
|
|
29
|
+
- [Testing](docs/testing.md): the RSpec and Minitest matchers (`have_railwatch_queries`, `have_railwatch_n_plus_one`, `record_railwatch_span`, `record_railwatch_exception`, `have_railwatch_outgoing_requests`, `have_railwatch_http_in_transaction`) and a CI performance-gate recipe.
|
|
30
30
|
- [Production source maps](docs/source-maps.md): hidden Vite maps, private upload before publishing assets, safe opt-in deletion, resolved browser stacks and default issue grouping.
|
|
31
31
|
- [AI assistants and MCP](docs/ai-and-mcp.md): Railwatch Cloud's MCP endpoint, how to get a token, paste-ready client configuration for Claude Code, Claude Desktop, Cursor, VS Code, and Zed, and every tool, prompt, and resource the server exposes.
|
|
32
32
|
- [Replacing Sentry](docs/replacing-sentry.md): a step-by-step migration — removing the gems, porting each option, rewriting each call site, breadcrumbs, spans, profiling, attachments, `before_send`, fingerprints, and release health.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: railwatch
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cole Robertson
|
|
@@ -406,6 +406,7 @@ files:
|
|
|
406
406
|
- db/railwatch_telemetry_migrate/20260922000000_add_covering_indexes_for_dashboard_aggregates.rb
|
|
407
407
|
- db/railwatch_telemetry_migrate/20260923000000_add_people_index.rb
|
|
408
408
|
- db/railwatch_telemetry_migrate/20260925000000_add_tenant_summary_index.rb
|
|
409
|
+
- db/railwatch_telemetry_migrate/20261008000000_add_in_transaction_to_outgoing_requests.rb
|
|
409
410
|
- docs/ai-and-mcp.md
|
|
410
411
|
- docs/configuration.md
|
|
411
412
|
- docs/embedded.md
|
|
@@ -453,6 +454,7 @@ files:
|
|
|
453
454
|
- lib/railwatch/minitest.rb
|
|
454
455
|
- lib/railwatch/patches.rb
|
|
455
456
|
- lib/railwatch/patches/inertia.rb
|
|
457
|
+
- lib/railwatch/patches/migration_busy_timeout.rb
|
|
456
458
|
- lib/railwatch/patches/net_http.rb
|
|
457
459
|
- lib/railwatch/patches/rake_task.rb
|
|
458
460
|
- lib/railwatch/patches/runner_command.rb
|