brainzlab 0.1.33 → 0.1.35

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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +38 -0
  3. data/lib/brainzlab/beacon/client.rb +4 -6
  4. data/lib/brainzlab/configuration.rb +363 -258
  5. data/lib/brainzlab/cortex/client.rb +4 -6
  6. data/lib/brainzlab/debug.rb +6 -6
  7. data/lib/brainzlab/dendrite/client.rb +4 -6
  8. data/lib/brainzlab/development/logger.rb +2 -2
  9. data/lib/brainzlab/development/store.rb +2 -4
  10. data/lib/brainzlab/devtools/middleware/error_page.rb +1 -1
  11. data/lib/brainzlab/errors.rb +5 -12
  12. data/lib/brainzlab/instrumentation/action_cable.rb +4 -4
  13. data/lib/brainzlab/instrumentation/action_controller.rb +12 -12
  14. data/lib/brainzlab/instrumentation/action_dispatch.rb +1 -1
  15. data/lib/brainzlab/instrumentation/action_mailbox.rb +7 -13
  16. data/lib/brainzlab/instrumentation/action_view.rb +29 -33
  17. data/lib/brainzlab/instrumentation/active_job.rb +6 -10
  18. data/lib/brainzlab/instrumentation/active_record.rb +53 -25
  19. data/lib/brainzlab/instrumentation/active_storage.rb +4 -4
  20. data/lib/brainzlab/instrumentation/active_support_cache.rb +6 -8
  21. data/lib/brainzlab/instrumentation/aws.rb +10 -7
  22. data/lib/brainzlab/instrumentation/grape.rb +37 -2
  23. data/lib/brainzlab/instrumentation/rails_deprecation.rb +1 -1
  24. data/lib/brainzlab/instrumentation/railties.rb +2 -2
  25. data/lib/brainzlab/nerve/client.rb +4 -6
  26. data/lib/brainzlab/pulse/client.rb +1 -1
  27. data/lib/brainzlab/pulse/explain_capture.rb +123 -0
  28. data/lib/brainzlab/pulse.rb +6 -1
  29. data/lib/brainzlab/rails/railtie.rb +1 -3
  30. data/lib/brainzlab/reflex/client.rb +1 -1
  31. data/lib/brainzlab/sentinel/client.rb +4 -6
  32. data/lib/brainzlab/signal/client.rb +16 -2
  33. data/lib/brainzlab/signal.rb +8 -0
  34. data/lib/brainzlab/synapse/client.rb +4 -6
  35. data/lib/brainzlab/testing/event_store.rb +3 -3
  36. data/lib/brainzlab/testing/matchers.rb +4 -4
  37. data/lib/brainzlab/testing.rb +5 -3
  38. data/lib/brainzlab/utilities/route_path_normalizer.rb +91 -0
  39. data/lib/brainzlab/utilities.rb +1 -0
  40. data/lib/brainzlab/vault/client.rb +4 -6
  41. data/lib/brainzlab/version.rb +1 -1
  42. data/lib/brainzlab/vision/client.rb +3 -3
  43. data/lib/brainzlab.rb +1 -0
  44. data/lib/fluyenta-ruby.rb +1 -1
  45. metadata +3 -1
@@ -45,6 +45,9 @@ module BrainzLab
45
45
  :pulse_sample_tier,
46
46
  :pulse_always_keep_slow_ms,
47
47
  :pulse_excluded_paths,
48
+ :pulse_explain_enabled,
49
+ :pulse_explain_threshold,
50
+ :pulse_explain_timeout,
48
51
  :flux_enabled,
49
52
  :flux_url,
50
53
  :flux_api_key,
@@ -196,262 +199,13 @@ module BrainzLab
196
199
  }.freeze
197
200
 
198
201
  def initialize
199
- # SDK enabled flag — set BRAINZLAB_SDK_ENABLED=false to completely disable
200
- @enabled = ENV.fetch('BRAINZLAB_SDK_ENABLED', 'true') != 'false'
201
-
202
- # Authentication
203
- @secret_key = ENV.fetch('BRAINZLAB_SECRET_KEY', nil)
204
-
205
- # Environment
206
- @environment = ENV['BRAINZLAB_ENVIRONMENT'] || detect_environment
207
- @service = ENV.fetch('BRAINZLAB_SERVICE', nil)
208
- @host = ENV['BRAINZLAB_HOST'] || detect_host
209
-
210
- # App name for auto-provisioning
211
- @app_name = ENV.fetch('BRAINZLAB_APP_NAME', nil)
212
-
213
- # Git context
214
- @commit = ENV['GIT_COMMIT'] || ENV['COMMIT_SHA'] || detect_git_commit
215
- @branch = ENV['GIT_BRANCH'] || ENV['BRANCH_NAME'] || detect_git_branch
216
-
217
- # Debug mode - enables verbose logging
218
- @debug = ENV['BRAINZLAB_DEBUG'] == 'true'
219
-
220
- # SDK mode - :production (default) or :development (offline, local storage)
221
- @mode = ENV['BRAINZLAB_MODE']&.to_sym || :production
222
-
223
- # Development mode settings
224
- @development_db_path = ENV['BRAINZLAB_DEV_DB_PATH'] || 'tmp/brainzlab.sqlite3'
225
- @development_log_output = $stdout
226
-
227
- # Disable self-tracking - prevents services from tracking to themselves
228
- # e.g., Recall won't log to itself, Reflex won't track errors to itself
229
- @disable_self_tracking = ENV.fetch('BRAINZLAB_DISABLE_SELF_TRACKING', 'true') == 'true'
230
-
231
- # Recall settings
232
- @recall_enabled = true
233
- @recall_url = ENV['RECALL_URL'] || detect_product_url('recall')
234
- @recall_min_level = :debug
235
- @recall_buffer_size = 50
236
- @recall_flush_interval = 5
237
- @recall_master_key = ENV.fetch('RECALL_MASTER_KEY', nil)
238
- @recall_auto_provision = true
239
-
240
- # Reflex settings
241
- @reflex_enabled = true
242
- @reflex_url = ENV['REFLEX_URL'] || detect_product_url('reflex')
243
- @reflex_api_key = ENV.fetch('REFLEX_API_KEY', nil)
244
- @reflex_master_key = ENV.fetch('REFLEX_MASTER_KEY', nil)
245
- @reflex_auto_provision = true
246
- @reflex_excluded_exceptions = []
247
- @reflex_before_send = nil
248
- @reflex_sample_rate = nil
249
- @reflex_fingerprint = nil # Custom fingerprint callback
250
-
251
- # Pulse settings
252
- @pulse_enabled = true
253
- @pulse_url = ENV['PULSE_URL'] || detect_product_url('pulse')
254
- @pulse_api_key = ENV.fetch('PULSE_API_KEY', nil)
255
- @pulse_master_key = ENV.fetch('PULSE_MASTER_KEY', nil)
256
- @pulse_auto_provision = true
257
- @pulse_buffer_size = 50
258
- @pulse_flush_interval = 5
259
- # Trace sampling. Explicit rate (0.0–1.0) wins; otherwise derived from the
260
- # tier. Defaults to the "internal" tier so the Brainz fleet samples ~10%
261
- # (error traces are always kept). Client-facing apps set
262
- # BRAINZLAB_PULSE_TIER=client (or PULSE_SAMPLE_RATE) to keep everything.
263
- @pulse_sample_rate = (v = ENV['PULSE_SAMPLE_RATE'] || ENV['BRAINZLAB_PULSE_SAMPLE_RATE']).nil? || v.to_s.strip.empty? ? nil : v.to_f
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
269
- @pulse_excluded_paths = %w[/health /ping /up /assets]
270
-
271
- # Flux settings
272
- @flux_enabled = true
273
- @flux_url = ENV['FLUX_URL'] || 'https://flux.brainzlab.ai'
274
- @flux_api_key = ENV.fetch('FLUX_API_KEY', nil)
275
- @flux_ingest_key = ENV.fetch('FLUX_INGEST_KEY', nil)
276
- @flux_master_key = ENV.fetch('FLUX_MASTER_KEY', nil)
277
- @flux_auto_provision = true
278
- @flux_buffer_size = 100
279
- @flux_flush_interval = 5
280
-
281
- # Signal settings
282
- @signal_enabled = true
283
- @signal_url = ENV['SIGNAL_URL'] || detect_product_url('signal')
284
- @signal_api_key = ENV.fetch('SIGNAL_API_KEY', nil)
285
- @signal_ingest_key = ENV.fetch('SIGNAL_INGEST_KEY', nil)
286
- @signal_master_key = ENV.fetch('SIGNAL_MASTER_KEY', nil)
287
- @signal_auto_provision = true
288
-
289
- # Vault settings
290
- @vault_enabled = true
291
- @vault_url = ENV['VAULT_URL'] || 'https://vault.brainzlab.ai'
292
- @vault_api_key = ENV.fetch('VAULT_API_KEY', nil)
293
- @vault_master_key = ENV.fetch('VAULT_MASTER_KEY', nil)
294
- @vault_auto_provision = true
295
- @vault_cache_enabled = true
296
- @vault_cache_ttl = 300 # 5 minutes
297
- @vault_auto_load = ENV.fetch('VAULT_AUTO_LOAD', 'false') == 'true' # Auto-load secrets into ENV
298
- @vault_load_provider_keys = true # Also load provider keys (OpenAI, etc.)
299
-
300
- # Vision settings (AI browser automation)
301
- @vision_enabled = true
302
- @vision_url = ENV['VISION_URL'] || 'https://vision.brainzlab.ai'
303
- @vision_api_key = ENV.fetch('VISION_API_KEY', nil)
304
- @vision_ingest_key = ENV.fetch('VISION_INGEST_KEY', nil)
305
- @vision_master_key = ENV.fetch('VISION_MASTER_KEY', nil)
306
- @vision_auto_provision = true
307
- @vision_default_model = ENV['VISION_DEFAULT_MODEL'] || 'claude-sonnet-4'
308
- @vision_default_browser_provider = ENV['VISION_DEFAULT_BROWSER_PROVIDER'] || 'local'
309
-
310
- # Cortex settings (feature flags)
311
- @cortex_enabled = true
312
- @cortex_url = ENV['CORTEX_URL'] || 'https://cortex.brainzlab.ai'
313
- @cortex_api_key = ENV.fetch('CORTEX_API_KEY', nil)
314
- @cortex_master_key = ENV.fetch('CORTEX_MASTER_KEY', nil)
315
- @cortex_auto_provision = true
316
- @cortex_cache_enabled = true
317
- @cortex_cache_ttl = 60 # 1 minute
318
- @cortex_default_context = {}
319
-
320
- # Beacon settings (uptime monitoring)
321
- @beacon_enabled = true
322
- @beacon_url = ENV['BEACON_URL'] || 'https://beacon.brainzlab.ai'
323
- @beacon_api_key = ENV.fetch('BEACON_API_KEY', nil)
324
- @beacon_master_key = ENV.fetch('BEACON_MASTER_KEY', nil)
325
- @beacon_auto_provision = true
326
-
327
- # Nerve settings (job monitoring)
328
- @nerve_enabled = true
329
- @nerve_url = ENV['NERVE_URL'] || 'https://nerve.brainzlab.ai'
330
- @nerve_api_key = ENV.fetch('NERVE_API_KEY', nil)
331
- @nerve_master_key = ENV.fetch('NERVE_MASTER_KEY', nil)
332
- @nerve_auto_provision = true
333
-
334
- # Dendrite settings (AI documentation)
335
- @dendrite_enabled = true
336
- @dendrite_url = ENV['DENDRITE_URL'] || 'https://dendrite.brainzlab.ai'
337
- @dendrite_api_key = ENV.fetch('DENDRITE_API_KEY', nil)
338
- @dendrite_master_key = ENV.fetch('DENDRITE_MASTER_KEY', nil)
339
- @dendrite_auto_provision = true
340
-
341
- # Sentinel settings (host monitoring)
342
- @sentinel_enabled = true
343
- @sentinel_url = ENV['SENTINEL_URL'] || 'https://sentinel.brainzlab.ai'
344
- @sentinel_api_key = ENV.fetch('SENTINEL_API_KEY', nil)
345
- @sentinel_agent_key = ENV.fetch('SENTINEL_AGENT_KEY', nil)
346
- @sentinel_master_key = ENV.fetch('SENTINEL_MASTER_KEY', nil)
347
- @sentinel_auto_provision = true
348
-
349
- # Synapse settings (AI development orchestration)
350
- @synapse_enabled = true
351
- @synapse_url = ENV['SYNAPSE_URL'] || 'https://synapse.brainzlab.ai'
352
- @synapse_api_key = ENV.fetch('SYNAPSE_API_KEY', nil)
353
- @synapse_master_key = ENV.fetch('SYNAPSE_MASTER_KEY', nil)
354
- @synapse_auto_provision = true
355
-
356
- # Filtering
357
- @scrub_fields = %i[password password_confirmation token api_key secret]
358
-
359
- # Internal logger for debugging SDK issues
360
- @logger = nil
361
-
362
- # Debug callbacks
363
- # Called when an SDK error occurs (lambda/proc receiving error object and context hash)
364
- @on_error = nil
365
- # Called before each API request (lambda/proc receiving service, method, path, and payload)
366
- @on_send = nil
367
-
368
- # Instrumentation
369
- @instrument_http = true # Enable HTTP client instrumentation (Net::HTTP, Faraday, HTTParty)
370
- @instrument_active_record = true # AR breadcrumbs for Reflex
371
- @instrument_redis = true # Redis command instrumentation
372
- @instrument_sidekiq = true # Sidekiq job instrumentation
373
- @instrument_graphql = true # GraphQL query instrumentation
374
- @instrument_mongodb = true # MongoDB/Mongoid instrumentation
375
- @instrument_elasticsearch = true # Elasticsearch instrumentation
376
- @instrument_action_controller = true # ActionController instrumentation (requests, redirects, filters)
377
- @instrument_action_view = true # ActionView instrumentation (templates, partials, collections)
378
- @instrument_action_mailer = true # ActionMailer instrumentation
379
- @instrument_active_job = true # ActiveJob instrumentation (enqueue, perform, retry, discard)
380
- @instrument_active_support_cache = true # ActiveSupport::Cache instrumentation (read, write, fetch)
381
- @instrument_delayed_job = true # Delayed::Job instrumentation
382
- @instrument_grape = true # Grape API instrumentation
383
- @instrument_solid_queue = true # Solid Queue job instrumentation
384
- @instrument_good_job = true # GoodJob instrumentation
385
- @instrument_resque = true # Resque instrumentation
386
- @instrument_excon = true # Excon HTTP client instrumentation
387
- @instrument_typhoeus = true # Typhoeus HTTP client instrumentation
388
- @instrument_dalli = true # Dalli/Memcached instrumentation
389
- @instrument_aws = true # AWS SDK instrumentation
390
- @instrument_stripe = true # Stripe API instrumentation
391
- @instrument_active_storage = true # ActiveStorage instrumentation (uploads, downloads, transforms)
392
- @instrument_action_cable = true # ActionCable WebSocket instrumentation
393
- @instrument_action_dispatch = true # ActionDispatch instrumentation (middleware, redirects, requests)
394
- @instrument_rails_deprecation = true # Rails deprecation warnings tracking
395
- @instrument_action_mailbox = true # ActionMailbox inbound email processing instrumentation
396
- @instrument_railties = true # Railties config initializer loading instrumentation
397
- @http_ignore_hosts = %w[localhost 127.0.0.1]
398
- @redis_ignore_commands = %w[ping info] # Commands to skip tracking
399
-
400
- # ActiveRecord slow-query / N+1 detection — all tunable so a single chatty
401
- # default can't flood Recall. Slow-query duration is APM data (Pulse already
402
- # records it), so it defaults to a quiet level (:debug) and a higher threshold.
403
- @slow_query_threshold = (ENV['BRAINZLAB_SLOW_QUERY_THRESHOLD'] || 500).to_i # ms (was hardcoded 100)
404
- @very_slow_query_threshold = (ENV['BRAINZLAB_VERY_SLOW_QUERY_THRESHOLD'] || 2000).to_i # ms (was hardcoded 1000)
405
- @slow_query_log_level = (ENV['BRAINZLAB_SLOW_QUERY_LOG_LEVEL'] || 'debug').to_sym # was :warn → floods
406
- @very_slow_query_log_level = (ENV['BRAINZLAB_VERY_SLOW_QUERY_LOG_LEVEL'] || 'warn').to_sym
407
- @n_plus_one_threshold = (ENV['BRAINZLAB_N_PLUS_ONE_THRESHOLD'] || 10).to_i # was hardcoded 5
408
- @n_plus_one_log_level = (ENV['BRAINZLAB_N_PLUS_ONE_LOG_LEVEL'] || 'debug').to_sym # was :warning
409
-
410
- # Per-instrumentation slow / very-slow thresholds (ms) — all env-tunable so a
411
- # single low default can't flood Recall or wrongly escalate normal latency to
412
- # :error. Defaults preserve prior behaviour EXCEPT cache, which is raised off
413
- # DB-backed Solid Cache's normal write latency (~50ms paid a DB round-trip and
414
- # tripped the old VERY_SLOW=50 → thousands of false :error logs/hour).
415
- @slow_request_threshold = (ENV['BRAINZLAB_SLOW_REQUEST_THRESHOLD'] || 500).to_i
416
- @very_slow_request_threshold = (ENV['BRAINZLAB_VERY_SLOW_REQUEST_THRESHOLD'] || 2000).to_i
417
- @slow_cache_threshold = (ENV['BRAINZLAB_SLOW_CACHE_THRESHOLD'] || 100).to_i # was 10
418
- @very_slow_cache_threshold = (ENV['BRAINZLAB_VERY_SLOW_CACHE_THRESHOLD'] || 500).to_i # was 50 → flooded :error
419
- @slow_render_threshold = (ENV['BRAINZLAB_SLOW_RENDER_THRESHOLD'] || 50).to_i
420
- @very_slow_render_threshold = (ENV['BRAINZLAB_VERY_SLOW_RENDER_THRESHOLD'] || 200).to_i
421
- @slow_storage_threshold = (ENV['BRAINZLAB_SLOW_STORAGE_THRESHOLD'] || 500).to_i
422
- @very_slow_storage_threshold = (ENV['BRAINZLAB_VERY_SLOW_STORAGE_THRESHOLD'] || 2000).to_i
423
- @slow_cable_threshold = (ENV['BRAINZLAB_SLOW_CABLE_THRESHOLD'] || 100).to_i
424
- @very_slow_cable_threshold = (ENV['BRAINZLAB_VERY_SLOW_CABLE_THRESHOLD'] || 500).to_i
425
- @slow_initializer_threshold = (ENV['BRAINZLAB_SLOW_INITIALIZER_THRESHOLD'] || 100).to_i
426
- @very_slow_initializer_threshold = (ENV['BRAINZLAB_VERY_SLOW_INITIALIZER_THRESHOLD'] || 500).to_i
427
- @slow_middleware_threshold = (ENV['BRAINZLAB_SLOW_MIDDLEWARE_THRESHOLD'] || 50).to_i
428
- @very_slow_middleware_threshold = (ENV['BRAINZLAB_VERY_SLOW_MIDDLEWARE_THRESHOLD'] || 200).to_i
429
- @slow_mailbox_threshold = (ENV['BRAINZLAB_SLOW_MAILBOX_THRESHOLD'] || 1000).to_i
430
- @very_slow_mailbox_threshold = (ENV['BRAINZLAB_VERY_SLOW_MAILBOX_THRESHOLD'] || 5000).to_i
431
- @slow_job_threshold = (ENV['BRAINZLAB_SLOW_JOB_THRESHOLD'] || 5000).to_i
432
- @very_slow_job_threshold = (ENV['BRAINZLAB_VERY_SLOW_JOB_THRESHOLD'] || 30_000).to_i
433
-
434
- # Log formatter settings
435
- @log_formatter_enabled = true
436
- @log_formatter_colors = nil # auto-detect TTY
437
- @log_formatter_hide_assets = false
438
- @log_formatter_compact_assets = true
439
- @log_formatter_show_params = true
440
-
441
- # DevTools settings (development error page and debug panel)
442
- @devtools_enabled = true
443
- @devtools_error_page_enabled = true
444
- @devtools_debug_panel_enabled = true
445
- @devtools_allowed_environments = %w[development test]
446
- @devtools_allowed_ips = ['127.0.0.1', '::1', '172.16.0.0/12', '192.168.0.0/16', '10.0.0.0/8']
447
- @devtools_asset_path = '/__brainzlab__'
448
- @devtools_panel_position = 'bottom-right'
449
- @devtools_expand_by_default = false
450
-
451
- # Rails instrumentation delegation
452
- # When true, brainzlab-rails gem handles Rails-specific instrumentation
453
- # SDK will only install non-Rails instrumentation (HTTP clients, Redis, etc.)
454
- @rails_instrumentation_handled_externally = false
202
+ init_core_defaults
203
+ init_observability_defaults
204
+ init_platform_defaults
205
+ init_runtime_defaults
206
+ init_instrumentation_defaults
207
+ init_threshold_defaults
208
+ init_presentation_defaults
455
209
  end
456
210
 
457
211
  def recall_min_level=(level)
@@ -498,6 +252,63 @@ module BrainzLab
498
252
  @enabled && !@secret_key.nil? && !@secret_key.empty?
499
253
  end
500
254
 
255
+ # Settings validated eagerly by #validate!, grouped by the shape they must
256
+ # have. Kept as data so adding a setting is a one-line change here rather
257
+ # than another branch in the validation logic.
258
+ SAMPLE_RATE_SETTINGS = %i[pulse_sample_rate reflex_sample_rate].freeze
259
+
260
+ POSITIVE_INTEGER_SETTINGS = %i[
261
+ recall_buffer_size recall_flush_interval
262
+ pulse_buffer_size pulse_flush_interval
263
+ flux_buffer_size flux_flush_interval
264
+ pulse_explain_timeout
265
+ ].freeze
266
+
267
+ NON_NEGATIVE_NUMBER_SETTINGS = %i[
268
+ pulse_always_keep_slow_ms
269
+ slow_query_threshold very_slow_query_threshold
270
+ slow_request_threshold very_slow_request_threshold
271
+ n_plus_one_threshold pulse_explain_threshold
272
+ ].freeze
273
+
274
+ # Validate the configuration and raise on anything that would misbehave
275
+ # silently at runtime. Called by BrainzLab.configure once the user's block
276
+ # has been applied, so a typo surfaces at boot rather than as missing
277
+ # telemetry days later.
278
+ #
279
+ # @return [Configuration] self, when every setting is valid
280
+ # @raise [ConfigurationError] listing every invalid setting at once
281
+ def validate!
282
+ problems = []
283
+
284
+ SAMPLE_RATE_SETTINGS.each do |setting|
285
+ value = public_send(setting)
286
+ next if value.nil? # nil means "fall back to the tier default"
287
+
288
+ problems << "#{setting} must be a number between 0 and 1 (got #{value.inspect})" unless value.is_a?(Numeric) && value.between?(0, 1)
289
+ end
290
+
291
+ POSITIVE_INTEGER_SETTINGS.each do |setting|
292
+ value = public_send(setting)
293
+ problems << "#{setting} must be a positive number (got #{value.inspect})" unless positive_number?(value)
294
+ end
295
+
296
+ NON_NEGATIVE_NUMBER_SETTINGS.each do |setting|
297
+ value = public_send(setting)
298
+ problems << "#{setting} must be a non-negative number (got #{value.inspect})" unless value.is_a?(Numeric) && !value.negative?
299
+ end
300
+
301
+ return self if problems.empty?
302
+
303
+ raise ConfigurationError.new(
304
+ "Invalid BrainzLab configuration: #{problems.join('; ')}",
305
+ hint: 'Fix these values in your BrainzLab.configure block or the matching environment variables. ' \
306
+ 'Sample rates are fractions (0.5 = 50%), not percentages.',
307
+ code: 'invalid_configuration',
308
+ context: { problems: problems }
309
+ )
310
+ end
311
+
501
312
  def enabled?
502
313
  @enabled == true
503
314
  end
@@ -526,7 +337,7 @@ module BrainzLab
526
337
  PULSE_TIER_SAMPLE_RATES = {
527
338
  'internal' => 0.1,
528
339
  'standard' => 0.5,
529
- 'client' => 1.0,
340
+ 'client' => 1.0,
530
341
  'critical' => 1.0
531
342
  }.freeze
532
343
 
@@ -642,7 +453,11 @@ module BrainzLab
642
453
  @synapse_url
643
454
  ].compact
644
455
 
645
- urls.filter_map { |url| URI.parse(url).host rescue nil }.uniq
456
+ urls.filter_map do |url|
457
+ URI.parse(url).host
458
+ rescue StandardError
459
+ nil
460
+ end.uniq
646
461
  end
647
462
  end
648
463
 
@@ -714,6 +529,296 @@ module BrainzLab
714
529
 
715
530
  private
716
531
 
532
+ # Core identity: enablement, auth, environment, git context, SDK mode.
533
+ def init_core_defaults
534
+ # SDK enabled flag — set BRAINZLAB_SDK_ENABLED=false to completely disable
535
+ @enabled = ENV.fetch('BRAINZLAB_SDK_ENABLED', 'true') != 'false'
536
+
537
+ # Authentication
538
+ @secret_key = ENV.fetch('BRAINZLAB_SECRET_KEY', nil)
539
+
540
+ # Environment
541
+ @environment = ENV['BRAINZLAB_ENVIRONMENT'] || detect_environment
542
+ @service = ENV.fetch('BRAINZLAB_SERVICE', nil)
543
+ @host = ENV['BRAINZLAB_HOST'] || detect_host
544
+
545
+ # App name for auto-provisioning
546
+ @app_name = ENV.fetch('BRAINZLAB_APP_NAME', nil)
547
+
548
+ # Git context
549
+ @commit = ENV['GIT_COMMIT'] || ENV['COMMIT_SHA'] || detect_git_commit
550
+ @branch = ENV['GIT_BRANCH'] || ENV['BRANCH_NAME'] || detect_git_branch
551
+
552
+ # Debug mode - enables verbose logging
553
+ @debug = ENV['BRAINZLAB_DEBUG'] == 'true'
554
+
555
+ # SDK mode - :production (default) or :development (offline, local storage)
556
+ @mode = ENV['BRAINZLAB_MODE']&.to_sym || :production
557
+
558
+ # Development mode settings
559
+ @development_db_path = ENV['BRAINZLAB_DEV_DB_PATH'] || 'tmp/brainzlab.sqlite3'
560
+ @development_log_output = $stdout
561
+
562
+ # Disable self-tracking - prevents services from tracking to themselves
563
+ # e.g., Recall won't log to itself, Reflex won't track errors to itself
564
+ @disable_self_tracking = ENV.fetch('BRAINZLAB_DISABLE_SELF_TRACKING', 'true') == 'true'
565
+ end
566
+
567
+ # The five observability products: Recall, Reflex, Pulse, Flux, Signal.
568
+ def init_observability_defaults
569
+ # Recall settings
570
+ @recall_enabled = true
571
+ @recall_url = ENV['RECALL_URL'] || detect_product_url('recall')
572
+ @recall_min_level = :debug
573
+ @recall_buffer_size = 50
574
+ @recall_flush_interval = 5
575
+ @recall_master_key = ENV.fetch('RECALL_MASTER_KEY', nil)
576
+ @recall_auto_provision = true
577
+
578
+ # Reflex settings
579
+ @reflex_enabled = true
580
+ @reflex_url = ENV['REFLEX_URL'] || detect_product_url('reflex')
581
+ @reflex_api_key = ENV.fetch('REFLEX_API_KEY', nil)
582
+ @reflex_master_key = ENV.fetch('REFLEX_MASTER_KEY', nil)
583
+ @reflex_auto_provision = true
584
+ @reflex_excluded_exceptions = []
585
+ @reflex_before_send = nil
586
+ @reflex_sample_rate = nil
587
+ @reflex_fingerprint = nil # Custom fingerprint callback
588
+
589
+ # Pulse settings
590
+ @pulse_enabled = true
591
+ @pulse_url = ENV['PULSE_URL'] || detect_product_url('pulse')
592
+ @pulse_api_key = ENV.fetch('PULSE_API_KEY', nil)
593
+ @pulse_master_key = ENV.fetch('PULSE_MASTER_KEY', nil)
594
+ @pulse_auto_provision = true
595
+ @pulse_buffer_size = 50
596
+ @pulse_flush_interval = 5
597
+ # Trace sampling. Explicit rate (0.0–1.0) wins; otherwise derived from the
598
+ # tier. Defaults to the "internal" tier so the Brainz fleet samples ~10%
599
+ # (error traces are always kept). Client-facing apps set
600
+ # BRAINZLAB_PULSE_TIER=client (or PULSE_SAMPLE_RATE) to keep everything.
601
+ @pulse_sample_rate = (v = ENV['PULSE_SAMPLE_RATE'] || ENV.fetch('BRAINZLAB_PULSE_SAMPLE_RATE', nil)).nil? || v.to_s.strip.empty? ? nil : v.to_f
602
+ @pulse_sample_tier = (ENV['BRAINZLAB_PULSE_TIER'] || 'internal').to_s.strip.downcase
603
+ # Traces at/above this duration (ms) bypass sampling and are ALWAYS kept,
604
+ # so slow requests / N+1s are never sampled away (APM's whole purpose).
605
+ # Set to 0 to disable. Default 1000ms.
606
+ @pulse_always_keep_slow_ms = (ENV['BRAINZLAB_PULSE_ALWAYS_KEEP_SLOW_MS'] || 1000).to_i
607
+ @pulse_excluded_paths = %w[/health /ping /up /assets]
608
+
609
+ # Flux settings
610
+ @flux_enabled = true
611
+ @flux_url = ENV['FLUX_URL'] || 'https://flux.brainzlab.ai'
612
+ @flux_api_key = ENV.fetch('FLUX_API_KEY', nil)
613
+ @flux_ingest_key = ENV.fetch('FLUX_INGEST_KEY', nil)
614
+ @flux_master_key = ENV.fetch('FLUX_MASTER_KEY', nil)
615
+ @flux_auto_provision = true
616
+ @flux_buffer_size = 100
617
+ @flux_flush_interval = 5
618
+
619
+ # Signal settings
620
+ @signal_enabled = true
621
+ @signal_url = ENV['SIGNAL_URL'] || detect_product_url('signal')
622
+ @signal_api_key = ENV.fetch('SIGNAL_API_KEY', nil)
623
+ @signal_ingest_key = ENV.fetch('SIGNAL_INGEST_KEY', nil)
624
+ @signal_master_key = ENV.fetch('SIGNAL_MASTER_KEY', nil)
625
+ @signal_auto_provision = true
626
+ end
627
+
628
+ # The platform services: Vault, Vision, Cortex, Beacon, Nerve, Dendrite, Sentinel, Synapse.
629
+ def init_platform_defaults
630
+ # Vault settings
631
+ @vault_enabled = true
632
+ @vault_url = ENV['VAULT_URL'] || 'https://vault.brainzlab.ai'
633
+ @vault_api_key = ENV.fetch('VAULT_API_KEY', nil)
634
+ @vault_master_key = ENV.fetch('VAULT_MASTER_KEY', nil)
635
+ @vault_auto_provision = true
636
+ @vault_cache_enabled = true
637
+ @vault_cache_ttl = 300 # 5 minutes
638
+ @vault_auto_load = ENV.fetch('VAULT_AUTO_LOAD', 'false') == 'true' # Auto-load secrets into ENV
639
+ @vault_load_provider_keys = true # Also load provider keys (OpenAI, etc.)
640
+
641
+ # Vision settings (AI browser automation)
642
+ @vision_enabled = true
643
+ @vision_url = ENV['VISION_URL'] || 'https://vision.brainzlab.ai'
644
+ @vision_api_key = ENV.fetch('VISION_API_KEY', nil)
645
+ @vision_ingest_key = ENV.fetch('VISION_INGEST_KEY', nil)
646
+ @vision_master_key = ENV.fetch('VISION_MASTER_KEY', nil)
647
+ @vision_auto_provision = true
648
+ @vision_default_model = ENV['VISION_DEFAULT_MODEL'] || 'claude-sonnet-4'
649
+ @vision_default_browser_provider = ENV['VISION_DEFAULT_BROWSER_PROVIDER'] || 'local'
650
+
651
+ # Cortex settings (feature flags)
652
+ @cortex_enabled = true
653
+ @cortex_url = ENV['CORTEX_URL'] || 'https://cortex.brainzlab.ai'
654
+ @cortex_api_key = ENV.fetch('CORTEX_API_KEY', nil)
655
+ @cortex_master_key = ENV.fetch('CORTEX_MASTER_KEY', nil)
656
+ @cortex_auto_provision = true
657
+ @cortex_cache_enabled = true
658
+ @cortex_cache_ttl = 60 # 1 minute
659
+ @cortex_default_context = {}
660
+
661
+ # Beacon settings (uptime monitoring)
662
+ @beacon_enabled = true
663
+ @beacon_url = ENV['BEACON_URL'] || 'https://beacon.brainzlab.ai'
664
+ @beacon_api_key = ENV.fetch('BEACON_API_KEY', nil)
665
+ @beacon_master_key = ENV.fetch('BEACON_MASTER_KEY', nil)
666
+ @beacon_auto_provision = true
667
+
668
+ # Nerve settings (job monitoring)
669
+ @nerve_enabled = true
670
+ @nerve_url = ENV['NERVE_URL'] || 'https://nerve.brainzlab.ai'
671
+ @nerve_api_key = ENV.fetch('NERVE_API_KEY', nil)
672
+ @nerve_master_key = ENV.fetch('NERVE_MASTER_KEY', nil)
673
+ @nerve_auto_provision = true
674
+
675
+ # Dendrite settings (AI documentation)
676
+ @dendrite_enabled = true
677
+ @dendrite_url = ENV['DENDRITE_URL'] || 'https://dendrite.brainzlab.ai'
678
+ @dendrite_api_key = ENV.fetch('DENDRITE_API_KEY', nil)
679
+ @dendrite_master_key = ENV.fetch('DENDRITE_MASTER_KEY', nil)
680
+ @dendrite_auto_provision = true
681
+
682
+ # Sentinel settings (host monitoring)
683
+ @sentinel_enabled = true
684
+ @sentinel_url = ENV['SENTINEL_URL'] || 'https://sentinel.brainzlab.ai'
685
+ @sentinel_api_key = ENV.fetch('SENTINEL_API_KEY', nil)
686
+ @sentinel_agent_key = ENV.fetch('SENTINEL_AGENT_KEY', nil)
687
+ @sentinel_master_key = ENV.fetch('SENTINEL_MASTER_KEY', nil)
688
+ @sentinel_auto_provision = true
689
+
690
+ # Synapse settings (AI development orchestration)
691
+ @synapse_enabled = true
692
+ @synapse_url = ENV['SYNAPSE_URL'] || 'https://synapse.brainzlab.ai'
693
+ @synapse_api_key = ENV.fetch('SYNAPSE_API_KEY', nil)
694
+ @synapse_master_key = ENV.fetch('SYNAPSE_MASTER_KEY', nil)
695
+ @synapse_auto_provision = true
696
+ end
697
+
698
+ # Scrubbing, internal logger and debug callbacks.
699
+ def init_runtime_defaults
700
+ # Filtering
701
+ @scrub_fields = %i[password password_confirmation token api_key secret]
702
+
703
+ # Internal logger for debugging SDK issues
704
+ @logger = nil
705
+
706
+ # Debug callbacks
707
+ # Called when an SDK error occurs (lambda/proc receiving error object and context hash)
708
+ @on_error = nil
709
+ # Called before each API request (lambda/proc receiving service, method, path, and payload)
710
+ @on_send = nil
711
+ end
712
+
713
+ # Which integrations auto-instrument, and what they ignore.
714
+ def init_instrumentation_defaults
715
+ # Instrumentation
716
+ @instrument_http = true # Enable HTTP client instrumentation (Net::HTTP, Faraday, HTTParty)
717
+ @instrument_active_record = true # AR breadcrumbs for Reflex
718
+ @instrument_redis = true # Redis command instrumentation
719
+ @instrument_sidekiq = true # Sidekiq job instrumentation
720
+ @instrument_graphql = true # GraphQL query instrumentation
721
+ @instrument_mongodb = true # MongoDB/Mongoid instrumentation
722
+ @instrument_elasticsearch = true # Elasticsearch instrumentation
723
+ @instrument_action_controller = true # ActionController instrumentation (requests, redirects, filters)
724
+ @instrument_action_view = true # ActionView instrumentation (templates, partials, collections)
725
+ @instrument_action_mailer = true # ActionMailer instrumentation
726
+ @instrument_active_job = true # ActiveJob instrumentation (enqueue, perform, retry, discard)
727
+ @instrument_active_support_cache = true # ActiveSupport::Cache instrumentation (read, write, fetch)
728
+ @instrument_delayed_job = true # Delayed::Job instrumentation
729
+ @instrument_grape = true # Grape API instrumentation
730
+ @instrument_solid_queue = true # Solid Queue job instrumentation
731
+ @instrument_good_job = true # GoodJob instrumentation
732
+ @instrument_resque = true # Resque instrumentation
733
+ @instrument_excon = true # Excon HTTP client instrumentation
734
+ @instrument_typhoeus = true # Typhoeus HTTP client instrumentation
735
+ @instrument_dalli = true # Dalli/Memcached instrumentation
736
+ @instrument_aws = true # AWS SDK instrumentation
737
+ @instrument_stripe = true # Stripe API instrumentation
738
+ @instrument_active_storage = true # ActiveStorage instrumentation (uploads, downloads, transforms)
739
+ @instrument_action_cable = true # ActionCable WebSocket instrumentation
740
+ @instrument_action_dispatch = true # ActionDispatch instrumentation (middleware, redirects, requests)
741
+ @instrument_rails_deprecation = true # Rails deprecation warnings tracking
742
+ @instrument_action_mailbox = true # ActionMailbox inbound email processing instrumentation
743
+ @instrument_railties = true # Railties config initializer loading instrumentation
744
+ @http_ignore_hosts = %w[localhost 127.0.0.1]
745
+ @redis_ignore_commands = %w[ping info] # Commands to skip tracking
746
+ end
747
+
748
+ # Slow / very-slow thresholds and N+1 detection, all env-tunable.
749
+ def init_threshold_defaults
750
+ # ActiveRecord slow-query / N+1 detection — all tunable so a single chatty
751
+ # default can't flood Recall. Slow-query duration is APM data (Pulse already
752
+ # records it), so it defaults to a quiet level (:debug) and a higher threshold.
753
+ @slow_query_threshold = (ENV['BRAINZLAB_SLOW_QUERY_THRESHOLD'] || 500).to_i # ms (was hardcoded 100)
754
+
755
+ # Query plan capture. Off by default: it issues an extra (read-only)
756
+ # EXPLAIN against the app's own database, so an app opts in deliberately.
757
+ # Threshold defaults to the slow query threshold — a fast query has no
758
+ # plan worth looking at.
759
+ @pulse_explain_enabled = ENV['BRAINZLAB_PULSE_EXPLAIN_ENABLED'] == 'true'
760
+ @pulse_explain_threshold = (ENV['BRAINZLAB_PULSE_EXPLAIN_THRESHOLD'] || @slow_query_threshold).to_i
761
+ @pulse_explain_timeout = (ENV['BRAINZLAB_PULSE_EXPLAIN_TIMEOUT'] || 2).to_i
762
+ @very_slow_query_threshold = (ENV['BRAINZLAB_VERY_SLOW_QUERY_THRESHOLD'] || 2000).to_i # ms (was hardcoded 1000)
763
+ @slow_query_log_level = (ENV['BRAINZLAB_SLOW_QUERY_LOG_LEVEL'] || 'debug').to_sym # was :warn → floods
764
+ @very_slow_query_log_level = (ENV['BRAINZLAB_VERY_SLOW_QUERY_LOG_LEVEL'] || 'warn').to_sym
765
+ @n_plus_one_threshold = (ENV['BRAINZLAB_N_PLUS_ONE_THRESHOLD'] || 10).to_i # was hardcoded 5
766
+ @n_plus_one_log_level = (ENV['BRAINZLAB_N_PLUS_ONE_LOG_LEVEL'] || 'debug').to_sym # was :warning
767
+
768
+ # Per-instrumentation slow / very-slow thresholds (ms) — all env-tunable so a
769
+ # single low default can't flood Recall or wrongly escalate normal latency to
770
+ # :error. Defaults preserve prior behaviour EXCEPT cache, which is raised off
771
+ # DB-backed Solid Cache's normal write latency (~50ms paid a DB round-trip and
772
+ # tripped the old VERY_SLOW=50 → thousands of false :error logs/hour).
773
+ @slow_request_threshold = (ENV['BRAINZLAB_SLOW_REQUEST_THRESHOLD'] || 500).to_i
774
+ @very_slow_request_threshold = (ENV['BRAINZLAB_VERY_SLOW_REQUEST_THRESHOLD'] || 2000).to_i
775
+ @slow_cache_threshold = (ENV['BRAINZLAB_SLOW_CACHE_THRESHOLD'] || 100).to_i # was 10
776
+ @very_slow_cache_threshold = (ENV['BRAINZLAB_VERY_SLOW_CACHE_THRESHOLD'] || 500).to_i # was 50 → flooded :error
777
+ @slow_render_threshold = (ENV['BRAINZLAB_SLOW_RENDER_THRESHOLD'] || 50).to_i
778
+ @very_slow_render_threshold = (ENV['BRAINZLAB_VERY_SLOW_RENDER_THRESHOLD'] || 200).to_i
779
+ @slow_storage_threshold = (ENV['BRAINZLAB_SLOW_STORAGE_THRESHOLD'] || 500).to_i
780
+ @very_slow_storage_threshold = (ENV['BRAINZLAB_VERY_SLOW_STORAGE_THRESHOLD'] || 2000).to_i
781
+ @slow_cable_threshold = (ENV['BRAINZLAB_SLOW_CABLE_THRESHOLD'] || 100).to_i
782
+ @very_slow_cable_threshold = (ENV['BRAINZLAB_VERY_SLOW_CABLE_THRESHOLD'] || 500).to_i
783
+ @slow_initializer_threshold = (ENV['BRAINZLAB_SLOW_INITIALIZER_THRESHOLD'] || 100).to_i
784
+ @very_slow_initializer_threshold = (ENV['BRAINZLAB_VERY_SLOW_INITIALIZER_THRESHOLD'] || 500).to_i
785
+ @slow_middleware_threshold = (ENV['BRAINZLAB_SLOW_MIDDLEWARE_THRESHOLD'] || 50).to_i
786
+ @very_slow_middleware_threshold = (ENV['BRAINZLAB_VERY_SLOW_MIDDLEWARE_THRESHOLD'] || 200).to_i
787
+ @slow_mailbox_threshold = (ENV['BRAINZLAB_SLOW_MAILBOX_THRESHOLD'] || 1000).to_i
788
+ @very_slow_mailbox_threshold = (ENV['BRAINZLAB_VERY_SLOW_MAILBOX_THRESHOLD'] || 5000).to_i
789
+ @slow_job_threshold = (ENV['BRAINZLAB_SLOW_JOB_THRESHOLD'] || 5000).to_i
790
+ @very_slow_job_threshold = (ENV['BRAINZLAB_VERY_SLOW_JOB_THRESHOLD'] || 30_000).to_i
791
+ end
792
+
793
+ # Log formatter, DevTools and Rails delegation.
794
+ def init_presentation_defaults
795
+ # Log formatter settings
796
+ @log_formatter_enabled = true
797
+ @log_formatter_colors = nil # auto-detect TTY
798
+ @log_formatter_hide_assets = false
799
+ @log_formatter_compact_assets = true
800
+ @log_formatter_show_params = true
801
+
802
+ # DevTools settings (development error page and debug panel)
803
+ @devtools_enabled = true
804
+ @devtools_error_page_enabled = true
805
+ @devtools_debug_panel_enabled = true
806
+ @devtools_allowed_environments = %w[development test]
807
+ @devtools_allowed_ips = ['127.0.0.1', '::1', '172.16.0.0/12', '192.168.0.0/16', '10.0.0.0/8']
808
+ @devtools_asset_path = '/__brainzlab__'
809
+ @devtools_panel_position = 'bottom-right'
810
+ @devtools_expand_by_default = false
811
+
812
+ # Rails instrumentation delegation
813
+ # When true, brainzlab-rails gem handles Rails-specific instrumentation
814
+ # SDK will only install non-Rails instrumentation (HTTP clients, Redis, etc.)
815
+ @rails_instrumentation_handled_externally = false
816
+ end
817
+
818
+ def positive_number?(value)
819
+ value.is_a?(Numeric) && value.positive?
820
+ end
821
+
717
822
  def detect_environment
718
823
  return ::Rails.env.to_s if defined?(::Rails) && ::Rails.respond_to?(:env)
719
824
  return ENV['RACK_ENV'] if ENV['RACK_ENV']