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,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
class Context
|
|
5
|
+
THREAD_KEY = :brainzlab_context
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
def current
|
|
9
|
+
Thread.current[THREAD_KEY] ||= new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def clear!
|
|
13
|
+
Thread.current[THREAD_KEY] = nil
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
attr_accessor :user, :request_id, :session_id, :request_method, :request_path, :request_url, :request_params,
|
|
18
|
+
:request_headers, :controller, :action
|
|
19
|
+
attr_reader :extra, :tags, :breadcrumbs
|
|
20
|
+
|
|
21
|
+
def initialize
|
|
22
|
+
@user = {}
|
|
23
|
+
@extra = {}
|
|
24
|
+
@tags = {}
|
|
25
|
+
@request_id = nil
|
|
26
|
+
@session_id = nil
|
|
27
|
+
@request_method = nil
|
|
28
|
+
@request_path = nil
|
|
29
|
+
@request_url = nil
|
|
30
|
+
@request_params = nil
|
|
31
|
+
@request_headers = nil
|
|
32
|
+
@controller = nil
|
|
33
|
+
@action = nil
|
|
34
|
+
@stack = []
|
|
35
|
+
@breadcrumbs = Reflex::Breadcrumbs.new
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def set_user(id: nil, email: nil, name: nil, **extra)
|
|
39
|
+
@user = { id: id, email: email, name: name }.compact.merge(extra)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def set_context(**data)
|
|
43
|
+
@extra.merge!(data)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def set_tags(**data)
|
|
47
|
+
@tags.merge!(data)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_context(**data)
|
|
51
|
+
push_context(data)
|
|
52
|
+
yield
|
|
53
|
+
ensure
|
|
54
|
+
pop_context
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def to_hash
|
|
58
|
+
result = {}
|
|
59
|
+
result[:request_id] = @request_id if @request_id
|
|
60
|
+
result[:session_id] = @session_id if @session_id
|
|
61
|
+
|
|
62
|
+
merged_extra = @extra.dup
|
|
63
|
+
@stack.each { |ctx| merged_extra.merge!(ctx) }
|
|
64
|
+
|
|
65
|
+
result[:user] = @user unless @user.empty?
|
|
66
|
+
result[:tags] = @tags unless @tags.empty?
|
|
67
|
+
result[:context] = merged_extra unless merged_extra.empty?
|
|
68
|
+
|
|
69
|
+
result
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def data_hash
|
|
73
|
+
merged = @extra.dup
|
|
74
|
+
@stack.each { |ctx| merged.merge!(ctx) }
|
|
75
|
+
merged[:user] = @user unless @user.empty?
|
|
76
|
+
merged[:tags] = @tags unless @tags.empty?
|
|
77
|
+
merged
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def push_context(data)
|
|
83
|
+
@stack.push(data)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def pop_context
|
|
87
|
+
@stack.pop
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Cortex
|
|
5
|
+
class Cache
|
|
6
|
+
def initialize(ttl = 60)
|
|
7
|
+
@ttl = ttl
|
|
8
|
+
@store = {}
|
|
9
|
+
@timestamps = {}
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(key)
|
|
14
|
+
@mutex.synchronize do
|
|
15
|
+
return nil unless @store.key?(key)
|
|
16
|
+
return nil if expired?(key)
|
|
17
|
+
|
|
18
|
+
@store[key]
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def set(key, value)
|
|
23
|
+
@mutex.synchronize do
|
|
24
|
+
@store[key] = value
|
|
25
|
+
@timestamps[key] = Time.now
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def has?(key)
|
|
30
|
+
@mutex.synchronize do
|
|
31
|
+
@store.key?(key) && !expired?(key)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def delete(key)
|
|
36
|
+
@mutex.synchronize do
|
|
37
|
+
@store.delete(key)
|
|
38
|
+
@timestamps.delete(key)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clear!
|
|
43
|
+
@mutex.synchronize do
|
|
44
|
+
@store.clear
|
|
45
|
+
@timestamps.clear
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def expired?(key)
|
|
52
|
+
timestamp = @timestamps[key]
|
|
53
|
+
return true unless timestamp
|
|
54
|
+
|
|
55
|
+
Time.now - timestamp > @ttl
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'net/http'
|
|
4
|
+
require 'json'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'cgi'
|
|
7
|
+
|
|
8
|
+
module BrainzLab
|
|
9
|
+
module Cortex
|
|
10
|
+
class Client
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@config = config
|
|
13
|
+
@base_url = config.cortex_url || 'https://cortex.brainzlab.ai'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Evaluate a single flag
|
|
17
|
+
def evaluate(flag_name, context: {})
|
|
18
|
+
response = request(
|
|
19
|
+
:post,
|
|
20
|
+
'/api/v1/evaluate',
|
|
21
|
+
body: {
|
|
22
|
+
flag: flag_name,
|
|
23
|
+
context: context
|
|
24
|
+
}
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
28
|
+
|
|
29
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
30
|
+
data[:result]
|
|
31
|
+
rescue StandardError => e
|
|
32
|
+
log_error('evaluate', e)
|
|
33
|
+
nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Evaluate multiple flags at once
|
|
37
|
+
def evaluate_all(context: {})
|
|
38
|
+
response = request(
|
|
39
|
+
:post,
|
|
40
|
+
'/api/v1/evaluate/batch',
|
|
41
|
+
body: { context: context }
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
return {} unless response.is_a?(Net::HTTPSuccess)
|
|
45
|
+
|
|
46
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
47
|
+
data[:flags] || {}
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
log_error('evaluate_all', e)
|
|
50
|
+
{}
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# List all flags
|
|
54
|
+
def list
|
|
55
|
+
response = request(:get, '/api/v1/flags')
|
|
56
|
+
|
|
57
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
58
|
+
|
|
59
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
60
|
+
data[:flags] || []
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
log_error('list', e)
|
|
63
|
+
[]
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Get flag details
|
|
67
|
+
def get_flag(flag_name)
|
|
68
|
+
response = request(:get, "/api/v1/flags/#{CGI.escape(flag_name.to_s)}")
|
|
69
|
+
|
|
70
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
71
|
+
|
|
72
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
73
|
+
rescue StandardError => e
|
|
74
|
+
log_error('get_flag', e)
|
|
75
|
+
nil
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def provision(project_id:, app_name:)
|
|
79
|
+
response = request(
|
|
80
|
+
:post,
|
|
81
|
+
'/api/v1/projects/provision',
|
|
82
|
+
body: { project_id: project_id, app_name: app_name },
|
|
83
|
+
use_service_key: true
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
log_error('provision', e)
|
|
89
|
+
false
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
private
|
|
93
|
+
|
|
94
|
+
def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
|
|
95
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
96
|
+
|
|
97
|
+
uri.query = URI.encode_www_form(params) if params
|
|
98
|
+
|
|
99
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
100
|
+
http.use_ssl = uri.scheme == 'https'
|
|
101
|
+
http.open_timeout = 5
|
|
102
|
+
http.read_timeout = 10
|
|
103
|
+
|
|
104
|
+
request = case method
|
|
105
|
+
when :get
|
|
106
|
+
Net::HTTP::Get.new(uri)
|
|
107
|
+
when :post
|
|
108
|
+
Net::HTTP::Post.new(uri)
|
|
109
|
+
when :put
|
|
110
|
+
Net::HTTP::Put.new(uri)
|
|
111
|
+
when :delete
|
|
112
|
+
Net::HTTP::Delete.new(uri)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Set headers
|
|
116
|
+
request['Content-Type'] = 'application/json'
|
|
117
|
+
request['Accept'] = 'application/json'
|
|
118
|
+
|
|
119
|
+
if use_service_key
|
|
120
|
+
request['X-Service-Key'] = @config.cortex_master_key || @config.secret_key
|
|
121
|
+
else
|
|
122
|
+
auth_key = @config.cortex_api_key || @config.secret_key
|
|
123
|
+
request['Authorization'] = "Bearer #{auth_key}" if auth_key
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
headers.each { |k, v| request[k] = v }
|
|
127
|
+
|
|
128
|
+
# Set body
|
|
129
|
+
request.body = body.to_json if body
|
|
130
|
+
|
|
131
|
+
http.request(request)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def log_error(operation, error)
|
|
135
|
+
structured_error = ErrorHandler.wrap(error, service: 'Cortex', operation: operation)
|
|
136
|
+
BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{structured_error.message}")
|
|
137
|
+
|
|
138
|
+
# Call on_error callback if configured
|
|
139
|
+
if @config.on_error
|
|
140
|
+
@config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def handle_response_error(response, operation)
|
|
145
|
+
return if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated) || response.is_a?(Net::HTTPNoContent)
|
|
146
|
+
|
|
147
|
+
structured_error = ErrorHandler.from_response(response, service: 'Cortex', operation: operation)
|
|
148
|
+
BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{structured_error.message}")
|
|
149
|
+
|
|
150
|
+
# Call on_error callback if configured
|
|
151
|
+
if @config.on_error
|
|
152
|
+
@config.on_error.call(structured_error, { service: 'Cortex', operation: operation })
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
structured_error
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Cortex
|
|
5
|
+
class Provisioner
|
|
6
|
+
def initialize(config)
|
|
7
|
+
@config = config
|
|
8
|
+
@provisioned = false
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def ensure_project!
|
|
12
|
+
return if @provisioned
|
|
13
|
+
return unless @config.cortex_auto_provision
|
|
14
|
+
return unless valid_auth?
|
|
15
|
+
|
|
16
|
+
@provisioned = true
|
|
17
|
+
|
|
18
|
+
# Try to provision with Platform project ID
|
|
19
|
+
project_id = detect_project_id
|
|
20
|
+
return unless project_id
|
|
21
|
+
|
|
22
|
+
client = Client.new(@config)
|
|
23
|
+
client.provision(
|
|
24
|
+
project_id: project_id,
|
|
25
|
+
app_name: @config.app_name || @config.service
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
BrainzLab.debug_log("[Cortex::Provisioner] Project provisioned: #{project_id}")
|
|
29
|
+
rescue StandardError => e
|
|
30
|
+
BrainzLab.debug_log("[Cortex::Provisioner] Provisioning failed: #{e.message}")
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def valid_auth?
|
|
36
|
+
key = @config.cortex_api_key || @config.cortex_master_key || @config.secret_key
|
|
37
|
+
!key.nil? && !key.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def detect_project_id
|
|
41
|
+
# Try environment variable first
|
|
42
|
+
return ENV['BRAINZLAB_PROJECT_ID'] if ENV['BRAINZLAB_PROJECT_ID']
|
|
43
|
+
|
|
44
|
+
# Could also detect from Platform API if we have a secret key
|
|
45
|
+
nil
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'cortex/client'
|
|
4
|
+
require_relative 'cortex/cache'
|
|
5
|
+
require_relative 'cortex/provisioner'
|
|
6
|
+
|
|
7
|
+
module BrainzLab
|
|
8
|
+
module Cortex
|
|
9
|
+
class << self
|
|
10
|
+
# Check if a feature flag is enabled
|
|
11
|
+
# @param flag_name [String, Symbol] The flag name
|
|
12
|
+
# @param context [Hash] Evaluation context (user, attributes, etc.)
|
|
13
|
+
# @return [Boolean] True if the flag is enabled
|
|
14
|
+
#
|
|
15
|
+
# @example
|
|
16
|
+
# if BrainzLab::Cortex.enabled?(:new_checkout, user: current_user)
|
|
17
|
+
# render_new_checkout
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
def enabled?(flag_name, **context)
|
|
21
|
+
result = get(flag_name, **context)
|
|
22
|
+
[true, 'true'].include?(result)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Check if a feature flag is disabled
|
|
26
|
+
# @param flag_name [String, Symbol] The flag name
|
|
27
|
+
# @param context [Hash] Evaluation context
|
|
28
|
+
# @return [Boolean] True if the flag is disabled
|
|
29
|
+
def disabled?(flag_name, **context)
|
|
30
|
+
!enabled?(flag_name, **context)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Get the value of a feature flag
|
|
34
|
+
# @param flag_name [String, Symbol] The flag name
|
|
35
|
+
# @param context [Hash] Evaluation context
|
|
36
|
+
# @param default [Object] Default value if flag not found
|
|
37
|
+
# @return [Object] The flag value
|
|
38
|
+
#
|
|
39
|
+
# @example
|
|
40
|
+
# limit = BrainzLab::Cortex.get(:rate_limit, user: current_user, default: 100)
|
|
41
|
+
#
|
|
42
|
+
def get(flag_name, default: nil, **context)
|
|
43
|
+
return default unless module_enabled?
|
|
44
|
+
|
|
45
|
+
ensure_provisioned!
|
|
46
|
+
return default unless BrainzLab.configuration.cortex_valid?
|
|
47
|
+
|
|
48
|
+
flag_key = flag_name.to_s
|
|
49
|
+
merged_context = merge_context(context)
|
|
50
|
+
cache_key = build_cache_key(flag_key, merged_context)
|
|
51
|
+
|
|
52
|
+
# Check cache first
|
|
53
|
+
return cache.get(cache_key) if BrainzLab.configuration.cortex_cache_enabled && cache.has?(cache_key)
|
|
54
|
+
|
|
55
|
+
result = client.evaluate(flag_key, context: merged_context)
|
|
56
|
+
|
|
57
|
+
if result.nil?
|
|
58
|
+
default
|
|
59
|
+
else
|
|
60
|
+
cache.set(cache_key, result) if BrainzLab.configuration.cortex_cache_enabled
|
|
61
|
+
result
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Get the variant for an A/B test flag
|
|
66
|
+
# @param flag_name [String, Symbol] The flag name
|
|
67
|
+
# @param context [Hash] Evaluation context
|
|
68
|
+
# @param default [String] Default variant if flag not found
|
|
69
|
+
# @return [String, nil] The variant name
|
|
70
|
+
#
|
|
71
|
+
# @example
|
|
72
|
+
# variant = BrainzLab::Cortex.variant(:checkout_experiment, user: current_user)
|
|
73
|
+
# case variant
|
|
74
|
+
# when "control" then render_control
|
|
75
|
+
# when "treatment_a" then render_treatment_a
|
|
76
|
+
# when "treatment_b" then render_treatment_b
|
|
77
|
+
# end
|
|
78
|
+
#
|
|
79
|
+
def variant(flag_name, default: nil, **context)
|
|
80
|
+
result = get(flag_name, **context)
|
|
81
|
+
result.is_a?(String) ? result : default
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Get all flags for a context
|
|
85
|
+
# @param context [Hash] Evaluation context
|
|
86
|
+
# @return [Hash] All flag values
|
|
87
|
+
#
|
|
88
|
+
# @example
|
|
89
|
+
# flags = BrainzLab::Cortex.all(user: current_user)
|
|
90
|
+
# flags[:new_checkout] # => true
|
|
91
|
+
# flags[:rate_limit] # => 200
|
|
92
|
+
#
|
|
93
|
+
def all(**context)
|
|
94
|
+
return {} unless module_enabled?
|
|
95
|
+
|
|
96
|
+
ensure_provisioned!
|
|
97
|
+
return {} unless BrainzLab.configuration.cortex_valid?
|
|
98
|
+
|
|
99
|
+
merged_context = merge_context(context)
|
|
100
|
+
client.evaluate_all(context: merged_context)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# List all flag definitions
|
|
104
|
+
# @return [Array<Hash>] List of flag metadata
|
|
105
|
+
def list_flags
|
|
106
|
+
return [] unless module_enabled?
|
|
107
|
+
|
|
108
|
+
ensure_provisioned!
|
|
109
|
+
return [] unless BrainzLab.configuration.cortex_valid?
|
|
110
|
+
|
|
111
|
+
client.list
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Get a flag's configuration
|
|
115
|
+
# @param flag_name [String, Symbol] The flag name
|
|
116
|
+
# @return [Hash, nil] Flag configuration
|
|
117
|
+
def flag_config(flag_name)
|
|
118
|
+
return nil unless module_enabled?
|
|
119
|
+
|
|
120
|
+
ensure_provisioned!
|
|
121
|
+
return nil unless BrainzLab.configuration.cortex_valid?
|
|
122
|
+
|
|
123
|
+
client.get_flag(flag_name.to_s)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Clear the flag cache
|
|
127
|
+
def clear_cache!
|
|
128
|
+
cache.clear!
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# === Context Helpers ===
|
|
132
|
+
|
|
133
|
+
# Set default context for all evaluations in current request
|
|
134
|
+
# @param context [Hash] Context to merge
|
|
135
|
+
def set_context(**context)
|
|
136
|
+
Thread.current[:cortex_context] = (Thread.current[:cortex_context] || {}).merge(context)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Clear the current context
|
|
140
|
+
def clear_context!
|
|
141
|
+
Thread.current[:cortex_context] = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Evaluate flags with a temporary context
|
|
145
|
+
# @param context [Hash] Temporary context
|
|
146
|
+
def with_context(**context)
|
|
147
|
+
previous = Thread.current[:cortex_context]
|
|
148
|
+
Thread.current[:cortex_context] = (previous || {}).merge(context)
|
|
149
|
+
yield
|
|
150
|
+
ensure
|
|
151
|
+
Thread.current[:cortex_context] = previous
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# === INTERNAL ===
|
|
155
|
+
|
|
156
|
+
def ensure_provisioned!
|
|
157
|
+
return if @provisioned
|
|
158
|
+
|
|
159
|
+
@provisioned = true
|
|
160
|
+
provisioner.ensure_project!
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def provisioner
|
|
164
|
+
@provisioner ||= Provisioner.new(BrainzLab.configuration)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def client
|
|
168
|
+
@client ||= Client.new(BrainzLab.configuration)
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def cache
|
|
172
|
+
@cache ||= Cache.new(BrainzLab.configuration.cortex_cache_ttl)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def reset!
|
|
176
|
+
@client = nil
|
|
177
|
+
@provisioner = nil
|
|
178
|
+
@cache = nil
|
|
179
|
+
@provisioned = false
|
|
180
|
+
Thread.current[:cortex_context] = nil
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private
|
|
184
|
+
|
|
185
|
+
def module_enabled?
|
|
186
|
+
BrainzLab.configuration.cortex_enabled
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def merge_context(context)
|
|
190
|
+
default_context = BrainzLab.configuration.cortex_default_context || {}
|
|
191
|
+
thread_context = Thread.current[:cortex_context] || {}
|
|
192
|
+
|
|
193
|
+
# Also include user from BrainzLab context if available
|
|
194
|
+
brainzlab_context = {}
|
|
195
|
+
brainzlab_context[:user] = BrainzLab::Context.current.user if BrainzLab::Context.current.user
|
|
196
|
+
|
|
197
|
+
# Normalize user context
|
|
198
|
+
merged = default_context.merge(brainzlab_context).merge(thread_context).merge(context)
|
|
199
|
+
|
|
200
|
+
# Convert user object to hash if needed
|
|
201
|
+
if merged[:user].respond_to?(:id)
|
|
202
|
+
merged[:user] = {
|
|
203
|
+
id: merged[:user].id.to_s,
|
|
204
|
+
email: merged[:user].try(:email),
|
|
205
|
+
name: merged[:user].try(:name)
|
|
206
|
+
}.compact
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
merged
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def build_cache_key(flag_name, context)
|
|
213
|
+
# Include relevant context in cache key
|
|
214
|
+
user_id = context.dig(:user, :id) || context[:user_id]
|
|
215
|
+
env = BrainzLab.configuration.environment
|
|
216
|
+
|
|
217
|
+
parts = [env, flag_name]
|
|
218
|
+
parts << "u:#{user_id}" if user_id
|
|
219
|
+
parts.join(':')
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|