brainzlab 0.1.31 → 0.1.33

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: 5c7573fc851c73fe6b684611c10fb6799369c4817cc48b2fa7b18c6e84ac2188
4
- data.tar.gz: 792060817bb43eafa993a0a34206d7fd53b30d94ebb861205b02775243f9292d
3
+ metadata.gz: a9a9a15a426d4a6853086c9a36851e57bd14736c5d11dbeb70fb510fe5fc1c10
4
+ data.tar.gz: 19f26af342b3d9150eacb2615da08fa063cc671388f7b97a87b207cd210cf6fc
5
5
  SHA512:
6
- metadata.gz: a16f37708f020bdfa8f3aaf3131ef7cbb572b086cea2ec1567eb56e2d810042ed90b2ff8f17c1d84401b50bbe1a66f1fb221b1ad8948c2df8d28b44aecf1f605
7
- data.tar.gz: 3dd6909b4dc67fa7cb47767f6ca001e7df4547ebe5c1a99f08efe12f8d0285da58f62ef99f9ad9a9884b7526a12cd7a6018be45993b0dee131dda696fa37fb47
6
+ metadata.gz: b149c1fbf2c0f13fa34d0b2d2e6df634a1dc01f087f8174ba58fcc0bf05ea0301365721131e03441bf0af7e3b443d5a86acc5dfa141ba4fa9af2c24c610782a9
7
+ data.tar.gz: 737cd856a43c4c3555a566653692fd32d04798e464e24bad80d189610b82e81c0d0b2860cda232fae66063477b21687858c14e5178050a771af6745b9741d46f
@@ -43,6 +43,7 @@ module BrainzLab
43
43
  :pulse_flush_interval,
44
44
  :pulse_sample_rate,
45
45
  :pulse_sample_tier,
46
+ :pulse_always_keep_slow_ms,
46
47
  :pulse_excluded_paths,
47
48
  :flux_enabled,
48
49
  :flux_url,
@@ -261,6 +262,10 @@ module BrainzLab
261
262
  # BRAINZLAB_PULSE_TIER=client (or PULSE_SAMPLE_RATE) to keep everything.
262
263
  @pulse_sample_rate = (v = ENV['PULSE_SAMPLE_RATE'] || ENV['BRAINZLAB_PULSE_SAMPLE_RATE']).nil? || v.to_s.strip.empty? ? nil : v.to_f
263
264
  @pulse_sample_tier = (ENV['BRAINZLAB_PULSE_TIER'] || 'internal').to_s.strip.downcase
265
+ # Traces at/above this duration (ms) bypass sampling and are ALWAYS kept,
266
+ # so slow requests / N+1s are never sampled away (APM's whole purpose).
267
+ # Set to 0 to disable. Default 1000ms.
268
+ @pulse_always_keep_slow_ms = (ENV['BRAINZLAB_PULSE_ALWAYS_KEEP_SLOW_MS'] || 1000).to_i
264
269
  @pulse_excluded_paths = %w[/health /ping /up /assets]
265
270
 
266
271
  # Flux settings
@@ -72,11 +72,24 @@ module BrainzLab
72
72
  # effective per-tier sample rate (internal=0.1, standard=0.5, client=1.0).
73
73
  def keep_trace?(payload)
74
74
  return true if payload[:error] == true || payload['error'] == true
75
+ # Always keep SLOW traces, regardless of sampling. Surfacing slow
76
+ # requests (N+1s, slow endpoints) is the whole point of APM — sampling
77
+ # them out blinds debugging, which is exactly how an inquiries-page N+1
78
+ # stayed invisible on a 10%-sampled app.
79
+ return true if slow_trace?(payload)
75
80
 
76
81
  rate = @config.effective_pulse_sample_rate
77
82
  rate >= 1.0 || rand < rate
78
83
  end
79
84
 
85
+ def slow_trace?(payload)
86
+ threshold = @config.pulse_always_keep_slow_ms.to_i
87
+ return false unless threshold.positive?
88
+
89
+ duration = payload[:duration_ms] || payload['duration_ms']
90
+ !duration.nil? && duration.to_f >= threshold
91
+ end
92
+
80
93
  def buffer_trace(payload)
81
94
  should_flush = false
82
95
 
@@ -44,8 +44,8 @@ module BrainzLab
44
44
  duration_ms = ((ended_at - trace[:started_at]) * 1000).round(2)
45
45
 
46
46
  payload = trace.merge(
47
- ended_at: ended_at.iso8601(3),
48
- started_at: trace[:started_at].utc.iso8601(3),
47
+ ended_at: iso_ts(ended_at),
48
+ started_at: iso_ts(trace[:started_at]),
49
49
  duration_ms: duration_ms,
50
50
  error: error,
51
51
  error_class: error_class,
@@ -99,8 +99,8 @@ module BrainzLab
99
99
  parent_span_id: span[:parent_span_id],
100
100
  name: span[:name],
101
101
  kind: span[:kind],
102
- started_at: span[:started_at].utc.iso8601(3),
103
- ended_at: span[:ended_at].utc.iso8601(3),
102
+ started_at: iso_ts(span[:started_at]),
103
+ ended_at: iso_ts(span[:ended_at]),
104
104
  duration_ms: span[:duration_ms],
105
105
  error: span[:error],
106
106
  error_class: span[:error_class],
@@ -108,6 +108,21 @@ module BrainzLab
108
108
  data: span[:data]
109
109
  }.compact
110
110
  end
111
+
112
+ # Rails 8 changed ActiveSupport::Notifications::Event#time/#end to return a
113
+ # Float (monotonic), so spans carry Float timestamps. Calling #utc on them
114
+ # raised NoMethodError, crashing trace serialization (error/job traces) on
115
+ # Rails 8 apps. Coerce Time/Float/Integer/String safely.
116
+ def iso_ts(ts)
117
+ return nil if ts.nil?
118
+
119
+ case ts
120
+ when Time, DateTime then ts.utc.iso8601(3)
121
+ when Numeric then Time.at(ts).utc.iso8601(3)
122
+ when String then ts
123
+ else ts.to_s
124
+ end
125
+ end
111
126
  end
112
127
  end
113
128
  end
@@ -30,8 +30,12 @@ module BrainzLab
30
30
  end
31
31
  end
32
32
 
33
- # Add request context middleware (runs early)
34
- app.middleware.insert_after ActionDispatch::RequestId, BrainzLab::Rails::Middleware
33
+ # Add request context middleware. Gated on `enabled` so the kill switch
34
+ # (BRAINZLAB_SDK_ENABLED=false) fully removes it — it previously loaded
35
+ # unconditionally, so disabling didn't stop its session interference.
36
+ if BrainzLab.configuration.enabled
37
+ app.middleware.insert_after ActionDispatch::RequestId, BrainzLab::Rails::Middleware
38
+ end
35
39
 
36
40
  # Add DevTools middlewares if enabled
37
41
  if BrainzLab.configuration.devtools_enabled
@@ -220,16 +224,10 @@ module BrainzLab
220
224
  # Store request_id in thread local for log subscriber
221
225
  Thread.current[:brainzlab_request_id] = request_id
222
226
 
223
- # Capture session_id - access session to ensure it's loaded
224
- if request.session.respond_to?(:id)
225
- # Force session load by accessing it
226
- session_id = begin
227
- request.session.id
228
- rescue StandardError
229
- nil
230
- end
231
- context.session_id = session_id.to_s if session_id.present?
232
- end
227
+ # NOTE: session_id is captured AFTER @app.call (see below), never here.
228
+ # Forcing a session load on the way in — before the app's session
229
+ # middleware and any reset_session (login) — drops the session on
230
+ # cookie/active_record_store apps, causing login loops.
233
231
 
234
232
  # Capture full request info for Reflex
235
233
  context.request_method = request.request_method
@@ -276,6 +274,19 @@ module BrainzLab
276
274
 
277
275
  status, headers, response = @app.call(env)
278
276
 
277
+ # Capture session_id now that the app (and its session middleware) has
278
+ # run. Read it only if the session was already loaded — never force a
279
+ # load — so we never disturb the session lifecycle / Set-Cookie.
280
+ begin
281
+ rack_session = env['rack.session']
282
+ if rack_session && (!rack_session.respond_to?(:loaded?) || rack_session.loaded?)
283
+ sid = rack_session.respond_to?(:id) ? rack_session.id : nil
284
+ context.session_id = sid.to_s if sid && !sid.to_s.empty?
285
+ end
286
+ rescue StandardError
287
+ nil
288
+ end
289
+
279
290
  # Add breadcrumb for response
280
291
  BrainzLab::Reflex.add_breadcrumb(
281
292
  "Response #{status}",
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BrainzLab
4
- VERSION = '0.1.31'
4
+ VERSION = '0.1.33'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainzlab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.31
4
+ version: 0.1.33
5
5
  platform: ruby
6
6
  authors:
7
7
  - BrainzLab