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,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