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
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Railwatch
|
|
4
|
+
# Deploy metadata gathered for railwatch:deploy. The commit list is what lets
|
|
5
|
+
# the platform show a diff of what actually shipped between two deploys; it
|
|
6
|
+
# comes back empty inside an app container, which has the code but not the
|
|
7
|
+
# git history (the Kamal post-deploy hook runs on the deployer, which does).
|
|
8
|
+
module DeployMetadata
|
|
9
|
+
FORMAT = "%H%x1f%an%x1f%s%x1f%cI"
|
|
10
|
+
MAX_COMMITS = 50
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# Newest first, capped at MAX_COMMITS.
|
|
15
|
+
def commits
|
|
16
|
+
return [] unless File.exist?(".git")
|
|
17
|
+
|
|
18
|
+
git_log.each_line.filter_map do |line|
|
|
19
|
+
sha, author, message, at = line.chomp.split("\x1f")
|
|
20
|
+
{ sha: sha, author: author, message: message, at: at } unless sha.nil? || sha.empty?
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def git_log
|
|
25
|
+
`git log -n #{MAX_COMMITS} --format='#{FORMAT}' 2>/dev/null`
|
|
26
|
+
rescue StandardError
|
|
27
|
+
""
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Where this install's platform lives, derived from config.ingest_url:
|
|
32
|
+
# railwatch:token and railwatch:mcp point at the same host the gem already
|
|
33
|
+
# ships to, so a self-hosted app never gets told to visit railwatch.rebulk.com.
|
|
34
|
+
class Endpoints
|
|
35
|
+
HOSTED_HOST = "railwatch.rebulk.com"
|
|
36
|
+
|
|
37
|
+
def initialize(config)
|
|
38
|
+
@uri = URI.parse(config.ingest_url)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def host_url
|
|
42
|
+
port = @uri.port && @uri.port != @uri.default_port ? ":#{@uri.port}" : ""
|
|
43
|
+
"#{@uri.scheme}://#{@uri.host}#{port}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def url(path)
|
|
47
|
+
"#{host_url}#{path}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self_hosted?
|
|
51
|
+
@uri.host != HOSTED_HOST
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
namespace :railwatch do
|
|
57
|
+
desc "Upload build source maps privately: railwatch:sourcemaps[directory,delete] (directory defaults to public; delete defaults to false)"
|
|
58
|
+
task :sourcemaps, [ :directory, :delete ] => :environment do |_task, args|
|
|
59
|
+
require "railwatch/source_maps"
|
|
60
|
+
directory = args[:directory] || ENV["RAILWATCH_SOURCEMAPS_DIR"] || "public"
|
|
61
|
+
delete = (args[:delete] || ENV["RAILWATCH_SOURCEMAPS_DELETE"]) == "true"
|
|
62
|
+
count = Railwatch::SourceMaps.new(Railwatch.config).upload(directory: directory, delete: delete)
|
|
63
|
+
puts "Uploaded #{count} source maps for #{Railwatch.config.deploy}#{' and deleted acknowledged files' if delete}"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
desc "Check that the app can reach Railwatch with the configured token"
|
|
67
|
+
task status: :environment do
|
|
68
|
+
unless Railwatch.config.token.present?
|
|
69
|
+
abort "RAILWATCH_TOKEN is not set"
|
|
70
|
+
end
|
|
71
|
+
transport = Railwatch::Transport::Http.new(Railwatch.config)
|
|
72
|
+
if transport.ping
|
|
73
|
+
puts "Railwatch OK: #{Railwatch.config.ingest_url} (deploy=#{Railwatch.config.deploy || 'unset'}, server=#{Railwatch.config.server})"
|
|
74
|
+
else
|
|
75
|
+
abort "Railwatch unreachable at #{Railwatch.config.ingest_url}"
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
desc "Check a Railwatch install end to end: token, ingest, middleware, routes, deploy, hooks, test helpers"
|
|
80
|
+
task doctor: :environment do
|
|
81
|
+
config = Railwatch.config
|
|
82
|
+
blockers = []
|
|
83
|
+
check = lambda do |ok, label, detail, fatal: false|
|
|
84
|
+
puts "#{ok ? "✓" : "✗"} #{label}: #{detail}"
|
|
85
|
+
blockers << label if !ok && fatal
|
|
86
|
+
ok
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
token = config.token.to_s
|
|
90
|
+
check.call(!token.empty?, "token",
|
|
91
|
+
token.empty? ? "RAILWATCH_TOKEN is not set" : Railwatch::SecretSafety.token_preview(token),
|
|
92
|
+
fatal: true)
|
|
93
|
+
|
|
94
|
+
exposed_token_files = Railwatch::SecretSafety.tracked_plaintext_token_files(root: Rails.root)
|
|
95
|
+
check.call(exposed_token_files.empty?, "token storage",
|
|
96
|
+
exposed_token_files.empty? ? "no tracked plaintext Railwatch token found" :
|
|
97
|
+
"plaintext token found in tracked file(s): #{exposed_token_files.join(', ')}",
|
|
98
|
+
fatal: true)
|
|
99
|
+
|
|
100
|
+
ingest = URI.parse(config.ingest_url) rescue nil
|
|
101
|
+
check.call(ingest.is_a?(URI::HTTP), "ingest url", config.ingest_url)
|
|
102
|
+
|
|
103
|
+
transport_security = config.ingest_url_allowed?
|
|
104
|
+
check.call(transport_security, "ingest transport security",
|
|
105
|
+
if ingest&.scheme == "https"
|
|
106
|
+
"HTTPS with certificate verification"
|
|
107
|
+
elsif transport_security
|
|
108
|
+
"plain HTTP explicitly allowed for #{ingest.host}"
|
|
109
|
+
else
|
|
110
|
+
"plain HTTP refused; use HTTPS or set RAILWATCH_ALLOW_HTTP=true"
|
|
111
|
+
end)
|
|
112
|
+
|
|
113
|
+
check.call(Railwatch::Transport::Http.new(config).ping, "ingest reachable",
|
|
114
|
+
"GET #{URI.join(config.ingest_url, '/ingest/ping')}", fatal: true)
|
|
115
|
+
|
|
116
|
+
middleware = Rails.application.middleware.map(&:name)
|
|
117
|
+
position = middleware.index("Railwatch::Middleware::Request")
|
|
118
|
+
check.call(!position.nil?, "request middleware",
|
|
119
|
+
position ? "Railwatch::Middleware::Request at position #{position}" : "not in the middleware stack")
|
|
120
|
+
|
|
121
|
+
mount = Rails.application.routes.routes.find { |r| r.app.respond_to?(:app) && r.app.app == Railwatch::Engine }
|
|
122
|
+
beacon = Railwatch::Engine.routes.routes.any? { |r| r.defaults[:controller] == "railwatch/beacon" && r.defaults[:action] == "create" }
|
|
123
|
+
check.call(!mount.nil? && beacon, "engine mounted",
|
|
124
|
+
mount ? "POST #{mount.path.spec}/beacon -> railwatch/beacon#create" : %(add `mount Railwatch::Engine, at: "/railwatch"` to config/routes.rb))
|
|
125
|
+
|
|
126
|
+
source = config.deploy_source
|
|
127
|
+
if source == "config/initializers/railwatch.rb"
|
|
128
|
+
detected_source = nil
|
|
129
|
+
detected = Railwatch::ReleaseDetector.detect(project_root: Rails.root) { |found| detected_source = found }
|
|
130
|
+
source = detected_source if detected == config.deploy
|
|
131
|
+
end
|
|
132
|
+
check.call(config.deploy.present?, "deploy",
|
|
133
|
+
config.deploy.present? ? "#{config.deploy} (from #{source})" : "none: set RAILWATCH_DEPLOY")
|
|
134
|
+
|
|
135
|
+
check.call(true, "sample rates", config.sample.map { |kind, rate| "#{kind}=#{rate}" }.join(" "))
|
|
136
|
+
check.call(true, "ignored record types", config.ignore.empty? ? "none" : config.ignore.join(", "))
|
|
137
|
+
|
|
138
|
+
check.call(true, "interactive sessions",
|
|
139
|
+
"console=#{config.capture_console ? 'captured' : 'quiet'} " \
|
|
140
|
+
"runner scratch paths=#{config.interactive_runner_paths.join(' ')} " \
|
|
141
|
+
"(a typed/piped runner ships its command record, not its exception)")
|
|
142
|
+
|
|
143
|
+
hook = Rails.root.join(".kamal/hooks/post-deploy")
|
|
144
|
+
check.call(hook.exist? && hook.read.include?("railwatch"), "kamal post-deploy hook",
|
|
145
|
+
hook.exist? ? hook.to_s : "not found (only needed when deploying with Kamal)")
|
|
146
|
+
|
|
147
|
+
client = Rails.root.join("app/frontend/lib/railwatch.ts")
|
|
148
|
+
check.call(client.exist?, "browser client",
|
|
149
|
+
client.exist? ? "#{client} (call startRailwatch() from your Inertia entrypoint)" : "not found (only needed for Inertia visit timing)")
|
|
150
|
+
|
|
151
|
+
# The client file existing is not the same as it running: startRailwatch()
|
|
152
|
+
# has to be called from an entrypoint or no visit is ever timed.
|
|
153
|
+
entrypoints = Dir[Rails.root.join("app/frontend/entrypoints/*")].select { |f| File.file?(f) }
|
|
154
|
+
started = entrypoints.select { |f| File.read(f).include?("startRailwatch") }
|
|
155
|
+
check.call(started.any?, "browser client imported",
|
|
156
|
+
if started.any?
|
|
157
|
+
started.map { |f| Pathname.new(f).relative_path_from(Rails.root).to_s }.join(", ")
|
|
158
|
+
elsif entrypoints.any?
|
|
159
|
+
"no entrypoint in app/frontend/entrypoints calls startRailwatch()"
|
|
160
|
+
else
|
|
161
|
+
"no app/frontend/entrypoints (only needed for Inertia visit timing)"
|
|
162
|
+
end)
|
|
163
|
+
|
|
164
|
+
backend = Railwatch::Profiler.backend
|
|
165
|
+
check.call(!backend.nil?, "profiler backend",
|
|
166
|
+
backend || %(none -- add `gem "vernier"` (Ruby >= 3.2) or `gem "stackprof"` to profile slow executions))
|
|
167
|
+
|
|
168
|
+
rails_helper = Rails.root.join("spec/rails_helper.rb")
|
|
169
|
+
test_helper = Rails.root.join("test/test_helper.rb")
|
|
170
|
+
wired = if rails_helper.exist? && rails_helper.read.include?("railwatch/rspec")
|
|
171
|
+
%(spec/rails_helper.rb requires "railwatch/rspec")
|
|
172
|
+
elsif test_helper.exist? && test_helper.read.include?("railwatch/minitest")
|
|
173
|
+
%(test/test_helper.rb requires "railwatch/minitest")
|
|
174
|
+
end
|
|
175
|
+
check.call(!wired.nil?, "test matchers", wired || %(add `require "railwatch/rspec"` (or "railwatch/minitest") -- see docs/testing.md))
|
|
176
|
+
|
|
177
|
+
abort "\nrailwatch:doctor failed: #{blockers.join(', ')}" if blockers.any?
|
|
178
|
+
puts "\nRailwatch is wired up."
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
desc "Print where to create an ingest token for this app's Railwatch platform"
|
|
182
|
+
task token: :environment do
|
|
183
|
+
base = Railwatch::Endpoints.new(Railwatch.config)
|
|
184
|
+
puts <<~TEXT
|
|
185
|
+
Railwatch platform: #{base.host_url}
|
|
186
|
+
|
|
187
|
+
1. Sign in (or sign up) at #{base.url("/dashboard")}
|
|
188
|
+
2. New application, then New environment (production, staging, ...)
|
|
189
|
+
3. The environment's token (lt_...) is shown once, right after it is created.
|
|
190
|
+
|
|
191
|
+
Then set it where this app reads its environment:
|
|
192
|
+
|
|
193
|
+
RAILWATCH_TOKEN=lt_...#{"\n RAILWATCH_INGEST_URL=#{base.host_url}" if base.self_hosted?}
|
|
194
|
+
|
|
195
|
+
With Kamal: bin/rails generate railwatch:install --prompt-token --kamal-secrets
|
|
196
|
+
Verify: bin/rails railwatch:doctor
|
|
197
|
+
|
|
198
|
+
An existing environment's token can be rotated from its Settings page;
|
|
199
|
+
the prefix shown in the UI is the first 12 characters of the token.
|
|
200
|
+
TEXT
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
desc "Print ready-to-paste MCP client configuration for this app's Railwatch platform"
|
|
204
|
+
task mcp: :environment do
|
|
205
|
+
base = Railwatch::Endpoints.new(Railwatch.config)
|
|
206
|
+
mcp = base.url("/mcp")
|
|
207
|
+
token = "lnt_your_token_here"
|
|
208
|
+
puts <<~TEXT
|
|
209
|
+
Railwatch MCP server: #{mcp}
|
|
210
|
+
|
|
211
|
+
An MCP token is per person, not per app: Settings -> Profile -> "API & MCP
|
|
212
|
+
token" at #{base.url("/settings/profile")}. It starts with lnt_ and is
|
|
213
|
+
shown once. Everything below is scoped to whatever accounts that user
|
|
214
|
+
belongs to.
|
|
215
|
+
|
|
216
|
+
Claude Code
|
|
217
|
+
claude mcp add railwatch --transport http #{mcp} --header "Authorization: Bearer #{token}"
|
|
218
|
+
|
|
219
|
+
Claude Desktop (claude_desktop_config.json) -- via the mcp-remote shim
|
|
220
|
+
{
|
|
221
|
+
"mcpServers": {
|
|
222
|
+
"railwatch": {
|
|
223
|
+
"command": "npx",
|
|
224
|
+
"args": ["-y", "mcp-remote", "#{mcp}", "--header", "Authorization: Bearer #{token}"]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
Cursor (.cursor/mcp.json)
|
|
230
|
+
{
|
|
231
|
+
"mcpServers": {
|
|
232
|
+
"railwatch": {
|
|
233
|
+
"url": "#{mcp}",
|
|
234
|
+
"headers": { "Authorization": "Bearer #{token}" }
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
VS Code (.vscode/mcp.json)
|
|
240
|
+
{
|
|
241
|
+
"servers": {
|
|
242
|
+
"railwatch": {
|
|
243
|
+
"type": "http",
|
|
244
|
+
"url": "#{mcp}",
|
|
245
|
+
"headers": { "Authorization": "Bearer #{token}" }
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
Zed (settings.json) -- via the mcp-remote shim
|
|
251
|
+
{
|
|
252
|
+
"context_servers": {
|
|
253
|
+
"railwatch": {
|
|
254
|
+
"source": "custom",
|
|
255
|
+
"command": "npx",
|
|
256
|
+
"args": ["-y", "mcp-remote", "#{mcp}", "--header", "Authorization: Bearer #{token}"]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
Test it without a client
|
|
262
|
+
curl -sS #{mcp} \\
|
|
263
|
+
-H "Authorization: Bearer #{token}" \\
|
|
264
|
+
-H "Content-Type: application/json" \\
|
|
265
|
+
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
|
|
266
|
+
|
|
267
|
+
What it exposes: tools (applications, issues, slow routes, executions,
|
|
268
|
+
logs, profiles, alerts, query plans, deploys, release health), prompts
|
|
269
|
+
(triage_issue, slow_route, daily_summary), and resources -- including
|
|
270
|
+
Railwatch's own docs at railwatch://docs/<name>. See docs/ai-and-mcp.md.
|
|
271
|
+
TEXT
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
desc "Send deploy metadata to Railwatch: rake railwatch:deploy[ref,name,url]"
|
|
275
|
+
task :deploy, [ :ref, :name, :url ] => :environment do |_t, args|
|
|
276
|
+
deploy = Railwatch.config.deploy or abort "RAILWATCH_DEPLOY (or KAMAL_VERSION) is not set"
|
|
277
|
+
abort "Plain HTTP ingest is disabled; use HTTPS or set RAILWATCH_ALLOW_HTTP=true" unless Railwatch.config.ingest_url_allowed?
|
|
278
|
+
uri = URI.join(Railwatch.config.ingest_url, "/ingest/deploys")
|
|
279
|
+
req = Net::HTTP::Post.new(uri)
|
|
280
|
+
req["Authorization"] = "Bearer #{Railwatch.config.token}"
|
|
281
|
+
req["Content-Type"] = "application/json"
|
|
282
|
+
req.body = JSON.generate(deploy: deploy, ref: args[:ref] || (`git rev-parse HEAD 2>/dev/null`.strip.presence),
|
|
283
|
+
name: args[:name], url: args[:url], server: Railwatch.config.server, timestamp: Time.now.utc.iso8601(6),
|
|
284
|
+
performer: ENV["KAMAL_PERFORMER"], destination: ENV["KAMAL_DESTINATION"],
|
|
285
|
+
service: ENV["KAMAL_SERVICE"], commits: Railwatch::DeployMetadata.commits)
|
|
286
|
+
res = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 10) { |h| h.request(req) }
|
|
287
|
+
res.is_a?(Net::HTTPSuccess) ? puts("Deploy #{deploy} recorded") : abort("Deploy failed: #{res.code} #{res.body}")
|
|
288
|
+
end
|
|
289
|
+
end
|
data/llms.txt
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Railwatch
|
|
2
|
+
|
|
3
|
+
> Railwatch is a Ruby gem that instruments a Rails application end to end —
|
|
4
|
+
> requests, jobs, scheduled tasks, rake/runner commands, database queries and
|
|
5
|
+
> N+1s, exceptions, cache, mail, notifications, broadcasts, outgoing HTTP,
|
|
6
|
+
> Active Storage, view renders, and logs — links every one of them into a
|
|
7
|
+
> single execution tree by `execution_id`/`trace_id`, and ships them to
|
|
8
|
+
> Railwatch Cloud from a background thread. It replaces a separate APM and a
|
|
9
|
+
> separate error tracker with one gem and one configuration block, costs
|
|
10
|
+
> under a millisecond of CPU per request, and never writes to the
|
|
11
|
+
> application's own database. Install is `bundle add railwatch` followed by
|
|
12
|
+
> `bin/rails generate railwatch:install`, which writes the initializer, mounts
|
|
13
|
+
> the beacon engine, adds a Kamal post-deploy hook and the Inertia browser
|
|
14
|
+
> client where the app has them, wires the test matchers, and then runs
|
|
15
|
+
> `bin/rails railwatch:doctor` to print a ✓/✗ line for every piece.
|
|
16
|
+
|
|
17
|
+
## Docs
|
|
18
|
+
|
|
19
|
+
- [README](README.md): what Railwatch is, the install, sampling, spans, profiling, tail sampling, and the Sentry option mapping table.
|
|
20
|
+
- [Getting started](docs/getting-started.md): five-minute install for a Rails 8 app, where the token comes from, the three optional lines, and deploying under Kamal, Docker/Heroku/Render, or no deploy tool at all.
|
|
21
|
+
- [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.
|
|
22
|
+
- [Record types](docs/records.md): every record type the gem ships and every attribute on it, sourced from the code that builds it.
|
|
23
|
+
- [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.
|
|
24
|
+
- [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.
|
|
25
|
+
- [AI assistants and MCP](docs/ai-and-mcp.md): the 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.
|
|
26
|
+
- [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.
|
|
27
|
+
- [Coming from Laravel Nightwatch](docs/replacing-nightwatch.md): the record-type mapping, what "execution" means in Railwatch, sampling parity, and the facade method names in Ruby.
|
|
28
|
+
- [Self-hosting](docs/self-hosting.md): pointing the gem at a self-hosted Railwatch Cloud, creating a token there, and checking the connection.
|
|
29
|
+
- [Troubleshooting](docs/troubleshooting.md): every failure mode paired with the `railwatch:doctor` line it shows up as — no records, doubled scheduled tasks, WebMock in specs, Puma fork, tail-sampling memory, missing profiler, missing deploy marker, Kamal hook, log search, empty tenants.
|
|
30
|
+
- [FAQ](docs/faq.md): overhead numbers and how they are measured, retention, what is redacted by default versus opt-in, SQLite, and what happens when the platform is unreachable.
|
|
31
|
+
|
|
32
|
+
## For coding agents
|
|
33
|
+
|
|
34
|
+
- [AGENTS.md](AGENTS.md): how to install and use Railwatch from inside a Rails app — the facade methods, the spec matchers, `railwatch:doctor`, and the MCP hookup. Duplicated verbatim as [CLAUDE.md](CLAUDE.md).
|
|
35
|
+
|
|
36
|
+
## Optional
|
|
37
|
+
|
|
38
|
+
- [CHANGELOG](CHANGELOG.md): released versions and what changed in each.
|
metadata
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: railwatch
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Cole Robertson
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 2026-09-14 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rails
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '8.1'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '9'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '8.1'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '9'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: base64
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - "~>"
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0.2'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - "~>"
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '0.2'
|
|
46
|
+
description: Railwatch instruments a Rails app end to end (requests, jobs, queries,
|
|
47
|
+
exceptions, cache, mail, broadcasts, outgoing HTTP, logs) and ships linked events
|
|
48
|
+
to Railwatch Cloud.
|
|
49
|
+
email:
|
|
50
|
+
- cole@rebulk.com
|
|
51
|
+
executables: []
|
|
52
|
+
extensions: []
|
|
53
|
+
extra_rdoc_files: []
|
|
54
|
+
files:
|
|
55
|
+
- AGENTS.md
|
|
56
|
+
- CHANGELOG.md
|
|
57
|
+
- MIT-LICENSE
|
|
58
|
+
- README.md
|
|
59
|
+
- app/controllers/railwatch/beacon_controller.rb
|
|
60
|
+
- config/routes.rb
|
|
61
|
+
- docs/ai-and-mcp.md
|
|
62
|
+
- docs/configuration.md
|
|
63
|
+
- docs/faq.md
|
|
64
|
+
- docs/getting-started.md
|
|
65
|
+
- docs/records.md
|
|
66
|
+
- docs/replacing-nightwatch.md
|
|
67
|
+
- docs/replacing-sentry.md
|
|
68
|
+
- docs/security.md
|
|
69
|
+
- docs/self-hosting.md
|
|
70
|
+
- docs/source-maps.md
|
|
71
|
+
- docs/testing.md
|
|
72
|
+
- docs/troubleshooting.md
|
|
73
|
+
- lib/generators/railwatch/install/install_generator.rb
|
|
74
|
+
- lib/generators/railwatch/install/templates/initializer.rb
|
|
75
|
+
- lib/generators/railwatch/install/templates/post-deploy
|
|
76
|
+
- lib/generators/railwatch/install/templates/railwatch.ts
|
|
77
|
+
- lib/railwatch.rb
|
|
78
|
+
- lib/railwatch/attachments.rb
|
|
79
|
+
- lib/railwatch/backtrace.rb
|
|
80
|
+
- lib/railwatch/buffer.rb
|
|
81
|
+
- lib/railwatch/clock.rb
|
|
82
|
+
- lib/railwatch/configuration.rb
|
|
83
|
+
- lib/railwatch/console.rb
|
|
84
|
+
- lib/railwatch/context.rb
|
|
85
|
+
- lib/railwatch/controller_helpers.rb
|
|
86
|
+
- lib/railwatch/current.rb
|
|
87
|
+
- lib/railwatch/engine.rb
|
|
88
|
+
- lib/railwatch/execution.rb
|
|
89
|
+
- lib/railwatch/faraday.rb
|
|
90
|
+
- lib/railwatch/health.rb
|
|
91
|
+
- lib/railwatch/job_tracing.rb
|
|
92
|
+
- lib/railwatch/middleware/request.rb
|
|
93
|
+
- lib/railwatch/minitest.rb
|
|
94
|
+
- lib/railwatch/patches.rb
|
|
95
|
+
- lib/railwatch/patches/inertia.rb
|
|
96
|
+
- lib/railwatch/patches/net_http.rb
|
|
97
|
+
- lib/railwatch/patches/rake_task.rb
|
|
98
|
+
- lib/railwatch/patches/runner_command.rb
|
|
99
|
+
- lib/railwatch/profiler.rb
|
|
100
|
+
- lib/railwatch/record.rb
|
|
101
|
+
- lib/railwatch/redactor.rb
|
|
102
|
+
- lib/railwatch/release_detector.rb
|
|
103
|
+
- lib/railwatch/reporter.rb
|
|
104
|
+
- lib/railwatch/rspec.rb
|
|
105
|
+
- lib/railwatch/sampler.rb
|
|
106
|
+
- lib/railwatch/secret_safety.rb
|
|
107
|
+
- lib/railwatch/sessions.rb
|
|
108
|
+
- lib/railwatch/source_maps.rb
|
|
109
|
+
- lib/railwatch/spec_helper.rb
|
|
110
|
+
- lib/railwatch/sql_normalizer.rb
|
|
111
|
+
- lib/railwatch/subscribers.rb
|
|
112
|
+
- lib/railwatch/subscribers/base.rb
|
|
113
|
+
- lib/railwatch/subscribers/broadcasts.rb
|
|
114
|
+
- lib/railwatch/subscribers/cache.rb
|
|
115
|
+
- lib/railwatch/subscribers/deprecations.rb
|
|
116
|
+
- lib/railwatch/subscribers/exceptions.rb
|
|
117
|
+
- lib/railwatch/subscribers/jobs.rb
|
|
118
|
+
- lib/railwatch/subscribers/logs.rb
|
|
119
|
+
- lib/railwatch/subscribers/mail.rb
|
|
120
|
+
- lib/railwatch/subscribers/notifications.rb
|
|
121
|
+
- lib/railwatch/subscribers/process_info.rb
|
|
122
|
+
- lib/railwatch/subscribers/queries.rb
|
|
123
|
+
- lib/railwatch/subscribers/requests.rb
|
|
124
|
+
- lib/railwatch/subscribers/storage.rb
|
|
125
|
+
- lib/railwatch/subscribers/users.rb
|
|
126
|
+
- lib/railwatch/subscribers/views.rb
|
|
127
|
+
- lib/railwatch/transport/http.rb
|
|
128
|
+
- lib/railwatch/version.rb
|
|
129
|
+
- lib/tasks/railwatch_tasks.rake
|
|
130
|
+
- llms.txt
|
|
131
|
+
homepage: https://railwatch.rebulk.com
|
|
132
|
+
licenses:
|
|
133
|
+
- MIT
|
|
134
|
+
metadata:
|
|
135
|
+
homepage_uri: https://railwatch.rebulk.com
|
|
136
|
+
source_code_uri: https://github.com/Rebulk/railwatch
|
|
137
|
+
changelog_uri: https://github.com/Rebulk/railwatch/blob/main/CHANGELOG.md
|
|
138
|
+
documentation_uri: https://github.com/Rebulk/railwatch/blob/main/README.md
|
|
139
|
+
rubygems_mfa_required: 'true'
|
|
140
|
+
rdoc_options: []
|
|
141
|
+
require_paths:
|
|
142
|
+
- lib
|
|
143
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
144
|
+
requirements:
|
|
145
|
+
- - ">="
|
|
146
|
+
- !ruby/object:Gem::Version
|
|
147
|
+
version: '3.4'
|
|
148
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
149
|
+
requirements:
|
|
150
|
+
- - ">="
|
|
151
|
+
- !ruby/object:Gem::Version
|
|
152
|
+
version: '0'
|
|
153
|
+
requirements: []
|
|
154
|
+
rubygems_version: 3.6.2
|
|
155
|
+
specification_version: 4
|
|
156
|
+
summary: First-class monitoring for Rails applications.
|
|
157
|
+
test_files: []
|