railwatch 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c0518c86cb66b33fcb2f673b39aa4f58036369f5cae2412b93d5df3b0cac00c
4
- data.tar.gz: def95a2ab24ed33f2b1264c59b0b749edf5dac98901816990775f8b53ad4ffbc
3
+ metadata.gz: 27a48b972fe7616fbf46c861f8b0f1a9ffb4d99146ec2121cb05274ea3dee031
4
+ data.tar.gz: 45c879afc12f731e446be6aa43f8a78b61d078a8f5a7cdc6294bd94c4dfe864e
5
5
  SHA512:
6
- metadata.gz: e3113af0a26ac031fab975b465b6f701a302de6df1e23c896cb883fe77c75fd66748ddcb2b4e6b865fc3adf3dc5c26576eb327220cdc5c32284d0a7a44e42535
7
- data.tar.gz: 15e04ae53599352b214a39fab1bc004cdbd264f88596385c1d84393ba0d3357ecd9be43b61f980d5e554e89cafc3f7909a23aa1740427936865b7b6109ae2597
6
+ metadata.gz: 29512ddbb519a85a1334531289be33d59d4ceb3cf0a8c7ef55d3db04ab41ae884adf65b64ddecd40d1b70254ca76ed0ce0bbb38ec03eee0125dbae5a8b7730de
7
+ data.tar.gz: 67d7a5573b95aa032a227a8dc10e440bfaee6681628de90d2948f074ceb15ffb9ea839968984beaaec545d0db161a266ba94e99cd2b3a0908c14c9e98969c2c7
data/AGENTS.md CHANGED
@@ -103,7 +103,7 @@ token at Settings → Profile → "API & MCP token", then:
103
103
 
104
104
  ```sh
105
105
  claude mcp add railwatch --transport http https://railwatch.rebulk.com/mcp \
106
- --header "Authorization: Bearer lnt_your_token_here"
106
+ --header "Authorization: Bearer rwp_your_token_here"
107
107
  ```
108
108
 
109
109
  `bin/rails railwatch:mcp` prints this and the Claude Desktop, Cursor, VS Code, and
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.1 (2026-09-14)
4
+
5
+ - Token prefixes are `rw_` for an environment's ingest token and `rwp_`
6
+ for a personal API and MCP token. They were `lt_` and `lnt_`, Lantern's
7
+ initials, which no longer name anything a user can see. The platform
8
+ authenticates by digest, so tokens minted before this keep working; the
9
+ doctor's plaintext-token scan matches both the old and the new ingest
10
+ prefix.
11
+
3
12
  ## 0.1.0 (2026-09-14)
4
13
 
5
14
  First public release.
data/README.md CHANGED
@@ -15,29 +15,49 @@ bin/rails generate railwatch:install --prompt-token # 2. hidden token input plu
15
15
  bin/rails railwatch:doctor # 3. check every piece is wired up after restart
16
16
  ```
17
17
 
18
- The gem, its Ruby namespace, and its require path share one name:
19
- `railwatch`, `Railwatch::*`, `require "railwatch"`. For a self-hosted
20
- deployment or an unreleased revision, use the Git source instead:
18
+ Getting the token, the generator's flags, and deploying with Kamal,
19
+ Docker, Heroku, or Render are covered in
20
+ [Getting started](docs/getting-started.md). For a self-hosted deployment
21
+ or an unreleased revision, use the Git source instead:
21
22
 
22
23
  ```ruby
23
24
  gem "railwatch", github: "Rebulk/railwatch"
24
25
  ```
25
26
 
26
- The generator writes `config/initializers/railwatch.rb`, mounts the beacon
27
- engine, and — where the app already has them — adds a Kamal `post-deploy`
28
- hook, the Inertia browser client with its `startRailwatch()` call, and
29
- `require "railwatch/rspec"` in `spec/rails_helper.rb`. `railwatch:doctor` prints
30
- a ✓/✗ checklist of all of it and exits non-zero if the token is missing or
31
- the ingest host is unreachable.
32
-
33
- Pass `--prompt-token` (and `--url=` when self-hosting) to read the token
34
- without echo or process-argument exposure. The generator writes it to `.env`
35
- only when Git confirms that file is ignored; otherwise it points you to Rails
36
- credentials or your deployment secret manager without printing the value. Add
37
- `--kamal-secrets` to wire `RAILWATCH_TOKEN` through `.kamal/secrets` and
38
- `config/deploy.yml`. It finishes by running `railwatch:doctor` for you.
39
- `bin/rails railwatch:token` says where to get a token; `bin/rails railwatch:mcp`
40
- prints ready-to-paste MCP client configuration.
27
+ ## What you get
28
+
29
+ - Requests, jobs, scheduled tasks, commands, queries, exceptions, and
30
+ logs, linked into one trace per execution
31
+ ([Record types](docs/records.md)).
32
+ - Sampling decided once per execution, plus tail sampling that keeps a
33
+ sampled-out request that turns out slow or raises
34
+ ([Configuration](docs/configuration.md)).
35
+ - An optional stack profiler through `vernier` or `stackprof`, off by
36
+ default ([Configuration](docs/configuration.md)).
37
+ - A browser client for Inertia apps: page-visit timing, Core Web Vitals,
38
+ and browser errors ([Getting started](docs/getting-started.md)).
39
+ - RSpec and Minitest matchers that turn a query budget into a CI gate
40
+ ([Testing](docs/testing.md)).
41
+ - An MCP server so Claude Code, Cursor, VS Code, or Zed can read your
42
+ production data ([AI assistants and MCP](docs/ai-and-mcp.md)).
43
+
44
+ Configuration lives in `config/initializers/railwatch.rb`; most options
45
+ also have a `RAILWATCH_*` environment variable.
46
+
47
+ ```ruby
48
+ Railwatch.configure do |c|
49
+ c.sample = { requests: 0.1, jobs: 1.0 }
50
+ c.user { |u| { id: u.id, name: u.name, email: u.email } }
51
+ end
52
+ ```
53
+
54
+ The same instrumentation runs in your test suite, so a spec can hold a
55
+ hot path to a query budget:
56
+
57
+ ```ruby
58
+ expect { get "/widgets" }.to have_railwatch_queries(at_most: 6)
59
+ expect { get "/widgets" }.not_to have_railwatch_n_plus_one
60
+ ```
41
61
 
42
62
  ## Documentation
43
63
 
@@ -67,156 +87,14 @@ prints ready-to-paste MCP client configuration.
67
87
  AI coding agents working on an app that uses Railwatch: [`llms.txt`](llms.txt)
68
88
  and [`AGENTS.md`](AGENTS.md).
69
89
 
70
- Configuration lives in `config/initializers/railwatch.rb`; every option has
71
- a `RAILWATCH_*` environment variable. Sampling is decided once per execution:
72
- a sampled-in request ships its whole tree, a sampled-out one ships nothing
73
- except unhandled exceptions.
74
-
75
- ```ruby
76
- Railwatch.configure do |c|
77
- c.sample = { requests: 0.1, jobs: 1.0 }
78
- c.user { |u| { id: u.id, name: u.name, email: u.email } }
79
- end
80
-
81
- class ReportsController < ApplicationController
82
- railwatch_sample 0.01, only: :index
83
- end
84
-
85
- Railwatch.ignore { ExpensiveSync.run }
86
- Railwatch.context(tenant: org.slug, plan: org.plan)
87
- ```
88
-
89
- Time any block of your own code as a `span` on the surrounding request,
90
- job, or command — the block's value is returned untouched:
91
-
92
- ```ruby
93
- Railwatch.span("pdf.render", template: "invoice", pages: 12) { renderer.call }
94
- ```
95
-
96
- When a span isn't enough to say where the time went, Railwatch can attach a
97
- real stack profile to an execution. Add `gem "vernier"` (Ruby ≥ 3.2) or
98
- `gem "stackprof"` to the Gemfile and set `c.profile_sample = 0.01` to
99
- profile 1% of executions, or `c.profile_slow_ms = 500` alongside
100
- `c.tail_sample_slow_ms` to profile the slow ones. The collapsed stacks
101
- ship as their own `profile` record, gzipped, and the request or job it
102
- belongs to is marked `profiled`. Off by default, and one Float comparison
103
- per execution while it stays off.
104
-
105
- Sampling can also be decided at the *end* of an execution instead of the
106
- start: set `c.tail_sample_slow_ms = 500` (or call `Railwatch.keep!`) and a
107
- head-sampled-out request that turns out to be slow, or to have raised,
108
- ships its whole tree anyway. Outgoing HTTP carries a W3C `traceparent`,
109
- and an inbound one is adopted, so a trace spans services.
110
-
111
- Inertia apps get real page-visit timing by calling `startRailwatch()` from the
112
- generated `app/frontend/lib/railwatch.ts`. Server-side rendering is timed
113
- automatically wherever `inertia_rails` SSR is already enabled — no extra
114
- configuration needed.
115
-
116
- Outgoing HTTP made through Faraday is instrumented by adding
117
- `Railwatch::Faraday` to the connection's middleware stack (`Net::HTTP` is
118
- already covered globally, with no setup); any other client can be wrapped
119
- with `Railwatch.instrument_outgoing`:
120
-
121
- ```ruby
122
- Faraday.new(url) { |f| f.use Railwatch::Faraday }
123
- Railwatch.instrument_outgoing(:get, url) { http_client.get(url) }
124
- ```
125
-
126
- ## Testing
127
-
128
- Production browser errors can be resolved to original source files using
129
- [private source-map uploads](docs/source-maps.md).
130
-
131
- The same instrumentation runs in your test suite, so a spec can hold a hot
132
- path to a query budget and CI can fail the pull request that regresses it:
133
-
134
- ```ruby
135
- expect { get "/widgets" }.to have_railwatch_queries(at_most: 6)
136
- expect { get "/widgets" }.not_to have_railwatch_n_plus_one
137
- ```
138
-
139
- Failures list the offending SQL. Set-up, every matcher (RSpec and Minitest),
140
- and a CI performance-gate recipe are in [`docs/testing.md`](docs/testing.md).
141
-
142
- ---
143
-
144
- Every attribute, the full public facade, sampling, redaction/rejection,
145
- transport/buffering behavior, the overhead gate, and the Kamal deploy hook
146
- are documented field-by-field in [`docs/configuration.md`](docs/configuration.md).
147
- Every record type Railwatch ships — `request`, `job_attempt`, `query`,
148
- `exception`, and the rest — is documented field-by-field, sourced directly
149
- from the code that builds it, in [`docs/records.md`](docs/records.md).
150
-
151
90
  ## Replacing Sentry
152
91
 
153
- Railwatch subscribes to `Rails.error` on install
154
- (`Rails.error.subscribe`), so any existing `Rails.error.report` or
155
- `Rails.error.handle` call which is how Sentry's own Rails integration
156
- is normally wired in is captured with no code changes. An unhandled
157
- exception bypasses the execution buffer: it is enqueued immediately and
158
- wakes the in-memory reporter without doing network I/O on the application
159
- thread. Delivery is still asynchronous and memory-only, so a hard kill,
160
- OOM, or process exit after the shutdown deadline can lose it.
161
-
162
- What differs from a dedicated error tracker: exceptions aren't reported in
163
- isolation — each one is linked (`execution_id`/`trace_id`) to the request,
164
- job, or command it happened inside, alongside every query, cache read,
165
- outgoing request, and log line from that same execution. There's no
166
- separate error-tracking SDK/config to maintain — `severity`, `handled`,
167
- and `context` all come from the same `Railwatch.configure` block and
168
- `Railwatch.context` calls used for everything else the gem instruments.
169
-
170
- ### Coming from Sentry
171
-
172
- | Sentry | Railwatch |
173
- |---|---|
174
- | `dsn:` | `RAILWATCH_TOKEN` (+ `RAILWATCH_INGEST_URL` for a self-hosted platform). |
175
- | `environment:` | `config.environment` — defaults to `Rails.env`, set it only to report under a different name. |
176
- | `release:` | `config.deploy` — auto-detected from the deploy platform or Git checkout. Stamped on every record. |
177
- | `traces_sample_rate:` / `profiles_sample_rate:` | `config.sample`, a rate per execution kind (`requests`, `jobs`, `commands`, `scheduled_tasks`, `exceptions`), decided once per execution rather than per event. Per-route: `railwatch_sample 0.01, only: :index`. |
178
- | `excluded_exceptions:` | `config.ignored_exceptions` — same default list, plus every named ancestor is matched, not just the exact class. |
179
- | `before_send:` / `before_send_transaction:` | `Railwatch.before_ingest { \|batch\| ... }` for the whole outgoing batch; `Railwatch.redact_queries`/`redact_logs`/... to scrub one record type in place; `Railwatch.reject_queries`/`reject_logs`/... to drop records by predicate. |
180
- | `fingerprint` / grouping rules | `Railwatch.fingerprint { \|error, default\| ... }` globally, `def railwatch_fingerprint` on your own error class, or `Railwatch.report(error, fingerprint: [...])` per call. The literal `:default` splices in the parts Railwatch would have hashed, like Sentry's `{{ default }}`. |
181
- | `include_local_variables:` | `config.capture_exception_locals`. |
182
- | `config.rails.active_job_report_on_retry_error` | `config.capture_job_retry_errors` (off by default). |
183
- | `send_default_pii:` | Deliberately split: `config.capture_request_payload` for params, `config.capture_job_arguments` for job arguments, `config.capture_response_body_on_error` for what a failing upstream sent back, `config.redact_headers`/`redact_params` for what's scrubbed, and the `Railwatch.user { ... }` block for who. There is no single "send everything" switch. |
184
- | Breadcrumbs | Not a separate concept — every query, cache read, outgoing request, log line, and view render is already a first-class record linked to its execution by `execution_id`/`trace_id`. The execution *is* the breadcrumb trail, and it's queryable. |
185
- | `Sentry.capture_message` | `Railwatch.report(error, ...)` for an exception; plain `Rails.logger` for a message — log lines at or above `config.log_level` become `log` records automatically. |
186
- | `Sentry.set_user` | `Railwatch.user { ... }` (a resolver block, evaluated per execution). |
187
- | `Sentry.set_tags` / `set_context` / `set_extras` | `Railwatch.context(key: value)` — serialized onto the parent record and every exception. |
188
- | `Sentry.with_child_span` | `Railwatch.span("name") { ... }`. |
189
- | `Sentry.add_attachment` | `Railwatch.attach("payload.json", data)` — a String, `Pathname`, or IO, gzipped on the wire and capped at `config.max_attachment_bytes`. `exception:` files it against that error's issue, and `Railwatch.report(error, attachments: { "payload.json" => data })` captures and attaches in one call. |
190
- | `Sentry.capture_check_in` (cron monitoring) | Automatic: Solid Queue recurring tasks become `scheduled_task` records with `task_key`, `schedule`, and `drift`. Nothing to instrument. |
191
- | `config.rails.report_rescued_exceptions` | `config.capture_rescued_exceptions` (on by default). |
192
- | Rack `X-Request-Start` queue time | Automatic: `queue_time` on every `request` record. |
193
- | `auto_session_tracking:` (release health) | Automatic: `session` records from the browser client and the request middleware, keyed on `config.deploy` as the release. `config.track_sessions` turns both off. |
194
-
195
- These mappings cover the Rails-server migration path. Browser Replay,
196
- native/mobile SDKs, some direct worker and scheduler entry points, and
197
- Sentry's broader managed integration catalog are not equivalent today.
198
- Use the supported-workload matrix in
199
- [`docs/replacing-sentry.md`](docs/replacing-sentry.md) before removing
200
- Sentry from an application that depends on those capabilities.
201
-
202
- `config.deploy` checks `RAILWATCH_DEPLOY`, `KAMAL_VERSION`, common Git and
203
- platform environment variables, `REVISION`, then `.git/HEAD`. Full 40-character
204
- SHAs are consistently shortened to 12 characters. Set
205
- `RAILWATCH_DETECT_DEPLOY=false` (or `c.detect_deploy = false`) to keep only the
206
- explicit `RAILWATCH_DEPLOY` and Kamal defaults.
207
-
208
- To report an exception manually (the `Rails.error.report`-equivalent):
209
-
210
- ```ruby
211
- Railwatch.report(error, handled: true, context: { order_id: order.id })
212
- ```
213
-
214
- `severity` defaults to `:warning` when `handled: true`, `:error`
215
- otherwise. See the `exception` section of
216
- [`docs/records.md`](docs/records.md) for the full field list, and
217
- [`docs/configuration.md`](docs/configuration.md) for redaction
218
- (`Railwatch.redact_exceptions`), `capture_exception_source`, and
219
- `Railwatch.on_unrecoverable` (Railwatch watching its own internal failures).
92
+ Railwatch subscribes to `Rails.error` on install, so existing
93
+ `Rails.error.report` and `Rails.error.handle` calls are captured with no
94
+ code changes. Each exception is linked to the request, job, or command
95
+ it happened inside. The option-by-option mapping and the
96
+ supported-workload matrix live in
97
+ [Replacing Sentry](docs/replacing-sentry.md).
220
98
 
221
99
  ## Development
222
100
 
data/docs/ai-and-mcp.md CHANGED
@@ -23,10 +23,10 @@ prints every block below with your platform's host already filled in.
23
23
 
24
24
  MCP tokens are **per person**, not per application: sign in to the platform,
25
25
  go to **Settings → Profile → "API & MCP token"**, and press *Generate token*.
26
- The token starts with `lnt_` and is shown once. It can reach every account
26
+ The token starts with `rwp_` and is shown once. It can reach every account
27
27
  your user belongs to, and nothing else.
28
28
 
29
- This is a different token from the `lt_...` ingest token the gem uses. The
29
+ This is a different token from the `rw_...` ingest token the gem uses. The
30
30
  ingest token writes telemetry for one environment; the MCP token reads it
31
31
  back as you.
32
32
 
@@ -36,13 +36,13 @@ each with a copy button — so the fastest path is: generate, copy, paste.
36
36
  ## 2. Connect a client
37
37
 
38
38
  Everything below uses `https://railwatch.rebulk.com/mcp`; substitute your own
39
- host if you self-host, and `lnt_your_token_here` for the token.
39
+ host if you self-host, and `rwp_your_token_here` for the token.
40
40
 
41
41
  ### Claude Code
42
42
 
43
43
  ```sh
44
44
  claude mcp add railwatch --transport http https://railwatch.rebulk.com/mcp \
45
- --header "Authorization: Bearer lnt_your_token_here"
45
+ --header "Authorization: Bearer rwp_your_token_here"
46
46
  ```
47
47
 
48
48
  ### Claude Desktop
@@ -60,7 +60,7 @@ Claude Desktop speaks stdio, so it needs the `mcp-remote` bridge. In
60
60
  "mcp-remote",
61
61
  "https://railwatch.rebulk.com/mcp",
62
62
  "--header",
63
- "Authorization: Bearer lnt_your_token_here"
63
+ "Authorization: Bearer rwp_your_token_here"
64
64
  ]
65
65
  }
66
66
  }
@@ -76,7 +76,7 @@ Claude Desktop speaks stdio, so it needs the `mcp-remote` bridge. In
76
76
  "mcpServers": {
77
77
  "railwatch": {
78
78
  "url": "https://railwatch.rebulk.com/mcp",
79
- "headers": { "Authorization": "Bearer lnt_your_token_here" }
79
+ "headers": { "Authorization": "Bearer rwp_your_token_here" }
80
80
  }
81
81
  }
82
82
  }
@@ -92,7 +92,7 @@ Claude Desktop speaks stdio, so it needs the `mcp-remote` bridge. In
92
92
  "railwatch": {
93
93
  "type": "http",
94
94
  "url": "https://railwatch.rebulk.com/mcp",
95
- "headers": { "Authorization": "Bearer lnt_your_token_here" }
95
+ "headers": { "Authorization": "Bearer rwp_your_token_here" }
96
96
  }
97
97
  }
98
98
  }
@@ -113,7 +113,7 @@ Claude Desktop speaks stdio, so it needs the `mcp-remote` bridge. In
113
113
  "mcp-remote",
114
114
  "https://railwatch.rebulk.com/mcp",
115
115
  "--header",
116
- "Authorization: Bearer lnt_your_token_here"
116
+ "Authorization: Bearer rwp_your_token_here"
117
117
  ]
118
118
  }
119
119
  }
@@ -124,7 +124,7 @@ Claude Desktop speaks stdio, so it needs the `mcp-remote` bridge. In
124
124
 
125
125
  ```sh
126
126
  curl -sS https://railwatch.rebulk.com/mcp \
127
- -H "Authorization: Bearer lnt_your_token_here" \
127
+ -H "Authorization: Bearer rwp_your_token_here" \
128
128
  -H "Content-Type: application/json" \
129
129
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
130
130
  ```