koderift-rails 1.3.4 → 2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8e49d9d7f56f87dde732a6465a3ced955d8960b692408f6818cf686e30965c8c
4
- data.tar.gz: 299cb7491b8211baa559aebbca51b7759929891caefe5048d562791431759202
3
+ metadata.gz: b77962b847bf6127f3029ab4cab2eb3f71ae3cf0d0041c6d2df6ad1108449c7e
4
+ data.tar.gz: cc0a1ab9373ca3544bf3d9b48bf08ea1afa269236d4385e9873a139ea258fb0f
5
5
  SHA512:
6
- metadata.gz: e01be29d2c18193b54c04d93ae80ce6bca85d58b8c3c57e92227d5e706c4e033d8d13cb103c285a61d90a19e14431f80eec95e2ace8ed38fe2bbb8f92cd9fabf
7
- data.tar.gz: b620b59cbe817e063ef7f862be7ed1fc0a36390b6465b4f52b20d59afe51f34de53a47dc72371f7ba14a0079c579a3946c16dbfa1bb3d7cd316c7ad381afe53f
6
+ metadata.gz: 274e3e08f2d325ef30063547658668b955eb32607c6705829c5de531b8ac40d63701d199a3fffb9d537f3298a29816dd491ba897a0ec93dc0237eebaa2964b24
7
+ data.tar.gz: d409cd77d3f77b28d86fb526ddff9b096314b8b12e4af3e1e687b281a98d811fabcb775b164eed5a785433ee6455697faabd1a25c3cb5953a8c8fadd3ccb1425
data/CHANGELOG.md CHANGED
@@ -6,6 +6,73 @@ This project follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
  Releases before 1.3.4 were installed from git and were never published to
7
7
  RubyGems; they are listed for continuity.
8
8
 
9
+ ## [2.1.0] - 2026-11-06
10
+
11
+ Security fix: the Browser RUM meta tag no longer publishes your project token.
12
+
13
+ ### Security
14
+ - `koderift_rum_meta_tag` used to render `config.project_token` into page HTML.
15
+ That token is the log agent's ingest secret, so anyone reading your page
16
+ source could use it to send log lines to your Koderift project, including
17
+ lines that count against its log quota. The tag now renders only the new
18
+ `config.rum_token`, a public token that can only send browser RUM data.
19
+ - If you rendered the RUM tag on 2.0 or older, treat your project token as
20
+ public. Once this version is deployed with `rum_token` set, rotate the ingest
21
+ token in Koderift (Log Analytics settings, "Rotate ingest token"), then update
22
+ `webhook_url` in the agent's `agent.yml` and `KODERIFT_PROJECT_TOKEN` in your
23
+ app, and restart the agent.
24
+
25
+ ### Added
26
+ - `config.rum_token`: your project's public RUM token, from the Log Analytics
27
+ setup page in Koderift. It starts with `rum_`.
28
+
29
+ ### Changed
30
+ - **Action needed if you use Browser RUM:** set `config.rum_token`. Without it
31
+ the meta tag renders nothing, and the gem logs one warning per process the
32
+ first time a page renders it. There is deliberately no fallback to
33
+ `project_token`. The tag also refuses a `rum_token` equal to `project_token`.
34
+
35
+ ### Notes
36
+ - Koderift keeps accepting RUM data sent with the old token until
37
+ 1 February 2027, so pages not yet upgraded keep reporting until then.
38
+ - `project_token` is still required to turn the gem on. The gem never sends it
39
+ anywhere; the log agent uses its own copy in `agent.yml`.
40
+
41
+ ## [2.0.0] - 2026-11-04
42
+
43
+ Manual error reporting, with a Sentry-compatible API.
44
+
45
+ Requires koderift-agent >= 1.4.0. Against an older agent, captures still reach
46
+ Raw Logs but not the Log Explorer or the Errors page.
47
+
48
+ ### Added
49
+ - `Koderift.capture_exception` and `Koderift.capture_message` for reporting
50
+ errors your code already handles. `capture_exception` returns an event with an
51
+ `event_id`, or nil when nothing was emitted.
52
+ - Scope (`configure_scope`, `with_scope`, `set_user`, `set_tags`, `set_tag`,
53
+ `set_extras`, `set_extra`, `set_context`), breadcrumbs
54
+ (`add_breadcrumb`, `Koderift::Breadcrumb`), and `exception_captured?`.
55
+ - Custom instrumentation: `with_child_span` records a span that renders in the
56
+ APM trace beside the SQL and HTTP spans, and `start_transaction` traces work
57
+ that is neither a request nor a Sidekiq job — a rake task or cron script.
58
+ - Uncaught rake task failures are now captured automatically. Previously a task
59
+ that raised wrote only to STDERR, which under cron is never in the Rails log,
60
+ so those failures were invisible.
61
+ - Configuration: `max_captures_per_minute` (default 100), `sample_rate`,
62
+ `before_send` and `excluded_exceptions`.
63
+
64
+ ### Fixed
65
+ - Sidekiq jobs enqueued through ActiveJob are reported under their own class
66
+ (`job["wrapped"]`, as Sidekiq logs them) instead of the adapter's wrapper,
67
+ which filed every ActiveJob under one `Sidekiq::ActiveJob::Wrapper` row.
68
+
69
+ ### Notes
70
+ - Captures appear under a new `manual` log source, and are excluded from the
71
+ APM request aggregates by it, so they cannot skew throughput, p95 or Apdex.
72
+ - The gem does not define `Sentry`. Migration is a namespace find-and-replace;
73
+ see the README for the full mapping and the short list of Sentry APIs with no
74
+ Koderift equivalent.
75
+
9
76
  ## [1.3.4] - 2026-08-27
10
77
 
11
78
  First release published to RubyGems.
data/README.md CHANGED
@@ -12,7 +12,7 @@ Requires Ruby >= 2.7 and Rails 7.1-8.x.
12
12
  Add to your `Gemfile`:
13
13
 
14
14
  ```ruby
15
- gem 'koderift-rails', '~> 1.3'
15
+ gem 'koderift-rails', '~> 2.1'
16
16
  ```
17
17
 
18
18
  Run `bundle install`. Lograge is a dependency of this gem and is installed
@@ -32,7 +32,37 @@ end
32
32
  ```
33
33
 
34
34
  Set `KODERIFT_PROJECT_TOKEN` in your environment to the token from your
35
- project's Log Analytics settings page in Koderift.
35
+ project's Log Analytics settings page in Koderift. It is the log agent's
36
+ ingest secret: keep it out of your repository and your page HTML.
37
+
38
+ ## Browser RUM
39
+
40
+ To collect Core Web Vitals and JS error counts from real browsers, set your
41
+ project's public RUM token (from the Log Analytics setup page; it starts with
42
+ `rum_`) and add the meta tag and snippet inside your layout's `<head>`:
43
+
44
+ ```ruby
45
+ Koderift::Rails.configure do |config|
46
+ config.project_token = ENV['KODERIFT_PROJECT_TOKEN']
47
+ config.rum_token = 'rum_...'
48
+ end
49
+ ```
50
+
51
+ ```erb
52
+ <%= koderift_rum_meta_tag %>
53
+ <script src="https://<your Koderift host>/ingest/rum.js" async defer></script>
54
+ ```
55
+
56
+ The meta tag publishes `rum_token` only. It can only send browser RUM data, so
57
+ it is safe in page HTML and safe to commit. The gem never renders
58
+ `project_token`, and it refuses a `rum_token` equal to it. Without a
59
+ `rum_token` the tag renders nothing and the gem logs one warning per process.
60
+ Set `config.rum_enabled = false` for API-only apps.
61
+
62
+ Upgrading from 2.0 or older with RUM in use? Those versions published
63
+ `project_token` in the meta tag. Set `rum_token`, deploy, then rotate the
64
+ ingest token in Koderift (Log Analytics settings) and update `webhook_url` in
65
+ the agent's `agent.yml`. See the [changelog](CHANGELOG.md).
36
66
 
37
67
  ## What it captures
38
68
 
@@ -47,8 +77,158 @@ project's Log Analytics settings page in Koderift.
47
77
  * Exception class, message, and cause chain
48
78
  * Stack traces (requires `keep_original_rails_log: true`)
49
79
 
80
+ ## Reporting handled errors
81
+
82
+ Automatic instrumentation only sees what reaches Rails: a request, a Sidekiq
83
+ job, or something logged at ERROR. An error your code *handles* — rescued,
84
+ retried, degraded gracefully, returned as a 200 — is exactly the kind worth
85
+ knowing about, and nothing reports it for you.
86
+
87
+ ```ruby
88
+ begin
89
+ sync_contractors
90
+ rescue StandardError => e
91
+ Rails.logger.error "contractor sync failed: #{e.class} - #{e.message}"
92
+ Koderift.capture_exception(e)
93
+ end
94
+ ```
95
+
96
+ `capture_exception` returns an event, or `nil` when nothing was emitted:
97
+
98
+ ```ruby
99
+ event_id = Koderift.capture_exception(error)&.event_id
100
+ ```
101
+
102
+ Paste that id into the Log Explorer search box to find the event.
103
+
104
+ ### Errors vs. messages
105
+
106
+ `capture_exception` produces a tracked Error — grouped by exception class and
107
+ `component#action`, counted, and resolvable. `capture_message` produces a
108
+ searchable log line, and *also* a tracked Error at `:error` or `:fatal`:
109
+
110
+ ```ruby
111
+ Koderift.capture_message("cache warm skipped", level: :info) # Log Explorer only
112
+ Koderift.capture_message("panel schema drifted", # a tracked Error
113
+ level: :error, name: "PanelSchemaDrift")
114
+ ```
115
+
116
+ `name:` is what separates one message group from another. Without it, every
117
+ unnamed error-level message from the same place groups together — deliberately,
118
+ because grouping on the message text itself would either mint a new Error per
119
+ interpolated value or collapse unrelated messages into one bucket.
120
+
121
+ `:warn` and `:info` messages never become tracked Errors; they are searchable in
122
+ the Log Explorer.
123
+
124
+ ### Context
125
+
126
+ ```ruby
127
+ Koderift.capture_exception(e,
128
+ tags: { region: "eu-west" },
129
+ extra: { order_id: order.id },
130
+ user: { id: current_user.id, email: current_user.email })
131
+ ```
132
+
133
+ Anything matching `config.filter_params` is stripped at every depth, as are the
134
+ keys `controller`, `method` and `job_class` (they collide with how the log agent
135
+ identifies request and job lines).
136
+
137
+ ### Scope, breadcrumbs and custom spans
138
+
139
+ ```ruby
140
+ Koderift.set_user(id: current_user.id)
141
+ Koderift.set_tag(:tenant, current_tenant.slug)
142
+ Koderift.with_scope { |scope| scope.set_tag(:step, "import") }
143
+
144
+ Koderift.add_breadcrumb(Koderift::Breadcrumb.new(category: "payment",
145
+ message: "charging card"))
146
+
147
+ Koderift.with_child_span(op: "stripe", description: "charge") do |span|
148
+ span.set_data(:amount, 500)
149
+ gateway.charge!
150
+ end
151
+ ```
152
+
153
+ Scope is per-thread and reset at the end of every request and job. Breadcrumbs
154
+ interleave with the SQL and render crumbs the gem already collects. A child span
155
+ becomes a `custom` span in the APM trace, next to the SQL and HTTP spans.
156
+
157
+ ### Rake tasks and cron
158
+
159
+ Uncaught rake failures are captured automatically — no task changes needed. This
160
+ matters because a task that raises writes to STDERR, which under cron goes to a
161
+ mail spool or `/dev/null`, so it was previously invisible.
162
+
163
+ To trace a task as a unit of work, wrap it:
164
+
165
+ ```ruby
166
+ Koderift.start_transaction(name: "ringley_panel:sync_recent_contractors", op: "rake") do
167
+ sync_contractors
168
+ end
169
+ ```
170
+
171
+ Its queries, outbound HTTP calls and custom spans then render as one trace.
172
+ Prefer this block form: `start_transaction` without a block returns a
173
+ transaction you must `finish` yourself, and an abandoned one leaves notification
174
+ subscribers attached to the process.
175
+
176
+ ### Configuration
177
+
178
+ ```ruby
179
+ Koderift::Rails.configure do |config|
180
+ config.max_captures_per_minute = 100 # nil to disable the cap
181
+ config.sample_rate = 1.0
182
+ config.excluded_exceptions = %w[ActiveRecord::RecordNotFound]
183
+ config.before_send = ->(payload, hint) { payload } # nil to drop
184
+ end
185
+ ```
186
+
187
+ A capture never raises, and drops are never silent — the rate limiter says on
188
+ the log when it starts dropping and how many it dropped.
189
+
190
+ ## Migrating from Sentry
191
+
192
+ Replace the `Sentry` namespace with `Koderift`; the call sites keep working.
193
+
194
+ | Sentry | Koderift |
195
+ | --- | --- |
196
+ | `Sentry.capture_exception(e, **opts, &blk)` | `Koderift.capture_exception` |
197
+ | `Sentry.capture_message(msg, **opts, &blk)` | `Koderift.capture_message` |
198
+ | `Sentry.capture_event(event)` | `Koderift.capture_event` |
199
+ | `Sentry.last_event_id` / `initialized?` | same |
200
+ | `Sentry.configure_scope` / `with_scope` | same |
201
+ | `Sentry.set_user` / `set_tags` / `set_tag` / `set_extras` / `set_extra` / `set_context` | same |
202
+ | `Sentry.get_current_scope` | same |
203
+ | `Sentry.add_breadcrumb` / `Sentry::Breadcrumb` | `Koderift.add_breadcrumb` / `Koderift::Breadcrumb` |
204
+ | `Sentry.start_transaction` / `with_child_span` | same |
205
+ | `Sentry.exception_captured?` | same |
206
+ | `Sentry.init { |c| ... }` | `Koderift::Rails.configure { |c| ... }` |
207
+ | `config.before_send` / `sample_rate` / `excluded_exceptions` | same |
208
+
209
+ Capture options are the same: `level:`, `tags:`, `extra:`, `user:`, `contexts:`,
210
+ `fingerprint:`, `backtrace:`, `hint:`, and `message:` on `capture_exception`.
211
+ Levels accept Sentry's names — `:warning` and `:debug` fold to `warn` and `info`.
212
+
213
+ `fingerprint:` is honoured by synthesising a grouping class, so
214
+ `fingerprint: %w[stripe timeout]` reports as `StripeTimeout: Net::ReadTimeout: ...`.
215
+
216
+ ### Not provided
217
+
218
+ These have no stub: a method either does something real or is absent, so you
219
+ find out at migration time rather than from a silent no-op.
220
+
221
+ | Sentry | Why |
222
+ | --- | --- |
223
+ | `capture_check_in` | Cron *monitors* — alerting when a job never runs. Cron errors and slowness are covered (above); detecting an absence needs schedule definitions Koderift does not have yet. |
224
+ | `close`, `clone_hub_to_current_thread`, `with_session_tracking` | Nothing to do behind them: captures are written synchronously with no in-process buffer, and all state is already thread-local. |
225
+ | `get_traceparent`, `get_baggage` | Distributed tracing works, via the `X-Koderift-Trace-ID` header this gem already propagates. |
226
+ | Profiling, release health | Configured inside `Sentry.init` with no call sites, so nothing to replace. Release comparison is on the Koderift deploy-impact view, keyed by the agent's `app_version`. |
227
+
50
228
  ## Requirements
51
229
 
52
230
  * Ruby >= 2.7
53
- * Rails >= 6.1
231
+ * Rails 7.1-8.x
54
232
  * lograge >= 0.11
233
+ * koderift-agent >= 1.4.0 for manual captures to reach the Log Explorer
234
+ (older agents ship them as raw log lines only)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+
5
+ module Koderift
6
+ module Rails
7
+ # A manually recorded breadcrumb. Serialises to the same shape
8
+ # Instrumentation already emits for SQL and partial renders, so manual and
9
+ # automatic crumbs interleave in one ordered trail rather than living in
10
+ # two lists the UI would have to merge.
11
+ class Breadcrumb
12
+ attr_reader :category, :message, :level, :data, :timestamp
13
+
14
+ def initialize(category: nil, message: nil, level: nil, data: nil, timestamp: nil)
15
+ @category = category.to_s.empty? ? 'manual' : category.to_s
16
+ @message = message.to_s
17
+ @level = level&.to_s
18
+ @data = data
19
+ @timestamp = timestamp || Time.now.utc.iso8601
20
+ end
21
+
22
+ def to_h
23
+ {
24
+ type: @category,
25
+ message: @message,
26
+ level: @level,
27
+ data: @data,
28
+ timestamp: @timestamp
29
+ }.compact
30
+ end
31
+ end
32
+
33
+ # The live breadcrumb trail for the current thread.
34
+ #
35
+ # Instrumentation.capture publishes ITS OWN array here for the duration of a
36
+ # request or job, so a manual crumb lands in the same list as the automatic
37
+ # SQL/render ones and ships with that unit's log line. Outside a captured
38
+ # unit the trail is a plain thread-local that captures still read, which is
39
+ # what makes breadcrumbs work in a rake task.
40
+ module Breadcrumbs
41
+ KEY = :koderift_breadcrumbs
42
+
43
+ class << self
44
+ def add(crumb)
45
+ crumb = Breadcrumb.new(message: crumb.to_s) unless crumb.is_a?(Breadcrumb)
46
+ buffer = (Thread.current[KEY] ||= [])
47
+ buffer << crumb.to_h
48
+
49
+ # Trim here rather than only at the end of Instrumentation.capture:
50
+ # outside a captured unit nothing else ever bounds this array, and a
51
+ # rake task adding crumbs in a loop would grow it without limit.
52
+ max = Koderift::Rails.configuration.max_breadcrumbs.to_i
53
+ buffer.shift(buffer.size - max) if max.positive? && buffer.size > max
54
+
55
+ crumb
56
+ rescue StandardError
57
+ nil
58
+ end
59
+
60
+ def current
61
+ (Thread.current[KEY] || []).dup
62
+ end
63
+
64
+ def clear
65
+ Thread.current[KEY] = nil
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end