brainzlab 0.1.1 → 0.1.3
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/LICENSE +6 -21
- data/README.md +24 -2
- data/lib/brainzlab/beacon/client.rb +207 -0
- data/lib/brainzlab/beacon/provisioner.rb +44 -0
- data/lib/brainzlab/beacon.rb +215 -0
- data/lib/brainzlab/configuration.rb +372 -32
- data/lib/brainzlab/context.rb +2 -3
- data/lib/brainzlab/cortex/cache.rb +59 -0
- data/lib/brainzlab/cortex/client.rb +139 -0
- data/lib/brainzlab/cortex/provisioner.rb +49 -0
- data/lib/brainzlab/cortex.rb +223 -0
- data/lib/brainzlab/dendrite/client.rb +230 -0
- data/lib/brainzlab/dendrite/provisioner.rb +44 -0
- data/lib/brainzlab/dendrite.rb +195 -0
- data/lib/brainzlab/devtools/assets/devtools.css +1106 -0
- data/lib/brainzlab/devtools/assets/devtools.js +322 -0
- data/lib/brainzlab/devtools/assets/logo.svg +6 -0
- data/lib/brainzlab/devtools/assets/templates/debug_panel.html.erb +500 -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/flux/buffer.rb +96 -0
- data/lib/brainzlab/flux/client.rb +68 -0
- data/lib/brainzlab/flux/provisioner.rb +57 -0
- data/lib/brainzlab/flux.rb +174 -0
- data/lib/brainzlab/instrumentation/action_mailer.rb +14 -13
- data/lib/brainzlab/instrumentation/active_record.rb +28 -13
- data/lib/brainzlab/instrumentation/aws.rb +183 -0
- data/lib/brainzlab/instrumentation/dalli.rb +108 -0
- data/lib/brainzlab/instrumentation/delayed_job.rb +27 -29
- data/lib/brainzlab/instrumentation/elasticsearch.rb +23 -24
- data/lib/brainzlab/instrumentation/excon.rb +152 -0
- data/lib/brainzlab/instrumentation/faraday.rb +3 -4
- data/lib/brainzlab/instrumentation/good_job.rb +102 -0
- data/lib/brainzlab/instrumentation/grape.rb +24 -24
- data/lib/brainzlab/instrumentation/graphql.rb +24 -23
- data/lib/brainzlab/instrumentation/httparty.rb +13 -14
- data/lib/brainzlab/instrumentation/mongodb.rb +7 -7
- data/lib/brainzlab/instrumentation/net_http.rb +6 -6
- data/lib/brainzlab/instrumentation/redis.rb +14 -21
- data/lib/brainzlab/instrumentation/resque.rb +114 -0
- data/lib/brainzlab/instrumentation/sidekiq.rb +29 -28
- 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 +84 -12
- data/lib/brainzlab/nerve/client.rb +215 -0
- data/lib/brainzlab/nerve/provisioner.rb +44 -0
- data/lib/brainzlab/nerve.rb +219 -0
- data/lib/brainzlab/pulse/client.rb +15 -11
- data/lib/brainzlab/pulse/instrumentation.rb +90 -53
- data/lib/brainzlab/pulse/propagation.rb +29 -29
- data/lib/brainzlab/pulse/provisioner.rb +12 -12
- data/lib/brainzlab/pulse/tracer.rb +4 -4
- data/lib/brainzlab/pulse.rb +14 -14
- data/lib/brainzlab/rails/log_formatter.rb +127 -121
- data/lib/brainzlab/rails/log_subscriber.rb +70 -77
- data/lib/brainzlab/rails/railtie.rb +96 -86
- data/lib/brainzlab/recall/buffer.rb +1 -1
- data/lib/brainzlab/recall/client.rb +14 -10
- data/lib/brainzlab/recall/logger.rb +16 -18
- data/lib/brainzlab/recall/provisioner.rb +29 -12
- data/lib/brainzlab/recall.rb +14 -11
- data/lib/brainzlab/reflex/breadcrumbs.rb +2 -2
- data/lib/brainzlab/reflex/client.rb +14 -10
- data/lib/brainzlab/reflex/provisioner.rb +12 -12
- data/lib/brainzlab/reflex.rb +31 -31
- data/lib/brainzlab/sentinel/client.rb +216 -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 +55 -0
- data/lib/brainzlab/signal.rb +136 -0
- data/lib/brainzlab/synapse/client.rb +288 -0
- data/lib/brainzlab/synapse/provisioner.rb +44 -0
- data/lib/brainzlab/synapse.rb +270 -0
- data/lib/brainzlab/utilities/circuit_breaker.rb +261 -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 +196 -0
- data/lib/brainzlab/vault/provisioner.rb +49 -0
- data/lib/brainzlab/vault.rb +262 -0
- data/lib/brainzlab/version.rb +1 -1
- data/lib/brainzlab/vision/client.rb +128 -0
- data/lib/brainzlab/vision/provisioner.rb +136 -0
- data/lib/brainzlab/vision.rb +155 -0
- data/lib/brainzlab-sdk.rb +1 -1
- data/lib/brainzlab.rb +112 -13
- data/lib/generators/brainzlab/install/install_generator.rb +29 -27
- metadata +60 -1
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
BrainzLab.debug_log("[Cortex::Client] #{operation} failed: #{error.message}")
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
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
|
|
@@ -0,0 +1,230 @@
|
|
|
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 Dendrite
|
|
10
|
+
class Client
|
|
11
|
+
def initialize(config)
|
|
12
|
+
@config = config
|
|
13
|
+
@base_url = config.dendrite_url || 'https://dendrite.brainzlab.ai'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Connect a repository
|
|
17
|
+
def connect_repository(url:, name: nil, branch: 'main', **options)
|
|
18
|
+
response = request(
|
|
19
|
+
:post,
|
|
20
|
+
'/api/v1/repositories',
|
|
21
|
+
body: {
|
|
22
|
+
url: url,
|
|
23
|
+
name: name,
|
|
24
|
+
branch: branch,
|
|
25
|
+
**options
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
return nil unless response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
30
|
+
|
|
31
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
log_error('connect_repository', e)
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Trigger sync for a repository
|
|
38
|
+
def sync_repository(repo_id)
|
|
39
|
+
response = request(:post, "/api/v1/repositories/#{repo_id}/sync")
|
|
40
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPAccepted)
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
log_error('sync_repository', e)
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Get repository status
|
|
47
|
+
def get_repository(repo_id)
|
|
48
|
+
response = request(:get, "/api/v1/repositories/#{repo_id}")
|
|
49
|
+
|
|
50
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
51
|
+
|
|
52
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
53
|
+
rescue StandardError => e
|
|
54
|
+
log_error('get_repository', e)
|
|
55
|
+
nil
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# List repositories
|
|
59
|
+
def list_repositories
|
|
60
|
+
response = request(:get, '/api/v1/repositories')
|
|
61
|
+
|
|
62
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
63
|
+
|
|
64
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
65
|
+
data[:repositories] || []
|
|
66
|
+
rescue StandardError => e
|
|
67
|
+
log_error('list_repositories', e)
|
|
68
|
+
[]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get wiki pages for a repository
|
|
72
|
+
def get_wiki(repo_id)
|
|
73
|
+
response = request(:get, "/api/v1/wiki/#{repo_id}")
|
|
74
|
+
|
|
75
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
76
|
+
|
|
77
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
78
|
+
rescue StandardError => e
|
|
79
|
+
log_error('get_wiki', e)
|
|
80
|
+
nil
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Get a specific wiki page
|
|
84
|
+
def get_wiki_page(repo_id, page_slug)
|
|
85
|
+
response = request(:get, "/api/v1/wiki/#{repo_id}/#{CGI.escape(page_slug)}")
|
|
86
|
+
|
|
87
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
88
|
+
|
|
89
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
90
|
+
rescue StandardError => e
|
|
91
|
+
log_error('get_wiki_page', e)
|
|
92
|
+
nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Semantic search across codebase
|
|
96
|
+
def search(repo_id, query, limit: 10)
|
|
97
|
+
response = request(
|
|
98
|
+
:get,
|
|
99
|
+
'/api/v1/search',
|
|
100
|
+
params: { repo_id: repo_id, q: query, limit: limit }
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
return [] unless response.is_a?(Net::HTTPSuccess)
|
|
104
|
+
|
|
105
|
+
data = JSON.parse(response.body, symbolize_names: true)
|
|
106
|
+
data[:results] || []
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
log_error('search', e)
|
|
109
|
+
[]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Ask a question about the codebase
|
|
113
|
+
def ask(repo_id, question, session_id: nil)
|
|
114
|
+
response = request(
|
|
115
|
+
:post,
|
|
116
|
+
'/api/v1/chat',
|
|
117
|
+
body: {
|
|
118
|
+
repo_id: repo_id,
|
|
119
|
+
question: question,
|
|
120
|
+
session_id: session_id
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
125
|
+
|
|
126
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
127
|
+
rescue StandardError => e
|
|
128
|
+
log_error('ask', e)
|
|
129
|
+
nil
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Explain a specific file or function
|
|
133
|
+
def explain(repo_id, path, symbol: nil)
|
|
134
|
+
response = request(
|
|
135
|
+
:post,
|
|
136
|
+
'/api/v1/explain',
|
|
137
|
+
body: {
|
|
138
|
+
repo_id: repo_id,
|
|
139
|
+
path: path,
|
|
140
|
+
symbol: symbol
|
|
141
|
+
}
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
145
|
+
|
|
146
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
147
|
+
rescue StandardError => e
|
|
148
|
+
log_error('explain', e)
|
|
149
|
+
nil
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Generate a diagram
|
|
153
|
+
def generate_diagram(repo_id, type:, scope: nil)
|
|
154
|
+
response = request(
|
|
155
|
+
:post,
|
|
156
|
+
'/api/v1/diagrams',
|
|
157
|
+
body: {
|
|
158
|
+
repo_id: repo_id,
|
|
159
|
+
type: type, # class, er, sequence, architecture
|
|
160
|
+
scope: scope
|
|
161
|
+
}
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
return nil unless response.is_a?(Net::HTTPSuccess)
|
|
165
|
+
|
|
166
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
167
|
+
rescue StandardError => e
|
|
168
|
+
log_error('generate_diagram', e)
|
|
169
|
+
nil
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def provision(project_id:, app_name:)
|
|
173
|
+
response = request(
|
|
174
|
+
:post,
|
|
175
|
+
'/api/v1/projects/provision',
|
|
176
|
+
body: { project_id: project_id, app_name: app_name },
|
|
177
|
+
use_service_key: true
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
|
|
181
|
+
rescue StandardError => e
|
|
182
|
+
log_error('provision', e)
|
|
183
|
+
false
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
private
|
|
187
|
+
|
|
188
|
+
def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
|
|
189
|
+
uri = URI.parse("#{@base_url}#{path}")
|
|
190
|
+
|
|
191
|
+
uri.query = URI.encode_www_form(params) if params
|
|
192
|
+
|
|
193
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
194
|
+
http.use_ssl = uri.scheme == 'https'
|
|
195
|
+
http.open_timeout = 10
|
|
196
|
+
http.read_timeout = 60 # Longer timeout for AI operations
|
|
197
|
+
|
|
198
|
+
request = case method
|
|
199
|
+
when :get
|
|
200
|
+
Net::HTTP::Get.new(uri)
|
|
201
|
+
when :post
|
|
202
|
+
Net::HTTP::Post.new(uri)
|
|
203
|
+
when :put
|
|
204
|
+
Net::HTTP::Put.new(uri)
|
|
205
|
+
when :delete
|
|
206
|
+
Net::HTTP::Delete.new(uri)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
request['Content-Type'] = 'application/json'
|
|
210
|
+
request['Accept'] = 'application/json'
|
|
211
|
+
|
|
212
|
+
if use_service_key
|
|
213
|
+
request['X-Service-Key'] = @config.dendrite_master_key || @config.secret_key
|
|
214
|
+
else
|
|
215
|
+
auth_key = @config.dendrite_api_key || @config.secret_key
|
|
216
|
+
request['Authorization'] = "Bearer #{auth_key}" if auth_key
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
headers.each { |k, v| request[k] = v }
|
|
220
|
+
request.body = body.to_json if body
|
|
221
|
+
|
|
222
|
+
http.request(request)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def log_error(operation, error)
|
|
226
|
+
BrainzLab.debug_log("[Dendrite::Client] #{operation} failed: #{error.message}")
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BrainzLab
|
|
4
|
+
module Dendrite
|
|
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.dendrite_auto_provision
|
|
14
|
+
return unless valid_auth?
|
|
15
|
+
|
|
16
|
+
@provisioned = true
|
|
17
|
+
|
|
18
|
+
project_id = detect_project_id
|
|
19
|
+
return unless project_id
|
|
20
|
+
|
|
21
|
+
client = Client.new(@config)
|
|
22
|
+
client.provision(
|
|
23
|
+
project_id: project_id,
|
|
24
|
+
app_name: @config.app_name || @config.service
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
BrainzLab.debug_log("[Dendrite::Provisioner] Project provisioned: #{project_id}")
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
BrainzLab.debug_log("[Dendrite::Provisioner] Provisioning failed: #{e.message}")
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def valid_auth?
|
|
35
|
+
key = @config.dendrite_api_key || @config.dendrite_master_key || @config.secret_key
|
|
36
|
+
!key.nil? && !key.empty?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def detect_project_id
|
|
40
|
+
ENV.fetch('BRAINZLAB_PROJECT_ID', nil)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|