abmeter 0.3.0 → 0.5.0
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/README.md +20 -9
- data/bin/generate_num_utils_parity_fixture.rb +36 -0
- data/lib/abmeter/api_error.rb +21 -1
- data/lib/abmeter/async_submitter.rb +56 -18
- data/lib/abmeter/client.rb +2 -0
- data/lib/abmeter/constants.rb +1 -0
- data/lib/abmeter/core/assignment_config/audience.rb +2 -0
- data/lib/abmeter/core/assignment_config/exposable.rb +5 -1
- data/lib/abmeter/core/user.rb +1 -1
- data/lib/abmeter/core/user_parameter_resolver.rb +0 -1
- data/lib/abmeter/resolver_provider.rb +22 -2
- data/lib/abmeter/version.rb +1 -1
- data/lib/abmeter.rb +20 -18
- metadata +5 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 127c157ec200964f199fb170d036d9b9898f717a1634002d02dcd211c3296387
|
|
4
|
+
data.tar.gz: 9774017d52cf7b9925eddd837fd4972a693cfd5b76f11c4fe20bd8692d85ddec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6174b2a6d2308658310f5a2f35df3d3a986f0979b2a87cd318b3f051469074fa1cf2ead76a53b4a18569dd50d28181b6bdde256c9086be27612e3e12525efa32
|
|
7
|
+
data.tar.gz: eee251586f7279d59edbb6049e5265156afd1858383752dfb4c280d97dc32dd04543c2e44cf8e8f9348046194ec6bcf5b91c651299a0b53e17716e0fdd069ca6
|
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# ABMeter Gem
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[ABMeter](https://abmeter.ai) is a feature-flag and A/B-testing platform. You define parameters, experiments, and feature flags in the ABMeter Lab; this gem reads the value assigned to each user in your Ruby app and reports events back.
|
|
4
4
|
|
|
5
5
|
> **Breaking change in 0.3.0:** `ABMeter.reset!` now discards queued data without any network I/O; use `ABMeter.reset` for the previous drain-on-exit behavior.
|
|
6
6
|
|
|
@@ -22,24 +22,35 @@ And then execute:
|
|
|
22
22
|
$ bundle install
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Getting an API key
|
|
26
|
+
|
|
27
|
+
Sign up at [abmeter.ai](https://abmeter.ai), then in the **Lab** open **API Keys** and create one. Expose it to your app (e.g. as `ABMETER_API_KEY`).
|
|
28
|
+
|
|
25
29
|
## Usage
|
|
26
30
|
|
|
31
|
+
Configure the client once at startup:
|
|
32
|
+
|
|
27
33
|
```ruby
|
|
28
|
-
# configure the client
|
|
29
34
|
ABMeter.configure do |config|
|
|
30
35
|
config.api_key = ENV['ABMETER_API_KEY']
|
|
31
36
|
end
|
|
37
|
+
```
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
user = ABMeter.user(id: current_user.id, email: current_user.email)
|
|
35
|
-
text = ABMeter.param('welcome_text', user)
|
|
39
|
+
The example below assumes a parameter `welcome_text` (in a space, with a variant, controlled by a running experiment or feature flag) and an event type `user_purchases_plan` already exist — create them through the ABMeter API or, more easily, your AI assistant over MCP. Until they do, `resolve_parameter` just returns the parameter's default and `track_event` rejects unknown event types.
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
```ruby
|
|
42
|
+
# In request handling: read the value assigned to this user. `user_id` is the
|
|
43
|
+
# only required field; an optional `email:` is available for predicate-based
|
|
44
|
+
# (email-pattern) audience targeting.
|
|
45
|
+
user = ABMeter::User.new(user_id: current_user.id)
|
|
46
|
+
text = ABMeter.resolve_parameter(user: user, parameter_slug: 'welcome_text')
|
|
47
|
+
|
|
48
|
+
# In business logic: record an action that feeds a metric.
|
|
49
|
+
ABMeter.track_event('user_purchases_plan', current_user.id, { plan: purchased_plan.name, price: purchased_plan.price })
|
|
41
50
|
```
|
|
42
51
|
|
|
52
|
+
The two calls are one loop: the experiment varies `welcome_text`, and the event feeds a metric whose effect the experiment measures across variants. Note that `resolve_parameter` is **not** a pure read — it resolves the value locally and records an exposure in the background. Full guides live at [abmeter.ai](https://abmeter.ai).
|
|
53
|
+
|
|
43
54
|
## Shutdown
|
|
44
55
|
|
|
45
56
|
```ruby
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# Regenerates the cross-language num-utils parity fixture consumed by every
|
|
5
|
+
# ported SDK (sdk-python/tests/test_num_utils_parity.py,
|
|
6
|
+
# sdk-node/tests/num-utils-parity.test.ts).
|
|
7
|
+
#
|
|
8
|
+
# Emits 1000 [salt, user_id, expected] triples where `expected` is the integer
|
|
9
|
+
# returned by ABMeter::Core::Utils::NumUtils.to_percentage. Each ported
|
|
10
|
+
# implementation must reproduce every row. Run from the abmeter-sdk directory:
|
|
11
|
+
#
|
|
12
|
+
# bundle exec ruby bin/generate_num_utils_parity_fixture.rb
|
|
13
|
+
#
|
|
14
|
+
# Regenerate only when the assignment algorithm changes (it shouldn't).
|
|
15
|
+
|
|
16
|
+
require 'json'
|
|
17
|
+
require_relative '../lib/abmeter/core/utils/num_utils'
|
|
18
|
+
|
|
19
|
+
FIXTURE_PATHS = [
|
|
20
|
+
'../../sdk-python/tests/fixtures/num_utils_parity.json',
|
|
21
|
+
'../../sdk-node/tests/fixtures/num_utils_parity.json'
|
|
22
|
+
].map { |path| File.expand_path(path, __dir__) }
|
|
23
|
+
ROW_COUNT = 1000
|
|
24
|
+
|
|
25
|
+
rows = Array.new(ROW_COUNT) do |i|
|
|
26
|
+
# Deterministic, varied inputs — mixed shapes exercise the hashing across
|
|
27
|
+
# salts and user-id formats without depending on RNG.
|
|
28
|
+
salt = "space-salt-#{i}-#{format('%04d', (i * 7) % 9973)}"
|
|
29
|
+
user_id = "user_#{((i * 31) + 5) % 100_003}"
|
|
30
|
+
[salt, user_id, ABMeter::Core::Utils::NumUtils.to_percentage(salt, user_id)]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
FIXTURE_PATHS.each do |path|
|
|
34
|
+
File.write(path, "#{JSON.pretty_generate(rows)}\n")
|
|
35
|
+
puts "Wrote #{rows.size} rows to #{path}"
|
|
36
|
+
end
|
data/lib/abmeter/api_error.rb
CHANGED
|
@@ -2,10 +2,16 @@ require 'active_support/core_ext/hash/indifferent_access'
|
|
|
2
2
|
|
|
3
3
|
module ABMeter
|
|
4
4
|
class APIError < StandardError
|
|
5
|
-
|
|
5
|
+
RATE_LIMITED_STATUS = 429
|
|
6
|
+
DEFAULT_RETRY_AFTER = 30 # seconds, when a 429 has no usable Retry-After
|
|
7
|
+
MIN_RETRY_AFTER = 1 # seconds, so a zero cannot spin a drain loop
|
|
8
|
+
MAX_RETRY_AFTER = 600 # seconds, the API's rate-limit window
|
|
9
|
+
|
|
10
|
+
attr_reader :code, :details, :status, :retry_after
|
|
6
11
|
|
|
7
12
|
def initialize(response)
|
|
8
13
|
@status = response.status
|
|
14
|
+
@retry_after = parse_retry_after(response.headers['Retry-After']) if rate_limited?
|
|
9
15
|
error_body = response.body
|
|
10
16
|
|
|
11
17
|
if error_body.is_a?(Hash)
|
|
@@ -39,6 +45,10 @@ module ABMeter
|
|
|
39
45
|
@status >= 500 || @status == 408 || @status == 429
|
|
40
46
|
end
|
|
41
47
|
|
|
48
|
+
def rate_limited?
|
|
49
|
+
@status == RATE_LIMITED_STATUS
|
|
50
|
+
end
|
|
51
|
+
|
|
42
52
|
def partial_failure?
|
|
43
53
|
@status == 400 && @details.is_a?(Hash) && !@details[:failures].nil? && !@details[:failures].empty?
|
|
44
54
|
end
|
|
@@ -46,5 +56,15 @@ module ABMeter
|
|
|
46
56
|
def failure_count
|
|
47
57
|
@details&.dig(:invalid_count) || 0
|
|
48
58
|
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# Retry-After as delta-seconds only; the API never sends an HTTP date.
|
|
63
|
+
def parse_retry_after(value)
|
|
64
|
+
seconds = Integer(value.to_s.strip, 10, exception: false)
|
|
65
|
+
return DEFAULT_RETRY_AFTER if seconds.nil? || seconds.negative?
|
|
66
|
+
|
|
67
|
+
seconds.clamp(MIN_RETRY_AFTER, MAX_RETRY_AFTER)
|
|
68
|
+
end
|
|
49
69
|
end
|
|
50
70
|
end
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'time' # Time#iso8601
|
|
2
|
+
|
|
1
3
|
module ABMeter
|
|
2
4
|
class AsyncSubmitter
|
|
3
5
|
# Private internal constants for async submitter behavior
|
|
@@ -16,6 +18,7 @@ module ABMeter
|
|
|
16
18
|
@stopping = false
|
|
17
19
|
@stop_mutex = Mutex.new
|
|
18
20
|
@stop_signal = ConditionVariable.new
|
|
21
|
+
@paused_until = nil
|
|
19
22
|
|
|
20
23
|
class << self
|
|
21
24
|
attr_reader :api_client, :flush_interval, :logger, :retry_queue
|
|
@@ -46,7 +49,10 @@ module ABMeter
|
|
|
46
49
|
})
|
|
47
50
|
end
|
|
48
51
|
|
|
52
|
+
# A no-op while a 429 pause lasts: new items wait in the main queue.
|
|
49
53
|
def flush
|
|
54
|
+
return if paused?
|
|
55
|
+
|
|
50
56
|
items = []
|
|
51
57
|
|
|
52
58
|
@mutex.synchronize do
|
|
@@ -54,7 +60,7 @@ module ABMeter
|
|
|
54
60
|
process_retry_queue
|
|
55
61
|
|
|
56
62
|
# Then process new items
|
|
57
|
-
items << @queue.pop while !@queue.empty? && items.size < BATCH_SIZE
|
|
63
|
+
items << @queue.pop while !paused? && !@queue.empty? && items.size < BATCH_SIZE
|
|
58
64
|
end
|
|
59
65
|
|
|
60
66
|
# Group by type and submit
|
|
@@ -118,6 +124,10 @@ module ABMeter
|
|
|
118
124
|
@queue.size
|
|
119
125
|
end
|
|
120
126
|
|
|
127
|
+
def paused?
|
|
128
|
+
!@paused_until.nil? && monotonic_now < @paused_until
|
|
129
|
+
end
|
|
130
|
+
|
|
121
131
|
private
|
|
122
132
|
|
|
123
133
|
def start_worker
|
|
@@ -136,7 +146,10 @@ module ABMeter
|
|
|
136
146
|
end
|
|
137
147
|
# Final drain on graceful stop — bounded by the caller's join timeout.
|
|
138
148
|
begin
|
|
139
|
-
|
|
149
|
+
until @queue.empty? && @retry_queue.empty?
|
|
150
|
+
wait_out_pause
|
|
151
|
+
flush
|
|
152
|
+
end
|
|
140
153
|
rescue StandardError => e
|
|
141
154
|
log_error("Final flush error: #{e.message}")
|
|
142
155
|
end
|
|
@@ -153,6 +166,11 @@ module ABMeter
|
|
|
153
166
|
end
|
|
154
167
|
end
|
|
155
168
|
|
|
169
|
+
def wait_out_pause
|
|
170
|
+
remaining = @paused_until.to_f - monotonic_now
|
|
171
|
+
sleep(remaining) if remaining.positive?
|
|
172
|
+
end
|
|
173
|
+
|
|
156
174
|
def request_stop
|
|
157
175
|
@stop_mutex.synchronize do
|
|
158
176
|
@stopping = true
|
|
@@ -168,6 +186,7 @@ module ABMeter
|
|
|
168
186
|
@flush_interval = DEFAULT_FLUSH_INTERVAL
|
|
169
187
|
@logger = nil
|
|
170
188
|
@stopping = false
|
|
189
|
+
@paused_until = nil
|
|
171
190
|
end
|
|
172
191
|
|
|
173
192
|
def submit_exposures(exposures)
|
|
@@ -186,10 +205,18 @@ module ABMeter
|
|
|
186
205
|
return
|
|
187
206
|
end
|
|
188
207
|
|
|
208
|
+
if paused?
|
|
209
|
+
add_to_retry_queue(type, items)
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
|
|
189
213
|
yield items
|
|
190
214
|
# Success - nothing to do
|
|
191
215
|
rescue ABMeter::APIError => e
|
|
192
|
-
if e.
|
|
216
|
+
if e.rate_limited?
|
|
217
|
+
pause_submissions(e.retry_after, type, items.size)
|
|
218
|
+
add_to_retry_queue(type, items)
|
|
219
|
+
elsif e.retryable?
|
|
193
220
|
log_error("Retryable error submitting #{items.size} #{type}s: #{e.message}")
|
|
194
221
|
add_to_retry_queue(type, items)
|
|
195
222
|
elsif e.partial_failure?
|
|
@@ -207,21 +234,23 @@ module ABMeter
|
|
|
207
234
|
|
|
208
235
|
def add_to_retry_queue(type, items)
|
|
209
236
|
@mutex.synchronize do
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
data: item,
|
|
216
|
-
attempts: 0
|
|
217
|
-
}
|
|
218
|
-
else
|
|
219
|
-
log_error("Retry queue full, dropping #{type}")
|
|
220
|
-
end
|
|
221
|
-
end
|
|
237
|
+
room = MAX_RETRY_QUEUE_SIZE - @retry_queue.size
|
|
238
|
+
kept = items.first([room, 0].max)
|
|
239
|
+
@retry_queue.concat(kept.map { |item| { type: type, data: item, attempts: 0 } })
|
|
240
|
+
dropped = items.size - kept.size
|
|
241
|
+
log_error("Retry queue full, dropping #{dropped} #{type}s") if dropped.positive?
|
|
222
242
|
end
|
|
223
243
|
end
|
|
224
244
|
|
|
245
|
+
def pause_submissions(seconds, type, count)
|
|
246
|
+
@paused_until = monotonic_now + seconds
|
|
247
|
+
log_warn("Rate limited, pausing submissions for #{seconds}s (#{count} #{type}s held for retry)")
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def monotonic_now
|
|
251
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
252
|
+
end
|
|
253
|
+
|
|
225
254
|
def process_retry_queue
|
|
226
255
|
return if @retry_queue.empty?
|
|
227
256
|
|
|
@@ -247,6 +276,8 @@ module ABMeter
|
|
|
247
276
|
|
|
248
277
|
def process_retry_batch(type, items)
|
|
249
278
|
return [] if items.empty?
|
|
279
|
+
# Held, not attempted: a pause set earlier in this flush.
|
|
280
|
+
return items if paused?
|
|
250
281
|
|
|
251
282
|
# Increment attempts and filter out max retries
|
|
252
283
|
active_items = items.filter_map do |item|
|
|
@@ -267,7 +298,12 @@ module ABMeter
|
|
|
267
298
|
yield data
|
|
268
299
|
[] # Success - return empty array
|
|
269
300
|
rescue ABMeter::APIError => e
|
|
270
|
-
if e.
|
|
301
|
+
if e.rate_limited?
|
|
302
|
+
pause_submissions(e.retry_after, type, data.size)
|
|
303
|
+
# The wait is not an attempt.
|
|
304
|
+
active_items.each { |item| item[:attempts] -= 1 }
|
|
305
|
+
active_items
|
|
306
|
+
elsif e.retryable?
|
|
271
307
|
log_error("Retry failed with retryable error for #{data.size} #{type}s: #{e.message}")
|
|
272
308
|
active_items # Return all items for retry
|
|
273
309
|
elsif e.partial_failure?
|
|
@@ -289,9 +325,11 @@ module ABMeter
|
|
|
289
325
|
end
|
|
290
326
|
|
|
291
327
|
def log_error(message)
|
|
292
|
-
|
|
328
|
+
@logger&.error("AsyncSubmitter: #{message}")
|
|
329
|
+
end
|
|
293
330
|
|
|
294
|
-
|
|
331
|
+
def log_warn(message)
|
|
332
|
+
@logger&.warn("AsyncSubmitter: #{message}")
|
|
295
333
|
end
|
|
296
334
|
end
|
|
297
335
|
|
data/lib/abmeter/client.rb
CHANGED
data/lib/abmeter/constants.rb
CHANGED
|
@@ -7,7 +7,11 @@ module ABMeter
|
|
|
7
7
|
protected
|
|
8
8
|
|
|
9
9
|
def resolve_parameter_value(parameter, variant)
|
|
10
|
-
variant
|
|
10
|
+
# Fall back to the default only when the variant does not set this
|
|
11
|
+
# parameter (nil). An explicitly-set falsy value (false, 0, "") is a
|
|
12
|
+
# real override and must win — `||` would wrongly drop a Boolean false.
|
|
13
|
+
value = variant&.parameter_value(parameter.slug)
|
|
14
|
+
value.nil? ? parameter.default_value : value
|
|
11
15
|
end
|
|
12
16
|
|
|
13
17
|
def validate_expose_parameter_args!(user_id, parameter, audience)
|
data/lib/abmeter/core/user.rb
CHANGED
|
@@ -43,7 +43,6 @@ module ABMeter
|
|
|
43
43
|
|
|
44
44
|
def validate_user!(user)
|
|
45
45
|
raise ArgumentError, 'User must have user_id' unless user.respond_to?(:user_id)
|
|
46
|
-
raise ArgumentError, 'User must have email' unless user.respond_to?(:email)
|
|
47
46
|
end
|
|
48
47
|
|
|
49
48
|
def find_matching_feature_flag(user, parameter_slug)
|
|
@@ -1,16 +1,22 @@
|
|
|
1
|
+
require_relative 'error_safety'
|
|
2
|
+
|
|
1
3
|
module ABMeter
|
|
2
4
|
class ResolverProvider
|
|
5
|
+
include ErrorSafety
|
|
6
|
+
|
|
3
7
|
attr_reader :last_fetched_at
|
|
4
8
|
|
|
5
9
|
def initialize(config:, api_client: nil)
|
|
6
10
|
raise 'Either api_key or static_config must be provided' unless config.api_key || config.static_config
|
|
7
11
|
|
|
12
|
+
@config = config
|
|
8
13
|
@api_client = api_client
|
|
9
14
|
@json_config = config.static_config
|
|
10
15
|
@fetch_interval = config.fetch_interval
|
|
11
16
|
|
|
12
17
|
@resolver = nil
|
|
13
18
|
@last_fetched_at = nil
|
|
19
|
+
@last_attempted_at = nil
|
|
14
20
|
|
|
15
21
|
# Build resolver immediately if using JSON config
|
|
16
22
|
@resolver = ABMeter::Core.build_resolver_from_json(@json_config) if @json_config
|
|
@@ -41,14 +47,23 @@ module ABMeter
|
|
|
41
47
|
def refresh_config_if_needed
|
|
42
48
|
return if @json_config # No refresh needed for JSON config
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
return unless !@resolver || time_to_refresh_config?
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
update_resolver
|
|
54
|
+
rescue StandardError => e
|
|
55
|
+
raise unless @resolver
|
|
56
|
+
|
|
57
|
+
serve_last_known_config(e)
|
|
58
|
+
end
|
|
45
59
|
end
|
|
46
60
|
|
|
47
61
|
def time_to_refresh_config?
|
|
48
|
-
@
|
|
62
|
+
@last_attempted_at.nil? || (Time.now - @last_attempted_at > @fetch_interval)
|
|
49
63
|
end
|
|
50
64
|
|
|
51
65
|
def update_resolver
|
|
66
|
+
@last_attempted_at = Time.now
|
|
52
67
|
response = @api_client.get_assignment_config
|
|
53
68
|
config_data = response[:config]
|
|
54
69
|
|
|
@@ -56,6 +71,11 @@ module ABMeter
|
|
|
56
71
|
@last_fetched_at = Time.now
|
|
57
72
|
end
|
|
58
73
|
|
|
74
|
+
def serve_last_known_config(error)
|
|
75
|
+
log_error('Failed to refresh assignment config, serving last known config', error)
|
|
76
|
+
call_error_callback(error)
|
|
77
|
+
end
|
|
78
|
+
|
|
59
79
|
def queue_exposure(exposure)
|
|
60
80
|
AsyncSubmitter.queue_exposure(exposure)
|
|
61
81
|
end
|
data/lib/abmeter/version.rb
CHANGED
data/lib/abmeter.rb
CHANGED
|
@@ -14,6 +14,12 @@ require_relative 'abmeter/async_submitter'
|
|
|
14
14
|
require_relative 'abmeter/error_safety'
|
|
15
15
|
|
|
16
16
|
module ABMeter
|
|
17
|
+
# Top-level alias for the canonical user value object. Lets callers write
|
|
18
|
+
# `ABMeter::User.new(user_id: ...)` instead of reaching into `Core::User`,
|
|
19
|
+
# mirroring `abmeter.User(...)` in the Python SDK. Same class object, so
|
|
20
|
+
# `is_a?`/`===`/`instance_of?` keep working; `Core::User` stays canonical.
|
|
21
|
+
User = Core::User
|
|
22
|
+
|
|
17
23
|
class << self
|
|
18
24
|
include ErrorSafety
|
|
19
25
|
|
|
@@ -42,7 +48,6 @@ module ABMeter
|
|
|
42
48
|
config.base_url = DEFAULT_BASE_URL
|
|
43
49
|
yield(config)
|
|
44
50
|
config.validate!
|
|
45
|
-
# Set logger level if both logger and log_level are configured
|
|
46
51
|
config.send(:set_logger_level, config.log_level)
|
|
47
52
|
@config = config
|
|
48
53
|
|
|
@@ -120,13 +125,13 @@ module ABMeter
|
|
|
120
125
|
|
|
121
126
|
# Logging and error handling
|
|
122
127
|
attr_accessor :logger # Logger instance (auto-detected by default)
|
|
123
|
-
attr_accessor :log_level #
|
|
128
|
+
attr_accessor :log_level # Level for the SDK's own logger; a logger you pass in keeps its level
|
|
124
129
|
attr_accessor :error_callback # Proc called on errors
|
|
125
130
|
|
|
126
131
|
def initialize
|
|
127
132
|
@flush_interval = DEFAULT_FLUSH_INTERVAL
|
|
128
133
|
@fetch_interval = DEFAULT_FETCH_INTERVAL
|
|
129
|
-
@logger =
|
|
134
|
+
@logger = host_logger || (@own_logger = build_own_logger)
|
|
130
135
|
end
|
|
131
136
|
|
|
132
137
|
def validate!
|
|
@@ -155,13 +160,13 @@ module ABMeter
|
|
|
155
160
|
private
|
|
156
161
|
|
|
157
162
|
def set_logger_level(log_level)
|
|
158
|
-
return unless @logger
|
|
163
|
+
return unless @own_logger && @logger.equal?(@own_logger)
|
|
159
164
|
|
|
160
|
-
@
|
|
165
|
+
@own_logger.level = logger_level_for(@own_logger, log_level || DEFAULT_LOG_LEVEL)
|
|
161
166
|
end
|
|
162
167
|
|
|
163
168
|
def logger_level_for(logger, log_level)
|
|
164
|
-
if defined?(Logger) &&
|
|
169
|
+
if defined?(Logger) && logger.is_a?(Logger)
|
|
165
170
|
return case log_level
|
|
166
171
|
when :debug then Logger::DEBUG
|
|
167
172
|
when :info then Logger::INFO
|
|
@@ -175,18 +180,15 @@ module ABMeter
|
|
|
175
180
|
@log_level
|
|
176
181
|
end
|
|
177
182
|
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
require 'logger'
|
|
188
|
-
Logger.new($stdout, progname: 'ABMeter')
|
|
189
|
-
end
|
|
183
|
+
def host_logger
|
|
184
|
+
Rails.logger if defined?(Rails) && Rails.respond_to?(:logger)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def build_own_logger
|
|
188
|
+
return SemanticLogger['ABMeter'] if defined?(SemanticLogger)
|
|
189
|
+
|
|
190
|
+
require 'logger'
|
|
191
|
+
Logger.new($stdout, progname: 'ABMeter')
|
|
190
192
|
end
|
|
191
193
|
end
|
|
192
194
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: abmeter
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- ABMeter
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-01 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -41,7 +41,6 @@ dependencies:
|
|
|
41
41
|
description: ABMeter SDK is a client library for interacting with the ABMeter testing
|
|
42
42
|
service
|
|
43
43
|
email:
|
|
44
|
-
- info@abmeter.com
|
|
45
44
|
executables: []
|
|
46
45
|
extensions: []
|
|
47
46
|
extra_rdoc_files: []
|
|
@@ -49,6 +48,7 @@ files:
|
|
|
49
48
|
- LICENSE.txt
|
|
50
49
|
- README.md
|
|
51
50
|
- bin/console
|
|
51
|
+
- bin/generate_num_utils_parity_fixture.rb
|
|
52
52
|
- bin/rspec
|
|
53
53
|
- lib/abmeter.rb
|
|
54
54
|
- lib/abmeter/api_error.rb
|
|
@@ -71,11 +71,11 @@ files:
|
|
|
71
71
|
- lib/abmeter/error_safety.rb
|
|
72
72
|
- lib/abmeter/resolver_provider.rb
|
|
73
73
|
- lib/abmeter/version.rb
|
|
74
|
-
homepage: https://
|
|
74
|
+
homepage: https://abmeter.ai
|
|
75
75
|
licenses:
|
|
76
76
|
- MIT
|
|
77
77
|
metadata:
|
|
78
|
-
homepage_uri: https://
|
|
78
|
+
homepage_uri: https://abmeter.ai
|
|
79
79
|
source_code_uri: https://github.com/abmeter/abmeter-ruby
|
|
80
80
|
rubygems_mfa_required: 'true'
|
|
81
81
|
post_install_message:
|