fluyenta-ruby 0.1.14

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 (121) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE +11 -0
  4. data/README.md +571 -0
  5. data/lib/brainzlab/beacon/client.rb +227 -0
  6. data/lib/brainzlab/beacon/provisioner.rb +44 -0
  7. data/lib/brainzlab/beacon.rb +215 -0
  8. data/lib/brainzlab/configuration.rb +676 -0
  9. data/lib/brainzlab/context.rb +90 -0
  10. data/lib/brainzlab/cortex/cache.rb +59 -0
  11. data/lib/brainzlab/cortex/client.rb +159 -0
  12. data/lib/brainzlab/cortex/provisioner.rb +49 -0
  13. data/lib/brainzlab/cortex.rb +223 -0
  14. data/lib/brainzlab/debug.rb +305 -0
  15. data/lib/brainzlab/dendrite/client.rb +250 -0
  16. data/lib/brainzlab/dendrite/provisioner.rb +44 -0
  17. data/lib/brainzlab/dendrite.rb +195 -0
  18. data/lib/brainzlab/development/logger.rb +150 -0
  19. data/lib/brainzlab/development/store.rb +121 -0
  20. data/lib/brainzlab/development.rb +72 -0
  21. data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
  22. data/lib/brainzlab/devtools/assets/devtools.js +396 -0
  23. data/lib/brainzlab/devtools/assets/logo.svg +6 -0
  24. data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
  25. data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
  26. data/lib/brainzlab/devtools/data/collector.rb +248 -0
  27. data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
  28. data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
  29. data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
  30. data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
  31. data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
  32. data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
  33. data/lib/brainzlab/devtools.rb +75 -0
  34. data/lib/brainzlab/errors.rb +490 -0
  35. data/lib/brainzlab/flux/buffer.rb +96 -0
  36. data/lib/brainzlab/flux/client.rb +68 -0
  37. data/lib/brainzlab/flux/provisioner.rb +124 -0
  38. data/lib/brainzlab/flux.rb +184 -0
  39. data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
  40. data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
  41. data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
  42. data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
  43. data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
  44. data/lib/brainzlab/instrumentation/action_view.rb +380 -0
  45. data/lib/brainzlab/instrumentation/active_job.rb +569 -0
  46. data/lib/brainzlab/instrumentation/active_record.rb +559 -0
  47. data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
  48. data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
  49. data/lib/brainzlab/instrumentation/aws.rb +183 -0
  50. data/lib/brainzlab/instrumentation/dalli.rb +108 -0
  51. data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
  52. data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
  53. data/lib/brainzlab/instrumentation/excon.rb +152 -0
  54. data/lib/brainzlab/instrumentation/faraday.rb +181 -0
  55. data/lib/brainzlab/instrumentation/good_job.rb +102 -0
  56. data/lib/brainzlab/instrumentation/grape.rb +293 -0
  57. data/lib/brainzlab/instrumentation/graphql.rb +252 -0
  58. data/lib/brainzlab/instrumentation/httparty.rb +193 -0
  59. data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
  60. data/lib/brainzlab/instrumentation/net_http.rb +114 -0
  61. data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
  62. data/lib/brainzlab/instrumentation/railties.rb +134 -0
  63. data/lib/brainzlab/instrumentation/redis.rb +324 -0
  64. data/lib/brainzlab/instrumentation/resque.rb +114 -0
  65. data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
  66. data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
  67. data/lib/brainzlab/instrumentation/stripe.rb +163 -0
  68. data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
  69. data/lib/brainzlab/instrumentation.rb +360 -0
  70. data/lib/brainzlab/nerve/client.rb +235 -0
  71. data/lib/brainzlab/nerve/provisioner.rb +44 -0
  72. data/lib/brainzlab/nerve.rb +219 -0
  73. data/lib/brainzlab/pulse/client.rb +203 -0
  74. data/lib/brainzlab/pulse/instrumentation.rb +401 -0
  75. data/lib/brainzlab/pulse/propagation.rb +241 -0
  76. data/lib/brainzlab/pulse/provisioner.rb +114 -0
  77. data/lib/brainzlab/pulse/tracer.rb +111 -0
  78. data/lib/brainzlab/pulse.rb +294 -0
  79. data/lib/brainzlab/rails/log_formatter.rb +807 -0
  80. data/lib/brainzlab/rails/log_subscriber.rb +334 -0
  81. data/lib/brainzlab/rails/railtie.rb +606 -0
  82. data/lib/brainzlab/recall/buffer.rb +66 -0
  83. data/lib/brainzlab/recall/client.rb +158 -0
  84. data/lib/brainzlab/recall/logger.rb +116 -0
  85. data/lib/brainzlab/recall/provisioner.rb +130 -0
  86. data/lib/brainzlab/recall.rb +175 -0
  87. data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
  88. data/lib/brainzlab/reflex/client.rb +150 -0
  89. data/lib/brainzlab/reflex/provisioner.rb +116 -0
  90. data/lib/brainzlab/reflex.rb +421 -0
  91. data/lib/brainzlab/sentinel/client.rb +236 -0
  92. data/lib/brainzlab/sentinel/provisioner.rb +44 -0
  93. data/lib/brainzlab/sentinel.rb +165 -0
  94. data/lib/brainzlab/signal/client.rb +60 -0
  95. data/lib/brainzlab/signal/provisioner.rb +115 -0
  96. data/lib/brainzlab/signal.rb +136 -0
  97. data/lib/brainzlab/synapse/client.rb +308 -0
  98. data/lib/brainzlab/synapse/provisioner.rb +44 -0
  99. data/lib/brainzlab/synapse.rb +270 -0
  100. data/lib/brainzlab/testing/event_store.rb +377 -0
  101. data/lib/brainzlab/testing/helpers.rb +650 -0
  102. data/lib/brainzlab/testing/matchers.rb +391 -0
  103. data/lib/brainzlab/testing.rb +327 -0
  104. data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
  105. data/lib/brainzlab/utilities/health_check.rb +294 -0
  106. data/lib/brainzlab/utilities/log_formatter.rb +254 -0
  107. data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
  108. data/lib/brainzlab/utilities.rb +17 -0
  109. data/lib/brainzlab/vault/cache.rb +80 -0
  110. data/lib/brainzlab/vault/client.rb +216 -0
  111. data/lib/brainzlab/vault/provisioner.rb +49 -0
  112. data/lib/brainzlab/vault.rb +262 -0
  113. data/lib/brainzlab/version.rb +5 -0
  114. data/lib/brainzlab/vision/client.rb +175 -0
  115. data/lib/brainzlab/vision/provisioner.rb +136 -0
  116. data/lib/brainzlab/vision.rb +155 -0
  117. data/lib/brainzlab-sdk.rb +3 -0
  118. data/lib/brainzlab.rb +306 -0
  119. data/lib/generators/brainzlab/install/install_generator.rb +63 -0
  120. data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
  121. metadata +251 -0
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'uri'
6
+ require 'fileutils'
7
+
8
+ module BrainzLab
9
+ module Flux
10
+ class Provisioner
11
+ CACHE_DIR = ENV.fetch('BRAINZLAB_CACHE_DIR') { File.join(Dir.home, '.brainzlab') }
12
+
13
+ def initialize(config)
14
+ @config = config
15
+ end
16
+
17
+ def ensure_project!
18
+ return unless should_provision?
19
+
20
+ # Try cached credentials first
21
+ if (cached = load_cached_credentials)
22
+ apply_credentials(cached)
23
+ return cached
24
+ end
25
+
26
+ # Provision new project
27
+ project = provision_project
28
+ return unless project
29
+
30
+ # Cache and apply credentials
31
+ cache_credentials(project)
32
+ apply_credentials(project)
33
+
34
+ project
35
+ end
36
+
37
+ private
38
+
39
+ def should_provision?
40
+ # Already have credentials
41
+ return false if @config.flux_ingest_key.to_s.strip.length.positive?
42
+ return false if @config.flux_api_key.to_s.strip.length.positive?
43
+
44
+ # Need auto_provision enabled
45
+ return false unless @config.flux_auto_provision
46
+
47
+ # Need app_name for project name
48
+ return false unless @config.app_name.to_s.strip.length.positive?
49
+
50
+ # Need master key for provisioning
51
+ return false unless @config.flux_master_key.to_s.strip.length.positive?
52
+
53
+ # Need flux_url
54
+ return false unless @config.flux_url.to_s.strip.length.positive?
55
+
56
+ true
57
+ end
58
+
59
+ def provision_project
60
+ BrainzLab.debug_log('[Flux] Auto-provisioning project...')
61
+
62
+ uri = URI.parse("#{@config.flux_url}/api/v1/projects/provision")
63
+ http = Net::HTTP.new(uri.host, uri.port)
64
+ http.use_ssl = uri.scheme == 'https'
65
+ http.open_timeout = 5
66
+ http.read_timeout = 10
67
+
68
+ request = Net::HTTP::Post.new(uri.path)
69
+ request['Content-Type'] = 'application/json'
70
+ request['X-Master-Key'] = @config.flux_master_key
71
+ request['User-Agent'] = "brainzlab-sdk/#{BrainzLab::VERSION}"
72
+ request.body = {
73
+ name: @config.app_name,
74
+ environment: @config.environment
75
+ }.to_json
76
+
77
+ response = http.request(request)
78
+
79
+ if response.is_a?(Net::HTTPSuccess)
80
+ data = JSON.parse(response.body, symbolize_names: true)
81
+ BrainzLab.debug_log('[Flux] Project provisioned successfully')
82
+ data
83
+ else
84
+ BrainzLab.debug_log("[Flux] Provisioning failed: #{response.code} - #{response.body}")
85
+ nil
86
+ end
87
+ rescue StandardError => e
88
+ BrainzLab.debug_log("[Flux] Provisioning error: #{e.message}")
89
+ nil
90
+ end
91
+
92
+ def load_cached_credentials
93
+ path = cache_file_path
94
+ return nil unless File.exist?(path)
95
+
96
+ data = JSON.parse(File.read(path), symbolize_names: true)
97
+
98
+ # Validate cached data has required keys
99
+ return nil unless data[:ingest_key]
100
+
101
+ data
102
+ rescue StandardError => e
103
+ BrainzLab.debug_log("[Flux] Failed to load cached credentials: #{e.message}")
104
+ nil
105
+ end
106
+
107
+ def cache_credentials(project)
108
+ FileUtils.mkdir_p(CACHE_DIR)
109
+ File.write(cache_file_path, JSON.generate(project))
110
+ rescue StandardError => e
111
+ BrainzLab.debug_log("[Flux] Failed to cache credentials: #{e.message}")
112
+ end
113
+
114
+ def cache_file_path
115
+ File.join(CACHE_DIR, "#{@config.app_name}.flux.json")
116
+ end
117
+
118
+ def apply_credentials(project)
119
+ @config.flux_ingest_key = project[:ingest_key]
120
+ @config.flux_api_key = project[:api_key]
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'flux/client'
4
+ require_relative 'flux/buffer'
5
+ require_relative 'flux/provisioner'
6
+
7
+ module BrainzLab
8
+ module Flux
9
+ class << self
10
+ # === EVENTS ===
11
+
12
+ # Track a custom event
13
+ # @param name [String] Event name (e.g., 'user.signup', 'order.completed')
14
+ # @param properties [Hash] Event properties
15
+ def track(name, properties = {})
16
+ return unless enabled?
17
+
18
+ ensure_provisioned!
19
+ return unless BrainzLab.configuration.flux_valid?
20
+
21
+ event = {
22
+ name: name,
23
+ timestamp: Time.now.utc.iso8601(3),
24
+ properties: properties.except(:user_id, :value, :tags, :session_id),
25
+ user_id: properties[:user_id],
26
+ session_id: properties[:session_id],
27
+ value: properties[:value],
28
+ tags: properties[:tags] || {},
29
+ environment: BrainzLab.configuration.environment,
30
+ service: BrainzLab.configuration.service
31
+ }
32
+
33
+ buffer.add(:event, event)
34
+ end
35
+
36
+ # Track event for a specific user
37
+ def track_for_user(user, name, properties = {})
38
+ user_id = user.respond_to?(:id) ? user.id.to_s : user.to_s
39
+ track(name, properties.merge(user_id: user_id))
40
+ end
41
+
42
+ # === METRICS ===
43
+
44
+ # Gauge: Current value (overwrites)
45
+ def gauge(name, value, tags: {})
46
+ return unless enabled?
47
+
48
+ ensure_provisioned!
49
+ return unless BrainzLab.configuration.flux_valid?
50
+
51
+ metric = {
52
+ type: 'gauge',
53
+ name: name,
54
+ value: value,
55
+ tags: tags,
56
+ timestamp: Time.now.utc.iso8601(3)
57
+ }
58
+
59
+ buffer.add(:metric, metric)
60
+ end
61
+
62
+ # Counter: Increment value
63
+ def increment(name, value = 1, tags: {})
64
+ return unless enabled?
65
+
66
+ ensure_provisioned!
67
+ return unless BrainzLab.configuration.flux_valid?
68
+
69
+ metric = {
70
+ type: 'counter',
71
+ name: name,
72
+ value: value,
73
+ tags: tags,
74
+ timestamp: Time.now.utc.iso8601(3)
75
+ }
76
+
77
+ buffer.add(:metric, metric)
78
+ end
79
+
80
+ # Counter: Decrement value
81
+ def decrement(name, value = 1, tags: {})
82
+ increment(name, -value, tags: tags)
83
+ end
84
+
85
+ # Distribution: Statistical aggregation
86
+ def distribution(name, value, tags: {})
87
+ return unless enabled?
88
+
89
+ ensure_provisioned!
90
+ return unless BrainzLab.configuration.flux_valid?
91
+
92
+ metric = {
93
+ type: 'distribution',
94
+ name: name,
95
+ value: value,
96
+ tags: tags,
97
+ timestamp: Time.now.utc.iso8601(3)
98
+ }
99
+
100
+ buffer.add(:metric, metric)
101
+ end
102
+
103
+ # Histogram: Alias for distribution (for compatibility with brainzlab-rails)
104
+ def histogram(name, value, tags: {})
105
+ distribution(name, value, tags: tags)
106
+ end
107
+
108
+ # Timing: Record duration in milliseconds (alias for distribution)
109
+ def timing(name, value_ms, tags: {})
110
+ distribution(name, value_ms, tags: tags.merge(unit: 'ms'))
111
+ end
112
+
113
+ # Set: Unique count (cardinality)
114
+ def set(name, value, tags: {})
115
+ return unless enabled?
116
+
117
+ ensure_provisioned!
118
+ return unless BrainzLab.configuration.flux_valid?
119
+
120
+ metric = {
121
+ type: 'set',
122
+ name: name,
123
+ value: value.to_s,
124
+ tags: tags,
125
+ timestamp: Time.now.utc.iso8601(3)
126
+ }
127
+
128
+ buffer.add(:metric, metric)
129
+ end
130
+
131
+ # === CONVENIENCE METHODS ===
132
+
133
+ # Time a block and record as distribution
134
+ def measure(name, tags: {})
135
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
136
+ begin
137
+ yield
138
+ ensure
139
+ duration_ms = (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
140
+ distribution(name, duration_ms, tags: tags.merge(unit: 'ms'))
141
+ end
142
+ end
143
+
144
+ # Flush any buffered data immediately
145
+ def flush!
146
+ buffer.flush!
147
+ end
148
+
149
+ # === INTERNAL ===
150
+
151
+ def ensure_provisioned!
152
+ return if @provisioned
153
+
154
+ @provisioned = true
155
+ provisioner.ensure_project!
156
+ end
157
+
158
+ def provisioner
159
+ @provisioner ||= Provisioner.new(BrainzLab.configuration)
160
+ end
161
+
162
+ def client
163
+ @client ||= Client.new(BrainzLab.configuration)
164
+ end
165
+
166
+ def buffer
167
+ @buffer ||= Buffer.new(client)
168
+ end
169
+
170
+ def reset!
171
+ @client = nil
172
+ @buffer = nil
173
+ @provisioner = nil
174
+ @provisioned = false
175
+ end
176
+
177
+ private
178
+
179
+ def enabled?
180
+ BrainzLab.configuration.flux_effectively_enabled?
181
+ end
182
+ end
183
+ end
184
+ end
@@ -0,0 +1,351 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BrainzLab
4
+ module Instrumentation
5
+ class ActionCable
6
+ # Thresholds for slow operations (in milliseconds)
7
+ SLOW_ACTION_THRESHOLD = 100
8
+ VERY_SLOW_ACTION_THRESHOLD = 500
9
+
10
+ class << self
11
+ def install!
12
+ return unless defined?(::ActionCable)
13
+ return if @installed
14
+
15
+ install_perform_action_subscriber!
16
+ install_transmit_subscriber!
17
+ install_transmit_subscription_confirmation_subscriber!
18
+ install_transmit_subscription_rejection_subscriber!
19
+ install_broadcast_subscriber!
20
+
21
+ @installed = true
22
+ BrainzLab.debug_log('ActionCable instrumentation installed')
23
+ end
24
+
25
+ def installed?
26
+ @installed == true
27
+ end
28
+
29
+ private
30
+
31
+ # ============================================
32
+ # perform_action.action_cable
33
+ # ============================================
34
+ def install_perform_action_subscriber!
35
+ ActiveSupport::Notifications.subscribe('perform_action.action_cable') do |*args|
36
+ event = ActiveSupport::Notifications::Event.new(*args)
37
+ handle_perform_action(event)
38
+ end
39
+ end
40
+
41
+ def handle_perform_action(event)
42
+ payload = event.payload
43
+ duration = event.duration.round(2)
44
+
45
+ channel_class = payload[:channel_class]
46
+ action = payload[:action]
47
+ data = payload[:data]
48
+
49
+ # Determine level based on duration
50
+ level = case duration
51
+ when 0...SLOW_ACTION_THRESHOLD then :info
52
+ when SLOW_ACTION_THRESHOLD...VERY_SLOW_ACTION_THRESHOLD then :warning
53
+ else :error
54
+ end
55
+
56
+ # Record breadcrumb
57
+ if BrainzLab.configuration.reflex_effectively_enabled?
58
+ BrainzLab::Reflex.add_breadcrumb(
59
+ "Cable action: #{channel_class}##{action} (#{duration}ms)",
60
+ category: 'cable.action',
61
+ level: level,
62
+ data: {
63
+ channel: channel_class,
64
+ action: action,
65
+ duration_ms: duration
66
+ }.compact
67
+ )
68
+ end
69
+
70
+ # Add Pulse span
71
+ record_action_span(event, channel_class, action, duration, data)
72
+
73
+ # Log slow actions
74
+ log_slow_action(channel_class, action, duration) if duration >= SLOW_ACTION_THRESHOLD
75
+ rescue StandardError => e
76
+ BrainzLab.debug_log("ActionCable perform_action instrumentation failed: #{e.message}")
77
+ end
78
+
79
+ # ============================================
80
+ # transmit.action_cable
81
+ # ============================================
82
+ def install_transmit_subscriber!
83
+ ActiveSupport::Notifications.subscribe('transmit.action_cable') do |*args|
84
+ event = ActiveSupport::Notifications::Event.new(*args)
85
+ handle_transmit(event)
86
+ end
87
+ end
88
+
89
+ def handle_transmit(event)
90
+ payload = event.payload
91
+ duration = event.duration.round(2)
92
+
93
+ channel_class = payload[:channel_class]
94
+ data = payload[:data]
95
+ via = payload[:via]
96
+
97
+ # Record breadcrumb
98
+ if BrainzLab.configuration.reflex_effectively_enabled?
99
+ message = via ? "Cable transmit via #{via}" : 'Cable transmit'
100
+ BrainzLab::Reflex.add_breadcrumb(
101
+ "#{message}: #{channel_class} (#{duration}ms)",
102
+ category: 'cable.transmit',
103
+ level: :info,
104
+ data: {
105
+ channel: channel_class,
106
+ via: via,
107
+ duration_ms: duration
108
+ }.compact
109
+ )
110
+ end
111
+
112
+ # Add Pulse span
113
+ record_transmit_span(event, channel_class, duration, via)
114
+ rescue StandardError => e
115
+ BrainzLab.debug_log("ActionCable transmit instrumentation failed: #{e.message}")
116
+ end
117
+
118
+ # ============================================
119
+ # transmit_subscription_confirmation.action_cable
120
+ # ============================================
121
+ def install_transmit_subscription_confirmation_subscriber!
122
+ ActiveSupport::Notifications.subscribe('transmit_subscription_confirmation.action_cable') do |*args|
123
+ event = ActiveSupport::Notifications::Event.new(*args)
124
+ handle_subscription_confirmation(event)
125
+ end
126
+ end
127
+
128
+ def handle_subscription_confirmation(event)
129
+ payload = event.payload
130
+ duration = event.duration.round(2)
131
+
132
+ channel_class = payload[:channel_class]
133
+
134
+ # Record breadcrumb
135
+ if BrainzLab.configuration.reflex_effectively_enabled?
136
+ BrainzLab::Reflex.add_breadcrumb(
137
+ "Cable subscribed: #{channel_class}",
138
+ category: 'cable.subscribe',
139
+ level: :info,
140
+ data: {
141
+ channel: channel_class,
142
+ status: 'confirmed',
143
+ duration_ms: duration
144
+ }.compact
145
+ )
146
+ end
147
+
148
+ # Add Pulse span
149
+ record_subscription_span(event, channel_class, 'confirmed', duration)
150
+ rescue StandardError => e
151
+ BrainzLab.debug_log("ActionCable subscription confirmation instrumentation failed: #{e.message}")
152
+ end
153
+
154
+ # ============================================
155
+ # transmit_subscription_rejection.action_cable
156
+ # ============================================
157
+ def install_transmit_subscription_rejection_subscriber!
158
+ ActiveSupport::Notifications.subscribe('transmit_subscription_rejection.action_cable') do |*args|
159
+ event = ActiveSupport::Notifications::Event.new(*args)
160
+ handle_subscription_rejection(event)
161
+ end
162
+ end
163
+
164
+ def handle_subscription_rejection(event)
165
+ payload = event.payload
166
+ duration = event.duration.round(2)
167
+
168
+ channel_class = payload[:channel_class]
169
+
170
+ # Record breadcrumb - rejection is a warning
171
+ if BrainzLab.configuration.reflex_effectively_enabled?
172
+ BrainzLab::Reflex.add_breadcrumb(
173
+ "Cable subscription rejected: #{channel_class}",
174
+ category: 'cable.subscribe',
175
+ level: :warning,
176
+ data: {
177
+ channel: channel_class,
178
+ status: 'rejected',
179
+ duration_ms: duration
180
+ }.compact
181
+ )
182
+ end
183
+
184
+ # Add Pulse span
185
+ record_subscription_span(event, channel_class, 'rejected', duration)
186
+
187
+ # Log rejection to Recall
188
+ if BrainzLab.configuration.recall_effectively_enabled?
189
+ BrainzLab::Recall.warn(
190
+ "ActionCable subscription rejected",
191
+ channel: channel_class
192
+ )
193
+ end
194
+ rescue StandardError => e
195
+ BrainzLab.debug_log("ActionCable subscription rejection instrumentation failed: #{e.message}")
196
+ end
197
+
198
+ # ============================================
199
+ # broadcast.action_cable
200
+ # ============================================
201
+ def install_broadcast_subscriber!
202
+ ActiveSupport::Notifications.subscribe('broadcast.action_cable') do |*args|
203
+ event = ActiveSupport::Notifications::Event.new(*args)
204
+ handle_broadcast(event)
205
+ end
206
+ end
207
+
208
+ def handle_broadcast(event)
209
+ payload = event.payload
210
+ duration = event.duration.round(2)
211
+
212
+ broadcasting = payload[:broadcasting]
213
+ message = payload[:message]
214
+ coder = payload[:coder]
215
+
216
+ # Record breadcrumb
217
+ if BrainzLab.configuration.reflex_effectively_enabled?
218
+ BrainzLab::Reflex.add_breadcrumb(
219
+ "Cable broadcast: #{broadcasting} (#{duration}ms)",
220
+ category: 'cable.broadcast',
221
+ level: :info,
222
+ data: {
223
+ broadcasting: broadcasting,
224
+ coder: coder&.to_s,
225
+ duration_ms: duration
226
+ }.compact
227
+ )
228
+ end
229
+
230
+ # Add Pulse span
231
+ record_broadcast_span(event, broadcasting, duration, coder)
232
+ rescue StandardError => e
233
+ BrainzLab.debug_log("ActionCable broadcast instrumentation failed: #{e.message}")
234
+ end
235
+
236
+ # ============================================
237
+ # Span Recording Helpers
238
+ # ============================================
239
+ def record_action_span(event, channel_class, action, duration, data)
240
+ return unless BrainzLab.configuration.pulse_effectively_enabled?
241
+
242
+ tracer = BrainzLab::Pulse.tracer
243
+ return unless tracer.current_trace
244
+
245
+ span_data = {
246
+ span_id: SecureRandom.uuid,
247
+ name: "cable.action.#{action}",
248
+ kind: 'websocket',
249
+ started_at: event.time,
250
+ ended_at: event.end,
251
+ duration_ms: duration,
252
+ error: false,
253
+ data: {
254
+ 'cable.channel' => channel_class,
255
+ 'cable.action' => action
256
+ }.compact
257
+ }
258
+
259
+ tracer.current_spans << span_data
260
+ end
261
+
262
+ def record_transmit_span(event, channel_class, duration, via)
263
+ return unless BrainzLab.configuration.pulse_effectively_enabled?
264
+
265
+ tracer = BrainzLab::Pulse.tracer
266
+ return unless tracer.current_trace
267
+
268
+ span_data = {
269
+ span_id: SecureRandom.uuid,
270
+ name: 'cable.transmit',
271
+ kind: 'websocket',
272
+ started_at: event.time,
273
+ ended_at: event.end,
274
+ duration_ms: duration,
275
+ error: false,
276
+ data: {
277
+ 'cable.channel' => channel_class,
278
+ 'cable.via' => via
279
+ }.compact
280
+ }
281
+
282
+ tracer.current_spans << span_data
283
+ end
284
+
285
+ def record_subscription_span(event, channel_class, status, duration)
286
+ return unless BrainzLab.configuration.pulse_effectively_enabled?
287
+
288
+ tracer = BrainzLab::Pulse.tracer
289
+ return unless tracer.current_trace
290
+
291
+ span_data = {
292
+ span_id: SecureRandom.uuid,
293
+ name: 'cable.subscribe',
294
+ kind: 'websocket',
295
+ started_at: event.time,
296
+ ended_at: event.end,
297
+ duration_ms: duration,
298
+ error: status == 'rejected',
299
+ data: {
300
+ 'cable.channel' => channel_class,
301
+ 'cable.subscription_status' => status
302
+ }.compact
303
+ }
304
+
305
+ tracer.current_spans << span_data
306
+ end
307
+
308
+ def record_broadcast_span(event, broadcasting, duration, coder)
309
+ return unless BrainzLab.configuration.pulse_effectively_enabled?
310
+
311
+ tracer = BrainzLab::Pulse.tracer
312
+ return unless tracer.current_trace
313
+
314
+ span_data = {
315
+ span_id: SecureRandom.uuid,
316
+ name: 'cable.broadcast',
317
+ kind: 'websocket',
318
+ started_at: event.time,
319
+ ended_at: event.end,
320
+ duration_ms: duration,
321
+ error: false,
322
+ data: {
323
+ 'cable.broadcasting' => broadcasting,
324
+ 'cable.coder' => coder&.to_s
325
+ }.compact
326
+ }
327
+
328
+ tracer.current_spans << span_data
329
+ end
330
+
331
+ # ============================================
332
+ # Logging Helpers
333
+ # ============================================
334
+ def log_slow_action(channel_class, action, duration)
335
+ return unless BrainzLab.configuration.recall_effectively_enabled?
336
+
337
+ level = duration >= VERY_SLOW_ACTION_THRESHOLD ? :error : :warn
338
+
339
+ BrainzLab::Recall.send(
340
+ level,
341
+ "Slow ActionCable action: #{channel_class}##{action} (#{duration}ms)",
342
+ channel: channel_class,
343
+ action: action,
344
+ duration_ms: duration,
345
+ threshold_exceeded: duration >= VERY_SLOW_ACTION_THRESHOLD ? 'critical' : 'warning'
346
+ )
347
+ end
348
+ end
349
+ end
350
+ end
351
+ end