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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE +11 -0
- data/README.md +571 -0
- data/lib/brainzlab/beacon/client.rb +227 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +676 -0
- data/lib/brainzlab/context.rb +90 -0
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +159 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +250 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/development/logger.rb +150 -0
- data/lib/brainzlab/development/store.rb +121 -0
- data/lib/brainzlab/development.rb +72 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1329 -0
- data/lib/brainzlab/devtools/assets/devtools.js +396 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +511 -0
- data/lib/brainzlab/devtools/assets/templates/error_page.html.erb +1086 -0
- data/lib/brainzlab/devtools/data/collector.rb +248 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +63 -0
- data/lib/brainzlab/devtools/middleware/database_handler.rb +177 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +126 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +377 -0
- data/lib/brainzlab/devtools/renderers/debug_panel_renderer.rb +159 -0
- data/lib/brainzlab/devtools/renderers/error_page_renderer.rb +98 -0
- data/lib/brainzlab/devtools.rb +75 -0
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +124 -0
- data/lib/brainzlab/flux.rb +184 -0
- data/lib/brainzlab/instrumentation/action_cable.rb +351 -0
- data/lib/brainzlab/instrumentation/action_controller.rb +649 -0
- data/lib/brainzlab/instrumentation/action_dispatch.rb +259 -0
- data/lib/brainzlab/instrumentation/action_mailbox.rb +197 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +182 -0
- data/lib/brainzlab/instrumentation/action_view.rb +380 -0
- data/lib/brainzlab/instrumentation/active_job.rb +569 -0
- data/lib/brainzlab/instrumentation/active_record.rb +559 -0
- data/lib/brainzlab/instrumentation/active_storage.rb +541 -0
- data/lib/brainzlab/instrumentation/active_support_cache.rb +730 -0
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +234 -0
- data/lib/brainzlab/instrumentation/elasticsearch.rb +209 -0
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +181 -0
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +293 -0
- data/lib/brainzlab/instrumentation/graphql.rb +252 -0
- data/lib/brainzlab/instrumentation/httparty.rb +193 -0
- data/lib/brainzlab/instrumentation/mongodb.rb +187 -0
- data/lib/brainzlab/instrumentation/net_http.rb +114 -0
- data/lib/brainzlab/instrumentation/rails_deprecation.rb +139 -0
- data/lib/brainzlab/instrumentation/railties.rb +134 -0
- data/lib/brainzlab/instrumentation/redis.rb +324 -0
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +265 -0
- data/lib/brainzlab/instrumentation/solid_queue.rb +194 -0
- data/lib/brainzlab/instrumentation/stripe.rb +163 -0
- data/lib/brainzlab/instrumentation/typhoeus.rb +106 -0
- data/lib/brainzlab/instrumentation.rb +360 -0
- data/lib/brainzlab/nerve/client.rb +235 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +203 -0
- data/lib/brainzlab/pulse/instrumentation.rb +401 -0
- data/lib/brainzlab/pulse/propagation.rb +241 -0
- data/lib/brainzlab/pulse/provisioner.rb +114 -0
- data/lib/brainzlab/pulse/tracer.rb +111 -0
- data/lib/brainzlab/pulse.rb +294 -0
- data/lib/brainzlab/rails/log_formatter.rb +807 -0
- data/lib/brainzlab/rails/log_subscriber.rb +334 -0
- data/lib/brainzlab/rails/railtie.rb +606 -0
- data/lib/brainzlab/recall/buffer.rb +66 -0
- data/lib/brainzlab/recall/client.rb +158 -0
- data/lib/brainzlab/recall/logger.rb +116 -0
- data/lib/brainzlab/recall/provisioner.rb +130 -0
- data/lib/brainzlab/recall.rb +175 -0
- data/lib/brainzlab/reflex/breadcrumbs.rb +55 -0
- data/lib/brainzlab/reflex/client.rb +150 -0
- data/lib/brainzlab/reflex/provisioner.rb +116 -0
- data/lib/brainzlab/reflex.rb +421 -0
- data/lib/brainzlab/sentinel/client.rb +236 -0
- data/lib/brainzlab/sentinel/provisioner.rb +44 -0
- data/lib/brainzlab/sentinel.rb +165 -0
- data/lib/brainzlab/signal/client.rb +60 -0
- data/lib/brainzlab/signal/provisioner.rb +115 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +308 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/testing/event_store.rb +377 -0
- data/lib/brainzlab/testing/helpers.rb +650 -0
- data/lib/brainzlab/testing/matchers.rb +391 -0
- data/lib/brainzlab/testing.rb +327 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +290 -0
- data/lib/brainzlab/utilities/health_check.rb +294 -0
- data/lib/brainzlab/utilities/log_formatter.rb +254 -0
- data/lib/brainzlab/utilities/rate_limiter.rb +230 -0
- data/lib/brainzlab/utilities.rb +17 -0
- data/lib/brainzlab/vault/cache.rb +80 -0
- data/lib/brainzlab/vault/client.rb +216 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +5 -0
- data/lib/brainzlab/vision/client.rb +175 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +3 -0
- data/lib/brainzlab.rb +306 -0
- data/lib/generators/brainzlab/install/install_generator.rb +63 -0
- data/lib/generators/brainzlab/install/templates/brainzlab.rb.tt +77 -0
- metadata +251 -0
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Reflex
|
|
9
|
+
class Client
|
|
10
|
+
MAX_RETRIES = 3
|
|
11
|
+
RETRY_DELAY = 0.5
|
|
12
|
+
|
|
13
|
+
def initialize(config)
|
|
14
|
+
@config = config
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def send_error(payload)
|
|
18
|
+
return unless @config.reflex_enabled && @config.reflex_valid?
|
|
19
|
+
|
|
20
|
+
post('/api/v1/errors', payload)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def send_batch(payloads)
|
|
24
|
+
return unless @config.reflex_enabled && @config.reflex_valid?
|
|
25
|
+
return if payloads.empty?
|
|
26
|
+
|
|
27
|
+
post('/api/v1/errors/batch', { errors: payloads })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def post(path, body)
|
|
33
|
+
uri = URI.join(@config.reflex_url, path)
|
|
34
|
+
|
|
35
|
+
# Call on_send callback if configured
|
|
36
|
+
invoke_on_send(:reflex, :post, path, body)
|
|
37
|
+
|
|
38
|
+
# Log debug output for request
|
|
39
|
+
log_debug_request(path, body)
|
|
40
|
+
|
|
41
|
+
request = Net::HTTP::Post.new(uri)
|
|
42
|
+
request['Content-Type'] = 'application/json'
|
|
43
|
+
request['Authorization'] = "Bearer #{@config.reflex_auth_key}"
|
|
44
|
+
request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
|
|
45
|
+
request.body = JSON.generate(body)
|
|
46
|
+
|
|
47
|
+
execute_with_retry(uri, request, path)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
handle_error(e, context: { path: path, body_size: body.to_s.length })
|
|
50
|
+
nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def execute_with_retry(uri, request, path)
|
|
54
|
+
retries = 0
|
|
55
|
+
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
56
|
+
|
|
57
|
+
begin
|
|
58
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
59
|
+
http.use_ssl = uri.scheme == 'https'
|
|
60
|
+
http.open_timeout = 5
|
|
61
|
+
http.read_timeout = 10
|
|
62
|
+
|
|
63
|
+
response = http.request(request)
|
|
64
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
65
|
+
|
|
66
|
+
# Log debug output for response
|
|
67
|
+
log_debug_response(response.code.to_i, duration_ms)
|
|
68
|
+
|
|
69
|
+
case response.code.to_i
|
|
70
|
+
when 200..299
|
|
71
|
+
begin
|
|
72
|
+
JSON.parse(response.body)
|
|
73
|
+
rescue StandardError
|
|
74
|
+
{}
|
|
75
|
+
end
|
|
76
|
+
when 429, 500..599
|
|
77
|
+
raise RetryableError, "Server error: #{response.code}"
|
|
78
|
+
else
|
|
79
|
+
handle_error(
|
|
80
|
+
StandardError.new("Reflex API error: #{response.code}"),
|
|
81
|
+
context: { path: path, status: response.code, body: response.body }
|
|
82
|
+
)
|
|
83
|
+
nil
|
|
84
|
+
end
|
|
85
|
+
rescue RetryableError, Net::OpenTimeout, Net::ReadTimeout => e
|
|
86
|
+
retries += 1
|
|
87
|
+
if retries <= MAX_RETRIES
|
|
88
|
+
sleep(RETRY_DELAY * retries)
|
|
89
|
+
retry
|
|
90
|
+
end
|
|
91
|
+
duration_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time) * 1000).round(2)
|
|
92
|
+
log_debug_response(0, duration_ms, error: e.message)
|
|
93
|
+
handle_error(e, context: { path: path, retries: retries })
|
|
94
|
+
nil
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def log_debug_request(path, body)
|
|
99
|
+
return unless BrainzLab::Debug.enabled?
|
|
100
|
+
|
|
101
|
+
data = if body.is_a?(Hash) && body[:errors]
|
|
102
|
+
{ count: body[:errors].size }
|
|
103
|
+
elsif body.is_a?(Hash) && body[:exception]
|
|
104
|
+
{ exception: body[:exception][:type] }
|
|
105
|
+
else
|
|
106
|
+
{}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
BrainzLab::Debug.log_request(:reflex, 'POST', path, data: data)
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def log_debug_response(status, duration_ms, error: nil)
|
|
113
|
+
return unless BrainzLab::Debug.enabled?
|
|
114
|
+
|
|
115
|
+
BrainzLab::Debug.log_response(:reflex, status, duration_ms, error: error)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def invoke_on_send(service, method, path, payload)
|
|
119
|
+
return unless @config.on_send
|
|
120
|
+
|
|
121
|
+
@config.on_send.call(service, method, path, payload)
|
|
122
|
+
rescue StandardError => e
|
|
123
|
+
# Don't let callback errors break the SDK
|
|
124
|
+
log_error("on_send callback error: #{e.message}")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def handle_error(error, context: {})
|
|
128
|
+
log_error("#{error.message}")
|
|
129
|
+
|
|
130
|
+
# Call on_error callback if configured
|
|
131
|
+
return unless @config.on_error
|
|
132
|
+
|
|
133
|
+
@config.on_error.call(error, context.merge(service: :reflex))
|
|
134
|
+
rescue StandardError => e
|
|
135
|
+
# Don't let callback errors break the SDK
|
|
136
|
+
log_error("on_error callback error: #{e.message}")
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def log_error(message)
|
|
140
|
+
BrainzLab::Debug.log(message, level: :error) if BrainzLab::Debug.enabled?
|
|
141
|
+
|
|
142
|
+
return unless @config.logger
|
|
143
|
+
|
|
144
|
+
@config.logger.error("[BrainzLab::Reflex] #{message}")
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
class RetryableError < StandardError; end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'uri'
|
|
5
|
+
require 'json'
|
|
6
|
+
require 'fileutils'
|
|
7
|
+
|
|
8
|
+
module BrainzLab
|
|
9
|
+
module Reflex
|
|
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
|
+
return false unless @config.reflex_auto_provision
|
|
41
|
+
return false unless @config.app_name.to_s.strip.length.positive?
|
|
42
|
+
# Only skip if reflex_api_key is already set (not secret_key, which may be for Recall)
|
|
43
|
+
return false if @config.reflex_api_key.to_s.strip.length.positive?
|
|
44
|
+
return false unless @config.reflex_master_key.to_s.strip.length.positive?
|
|
45
|
+
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def provision_project
|
|
50
|
+
uri = URI.parse("#{@config.reflex_url}/api/v1/projects/provision")
|
|
51
|
+
request = Net::HTTP::Post.new(uri)
|
|
52
|
+
request['Content-Type'] = 'application/json'
|
|
53
|
+
request['X-Master-Key'] = @config.reflex_master_key
|
|
54
|
+
request['User-Agent'] = "brainzlab-sdk-ruby/#{BrainzLab::VERSION}"
|
|
55
|
+
request.body = JSON.generate({ name: @config.app_name })
|
|
56
|
+
|
|
57
|
+
response = execute(uri, request)
|
|
58
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
59
|
+
|
|
60
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
log_error("Failed to provision Reflex project: #{e.message}")
|
|
63
|
+
nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_cached_credentials
|
|
67
|
+
path = cache_file_path
|
|
68
|
+
return nil unless File.exist?(path)
|
|
69
|
+
|
|
70
|
+
data = JSON.parse(File.read(path), symbolize_names: true)
|
|
71
|
+
|
|
72
|
+
# Validate cached data has required keys
|
|
73
|
+
return nil unless data[:api_key]
|
|
74
|
+
|
|
75
|
+
data
|
|
76
|
+
rescue StandardError => e
|
|
77
|
+
log_error("Failed to load cached Reflex credentials: #{e.message}")
|
|
78
|
+
nil
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def cache_credentials(project)
|
|
82
|
+
FileUtils.mkdir_p(CACHE_DIR)
|
|
83
|
+
File.write(cache_file_path, JSON.generate(project))
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
log_error("Failed to cache Reflex credentials: #{e.message}")
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def cache_file_path
|
|
89
|
+
File.join(CACHE_DIR, "#{@config.app_name}.reflex.json")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def apply_credentials(project)
|
|
93
|
+
# Use reflex_api_key for Reflex if we have a separate key
|
|
94
|
+
# Otherwise fall back to shared secret_key
|
|
95
|
+
@config.reflex_api_key = project[:api_key]
|
|
96
|
+
|
|
97
|
+
# Also set service name from app_name if not already set
|
|
98
|
+
@config.service ||= @config.app_name
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def execute(uri, request)
|
|
102
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
103
|
+
http.use_ssl = uri.scheme == 'https'
|
|
104
|
+
http.open_timeout = 5
|
|
105
|
+
http.read_timeout = 10
|
|
106
|
+
http.request(request)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def log_error(message)
|
|
110
|
+
return unless @config.logger
|
|
111
|
+
|
|
112
|
+
@config.logger.error("[BrainzLab::Reflex] #{message}")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'reflex/client'
|
|
4
|
+
require_relative 'reflex/breadcrumbs'
|
|
5
|
+
require_relative 'reflex/provisioner'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Reflex
|
|
9
|
+
FILTERED_PARAMS = %w[password password_confirmation token api_key secret credit_card cvv ssn].freeze
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def capture(exception, **context)
|
|
13
|
+
return unless enabled?
|
|
14
|
+
return if capture_disabled?
|
|
15
|
+
return if excluded?(exception)
|
|
16
|
+
return if sampled_out?
|
|
17
|
+
|
|
18
|
+
# Log debug output for the operation
|
|
19
|
+
log_debug_capture(exception)
|
|
20
|
+
|
|
21
|
+
payload = build_payload(exception, context)
|
|
22
|
+
payload = run_before_send(payload, exception)
|
|
23
|
+
return if payload.nil?
|
|
24
|
+
|
|
25
|
+
# In development mode, log locally instead of sending to server
|
|
26
|
+
if BrainzLab.configuration.development_mode?
|
|
27
|
+
Development.record(service: :reflex, event_type: 'error', payload: payload)
|
|
28
|
+
return
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Auto-provision project on first capture if app_name is configured
|
|
32
|
+
ensure_provisioned!
|
|
33
|
+
|
|
34
|
+
return unless BrainzLab.configuration.reflex_valid?
|
|
35
|
+
|
|
36
|
+
client.send_error(payload)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def capture_message(message, level: :error, **context)
|
|
40
|
+
return unless enabled?
|
|
41
|
+
return if capture_disabled?
|
|
42
|
+
return if sampled_out?
|
|
43
|
+
|
|
44
|
+
# Log debug output for the operation
|
|
45
|
+
log_debug_message(message, level)
|
|
46
|
+
|
|
47
|
+
payload = build_message_payload(message, level, context)
|
|
48
|
+
payload = run_before_send(payload, nil)
|
|
49
|
+
return if payload.nil?
|
|
50
|
+
|
|
51
|
+
# In development mode, log locally instead of sending to server
|
|
52
|
+
if BrainzLab.configuration.development_mode?
|
|
53
|
+
Development.record(service: :reflex, event_type: 'message', payload: payload)
|
|
54
|
+
return
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Auto-provision project on first capture if app_name is configured
|
|
58
|
+
ensure_provisioned!
|
|
59
|
+
|
|
60
|
+
return unless BrainzLab.configuration.reflex_valid?
|
|
61
|
+
|
|
62
|
+
client.send_error(payload)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def ensure_provisioned!
|
|
66
|
+
return if @provisioned
|
|
67
|
+
|
|
68
|
+
@provisioned = true
|
|
69
|
+
provisioner.ensure_project!
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def provisioner
|
|
73
|
+
@provisioner ||= Provisioner.new(BrainzLab.configuration)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Temporarily disable capture within a block
|
|
77
|
+
def without_capture
|
|
78
|
+
previous = Thread.current[:brainzlab_capture_disabled]
|
|
79
|
+
Thread.current[:brainzlab_capture_disabled] = true
|
|
80
|
+
yield
|
|
81
|
+
ensure
|
|
82
|
+
Thread.current[:brainzlab_capture_disabled] = previous
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def client
|
|
86
|
+
@client ||= Client.new(BrainzLab.configuration)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def reset!
|
|
90
|
+
@client = nil
|
|
91
|
+
@provisioner = nil
|
|
92
|
+
@provisioned = false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private
|
|
96
|
+
|
|
97
|
+
def enabled?
|
|
98
|
+
BrainzLab.configuration.reflex_effectively_enabled?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def capture_disabled?
|
|
102
|
+
Thread.current[:brainzlab_capture_disabled] == true
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def excluded?(exception)
|
|
106
|
+
config = BrainzLab.configuration
|
|
107
|
+
config.reflex_excluded_exceptions.any? do |excluded|
|
|
108
|
+
case excluded
|
|
109
|
+
when String
|
|
110
|
+
exception.class.name == excluded || exception.class.to_s == excluded
|
|
111
|
+
when Class
|
|
112
|
+
exception.is_a?(excluded)
|
|
113
|
+
when Regexp
|
|
114
|
+
exception.class.name =~ excluded
|
|
115
|
+
else
|
|
116
|
+
false
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def sampled_out?
|
|
122
|
+
rate = BrainzLab.configuration.reflex_sample_rate
|
|
123
|
+
return false if rate.nil? || rate >= 1.0
|
|
124
|
+
|
|
125
|
+
rand > rate
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def run_before_send(payload, exception)
|
|
129
|
+
hook = BrainzLab.configuration.reflex_before_send
|
|
130
|
+
return payload unless hook
|
|
131
|
+
|
|
132
|
+
hook.call(payload, exception)
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def build_payload(exception, context)
|
|
136
|
+
config = BrainzLab.configuration
|
|
137
|
+
ctx = Context.current
|
|
138
|
+
|
|
139
|
+
payload = {
|
|
140
|
+
timestamp: Time.now.utc.iso8601(3),
|
|
141
|
+
error_class: exception.class.name,
|
|
142
|
+
message: exception.message,
|
|
143
|
+
backtrace: format_backtrace(exception.backtrace || []),
|
|
144
|
+
|
|
145
|
+
# Environment
|
|
146
|
+
environment: config.environment,
|
|
147
|
+
commit: config.commit,
|
|
148
|
+
branch: config.branch,
|
|
149
|
+
server_name: config.host,
|
|
150
|
+
|
|
151
|
+
# Request context
|
|
152
|
+
request_id: ctx.request_id
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
# Add request info if available
|
|
156
|
+
add_request_info(payload, ctx)
|
|
157
|
+
|
|
158
|
+
# Add user info
|
|
159
|
+
add_user_info(payload, ctx, context)
|
|
160
|
+
|
|
161
|
+
# Add context, tags, extra
|
|
162
|
+
add_context_data(payload, ctx, context)
|
|
163
|
+
|
|
164
|
+
# Add breadcrumbs
|
|
165
|
+
payload[:breadcrumbs] = ctx.breadcrumbs.to_a
|
|
166
|
+
|
|
167
|
+
# Add fingerprint for error grouping
|
|
168
|
+
payload[:fingerprint] = compute_fingerprint(exception, context, ctx)
|
|
169
|
+
|
|
170
|
+
payload
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def build_message_payload(message, level, context)
|
|
174
|
+
config = BrainzLab.configuration
|
|
175
|
+
ctx = Context.current
|
|
176
|
+
|
|
177
|
+
payload = {
|
|
178
|
+
timestamp: Time.now.utc.iso8601(3),
|
|
179
|
+
error_class: 'Message',
|
|
180
|
+
message: message.to_s,
|
|
181
|
+
level: level.to_s,
|
|
182
|
+
|
|
183
|
+
# Environment
|
|
184
|
+
environment: config.environment,
|
|
185
|
+
commit: config.commit,
|
|
186
|
+
branch: config.branch,
|
|
187
|
+
server_name: config.host,
|
|
188
|
+
|
|
189
|
+
# Request context
|
|
190
|
+
request_id: ctx.request_id
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
# Add request info if available
|
|
194
|
+
add_request_info(payload, ctx)
|
|
195
|
+
|
|
196
|
+
# Add user info
|
|
197
|
+
add_user_info(payload, ctx, context)
|
|
198
|
+
|
|
199
|
+
# Add context, tags, extra
|
|
200
|
+
add_context_data(payload, ctx, context)
|
|
201
|
+
|
|
202
|
+
# Add breadcrumbs
|
|
203
|
+
payload[:breadcrumbs] = ctx.breadcrumbs.to_a
|
|
204
|
+
|
|
205
|
+
payload
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def add_request_info(payload, ctx)
|
|
209
|
+
return unless ctx.request_path
|
|
210
|
+
|
|
211
|
+
payload[:request] = {
|
|
212
|
+
method: ctx.request_method,
|
|
213
|
+
path: ctx.request_path,
|
|
214
|
+
url: ctx.request_url,
|
|
215
|
+
params: filter_params(ctx.request_params),
|
|
216
|
+
headers: ctx.request_headers,
|
|
217
|
+
controller: ctx.controller,
|
|
218
|
+
action: ctx.action
|
|
219
|
+
}.compact
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def add_user_info(payload, ctx, context)
|
|
223
|
+
user = context[:user] || ctx.user
|
|
224
|
+
return if user.nil? || user.empty?
|
|
225
|
+
|
|
226
|
+
payload[:user] = {
|
|
227
|
+
id: user[:id]&.to_s,
|
|
228
|
+
email: user[:email],
|
|
229
|
+
name: user[:name]
|
|
230
|
+
}.compact
|
|
231
|
+
|
|
232
|
+
# Store additional user data
|
|
233
|
+
extra_user = user.except(:id, :email, :name)
|
|
234
|
+
payload[:user_data] = extra_user unless extra_user.empty?
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def add_context_data(payload, ctx, context)
|
|
238
|
+
# Tags from context + provided tags
|
|
239
|
+
tags = ctx.tags.merge(context[:tags] || {})
|
|
240
|
+
payload[:tags] = tags unless tags.empty?
|
|
241
|
+
|
|
242
|
+
# Extra data from context + provided extra
|
|
243
|
+
extra = ctx.data_hash.merge(context[:extra] || {})
|
|
244
|
+
extra = extra.except(:user, :tags) # Remove user and tags as they're separate
|
|
245
|
+
payload[:extra] = extra unless extra.empty?
|
|
246
|
+
|
|
247
|
+
# General context
|
|
248
|
+
payload[:context] = context.except(:user, :tags, :extra) unless context.except(:user, :tags, :extra).empty?
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def format_backtrace(backtrace)
|
|
252
|
+
backtrace.first(30).map do |line|
|
|
253
|
+
if line.is_a?(String)
|
|
254
|
+
parse_backtrace_line(line)
|
|
255
|
+
else
|
|
256
|
+
line
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
|
|
261
|
+
def parse_backtrace_line(line)
|
|
262
|
+
# Parse various Ruby backtrace formats:
|
|
263
|
+
# - "path/to/file.rb:42:in `method_name'" (backtick + single quote)
|
|
264
|
+
# - "path/to/file.rb:42:in 'method_name'" (single quotes)
|
|
265
|
+
# - "path/to/file.rb:42" (no method)
|
|
266
|
+
if line =~ /\A(.+):(\d+):in [`']([^']+)'?\z/
|
|
267
|
+
{
|
|
268
|
+
file: ::Regexp.last_match(1),
|
|
269
|
+
line: ::Regexp.last_match(2).to_i,
|
|
270
|
+
function: ::Regexp.last_match(3),
|
|
271
|
+
in_app: in_app_frame?(::Regexp.last_match(1))
|
|
272
|
+
}
|
|
273
|
+
elsif line =~ /\A(.+):(\d+)\z/
|
|
274
|
+
{
|
|
275
|
+
file: ::Regexp.last_match(1),
|
|
276
|
+
line: ::Regexp.last_match(2).to_i,
|
|
277
|
+
function: nil,
|
|
278
|
+
in_app: in_app_frame?(::Regexp.last_match(1))
|
|
279
|
+
}
|
|
280
|
+
else
|
|
281
|
+
# Still store file for display even if format is unexpected
|
|
282
|
+
{ file: line, line: nil, function: nil, in_app: false }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def in_app_frame?(path)
|
|
287
|
+
return false if path.nil?
|
|
288
|
+
return false if path.include?('vendor/')
|
|
289
|
+
return false if path.include?('/gems/')
|
|
290
|
+
return false if path.include?('/ruby/')
|
|
291
|
+
|
|
292
|
+
# Match both relative and absolute paths containing app/ or lib/
|
|
293
|
+
path.start_with?('app/', 'lib/', './app/', './lib/') ||
|
|
294
|
+
path.include?('/app/') ||
|
|
295
|
+
path.include?('/lib/')
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def filter_params(params)
|
|
299
|
+
return nil if params.nil?
|
|
300
|
+
|
|
301
|
+
scrub_fields = BrainzLab.configuration.scrub_fields + FILTERED_PARAMS.map(&:to_sym)
|
|
302
|
+
deep_filter(params, scrub_fields)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def deep_filter(obj, fields)
|
|
306
|
+
case obj
|
|
307
|
+
when Hash
|
|
308
|
+
obj.each_with_object({}) do |(key, value), result|
|
|
309
|
+
result[key] = if should_filter?(key, fields)
|
|
310
|
+
'[FILTERED]'
|
|
311
|
+
else
|
|
312
|
+
deep_filter(value, fields)
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
when Array
|
|
316
|
+
obj.map { |item| deep_filter(item, fields) }
|
|
317
|
+
else
|
|
318
|
+
obj
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def should_filter?(key, fields)
|
|
323
|
+
key_str = key.to_s.downcase
|
|
324
|
+
fields.any? do |field|
|
|
325
|
+
case field
|
|
326
|
+
when Regexp
|
|
327
|
+
key_str.match?(field)
|
|
328
|
+
else
|
|
329
|
+
key_str == field.to_s.downcase
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
# Compute fingerprint for error grouping
|
|
335
|
+
# Returns an array of strings that uniquely identify the error type
|
|
336
|
+
def compute_fingerprint(exception, context, ctx)
|
|
337
|
+
custom_callback = BrainzLab.configuration.reflex_fingerprint
|
|
338
|
+
|
|
339
|
+
if custom_callback
|
|
340
|
+
# Call user's custom fingerprint callback
|
|
341
|
+
result = custom_callback.call(exception, context, ctx)
|
|
342
|
+
|
|
343
|
+
# Normalize the result
|
|
344
|
+
case result
|
|
345
|
+
when Array
|
|
346
|
+
result.map(&:to_s)
|
|
347
|
+
when String
|
|
348
|
+
[result]
|
|
349
|
+
when nil
|
|
350
|
+
# nil means use default fingerprinting
|
|
351
|
+
default_fingerprint(exception)
|
|
352
|
+
else
|
|
353
|
+
[result.to_s]
|
|
354
|
+
end
|
|
355
|
+
else
|
|
356
|
+
default_fingerprint(exception)
|
|
357
|
+
end
|
|
358
|
+
rescue StandardError => e
|
|
359
|
+
BrainzLab.debug_log("Custom fingerprint callback failed: #{e.message}")
|
|
360
|
+
default_fingerprint(exception)
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
# Default fingerprint: error class + first in-app frame (or first frame)
|
|
364
|
+
def default_fingerprint(exception)
|
|
365
|
+
parts = [exception.class.name]
|
|
366
|
+
|
|
367
|
+
if exception.backtrace&.any?
|
|
368
|
+
# Try to find the first in-app frame
|
|
369
|
+
in_app_frame = exception.backtrace.find { |line| in_app_line?(line) }
|
|
370
|
+
frame = in_app_frame || exception.backtrace.first
|
|
371
|
+
|
|
372
|
+
if frame
|
|
373
|
+
# Normalize the frame (remove line numbers for consistent grouping)
|
|
374
|
+
normalized = normalize_frame_for_fingerprint(frame)
|
|
375
|
+
parts << normalized if normalized
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
parts
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def in_app_line?(line)
|
|
383
|
+
return false if line.nil?
|
|
384
|
+
return false if line.include?('vendor/')
|
|
385
|
+
return false if line.include?('/gems/')
|
|
386
|
+
|
|
387
|
+
line.start_with?('app/', 'lib/', './app/', './lib/') ||
|
|
388
|
+
line.include?('/app/') ||
|
|
389
|
+
line.include?('/lib/')
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def normalize_frame_for_fingerprint(frame)
|
|
393
|
+
return nil unless frame.is_a?(String)
|
|
394
|
+
|
|
395
|
+
# Extract file and method, normalize out line numbers
|
|
396
|
+
# "app/models/user.rb:42:in `save'" -> "app/models/user.rb:in `save'"
|
|
397
|
+
if frame =~ /\A(.+):\d+:in `(.+)'\z/
|
|
398
|
+
"#{::Regexp.last_match(1)}:in `#{::Regexp.last_match(2)}'"
|
|
399
|
+
elsif frame =~ /\A(.+):\d+\z/
|
|
400
|
+
::Regexp.last_match(1)
|
|
401
|
+
else
|
|
402
|
+
frame
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def log_debug_capture(exception)
|
|
407
|
+
return unless BrainzLab::Debug.enabled?
|
|
408
|
+
|
|
409
|
+
truncated_message = exception.message.to_s.length > 40 ? "#{exception.message.to_s[0..37]}..." : exception.message.to_s
|
|
410
|
+
BrainzLab::Debug.log_operation(:reflex, "capture #{exception.class.name}: \"#{truncated_message}\"")
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def log_debug_message(message, level)
|
|
414
|
+
return unless BrainzLab::Debug.enabled?
|
|
415
|
+
|
|
416
|
+
truncated_message = message.to_s.length > 40 ? "#{message.to_s[0..37]}..." : message.to_s
|
|
417
|
+
BrainzLab::Debug.log_operation(:reflex, "message [#{level.to_s.upcase}] \"#{truncated_message}\"")
|
|
418
|
+
end
|
|
419
|
+
end
|
|
420
|
+
end
|
|
421
|
+
end
|