dead_bro 0.2.20 → 0.2.22

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: 46ac9efa7b56801c96f5f9e20f113613fa60c10337b94ee84adc59d0ea38234f
4
- data.tar.gz: fa10f3f9436a350d8cea97caa71541ade145c88e479cd84195a2814dc43e4395
3
+ metadata.gz: 14aa1509e04c9bf1848883cc5f2a12e7783e5ac4c123c9bcddde301948e30bfb
4
+ data.tar.gz: b3ec01b52a1e63efda701520a014f040fff96cedcbf36fe0827f4a7e892aac90
5
5
  SHA512:
6
- metadata.gz: 935d4045021860632e6121b844afe1adca986297e03b0c54c0322721b6877112e1dd1e34ccda5ad1b91d52a6a5a05e829fc8c5f045ad74623d6a1b0a5e19ef66
7
- data.tar.gz: 9171b13a16ba440015e4df1e44f260b362224c8f0ed8752f6c1a3420c97667f35ffffc97879ed2c34a829be2f95b216b552bb5b52525aa1403c988d1c3955b24
6
+ metadata.gz: 0c1f24b1718fa9aacf383d22e3b6982e76e7c5aa2c1ccf989d9c411bb0771b0a5fae60d9a780bbe845fac84cafc1a7f876b305005b9e5f3c07f82a7005b0775b
7
+ data.tar.gz: 469f652352cb2544b882b82dc528dced3e4725a34da033a14dd54b6e7742691a189eed1079dac1684e2da24ba587a955d4fcf3034c1c50f6a2f0d9bf514b2311
data/CHANGELOG.md CHANGED
@@ -3,6 +3,39 @@
3
3
  ### Added
4
4
  - Monitor thread now sends a synchronous heartbeat on startup before the first collection tick. This ensures remote settings — including `monitor_enabled` — are applied from the very first reporting cycle, so Sidekiq workers and other non-web processes that have not yet sent any metrics still receive the correct configuration immediately on boot rather than waiting up to 60 seconds for the first scheduled tick.
5
5
 
6
+ ## [0.2.21] - 2026-06-02
7
+
8
+ ### Added
9
+ - **Per-span timing for the Request Trace view.** Every instrumented event now includes a `start_offset_ms` field — the wall-clock milliseconds from rack entry to when that event started. This powers the waterfall visualisation in the DeadBro dashboard without any configuration changes.
10
+ - `SqlSubscriber`: `start_offset_ms` is computed from `TRACKING_START_TIME_KEY` using the `started` timestamp provided by `ActiveSupport::Notifications`. Stored on both the raw per-query hash and on the first-occurrence aggregate entry (so the bar is positioned at the actual time the query first ran, not a fabricated cumulative offset).
11
+ - `ViewRenderingSubscriber`: same pattern applied to template, partial, and collection render events. `start_offset_ms` is captured for the first render of each unique identifier and stored on the aggregate.
12
+ - `CacheSubscriber`: `start_offset_ms` derived from the `started` `ActiveSupport::Notifications` timestamp, added to every cache event hash and passed through `build_event`.
13
+ - `RedisSubscriber`: `wall_start = Time.now` is captured at the entry point of each instrumented block (`call`, `call_pipeline`, `call_multi`). The monotonic clock is still used for duration accuracy; `wall_start` is used only for the offset relative to `TRACKING_START_TIME_KEY`. Also applied to the `ActiveSupport::Notifications` fallback path in `install_notifications_subscription!`.
14
+ - `ElasticsearchSubscriber`: `start_offset_ms` added to `build_event`; the `record` method (called from `HttpInstrumentation` for Net::HTTP-based ES requests) now accepts a `start_offset_ms:` keyword argument. The `ActiveSupport::Notifications` subscription path (`request.elasticsearch` / `request.elastic_transport`) computes it from `started`.
15
+ - `HttpInstrumentation`: `wall_start = Time.now` captured alongside the existing monotonic `start_time`. `start_offset_ms` is included in the HTTP outgoing payload and forwarded to `ElasticsearchSubscriber.record` for requests routed to an ES host.
16
+
17
+ ## [0.2.20] - 2026-05-29
18
+
19
+ ### Added
20
+ - Monitor thread sends a synchronous heartbeat immediately on startup (before the first scheduled collection tick) so that remote settings — including `monitor_enabled` — are applied from the very first reporting cycle. Sidekiq workers and other non-web processes now receive the correct configuration on boot rather than waiting up to 60 seconds for the first tick.
21
+ - `gem_version` field added to every heartbeat payload so the dashboard can display and compare the running gem version per application.
22
+ - `process_kind` included in all system monitor payloads, linking server metrics to the correct process type.
23
+ - `post_heartbeat` now accepts a `sync: true` keyword for situations that require a blocking network call before proceeding (used by the monitor startup path).
24
+
25
+ ## [0.2.19] - 2026-05-28
26
+
27
+ ### Added
28
+ - **Error fingerprinting**: every unhandled exception payload now includes a stable `fingerprint` string derived from the exception class, a normalised version of the message (numeric IDs and UUIDs stripped), and the top application stack frame. Identical errors that differ only in record IDs or UUIDs produce the same fingerprint, enabling reliable grouping and deduplication on the server.
29
+ - `DeadBro.process_kind` auto-detects the type of the current Ruby process by inspecting `$PROGRAM_NAME` and `/proc/self/cmdline`: returns `"web"` (Puma/Passenger/Unicorn/Falcon), `"worker"` (Sidekiq/GoodJob/SolidQueue/DelayedJob), `"console"`, `"task"`, or `"app"` as a fallback. The value is memoised after the first call.
30
+ - `process_kind` included in error event payloads so the backend knows whether an exception came from a web request or a background worker.
31
+
32
+ ## [0.2.18] - 2026-05-27
33
+
34
+ ### Added
35
+ - **N+1 detection in the gem**: SQL queries are normalised (bind parameters, numeric literals, and `IN (...)` lists replaced with `?`) and counted per request. When the same normalised query fires 5 or more times, it is flagged as `n_plus_one: true` on its aggregate entry. A backtrace is captured exactly at the N+1 threshold rather than on every execution, keeping overhead low while still pointing to the callsite.
36
+ - **SQL aggregation**: instead of shipping a raw array of every query, the gem now groups queries by normalised SQL and sends one aggregate entry per unique pattern with `count`, `total_duration_ms`, `min_duration_ms`, `max_duration_ms`, `total_allocations`, and `cached_count`. This reduces payload size on N+1-heavy requests and makes the SQL breakdown directly usable without server-side grouping.
37
+ - **View rendering aggregation**: template, partial, and collection renders are aggregated per identifier (last three path segments). Each entry carries `count`, `total_duration_ms`, `min_duration_ms`, `max_duration_ms`, `rendered_at_min/max`, and cache hit counts. Aggregation happens on the thread-local stack so there is no GC pressure from intermediate arrays.
38
+
6
39
  ## [0.2.17] - 2026-05-25
7
40
 
8
41
  ### Added
@@ -13,6 +46,12 @@
13
46
  ### Added
14
47
  - `ArObjectTracker`: subscribes to Rails' built-in `instantiation.active_record` notification to count the total number of ActiveRecord model instances hydrated during each request or background job. The count is reported as `ar_instantiation_count` in every payload. Uses a thread-local counter with an idempotent `subscribe!` guard, matching the same start/stop lifecycle as `GcTracker`. No monkey-patching required — Rails emits this event natively with a `record_count` field that accumulates correctly across batch loads.
15
48
 
49
+ ## [0.2.15] - 2026-05-24
50
+
51
+ ### Added
52
+ - **`GcTracker`**: records a GC snapshot at the start and end of every request and background job. Reports `gc_minor_runs`, `gc_major_runs`, `gc_allocated_objects`, `gc_time_ms`, and `heap_pages_increase` as a `gc_pressure` hash in every payload. Uses `GC.stat` and `GC::Profiler` with an idempotent subscribe guard; overhead is negligible when no GC cycles occur.
53
+ - **`SqlAllocListener`**: measures GC allocation deltas per SQL event by snapshotting `GC.stat[:total_allocated_objects]` in the notification `start` callback and diffing in `finish`. The delta is stored by notification ID and merged into the corresponding query's `allocations` field, allowing the dashboard to surface allocation-heavy queries independently of their duration.
54
+
16
55
  ## [0.2.14] - 2026-05-23
17
56
 
18
57
  ### Added
@@ -24,7 +24,9 @@ module DeadBro
24
24
  next unless Thread.current[THREAD_LOCAL_KEY]
25
25
 
26
26
  duration_ms = ((finished - started) * 1000.0).round(2)
27
- event = build_event(name, data, duration_ms)
27
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
28
+ start_offset_ms = tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil
29
+ event = build_event(name, data, duration_ms, start_offset_ms)
28
30
  if event && should_continue_tracking?
29
31
  Thread.current[THREAD_LOCAL_KEY] << event
30
32
  end
@@ -63,12 +65,13 @@ module DeadBro
63
65
  true
64
66
  end
65
67
 
66
- def self.build_event(name, data, duration_ms)
68
+ def self.build_event(name, data, duration_ms, start_offset_ms = nil)
67
69
  return nil unless data.is_a?(Hash)
68
70
 
69
71
  {
70
72
  event: name,
71
73
  duration_ms: duration_ms,
74
+ start_offset_ms: start_offset_ms,
72
75
  key: safe_key(data[:key]),
73
76
  keys_count: safe_keys_count(data[:keys]),
74
77
  hit: infer_hit(name, data),
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeadBro
4
+ module CpuTracker
5
+ THREAD_KEY = :dead_bro_cpu_start
6
+
7
+ def self.start_request_tracking
8
+ Thread.current[THREAD_KEY] = thread_cpu_ms
9
+ end
10
+
11
+ def self.stop_request_tracking
12
+ before = Thread.current[THREAD_KEY]
13
+ return nil if before.nil?
14
+ after = thread_cpu_ms
15
+ return nil if after.nil?
16
+ (after - before).round(2)
17
+ ensure
18
+ Thread.current[THREAD_KEY] = nil
19
+ end
20
+
21
+ def self.thread_cpu_ms
22
+ Process.clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond)
23
+ rescue
24
+ nil
25
+ end
26
+ end
27
+ end
@@ -13,12 +13,12 @@ module DeadBro
13
13
  end
14
14
 
15
15
  # Called by HttpInstrumentation when it detects a Net::HTTP request to an ES host.
16
- def self.record(method:, path:, status:, duration_ms:)
16
+ def self.record(method:, path:, status:, duration_ms:, start_offset_ms: nil)
17
17
  events = Thread.current[THREAD_LOCAL_KEY]
18
18
  return unless events
19
19
  return unless should_continue_tracking?
20
20
 
21
- events << build_event(method, path, status, duration_ms)
21
+ events << build_event(method, path, status, duration_ms, start_offset_ms)
22
22
  rescue
23
23
  end
24
24
 
@@ -120,20 +120,23 @@ module DeadBro
120
120
  duration_ms = ((finished - started) * 1000.0).round(2)
121
121
  method = payload[:method].to_s.upcase
122
122
  path = payload[:path].to_s
123
- events << build_event(method, path, payload[:status], duration_ms)
123
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
124
+ start_offset_ms = tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil
125
+ events << build_event(method, path, payload[:status], duration_ms, start_offset_ms)
124
126
  rescue
125
127
  end
126
128
  end
127
129
  rescue
128
130
  end
129
131
 
130
- def build_event(method, path, status, duration_ms)
132
+ def build_event(method, path, status, duration_ms, start_offset_ms = nil)
131
133
  {
132
134
  method: method.to_s.upcase,
133
135
  path: sanitize_path(path),
134
136
  operation: extract_operation(method, path),
135
137
  status: status,
136
- duration_ms: duration_ms
138
+ duration_ms: duration_ms,
139
+ start_offset_ms: start_offset_ms
137
140
  }
138
141
  end
139
142
  end
@@ -21,6 +21,7 @@ module DeadBro
21
21
  mod = Module.new do
22
22
  define_method(:request) do |req, body = nil, &block|
23
23
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
24
+ wall_start = Time.now
24
25
  response = nil
25
26
  error = nil
26
27
  begin
@@ -46,6 +47,9 @@ module DeadBro
46
47
  # must still be tracked; only skip the deadbro backend itself.
47
48
  skip_instrumentation = !is_es_host && uri && (uri.to_s.include?("localhost") || uri.to_s.include?("aberatii.com"))
48
49
 
50
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
51
+ start_offset_ms = tracking_start ? ((wall_start - tracking_start) * 1000.0).round(2) : nil
52
+
49
53
  if is_es_host
50
54
  # Route to elasticsearch subscriber instead of http_outgoing
51
55
  if Thread.current[DeadBro::ElasticsearchSubscriber::THREAD_LOCAL_KEY]
@@ -54,7 +58,8 @@ module DeadBro
54
58
  method: req.method,
55
59
  path: path,
56
60
  status: response && response.code.to_i,
57
- duration_ms: duration_ms
61
+ duration_ms: duration_ms,
62
+ start_offset_ms: start_offset_ms
58
63
  )
59
64
  end
60
65
  elsif !skip_instrumentation
@@ -67,6 +72,7 @@ module DeadBro
67
72
  path: (uri && uri.path) || req.path,
68
73
  status: response && response.code.to_i,
69
74
  duration_ms: duration_ms,
75
+ start_offset_ms: start_offset_ms,
70
76
  exception: error && error.class.name
71
77
  }
72
78
  if Thread.current[THREAD_LOCAL_KEY] && DeadBro::HttpInstrumentation.should_continue_tracking?
@@ -63,6 +63,7 @@ module DeadBro
63
63
  def record_redis_command(command)
64
64
  return yield unless Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
65
65
 
66
+ wall_start = Time.now
66
67
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
67
68
  error = nil
68
69
  begin
@@ -77,12 +78,15 @@ module DeadBro
77
78
 
78
79
  begin
79
80
  cmd_info = extract_command_info(command)
81
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
82
+ start_offset_ms = tracking_start ? ((wall_start - tracking_start) * 1000.0).round(2) : nil
80
83
  event = {
81
84
  event: "redis.command",
82
85
  command: cmd_info[:command],
83
86
  key: cmd_info[:key],
84
87
  args_count: cmd_info[:args_count],
85
88
  duration_ms: duration_ms,
89
+ start_offset_ms: start_offset_ms,
86
90
  db: safe_db(@db),
87
91
  error: error ? error.class.name : nil
88
92
  }
@@ -98,6 +102,7 @@ module DeadBro
98
102
  def record_redis_pipeline(pipeline)
99
103
  return yield unless Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
100
104
 
105
+ wall_start = Time.now
101
106
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
102
107
  begin
103
108
  result = yield
@@ -108,10 +113,13 @@ module DeadBro
108
113
 
109
114
  begin
110
115
  commands_count = pipeline.commands&.length || 0
116
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
117
+ start_offset_ms = tracking_start ? ((wall_start - tracking_start) * 1000.0).round(2) : nil
111
118
  event = {
112
119
  event: "redis.pipeline",
113
120
  commands_count: commands_count,
114
121
  duration_ms: duration_ms,
122
+ start_offset_ms: start_offset_ms,
115
123
  db: safe_db(@db)
116
124
  }
117
125
 
@@ -126,6 +134,7 @@ module DeadBro
126
134
  def record_redis_multi(multi)
127
135
  return yield unless Thread.current[RedisSubscriber::THREAD_LOCAL_KEY]
128
136
 
137
+ wall_start = Time.now
129
138
  start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
130
139
  begin
131
140
  result = yield
@@ -136,10 +145,13 @@ module DeadBro
136
145
 
137
146
  begin
138
147
  commands_count = multi.commands&.length || 0
148
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
149
+ start_offset_ms = tracking_start ? ((wall_start - tracking_start) * 1000.0).round(2) : nil
139
150
  event = {
140
151
  event: "redis.multi",
141
152
  commands_count: commands_count,
142
153
  duration_ms: duration_ms,
154
+ start_offset_ms: start_offset_ms,
143
155
  db: safe_db(@db)
144
156
  }
145
157
 
@@ -201,7 +213,9 @@ module DeadBro
201
213
  ActiveSupport::Notifications.subscribe(/\Aredis\..+\z/) do |name, started, finished, _unique_id, data|
202
214
  next unless Thread.current[THREAD_LOCAL_KEY]
203
215
  duration_ms = ((finished - started) * 1000.0).round(2)
204
- event = build_event(name, data, duration_ms)
216
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
217
+ start_offset_ms = tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil
218
+ event = build_event(name, data, duration_ms, start_offset_ms)
205
219
  if event && should_continue_tracking?
206
220
  Thread.current[THREAD_LOCAL_KEY] << event
207
221
  end
@@ -239,7 +253,7 @@ module DeadBro
239
253
  true
240
254
  end
241
255
 
242
- def self.build_event(name, data, duration_ms)
256
+ def self.build_event(name, data, duration_ms, start_offset_ms = nil)
243
257
  cmd = extract_command(data)
244
258
  {
245
259
  event: name.to_s,
@@ -247,6 +261,7 @@ module DeadBro
247
261
  key: cmd[:key],
248
262
  args_count: cmd[:args_count],
249
263
  duration_ms: duration_ms,
264
+ start_offset_ms: start_offset_ms,
250
265
  db: safe_db(data[:db])
251
266
  }
252
267
  rescue
@@ -106,6 +106,9 @@ module DeadBro
106
106
  duration_ms = ((finished - started) * 1000.0).round(2)
107
107
  original_sql = data[:sql]
108
108
 
109
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
110
+ start_offset_ms = tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil
111
+
109
112
  threshold = begin
110
113
  DeadBro.configuration.slow_query_threshold_ms
111
114
  rescue
@@ -133,6 +136,7 @@ module DeadBro
133
136
  sql: sanitized_sql,
134
137
  name: data[:name],
135
138
  duration_ms: duration_ms,
139
+ start_offset_ms: start_offset_ms,
136
140
  cached: data[:cached] || false,
137
141
  connection_id: data[:connection_id],
138
142
  trace: captured_trace,
@@ -167,6 +171,7 @@ module DeadBro
167
171
  total_allocations: allocations || 0,
168
172
  cached_count: (data[:cached] ? 1 : 0),
169
173
  n_plus_one: false,
174
+ start_offset_ms: start_offset_ms,
170
175
  backtrace: captured_trace,
171
176
  explain_plan: nil
172
177
  }
@@ -67,6 +67,9 @@ module DeadBro
67
67
  # Start AR object instantiation counting for this request
68
68
  DeadBro::ArObjectTracker.start_request_tracking if defined?(DeadBro::ArObjectTracker)
69
69
 
70
+ # Start CPU time tracking for this request (thread-local clock)
71
+ DeadBro::CpuTracker.start_request_tracking if defined?(DeadBro::CpuTracker)
72
+
70
73
  # Start outgoing HTTP accumulation for this request
71
74
  Thread.current[:dead_bro_http_events] = []
72
75
 
@@ -106,6 +109,7 @@ module DeadBro
106
109
  Thread.current[DeadBro::GcTracker::THREAD_KEY] = nil if defined?(DeadBro::GcTracker)
107
110
  # Bypass stop_request_tracking intentionally — cleanup only, no return value needed here.
108
111
  Thread.current[DeadBro::ArObjectTracker::THREAD_KEY] = nil if defined?(DeadBro::ArObjectTracker)
112
+ Thread.current[DeadBro::CpuTracker::THREAD_KEY] = nil if defined?(DeadBro::CpuTracker)
109
113
  Thread.current[DeadBro::TRACKING_START_TIME_KEY] = nil
110
114
  end
111
115
 
@@ -72,6 +72,9 @@ module DeadBro
72
72
  # Stop AR object instantiation tracking
73
73
  ar_instantiation_count = defined?(DeadBro::ArObjectTracker) ? DeadBro::ArObjectTracker.stop_request_tracking : nil
74
74
 
75
+ # Stop CPU time tracking — thread CPU time consumed by this request
76
+ cpu_time_ms = defined?(DeadBro::CpuTracker) ? DeadBro::CpuTracker.stop_request_tracking : nil
77
+
75
78
  # Stop view rendering tracking and get collected view events
76
79
  view_events = DeadBro::ViewRenderingSubscriber.stop_request_tracking
77
80
  view_performance = DeadBro::ViewRenderingSubscriber.analyze_view_performance(view_events)
@@ -191,6 +194,7 @@ module DeadBro
191
194
  db_connection_checkouts: db_connection_stats[:checkouts],
192
195
  gc_pressure: gc_pressure,
193
196
  ar_instantiation_count: ar_instantiation_count,
197
+ cpu_time_ms: cpu_time_ms,
194
198
  logs: DeadBro.logger.logs
195
199
  }
196
200
  client.post_metric(event_name: name, payload: payload)
@@ -214,13 +218,14 @@ module DeadBro
214
218
  DeadBro::DbConnectionSubscriber.stop_request_tracking if defined?(DeadBro::DbConnectionSubscriber)
215
219
  DeadBro::GcTracker.stop_request_tracking if defined?(DeadBro::GcTracker)
216
220
  DeadBro::ArObjectTracker.stop_request_tracking if defined?(DeadBro::ArObjectTracker)
221
+ DeadBro::CpuTracker.stop_request_tracking if defined?(DeadBro::CpuTracker)
217
222
  rescue
218
223
  # Best effort — draining must never raise from the notifications callback.
219
224
  end
220
225
 
221
226
  def self.safe_path(data)
222
227
  path = data[:path] || (data[:request] && data[:request].path)
223
- path.to_s
228
+ sanitize_string(path)
224
229
  rescue
225
230
  ""
226
231
  end
@@ -266,11 +271,16 @@ module DeadBro
266
271
  {}
267
272
  end
268
273
 
274
+ def self.sanitize_string(str)
275
+ str.to_s.gsub("\x00", "")
276
+ end
277
+
269
278
  # Recursively truncate values to reasonable sizes to avoid huge payloads
270
279
  def self.truncate_value(value, max_str: 200, max_array: 20, max_hash_keys: 30)
271
280
  case value
272
281
  when String
273
- (value.length > max_str) ? value[0, max_str] + "…" : value
282
+ sanitized = sanitize_string(value)
283
+ (sanitized.length > max_str) ? sanitized[0, max_str] + "…" : sanitized
274
284
  when Numeric, TrueClass, FalseClass, NilClass
275
285
  value
276
286
  when Array
@@ -295,7 +305,7 @@ module DeadBro
295
305
  elsif data[:request].respond_to?(:env)
296
306
  ua = data[:request].env && data[:request].env["HTTP_USER_AGENT"]
297
307
  end
298
- return ua.to_s[0..200]
308
+ return sanitize_string(ua)[0..200]
299
309
  end
300
310
 
301
311
  # Fallback to headers object/hash if present in notification data
@@ -303,7 +313,7 @@ module DeadBro
303
313
  headers = data[:headers]
304
314
  if headers.respond_to?(:[])
305
315
  ua = headers["HTTP_USER_AGENT"] || headers["User-Agent"] || headers["user-agent"]
306
- return ua.to_s[0..200]
316
+ return sanitize_string(ua)[0..200]
307
317
  elsif headers.respond_to?(:to_h)
308
318
  h = begin
309
319
  headers.to_h
@@ -311,14 +321,14 @@ module DeadBro
311
321
  {}
312
322
  end
313
323
  ua = h["HTTP_USER_AGENT"] || h["User-Agent"] || h["user-agent"]
314
- return ua.to_s[0..200]
324
+ return sanitize_string(ua)[0..200]
315
325
  end
316
326
  end
317
327
 
318
328
  # Fallback to env hash if present in notification data
319
329
  if data[:env].is_a?(Hash)
320
330
  ua = data[:env]["HTTP_USER_AGENT"]
321
- return ua.to_s[0..200]
331
+ return sanitize_string(ua)[0..200]
322
332
  end
323
333
 
324
334
  ""
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeadBro
4
- VERSION = "0.2.20"
4
+ VERSION = "0.2.22"
5
5
  end
@@ -14,24 +14,30 @@ module DeadBro
14
14
 
15
15
  def self.subscribe!(client: Client.new)
16
16
  ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_EVENT) do |_name, started, finished, _uid, data|
17
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
17
18
  add_view_event(type: "template", identifier: safe_identifier(data[:identifier]),
18
19
  duration_ms: ((finished - started) * 1000.0).round(2),
19
- rendered_at: Time.now.utc.to_i)
20
+ rendered_at: Time.now.utc.to_i,
21
+ start_offset_ms: tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil)
20
22
  end
21
23
 
22
24
  ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_EVENT) do |_name, started, finished, _uid, data|
25
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
23
26
  add_view_event(type: "partial", identifier: safe_identifier(data[:identifier]),
24
27
  duration_ms: ((finished - started) * 1000.0).round(2),
25
28
  cache_key: data[:cache_key],
26
- rendered_at: Time.now.utc.to_i)
29
+ rendered_at: Time.now.utc.to_i,
30
+ start_offset_ms: tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil)
27
31
  end
28
32
 
29
33
  ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_EVENT) do |_name, started, finished, _uid, data|
34
+ tracking_start = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
30
35
  add_view_event(type: "collection", identifier: safe_identifier(data[:identifier]),
31
36
  duration_ms: ((finished - started) * 1000.0).round(2),
32
37
  collection_count: (data[:count] || 0).to_i,
33
38
  collection_cached_count: (data[:cached_count] || 0).to_i,
34
- rendered_at: Time.now.utc.to_i)
39
+ rendered_at: Time.now.utc.to_i,
40
+ start_offset_ms: tracking_start ? ((started - tracking_start) * 1000.0).round(2) : nil)
35
41
  end
36
42
  rescue
37
43
  end
@@ -80,7 +86,8 @@ module DeadBro
80
86
  rendered_at_max: rendered_at,
81
87
  cache_hit_count: (view_info[:cache_key] ? 1 : 0),
82
88
  collection_count: view_info[:collection_count].to_i,
83
- collection_cached_count: view_info[:collection_cached_count].to_i
89
+ collection_cached_count: view_info[:collection_cached_count].to_i,
90
+ start_offset_ms: view_info[:start_offset_ms]
84
91
  }
85
92
  end
86
93
  end
data/lib/dead_bro.rb CHANGED
@@ -20,6 +20,7 @@ module DeadBro
20
20
  autoload :LightweightMemoryTracker, "dead_bro/lightweight_memory_tracker"
21
21
  autoload :GcTracker, "dead_bro/gc_tracker"
22
22
  autoload :ArObjectTracker, "dead_bro/ar_object_tracker"
23
+ autoload :CpuTracker, "dead_bro/cpu_tracker"
23
24
  autoload :MemoryHelpers, "dead_bro/memory_helpers"
24
25
  autoload :JobSubscriber, "dead_bro/job_subscriber"
25
26
  autoload :JobSqlTrackingMiddleware, "dead_bro/job_sql_tracking_middleware"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dead_bro
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.20
4
+ version: 0.2.22
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emanuel Comsa
@@ -33,6 +33,7 @@ files:
33
33
  - lib/dead_bro/collectors/sample_store.rb
34
34
  - lib/dead_bro/collectors/system.rb
35
35
  - lib/dead_bro/configuration.rb
36
+ - lib/dead_bro/cpu_tracker.rb
36
37
  - lib/dead_bro/db_connection_subscriber.rb
37
38
  - lib/dead_bro/dispatcher.rb
38
39
  - lib/dead_bro/elasticsearch_subscriber.rb