brainzlab 0.1.11 → 0.1.20
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 +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +210 -3
- data/lib/brainzlab/beacon/client.rb +21 -1
- data/lib/brainzlab/configuration.rb +81 -4
- data/lib/brainzlab/cortex/client.rb +21 -1
- data/lib/brainzlab/debug.rb +305 -0
- data/lib/brainzlab/dendrite/client.rb +21 -1
- 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 +245 -109
- data/lib/brainzlab/devtools/assets/devtools.js +40 -0
- data/lib/brainzlab/devtools/middleware/asset_server.rb +1 -0
- data/lib/brainzlab/devtools/middleware/debug_panel.rb +1 -0
- data/lib/brainzlab/devtools/middleware/error_page.rb +56 -8
- data/lib/brainzlab/errors.rb +490 -0
- data/lib/brainzlab/flux/buffer.rb +2 -2
- data/lib/brainzlab/flux/client.rb +2 -2
- data/lib/brainzlab/instrumentation/active_support_cache.rb +60 -30
- data/lib/brainzlab/instrumentation/net_http.rb +21 -16
- data/lib/brainzlab/instrumentation.rb +6 -0
- data/lib/brainzlab/nerve/client.rb +21 -1
- data/lib/brainzlab/pulse/client.rb +66 -5
- data/lib/brainzlab/pulse.rb +24 -5
- data/lib/brainzlab/rails/log_formatter.rb +1 -1
- data/lib/brainzlab/rails/railtie.rb +18 -3
- data/lib/brainzlab/recall/buffer.rb +3 -1
- data/lib/brainzlab/recall/client.rb +74 -6
- data/lib/brainzlab/recall.rb +19 -2
- data/lib/brainzlab/reflex/client.rb +66 -5
- data/lib/brainzlab/reflex.rb +40 -8
- data/lib/brainzlab/sentinel/client.rb +21 -1
- data/lib/brainzlab/synapse/client.rb +21 -1
- 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 +32 -3
- data/lib/brainzlab/vault/client.rb +21 -1
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +53 -6
- data/lib/brainzlab.rb +67 -0
- data/lib/fluyenta-ruby.rb +3 -0
- metadata +34 -11
|
@@ -4,6 +4,46 @@
|
|
|
4
4
|
(function() {
|
|
5
5
|
'use strict';
|
|
6
6
|
|
|
7
|
+
// ============================================
|
|
8
|
+
// DARK MODE SUPPORT
|
|
9
|
+
// ============================================
|
|
10
|
+
// Sync with brainzlab-theme localStorage key (used across all BrainzLab products)
|
|
11
|
+
function initDarkMode() {
|
|
12
|
+
const theme = localStorage.getItem('brainzlab-theme');
|
|
13
|
+
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
|
14
|
+
|
|
15
|
+
if (theme === 'dark' || (!theme && prefersDark)) {
|
|
16
|
+
document.documentElement.classList.add('dark');
|
|
17
|
+
} else {
|
|
18
|
+
document.documentElement.classList.remove('dark');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Listen for theme changes from other windows/tabs
|
|
23
|
+
function setupThemeListener() {
|
|
24
|
+
window.addEventListener('storage', function(e) {
|
|
25
|
+
if (e.key === 'brainzlab-theme') {
|
|
26
|
+
initDarkMode();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Also listen for system preference changes
|
|
31
|
+
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) {
|
|
32
|
+
// Only react if no explicit theme is set
|
|
33
|
+
if (!localStorage.getItem('brainzlab-theme')) {
|
|
34
|
+
if (e.matches) {
|
|
35
|
+
document.documentElement.classList.add('dark');
|
|
36
|
+
} else {
|
|
37
|
+
document.documentElement.classList.remove('dark');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Initialize dark mode immediately
|
|
44
|
+
initDarkMode();
|
|
45
|
+
setupThemeListener();
|
|
46
|
+
|
|
7
47
|
// Load Stimulus if not available
|
|
8
48
|
let stimulusApp = null;
|
|
9
49
|
let StimulusController = null;
|
|
@@ -38,6 +38,7 @@ module BrainzLab
|
|
|
38
38
|
return false unless DevTools.debug_panel_enabled?
|
|
39
39
|
return false unless DevTools.allowed_environment?
|
|
40
40
|
return false unless DevTools.allowed_ip?(extract_ip(env))
|
|
41
|
+
return false if env['REQUEST_METHOD'] == 'OPTIONS'
|
|
41
42
|
return false if asset_request?(env['PATH_INFO'])
|
|
42
43
|
return false if devtools_asset_request?(env['PATH_INFO'])
|
|
43
44
|
return false if turbo_stream_request?(env)
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
3
5
|
module BrainzLab
|
|
4
6
|
module DevTools
|
|
5
7
|
module Middleware
|
|
@@ -10,29 +12,33 @@ module BrainzLab
|
|
|
10
12
|
end
|
|
11
13
|
|
|
12
14
|
def call(env)
|
|
15
|
+
return @app.call(env) if env['REQUEST_METHOD'] == 'OPTIONS'
|
|
13
16
|
return @app.call(env) unless should_handle?(env)
|
|
14
17
|
|
|
15
18
|
begin
|
|
16
19
|
status, headers, body = @app.call(env)
|
|
17
20
|
|
|
18
21
|
# Check if this is an error response that we should intercept
|
|
19
|
-
if status >= 400 && html_response?(headers) && !json_request?(env)
|
|
22
|
+
if status >= 400 && html_response?(headers) && !json_request?(env) && !api_path?(env)
|
|
20
23
|
# Check if this looks like Rails' default error page
|
|
21
24
|
body_content = collect_body(body)
|
|
22
25
|
if body_content.include?('Action Controller: Exception caught') || body_content.include?('background: #C00')
|
|
23
26
|
# Extract exception info from the page
|
|
24
27
|
exception_info = extract_exception_from_html(body_content)
|
|
25
28
|
if exception_info
|
|
26
|
-
data = collect_debug_data_from_info(env, exception_info)
|
|
27
|
-
return render_error_page_from_info(exception_info, data)
|
|
29
|
+
data = collect_debug_data_from_info(env, exception_info, status)
|
|
30
|
+
return render_error_page_from_info(exception_info, data, status)
|
|
28
31
|
end
|
|
29
32
|
end
|
|
30
33
|
end
|
|
31
34
|
|
|
32
35
|
[status, headers, body]
|
|
33
36
|
rescue Exception => e
|
|
34
|
-
#
|
|
35
|
-
|
|
37
|
+
# For JSON/API requests, return a proper JSON error response
|
|
38
|
+
if json_request?(env) || api_path?(env)
|
|
39
|
+
capture_to_reflex(e)
|
|
40
|
+
return json_error_response(e)
|
|
41
|
+
end
|
|
36
42
|
|
|
37
43
|
# Still capture to Reflex if available
|
|
38
44
|
capture_to_reflex(e)
|
|
@@ -87,7 +93,7 @@ module BrainzLab
|
|
|
87
93
|
.gsub(' ', ' ')
|
|
88
94
|
end
|
|
89
95
|
|
|
90
|
-
def collect_debug_data_from_info(env, info)
|
|
96
|
+
def collect_debug_data_from_info(env, info, status = 500)
|
|
91
97
|
context = defined?(BrainzLab::Context) ? BrainzLab::Context.current : nil
|
|
92
98
|
collector_data = Data::Collector.get_request_data
|
|
93
99
|
|
|
@@ -113,7 +119,7 @@ module BrainzLab
|
|
|
113
119
|
}
|
|
114
120
|
end
|
|
115
121
|
|
|
116
|
-
def render_error_page_from_info(info, data)
|
|
122
|
+
def render_error_page_from_info(info, data, status = 500)
|
|
117
123
|
# Create a simple exception-like object
|
|
118
124
|
exception = StandardError.new(info[:message])
|
|
119
125
|
exception.define_singleton_method(:class) do
|
|
@@ -126,7 +132,7 @@ module BrainzLab
|
|
|
126
132
|
html = @renderer.render(exception, data)
|
|
127
133
|
|
|
128
134
|
[
|
|
129
|
-
|
|
135
|
+
status,
|
|
130
136
|
{
|
|
131
137
|
'Content-Type' => 'text/html; charset=utf-8',
|
|
132
138
|
'Content-Length' => html.bytesize.to_s,
|
|
@@ -169,6 +175,48 @@ module BrainzLab
|
|
|
169
175
|
env['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest'
|
|
170
176
|
end
|
|
171
177
|
|
|
178
|
+
def api_path?(env)
|
|
179
|
+
path = env['PATH_INFO'] || ''
|
|
180
|
+
path.start_with?('/api/')
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def exception_to_status(exception)
|
|
184
|
+
case exception.class.name
|
|
185
|
+
when 'ActionController::RoutingError', 'AbstractController::ActionNotFound'
|
|
186
|
+
404
|
|
187
|
+
when 'ActionController::MethodNotAllowed'
|
|
188
|
+
405
|
|
189
|
+
when 'ActionController::BadRequest', 'ActionDispatch::Http::Parameters::ParseError'
|
|
190
|
+
400
|
|
191
|
+
when 'ActionController::UnknownFormat'
|
|
192
|
+
406
|
|
193
|
+
else
|
|
194
|
+
500
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def json_error_response(exception)
|
|
199
|
+
status_code = exception_to_status(exception)
|
|
200
|
+
message = case status_code
|
|
201
|
+
when 400 then 'Bad request'
|
|
202
|
+
when 404 then 'Not found'
|
|
203
|
+
when 405 then 'Method not allowed'
|
|
204
|
+
when 406 then 'Not acceptable'
|
|
205
|
+
else 'Internal server error'
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
body = JSON.generate({ error: message })
|
|
209
|
+
[
|
|
210
|
+
status_code,
|
|
211
|
+
{
|
|
212
|
+
'Content-Type' => 'application/json; charset=utf-8',
|
|
213
|
+
'Content-Length' => body.bytesize.to_s,
|
|
214
|
+
'X-Content-Type-Options' => 'nosniff'
|
|
215
|
+
},
|
|
216
|
+
[body]
|
|
217
|
+
]
|
|
218
|
+
end
|
|
219
|
+
|
|
172
220
|
def capture_to_reflex(exception)
|
|
173
221
|
return unless defined?(BrainzLab::Reflex)
|
|
174
222
|
|
|
@@ -0,0 +1,490 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
# Base error class for all BrainzLab SDK errors.
|
|
5
|
+
# Provides structured error information including hints and documentation links.
|
|
6
|
+
#
|
|
7
|
+
# @example Raising a structured error
|
|
8
|
+
# raise BrainzLab::Error.new(
|
|
9
|
+
# "Operation failed",
|
|
10
|
+
# hint: "Check your network connection",
|
|
11
|
+
# docs_url: "https://docs.brainzlab.io/troubleshooting",
|
|
12
|
+
# code: "operation_failed"
|
|
13
|
+
# )
|
|
14
|
+
#
|
|
15
|
+
# @example Catching and inspecting errors
|
|
16
|
+
# begin
|
|
17
|
+
# BrainzLab::Vault.get("secret")
|
|
18
|
+
# rescue BrainzLab::Error => e
|
|
19
|
+
# puts e.message # What went wrong
|
|
20
|
+
# puts e.hint # How to fix it
|
|
21
|
+
# puts e.docs_url # Where to learn more
|
|
22
|
+
# puts e.code # Machine-readable code
|
|
23
|
+
# end
|
|
24
|
+
#
|
|
25
|
+
class Error < StandardError
|
|
26
|
+
# @return [String, nil] A helpful hint on how to resolve the error
|
|
27
|
+
attr_reader :hint
|
|
28
|
+
|
|
29
|
+
# @return [String, nil] URL to relevant documentation
|
|
30
|
+
attr_reader :docs_url
|
|
31
|
+
|
|
32
|
+
# @return [String, nil] Machine-readable error code for programmatic handling
|
|
33
|
+
attr_reader :code
|
|
34
|
+
|
|
35
|
+
# @return [Hash, nil] Additional context about the error
|
|
36
|
+
attr_reader :context
|
|
37
|
+
|
|
38
|
+
DOCS_BASE_URL = 'https://docs.brainzlab.io'
|
|
39
|
+
|
|
40
|
+
# Initialize a new BrainzLab error.
|
|
41
|
+
#
|
|
42
|
+
# @param message [String] The error message describing what went wrong
|
|
43
|
+
# @param hint [String, nil] A helpful hint on how to resolve the error
|
|
44
|
+
# @param docs_url [String, nil] URL to relevant documentation
|
|
45
|
+
# @param code [String, nil] Machine-readable error code
|
|
46
|
+
# @param context [Hash, nil] Additional context about the error
|
|
47
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
48
|
+
@message = message
|
|
49
|
+
@hint = hint
|
|
50
|
+
@docs_url = docs_url
|
|
51
|
+
@code = code
|
|
52
|
+
@context = context
|
|
53
|
+
super(message)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Format the error as a detailed string with hints and documentation links.
|
|
57
|
+
#
|
|
58
|
+
# @return [String] Formatted error message
|
|
59
|
+
def to_s
|
|
60
|
+
super
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Return a detailed formatted version of the error with hints and documentation links.
|
|
64
|
+
# Use this method when you want the full structured output.
|
|
65
|
+
#
|
|
66
|
+
# @return [String] Detailed formatted error message
|
|
67
|
+
def detailed_message(highlight: false, **_kwargs)
|
|
68
|
+
# Get the base message without class name duplication
|
|
69
|
+
base_msg = @message || super
|
|
70
|
+
|
|
71
|
+
parts = ["#{self.class.name}: #{base_msg}"]
|
|
72
|
+
|
|
73
|
+
parts << "" << "Hint: #{hint}" if hint
|
|
74
|
+
parts << "Docs: #{docs_url}" if docs_url
|
|
75
|
+
parts << "Code: #{code}" if code
|
|
76
|
+
|
|
77
|
+
if context && !context.empty?
|
|
78
|
+
parts << "" << "Context:"
|
|
79
|
+
context.each do |key, value|
|
|
80
|
+
parts << " #{key}: #{value}"
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
parts.join("\n")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Inspect the error for debugging
|
|
88
|
+
#
|
|
89
|
+
# @return [String] Inspection output
|
|
90
|
+
def inspect
|
|
91
|
+
"#<#{self.class.name}: #{message}#{" (#{code})" if code}>"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Return a hash representation of the error for logging/serialization.
|
|
95
|
+
#
|
|
96
|
+
# @return [Hash] Error details as a hash
|
|
97
|
+
def to_h
|
|
98
|
+
{
|
|
99
|
+
error_class: self.class.name,
|
|
100
|
+
message: message,
|
|
101
|
+
hint: hint,
|
|
102
|
+
docs_url: docs_url,
|
|
103
|
+
code: code,
|
|
104
|
+
context: context
|
|
105
|
+
}.compact
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Alias for to_h
|
|
109
|
+
def as_json
|
|
110
|
+
to_h
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Raised when the SDK is misconfigured or required configuration is missing.
|
|
115
|
+
#
|
|
116
|
+
# @example Missing API key
|
|
117
|
+
# raise BrainzLab::ConfigurationError.new(
|
|
118
|
+
# "API key is required",
|
|
119
|
+
# hint: "Set BRAINZLAB_SECRET_KEY environment variable or configure via BrainzLab.configure",
|
|
120
|
+
# code: "missing_api_key"
|
|
121
|
+
# )
|
|
122
|
+
#
|
|
123
|
+
class ConfigurationError < Error
|
|
124
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
125
|
+
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/configuration"
|
|
126
|
+
code ||= 'configuration_error'
|
|
127
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context)
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Raised when authentication fails due to invalid or expired credentials.
|
|
132
|
+
#
|
|
133
|
+
# @example Invalid API key
|
|
134
|
+
# raise BrainzLab::AuthenticationError.new(
|
|
135
|
+
# "Invalid API key",
|
|
136
|
+
# hint: "Check that your API key is correct and has not expired",
|
|
137
|
+
# code: "invalid_api_key"
|
|
138
|
+
# )
|
|
139
|
+
#
|
|
140
|
+
class AuthenticationError < Error
|
|
141
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
142
|
+
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/authentication"
|
|
143
|
+
code ||= 'authentication_error'
|
|
144
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# Raised when a connection to BrainzLab services cannot be established.
|
|
149
|
+
#
|
|
150
|
+
# @example Connection timeout
|
|
151
|
+
# raise BrainzLab::ConnectionError.new(
|
|
152
|
+
# "Connection timed out",
|
|
153
|
+
# hint: "Check your network connection and firewall settings",
|
|
154
|
+
# code: "connection_timeout"
|
|
155
|
+
# )
|
|
156
|
+
#
|
|
157
|
+
class ConnectionError < Error
|
|
158
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil)
|
|
159
|
+
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/troubleshooting#connection-issues"
|
|
160
|
+
code ||= 'connection_error'
|
|
161
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Raised when the rate limit for API requests has been exceeded.
|
|
166
|
+
#
|
|
167
|
+
# @example Rate limit exceeded
|
|
168
|
+
# raise BrainzLab::RateLimitError.new(
|
|
169
|
+
# "Rate limit exceeded",
|
|
170
|
+
# hint: "Wait before retrying or consider upgrading your plan",
|
|
171
|
+
# code: "rate_limit_exceeded",
|
|
172
|
+
# context: { retry_after: 60, limit: 1000, remaining: 0 }
|
|
173
|
+
# )
|
|
174
|
+
#
|
|
175
|
+
class RateLimitError < Error
|
|
176
|
+
# @return [Integer, nil] Seconds to wait before retrying
|
|
177
|
+
attr_reader :retry_after
|
|
178
|
+
|
|
179
|
+
# @return [Integer, nil] The rate limit ceiling
|
|
180
|
+
attr_reader :limit
|
|
181
|
+
|
|
182
|
+
# @return [Integer, nil] Remaining requests in the current window
|
|
183
|
+
attr_reader :remaining
|
|
184
|
+
|
|
185
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, retry_after: nil, limit: nil, remaining: nil)
|
|
186
|
+
@retry_after = retry_after
|
|
187
|
+
@limit = limit
|
|
188
|
+
@remaining = remaining
|
|
189
|
+
|
|
190
|
+
hint ||= retry_after ? "Wait #{retry_after} seconds before retrying" : 'Reduce request frequency or upgrade your plan'
|
|
191
|
+
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/rate-limits"
|
|
192
|
+
code ||= 'rate_limit_exceeded'
|
|
193
|
+
|
|
194
|
+
context ||= {}
|
|
195
|
+
context[:retry_after] = retry_after if retry_after
|
|
196
|
+
context[:limit] = limit if limit
|
|
197
|
+
context[:remaining] = remaining if remaining
|
|
198
|
+
|
|
199
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Raised when request parameters or data fail validation.
|
|
204
|
+
#
|
|
205
|
+
# @example Invalid parameter
|
|
206
|
+
# raise BrainzLab::ValidationError.new(
|
|
207
|
+
# "Invalid email format",
|
|
208
|
+
# hint: "Provide a valid email address (e.g., user@example.com)",
|
|
209
|
+
# code: "invalid_email",
|
|
210
|
+
# context: { field: "email", value: "invalid" }
|
|
211
|
+
# )
|
|
212
|
+
#
|
|
213
|
+
class ValidationError < Error
|
|
214
|
+
# @return [String, nil] The field that failed validation
|
|
215
|
+
attr_reader :field
|
|
216
|
+
|
|
217
|
+
# @return [Array<Hash>, nil] List of validation errors for multiple fields
|
|
218
|
+
attr_reader :errors
|
|
219
|
+
|
|
220
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, field: nil, errors: nil)
|
|
221
|
+
@field = field
|
|
222
|
+
@errors = errors
|
|
223
|
+
|
|
224
|
+
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/api-reference"
|
|
225
|
+
code ||= 'validation_error'
|
|
226
|
+
|
|
227
|
+
context ||= {}
|
|
228
|
+
context[:field] = field if field
|
|
229
|
+
context[:errors] = errors if errors
|
|
230
|
+
|
|
231
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Raised when a requested resource is not found.
|
|
236
|
+
#
|
|
237
|
+
# @example Resource not found
|
|
238
|
+
# raise BrainzLab::NotFoundError.new(
|
|
239
|
+
# "Secret 'database_url' not found",
|
|
240
|
+
# hint: "Verify the secret name and environment",
|
|
241
|
+
# code: "secret_not_found"
|
|
242
|
+
# )
|
|
243
|
+
#
|
|
244
|
+
class NotFoundError < Error
|
|
245
|
+
# @return [String, nil] The type of resource that was not found
|
|
246
|
+
attr_reader :resource_type
|
|
247
|
+
|
|
248
|
+
# @return [String, nil] The identifier of the resource that was not found
|
|
249
|
+
attr_reader :resource_id
|
|
250
|
+
|
|
251
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, resource_type: nil, resource_id: nil)
|
|
252
|
+
@resource_type = resource_type
|
|
253
|
+
@resource_id = resource_id
|
|
254
|
+
|
|
255
|
+
code ||= 'not_found'
|
|
256
|
+
|
|
257
|
+
context ||= {}
|
|
258
|
+
context[:resource_type] = resource_type if resource_type
|
|
259
|
+
context[:resource_id] = resource_id if resource_id
|
|
260
|
+
|
|
261
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Raised when a server-side error occurs.
|
|
266
|
+
#
|
|
267
|
+
# @example Server error
|
|
268
|
+
# raise BrainzLab::ServerError.new(
|
|
269
|
+
# "Internal server error",
|
|
270
|
+
# hint: "This is a temporary issue. Please retry your request.",
|
|
271
|
+
# code: "internal_server_error"
|
|
272
|
+
# )
|
|
273
|
+
#
|
|
274
|
+
class ServerError < Error
|
|
275
|
+
# @return [Integer, nil] HTTP status code from the server
|
|
276
|
+
attr_reader :status_code
|
|
277
|
+
|
|
278
|
+
# @return [String, nil] Request ID for support reference
|
|
279
|
+
attr_reader :request_id
|
|
280
|
+
|
|
281
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, status_code: nil, request_id: nil)
|
|
282
|
+
@status_code = status_code
|
|
283
|
+
@request_id = request_id
|
|
284
|
+
|
|
285
|
+
hint ||= 'This is a temporary issue. Please retry your request. If the problem persists, contact support.'
|
|
286
|
+
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/troubleshooting#server-errors"
|
|
287
|
+
code ||= 'server_error'
|
|
288
|
+
|
|
289
|
+
context ||= {}
|
|
290
|
+
context[:status_code] = status_code if status_code
|
|
291
|
+
context[:request_id] = request_id if request_id
|
|
292
|
+
|
|
293
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
# Raised when an operation times out.
|
|
298
|
+
#
|
|
299
|
+
# @example Request timeout
|
|
300
|
+
# raise BrainzLab::TimeoutError.new(
|
|
301
|
+
# "Request timed out after 30 seconds",
|
|
302
|
+
# hint: "The operation took too long. Try again or increase timeout settings.",
|
|
303
|
+
# code: "request_timeout"
|
|
304
|
+
# )
|
|
305
|
+
#
|
|
306
|
+
class TimeoutError < Error
|
|
307
|
+
# @return [Integer, nil] Timeout duration in seconds
|
|
308
|
+
attr_reader :timeout_seconds
|
|
309
|
+
|
|
310
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, timeout_seconds: nil)
|
|
311
|
+
@timeout_seconds = timeout_seconds
|
|
312
|
+
|
|
313
|
+
hint ||= 'The operation took too long. Try again or increase timeout settings.'
|
|
314
|
+
docs_url ||= "#{DOCS_BASE_URL}/sdk/ruby/configuration#timeouts"
|
|
315
|
+
code ||= 'timeout'
|
|
316
|
+
|
|
317
|
+
context ||= {}
|
|
318
|
+
context[:timeout_seconds] = timeout_seconds if timeout_seconds
|
|
319
|
+
|
|
320
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
# Raised when a service is temporarily unavailable.
|
|
325
|
+
#
|
|
326
|
+
# @example Service unavailable
|
|
327
|
+
# raise BrainzLab::ServiceUnavailableError.new(
|
|
328
|
+
# "Vault service is currently unavailable",
|
|
329
|
+
# hint: "The service is undergoing maintenance. Please try again later.",
|
|
330
|
+
# code: "vault_unavailable"
|
|
331
|
+
# )
|
|
332
|
+
#
|
|
333
|
+
class ServiceUnavailableError < Error
|
|
334
|
+
# @return [String, nil] The name of the unavailable service
|
|
335
|
+
attr_reader :service_name
|
|
336
|
+
|
|
337
|
+
def initialize(message = nil, hint: nil, docs_url: nil, code: nil, context: nil, service_name: nil)
|
|
338
|
+
@service_name = service_name
|
|
339
|
+
|
|
340
|
+
hint ||= 'The service is temporarily unavailable. Please try again later.'
|
|
341
|
+
docs_url ||= "#{DOCS_BASE_URL}/status"
|
|
342
|
+
code ||= 'service_unavailable'
|
|
343
|
+
|
|
344
|
+
context ||= {}
|
|
345
|
+
context[:service_name] = service_name if service_name
|
|
346
|
+
|
|
347
|
+
super(message, hint: hint, docs_url: docs_url, code: code, context: context.empty? ? nil : context)
|
|
348
|
+
end
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# Helper module for wrapping low-level errors into structured BrainzLab errors
|
|
352
|
+
module ErrorHandler
|
|
353
|
+
module_function
|
|
354
|
+
|
|
355
|
+
# Wrap a standard error into a structured BrainzLab error.
|
|
356
|
+
#
|
|
357
|
+
# @param error [StandardError] The original error
|
|
358
|
+
# @param service [String] The service name (e.g., 'Vault', 'Cortex')
|
|
359
|
+
# @param operation [String] The operation being performed
|
|
360
|
+
# @return [BrainzLab::Error] A structured BrainzLab error
|
|
361
|
+
def wrap(error, service:, operation:)
|
|
362
|
+
case error
|
|
363
|
+
when Net::OpenTimeout, Net::ReadTimeout, Timeout::Error
|
|
364
|
+
TimeoutError.new(
|
|
365
|
+
"#{service} #{operation} timed out: #{error.message}",
|
|
366
|
+
hint: 'Check your network connection or increase timeout settings.',
|
|
367
|
+
code: "#{service.downcase}_timeout",
|
|
368
|
+
context: { service: service, operation: operation }
|
|
369
|
+
)
|
|
370
|
+
when Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH, Errno::ENETUNREACH
|
|
371
|
+
ConnectionError.new(
|
|
372
|
+
"Unable to connect to #{service}: #{error.message}",
|
|
373
|
+
hint: 'Check that the service is running and accessible.',
|
|
374
|
+
code: "#{service.downcase}_connection_failed",
|
|
375
|
+
context: { service: service, operation: operation }
|
|
376
|
+
)
|
|
377
|
+
when SocketError
|
|
378
|
+
ConnectionError.new(
|
|
379
|
+
"DNS resolution failed for #{service}: #{error.message}",
|
|
380
|
+
hint: 'Check your network connection and DNS settings.',
|
|
381
|
+
code: "#{service.downcase}_dns_error",
|
|
382
|
+
context: { service: service, operation: operation }
|
|
383
|
+
)
|
|
384
|
+
when JSON::ParserError
|
|
385
|
+
ServerError.new(
|
|
386
|
+
"Invalid response from #{service}: #{error.message}",
|
|
387
|
+
hint: 'The server returned an unexpected response format.',
|
|
388
|
+
code: "#{service.downcase}_invalid_response",
|
|
389
|
+
context: { service: service, operation: operation }
|
|
390
|
+
)
|
|
391
|
+
when OpenSSL::SSL::SSLError
|
|
392
|
+
ConnectionError.new(
|
|
393
|
+
"SSL error connecting to #{service}: #{error.message}",
|
|
394
|
+
hint: 'Check SSL certificates and ensure the connection is secure.',
|
|
395
|
+
code: "#{service.downcase}_ssl_error",
|
|
396
|
+
context: { service: service, operation: operation }
|
|
397
|
+
)
|
|
398
|
+
else
|
|
399
|
+
Error.new(
|
|
400
|
+
"#{service} #{operation} failed: #{error.message}",
|
|
401
|
+
hint: 'An unexpected error occurred. Check the logs for more details.',
|
|
402
|
+
code: "#{service.downcase}_error",
|
|
403
|
+
context: { service: service, operation: operation, original_error: error.class.name }
|
|
404
|
+
)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# Convert an HTTP response to a structured error.
|
|
409
|
+
#
|
|
410
|
+
# @param response [Net::HTTPResponse] The HTTP response
|
|
411
|
+
# @param service [String] The service name
|
|
412
|
+
# @param operation [String] The operation being performed
|
|
413
|
+
# @return [BrainzLab::Error] A structured BrainzLab error
|
|
414
|
+
def from_response(response, service:, operation:)
|
|
415
|
+
status_code = response.code.to_i
|
|
416
|
+
body = parse_response_body(response)
|
|
417
|
+
message = body[:message] || body[:error] || "HTTP #{status_code}"
|
|
418
|
+
request_id = response['X-Request-Id']
|
|
419
|
+
|
|
420
|
+
case status_code
|
|
421
|
+
when 400
|
|
422
|
+
ValidationError.new(
|
|
423
|
+
message,
|
|
424
|
+
hint: body[:hint] || 'Check the request parameters.',
|
|
425
|
+
code: body[:code] || 'bad_request',
|
|
426
|
+
context: { service: service, operation: operation, status_code: status_code }
|
|
427
|
+
)
|
|
428
|
+
when 401
|
|
429
|
+
AuthenticationError.new(
|
|
430
|
+
message,
|
|
431
|
+
hint: body[:hint] || "Verify your #{service} API key is correct and active.",
|
|
432
|
+
code: body[:code] || 'unauthorized',
|
|
433
|
+
context: { service: service, operation: operation }
|
|
434
|
+
)
|
|
435
|
+
when 403
|
|
436
|
+
AuthenticationError.new(
|
|
437
|
+
message,
|
|
438
|
+
hint: body[:hint] || 'Your API key does not have permission for this operation.',
|
|
439
|
+
code: body[:code] || 'forbidden',
|
|
440
|
+
context: { service: service, operation: operation }
|
|
441
|
+
)
|
|
442
|
+
when 404
|
|
443
|
+
NotFoundError.new(
|
|
444
|
+
message,
|
|
445
|
+
hint: body[:hint] || 'The requested resource does not exist.',
|
|
446
|
+
code: body[:code] || 'not_found',
|
|
447
|
+
context: { service: service, operation: operation }
|
|
448
|
+
)
|
|
449
|
+
when 422
|
|
450
|
+
ValidationError.new(
|
|
451
|
+
message,
|
|
452
|
+
hint: body[:hint] || 'The request was well-formed but contained invalid data.',
|
|
453
|
+
code: body[:code] || 'unprocessable_entity',
|
|
454
|
+
errors: body[:errors],
|
|
455
|
+
context: { service: service, operation: operation, status_code: status_code }
|
|
456
|
+
)
|
|
457
|
+
when 429
|
|
458
|
+
RateLimitError.new(
|
|
459
|
+
message,
|
|
460
|
+
retry_after: response['Retry-After']&.to_i,
|
|
461
|
+
limit: response['X-RateLimit-Limit']&.to_i,
|
|
462
|
+
remaining: response['X-RateLimit-Remaining']&.to_i,
|
|
463
|
+
context: { service: service, operation: operation }
|
|
464
|
+
)
|
|
465
|
+
when 500..599
|
|
466
|
+
ServerError.new(
|
|
467
|
+
message,
|
|
468
|
+
hint: body[:hint] || 'A server error occurred. Please retry your request.',
|
|
469
|
+
code: body[:code] || "server_error_#{status_code}",
|
|
470
|
+
status_code: status_code,
|
|
471
|
+
request_id: request_id,
|
|
472
|
+
context: { service: service, operation: operation }
|
|
473
|
+
)
|
|
474
|
+
else
|
|
475
|
+
Error.new(
|
|
476
|
+
message,
|
|
477
|
+
hint: body[:hint],
|
|
478
|
+
code: body[:code] || "http_#{status_code}",
|
|
479
|
+
context: { service: service, operation: operation, status_code: status_code }
|
|
480
|
+
)
|
|
481
|
+
end
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
def parse_response_body(response)
|
|
485
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
486
|
+
rescue JSON::ParserError, TypeError
|
|
487
|
+
{}
|
|
488
|
+
end
|
|
489
|
+
end
|
|
490
|
+
end
|
|
@@ -76,7 +76,7 @@ module BrainzLab
|
|
|
76
76
|
|
|
77
77
|
@client.send_batch(events: events, metrics: metrics)
|
|
78
78
|
rescue StandardError => e
|
|
79
|
-
BrainzLab.
|
|
79
|
+
BrainzLab.debug_log("[Flux] Batch send failed: #{e.message}")
|
|
80
80
|
end
|
|
81
81
|
|
|
82
82
|
def start_flush_thread
|
|
@@ -86,7 +86,7 @@ module BrainzLab
|
|
|
86
86
|
begin
|
|
87
87
|
flush! if size.positive?
|
|
88
88
|
rescue StandardError => e
|
|
89
|
-
BrainzLab.
|
|
89
|
+
BrainzLab.debug_log("[Flux] Flush thread error: #{e.message}")
|
|
90
90
|
end
|
|
91
91
|
end
|
|
92
92
|
end
|