brainzlab 0.1.24 → 0.1.26

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6ab7b5c063e4371cad8decac72f4f0508c3ad4da1fedd8041a97c2f6601ee601
4
- data.tar.gz: c3af39c85de1dee2a20cc4586809c5e7634fe8aa85f4145716b56e342119e48b
3
+ metadata.gz: e298648a456d207e9ec5eb9be700b6aa898fc9b77666a3afd3c737d9c126705e
4
+ data.tar.gz: b87707b7eb15cfb9dd11ca461203d0ccd1a7199bdd0ce972327381ecb675d592
5
5
  SHA512:
6
- metadata.gz: 5581a93b79f51388c3d0ea6c5a1681418618bafbabfdb6e688210e7ae07cdf5d1a047fb414434c5f5171b95363734197555b5be6a2b6ad4c7f75cdf5740ce61f
7
- data.tar.gz: 3a71004cb62f9a74fb3fdd5979da80757ffd06b30461bfb970217fb972c01c92904db9949bf52a7b48e4d5b5c20f2f745e63b908b4d49ea5c84ce3bebcc0e553
6
+ metadata.gz: f834d99194f33b06a00f18016879e83fefcd92b5ac4bd589804a7629a2b123699ecf58082923920a72384502dacc1e40ced04ccfb2b36ec2cf6c2791adab2e3a
7
+ data.tar.gz: 3999f40f6af0bb7b952baf59a35c78acbb38f89e41638824c86dc38a0319f0d964911fd3bc096bba272738c5bc1190777ce98a633a6050f9b7103b59ef8f13c7
data/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [0.1.25] - 2026-06-07
6
+
7
+ ### Added
8
+
9
+ - **`BrainzLab::Cortex` management ("send") operations** — the Cortex client can now write feature flags, not just read them:
10
+ - `Cortex.create_flag(:key, name:, type:, description:, tags:)` → `POST /api/v1/flags`
11
+ - `Cortex.enable!(:key, environment:)` / `Cortex.disable!(:key, environment:)` → idempotent `POST /api/v1/flags/:key/toggle` with an explicit `enabled` flag
12
+ - `Cortex.set_percentage(:key, pct, environment:)` → `POST /api/v1/flags/:key/set_percentage`
13
+ - Writes clear the local flag cache. Reads (`enabled?`/`get`/`variant`/`all`/`list_flags`) and provisioning (`POST /api/v1/projects/provision`) now align with the Cortex service contract.
14
+
5
15
  ## [0.1.13] - 2026-02-24
6
16
 
7
17
  ### Fixed
@@ -75,6 +75,50 @@ module BrainzLab
75
75
  nil
76
76
  end
77
77
 
78
+ # Create (push) a flag definition. Returns the created flag hash or nil.
79
+ def create_flag(attrs)
80
+ response = request(:post, '/api/v1/flags', body: { flag: attrs })
81
+
82
+ return nil unless created_or_ok?(response)
83
+
84
+ JSON.parse(response.body, symbolize_names: true)[:flag]
85
+ rescue StandardError => e
86
+ log_error('create_flag', e)
87
+ nil
88
+ end
89
+
90
+ # Set a flag's enabled state in an environment. Returns the new state or nil.
91
+ def toggle(flag_name, enabled:, environment:)
92
+ response = request(
93
+ :post,
94
+ "/api/v1/flags/#{CGI.escape(flag_name)}/toggle",
95
+ body: { enabled: enabled, environment: environment }
96
+ )
97
+
98
+ return nil unless created_or_ok?(response)
99
+
100
+ JSON.parse(response.body, symbolize_names: true)
101
+ rescue StandardError => e
102
+ log_error('toggle', e)
103
+ nil
104
+ end
105
+
106
+ # Set a percentage flag's rollout in an environment. Returns the new state or nil.
107
+ def set_percentage(flag_name, percentage:, environment:)
108
+ response = request(
109
+ :post,
110
+ "/api/v1/flags/#{CGI.escape(flag_name)}/set_percentage",
111
+ body: { percentage: percentage, environment: environment }
112
+ )
113
+
114
+ return nil unless created_or_ok?(response)
115
+
116
+ JSON.parse(response.body, symbolize_names: true)
117
+ rescue StandardError => e
118
+ log_error('set_percentage', e)
119
+ nil
120
+ end
121
+
78
122
  def provision(project_id:, app_name:)
79
123
  response = request(
80
124
  :post,
@@ -91,6 +135,10 @@ module BrainzLab
91
135
 
92
136
  private
93
137
 
138
+ def created_or_ok?(response)
139
+ response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPCreated)
140
+ end
141
+
94
142
  def request(method, path, headers: {}, body: nil, params: nil, use_service_key: false)
95
143
  uri = URI.parse("#{@base_url}#{path}")
96
144
 
@@ -123,6 +123,52 @@ module BrainzLab
123
123
  client.get_flag(flag_name.to_s)
124
124
  end
125
125
 
126
+ # === Management ("send" / write operations) ===
127
+
128
+ # Create (push) a new feature flag definition.
129
+ # @param flag_name [String, Symbol] flag key (lowercase, underscores)
130
+ # @param name [String] human-readable name (defaults to the key)
131
+ # @param type [String] boolean | percentage | variant | segment
132
+ # @return [Hash, nil] the created flag, or nil if disabled/unconfigured
133
+ #
134
+ # @example
135
+ # BrainzLab::Cortex.create_flag(:new_checkout, name: "New Checkout")
136
+ def create_flag(flag_name, name: nil, type: 'boolean', description: nil, tags: [])
137
+ return nil unless writable?
138
+
139
+ client.create_flag(
140
+ key: flag_name.to_s,
141
+ name: name || flag_name.to_s,
142
+ flag_type: type,
143
+ description: description,
144
+ tags: tags
145
+ )
146
+ end
147
+
148
+ # Enable a flag in an environment (idempotent).
149
+ # @return [Hash, nil] the new state, or nil if disabled/unconfigured
150
+ def enable!(flag_name, environment: nil)
151
+ set_enabled(flag_name, true, environment: environment)
152
+ end
153
+
154
+ # Disable a flag in an environment (idempotent).
155
+ def disable!(flag_name, environment: nil)
156
+ set_enabled(flag_name, false, environment: environment)
157
+ end
158
+
159
+ # Set the rollout percentage (0-100) for a percentage flag.
160
+ def set_percentage(flag_name, percentage, environment: nil)
161
+ return nil unless writable?
162
+
163
+ result = client.set_percentage(
164
+ flag_name.to_s,
165
+ percentage: percentage,
166
+ environment: environment || BrainzLab.configuration.environment
167
+ )
168
+ clear_cache!
169
+ result
170
+ end
171
+
126
172
  # Clear the flag cache
127
173
  def clear_cache!
128
174
  cache.clear!
@@ -186,6 +232,26 @@ module BrainzLab
186
232
  BrainzLab.configuration.cortex_enabled
187
233
  end
188
234
 
235
+ # Write operations require the module on, a provisioned project, and a key.
236
+ def writable?
237
+ return false unless module_enabled?
238
+
239
+ ensure_provisioned!
240
+ BrainzLab.configuration.cortex_valid?
241
+ end
242
+
243
+ def set_enabled(flag_name, enabled, environment:)
244
+ return nil unless writable?
245
+
246
+ result = client.toggle(
247
+ flag_name.to_s,
248
+ enabled: enabled,
249
+ environment: environment || BrainzLab.configuration.environment
250
+ )
251
+ clear_cache!
252
+ result
253
+ end
254
+
189
255
  def merge_context(context)
190
256
  default_context = BrainzLab.configuration.cortex_default_context || {}
191
257
  thread_context = Thread.current[:cortex_context] || {}
@@ -13,8 +13,10 @@ module BrainzLab
13
13
  @metrics = []
14
14
  @mutex = Mutex.new
15
15
  @last_flush = Time.now
16
+ @shutdown = false
16
17
 
17
18
  start_flush_thread
19
+ setup_at_exit
18
20
  end
19
21
 
20
22
  def add(type, data)
@@ -80,8 +82,10 @@ module BrainzLab
80
82
  end
81
83
 
82
84
  def start_flush_thread
83
- Thread.new do
85
+ @flush_thread = Thread.new do
84
86
  loop do
87
+ break if @shutdown
88
+
85
89
  sleep FLUSH_INTERVAL
86
90
  begin
87
91
  flush! if size.positive?
@@ -90,6 +94,20 @@ module BrainzLab
90
94
  end
91
95
  end
92
96
  end
97
+ @flush_thread.abort_on_exception = false
98
+ end
99
+
100
+ # Flush whatever is buffered when the process exits (SIGTERM on deploy,
101
+ # graceful Puma worker shutdown) so the last batch isn't lost.
102
+ def setup_at_exit
103
+ at_exit do
104
+ @shutdown = true
105
+ begin
106
+ flush! if size.positive?
107
+ rescue StandardError
108
+ nil
109
+ end
110
+ end
93
111
  end
94
112
  end
95
113
  end
@@ -164,12 +164,22 @@ module BrainzLab
164
164
  end
165
165
 
166
166
  def buffer
167
- @buffer ||= Buffer.new(client)
167
+ # Fork-safety: the buffer owns a background flush thread that does NOT
168
+ # survive a fork (e.g. Puma workers). If we're now in a different
169
+ # process than the one that created the buffer, recreate it so each
170
+ # worker gets a live flush thread — otherwise web-emitted events/metrics
171
+ # buffer forever in the worker and never get sent.
172
+ if @buffer.nil? || @buffer_pid != Process.pid
173
+ @buffer = Buffer.new(client)
174
+ @buffer_pid = Process.pid
175
+ end
176
+ @buffer
168
177
  end
169
178
 
170
179
  def reset!
171
180
  @client = nil
172
181
  @buffer = nil
182
+ @buffer_pid = nil
173
183
  @provisioner = nil
174
184
  @provisioned = false
175
185
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module BrainzLab
4
- VERSION = '0.1.24'
4
+ VERSION = '0.1.26'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: brainzlab
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.24
4
+ version: 0.1.26
5
5
  platform: ruby
6
6
  authors:
7
7
  - BrainzLab