toggly 1.2.1 → 1.4.2

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: a484fa27dd7cc53210f2af73984c93d9109205f797172203aea42c44c4722569
4
- data.tar.gz: 79a64ed42c9497e8ce8ca0f59d4dc3c21e52e68ba5e24b3b76680767d2a4787b
3
+ metadata.gz: d58d487720427aea654a419504c20f753656ac862aa355bf25532a97446be208
4
+ data.tar.gz: 60ff9681f37a920b85e25e316ffda754fb8bfe018fef1e53ea34ebbc144f437a
5
5
  SHA512:
6
- metadata.gz: 07f924fb8addc26c7af35f31f0a023680201f097cfa6b26826c333a2dc3a2086c07ae094ca23413daf176546a886dbd39598914a79c0ae6cd35c8c0aeb5df564
7
- data.tar.gz: 353559ba9d83853a589d843f01b52cbdb9287074b7be77505784515df99b2505317d54225456b7818c210d050b5cd170abe1a8bc315545dad5f534f9a7537244
6
+ metadata.gz: 2dc9099207126915eb954e3f6f8f6ce6a9d502da83e88cf9bfcd54c2574f2f4310b03b69627334d33110e5d0ff940ea0e25d33ac34c62b5cd479713b3fc6f415
7
+ data.tar.gz: c308b2523eea162fd10680541b103650eb07a92b7e952ad90d60e27b80fcefcc85dd7f1a27c520f03eff64681f1db69c9caf950cb383dbf824455ecce3ee4a1f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.4.2] - 2026-10-16
9
+
10
+ ### Fixed
11
+ - Usage scopes that raise `Exception` (not only `StandardError`) record an
12
+ error outcome instead of treating the attempt as success [OPS-1758].
13
+ - A clean usage scope nested in an outer `rescue` no longer inherits that
14
+ exception from `$!` and records a failure [OPS-1758].
15
+ - `Interrupt`, `SystemExit`, and `SignalException` are attempt-only (no
16
+ error or duration), matching the Python usage-scope contract [OPS-1758].
17
+
18
+ ## [1.4.1] - 2026-10-15
19
+
20
+ ### Fixed
21
+ - `UsageScopeRunner` uses `ensure` so non-local exits still record success or
22
+ failure duration after the attempt [OPS-1758].
23
+
24
+ ## [1.4.0] - 2026-10-11
25
+
26
+ ### Added
27
+ - `Client#use` usage scope (attempt + duration + errors) and `VariantStats`
28
+ wire fields `errorCount`, `durationSumMs`, `errorDurationSumMs` [OPS-1758].
29
+
30
+ ### Fixed
31
+ - `Client#use` and `UsageScopeRunner.run` return the block's value [OPS-1758].
32
+
33
+ ## [1.3.0] - 2026-10-04
34
+
35
+ ### Added
36
+ - `Toggly::SegmentMembership` for Backend-key targeting-list membership updates.
37
+
8
38
  ## [1.2.1] - 2026-09-28
9
39
 
10
40
  ### Fixed
data/lib/toggly/client.rb CHANGED
@@ -273,6 +273,24 @@ module Toggly
273
273
  @telemetry.record_usage(feature_key.to_s, identity, variant: variant)
274
274
  end
275
275
 
276
+ # Run a block inside a usage scope (attempt + duration + errors). Does not evaluate the flag.
277
+ #
278
+ # @yieldparam scope [Telemetry::FeatureUsageScope]
279
+ def use(feature_key, identity: nil, variant: "enabled", &)
280
+ key = feature_key.to_s
281
+ resolved_identity = identity.nil? ? variant_user_id(nil) : identity
282
+ handlers = [
283
+ -> { @telemetry&.record_usage(key, resolved_identity, variant: variant) },
284
+ ->(ms) { @telemetry&.record_usage_scope_success(key, ms, variant: variant) },
285
+ ->(ms) { @telemetry&.record_usage_scope_failure(key, ms, variant: variant) }
286
+ ]
287
+ if @telemetry.nil? || !@telemetry.usage_enabled?
288
+ scope = Telemetry::FeatureUsageScope.new
289
+ return yield scope
290
+ end
291
+ Telemetry::UsageScopeRunner.run(handlers, &)
292
+ end
293
+
276
294
  # Record a feature viewed/rendered event
277
295
  #
278
296
  # @param feature_key [String, Symbol]
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "net/http"
5
+ require "uri"
6
+
7
+ module Toggly
8
+ # Backend-key client for targeting-list membership on app.toggly.io.
9
+ class SegmentMembership
10
+ def initialize(app_key:, app_base_url: "https://app.toggly.io")
11
+ raise ArgumentError, "app_key is required" if app_key.nil? || app_key.empty?
12
+
13
+ @app_key = app_key
14
+ @base = app_base_url.sub(%r{/+\z}, "")
15
+ end
16
+
17
+ def list_segments
18
+ request(Net::HTTP::Get, "/api/v2/segments")
19
+ end
20
+
21
+ def add_segment_members(segment, identifiers)
22
+ request(Net::HTTP::Post, items_path(segment), { identifiers: identifiers })
23
+ end
24
+
25
+ def remove_segment_members(segment, identifiers)
26
+ request(Net::HTTP::Delete, items_path(segment), { identifiers: identifiers })
27
+ end
28
+
29
+ def replace_segment_members(segment, identifiers)
30
+ request(Net::HTTP::Put, items_path(segment), { identifiers: identifiers })
31
+ end
32
+
33
+ private
34
+
35
+ def items_path(segment)
36
+ "/api/v2/segments/#{URI.encode_www_form_component(segment).gsub("+", "%20")}/items"
37
+ end
38
+
39
+ def request(klass, path, body = nil)
40
+ uri = URI.parse("#{@base}#{path}")
41
+ http = Net::HTTP.new(uri.host, uri.port)
42
+ http.use_ssl = uri.scheme == "https"
43
+ http.open_timeout = 30
44
+ http.read_timeout = 30
45
+ req = klass.new(uri)
46
+ req["Authorization"] = @app_key
47
+ req["Accept"] = "application/json"
48
+ if body
49
+ req["Content-Type"] = "application/json"
50
+ req.body = JSON.generate(body)
51
+ end
52
+ response = http.request(req)
53
+ unless response.is_a?(Net::HTTPSuccess)
54
+ raise Toggly::NetworkError.new(
55
+ "Segment membership failed",
56
+ status_code: response.code.to_i,
57
+ response_body: response.body
58
+ )
59
+ end
60
+
61
+ # Mutations may return 204/empty bodies; do not JSON-parse blank payloads.
62
+ body = response.body.to_s
63
+ return nil if body.strip.empty?
64
+
65
+ JSON.parse(body)
66
+ end
67
+ end
68
+ end
@@ -231,7 +231,10 @@ module Toggly
231
231
  checkCount: vs[:checkCount].to_i,
232
232
  requestCount: vs[:requestCount].to_i,
233
233
  usedCount: vs[:usedCount].to_i,
234
- viewedCount: vs[:viewedCount].to_i
234
+ viewedCount: vs[:viewedCount].to_i,
235
+ errorCount: vs[:errorCount].to_i,
236
+ durationSumMs: vs[:durationSumMs].to_i,
237
+ errorDurationSumMs: vs[:errorDurationSumMs].to_i
235
238
  )
236
239
  end
237
240
  sm
@@ -8,7 +8,7 @@
8
8
  require "google/protobuf"
9
9
  require "google/protobuf/timestamp_pb"
10
10
 
11
- descriptor_data = "\n\x0busage.proto\x12\x05Usage\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcf\x03\n\x0b\x46\x65\x61tureStat\x12\x0e\n\x06\x61ppKey\x18\x01 \x01(\t\x12\x13\n\x0b\x65nvironment\x18\x02 \x01(\t\x12(\n\x04time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12!\n\x05stats\x18\x04 \x03(\x0b\x32\x12.Usage.StatMessage\x12\x18\n\x10totalUniqueUsers\x18\x05 \x01(\x05\x12\x19\n\x0cinstanceName\x18\x06 \x01(\tH\x00\x88\x01\x01\x12\x39\n\x10processStartTime\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01\x88\x01\x01\x12\x17\n\nappVersion\x18\x08 \x01(\tH\x02\x88\x01\x01\x12\x18\n\x10uniqueUserHashes\x18\t \x03(\x05\x12 \n\x13\x64\x65\x66initionCacheHits\x18\n \x01(\x05H\x03\x88\x01\x01\x12\"\n\x15\x64\x65\x66initionCacheMisses\x18\x0b \x01(\x05H\x04\x88\x01\x01\x42\x0f\n\r_instanceNameB\x13\n\x11_processStartTimeB\r\n\x0b_appVersionB\x16\n\x14_definitionCacheHitsB\x18\n\x16_definitionCacheMisses\"\xf2\x03\n\x0bStatMessage\x12\x0f\n\x07\x66\x65\x61ture\x18\x01 \x01(\t\x12\x18\n\x0c\x65nabledCount\x18\x02 \x01(\x05\x42\x02\x18\x01\x12\x19\n\rdisabledCount\x18\x03 \x01(\x05\x42\x02\x18\x01\x12+\n#uniqueContextIdentifierEnabledCount\x18\x04 \x01(\x05\x12,\n$uniqueContextIdentifierDisabledCount\x18\x05 \x01(\x05\x12%\n\x19uniqueRequestEnabledCount\x18\x06 \x01(\x05\x42\x02\x18\x01\x12&\n\x1auniqueRequestDisabledCount\x18\x07 \x01(\x05\x42\x02\x18\x01\x12\x15\n\tusedCount\x18\x08 \x01(\x05\x42\x02\x18\x01\x12\x1c\n\x14uniqueUsersUsedCount\x18\t \x01(\x05\x12\x18\n\x10uniqueUserHashes\x18\n \x03(\x05\x12:\n\x0cvariantStats\x18\x0b \x03(\x0b\x32$.Usage.StatMessage.VariantStatsEntry\x12\x1e\n\x16uniqueViewedUserHashes\x18\x0c \x03(\x05\x1aH\n\x11VariantStatsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.Usage.VariantStats:\x02\x38\x01\"`\n\x0cVariantStats\x12\x12\n\ncheckCount\x18\x01 \x01(\x05\x12\x14\n\x0crequestCount\x18\x02 \x01(\x05\x12\x11\n\tusedCount\x18\x03 \x01(\x05\x12\x13\n\x0bviewedCount\x18\x04 \x01(\x05\"\"\n\nStatResult\x12\x14\n\x0c\x66\x65\x61tureCount\x18\x01 \x01(\x05\x32;\n\x05Usage\x12\x32\n\tSendStats\x12\x12.Usage.FeatureStat\x1a\x11.Usage.StatResultBKZIgithub.com/ops-ai/Toggly.FeatureManagement/toggly-go/toggly/usage/usagepbb\x06proto3"
11
+ descriptor_data = "\n\x0busage.proto\x12\x05Usage\x1a\x1fgoogle/protobuf/timestamp.proto\"\xcf\x03\n\x0b\x46\x65\x61tureStat\x12\x0e\n\x06\x61ppKey\x18\x01 \x01(\t\x12\x13\n\x0b\x65nvironment\x18\x02 \x01(\t\x12(\n\x04time\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12!\n\x05stats\x18\x04 \x03(\x0b\x32\x12.Usage.StatMessage\x12\x18\n\x10totalUniqueUsers\x18\x05 \x01(\x05\x12\x19\n\x0cinstanceName\x18\x06 \x01(\tH\x00\x88\x01\x01\x12\x39\n\x10processStartTime\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x01\x88\x01\x01\x12\x17\n\nappVersion\x18\x08 \x01(\tH\x02\x88\x01\x01\x12\x18\n\x10uniqueUserHashes\x18\t \x03(\x05\x12 \n\x13\x64\x65\x66initionCacheHits\x18\n \x01(\x05H\x03\x88\x01\x01\x12\"\n\x15\x64\x65\x66initionCacheMisses\x18\x0b \x01(\x05H\x04\x88\x01\x01\x42\x0f\n\r_instanceNameB\x13\n\x11_processStartTimeB\r\n\x0b_appVersionB\x16\n\x14_definitionCacheHitsB\x18\n\x16_definitionCacheMisses\"\xf2\x03\n\x0bStatMessage\x12\x0f\n\x07\x66\x65\x61ture\x18\x01 \x01(\t\x12\x18\n\x0c\x65nabledCount\x18\x02 \x01(\x05\x42\x02\x18\x01\x12\x19\n\rdisabledCount\x18\x03 \x01(\x05\x42\x02\x18\x01\x12+\n#uniqueContextIdentifierEnabledCount\x18\x04 \x01(\x05\x12,\n$uniqueContextIdentifierDisabledCount\x18\x05 \x01(\x05\x12%\n\x19uniqueRequestEnabledCount\x18\x06 \x01(\x05\x42\x02\x18\x01\x12&\n\x1auniqueRequestDisabledCount\x18\x07 \x01(\x05\x42\x02\x18\x01\x12\x15\n\tusedCount\x18\x08 \x01(\x05\x42\x02\x18\x01\x12\x1c\n\x14uniqueUsersUsedCount\x18\t \x01(\x05\x12\x18\n\x10uniqueUserHashes\x18\n \x03(\x05\x12:\n\x0cvariantStats\x18\x0b \x03(\x0b\x32$.Usage.StatMessage.VariantStatsEntry\x12\x1e\n\x16uniqueViewedUserHashes\x18\x0c \x03(\x05\x1aH\n\x11VariantStatsEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\"\n\x05value\x18\x02 \x01(\x0b\x32\x13.Usage.VariantStats:\x02\x38\x01\"\xa7\x01\n\x0cVariantStats\x12\x12\n\ncheckCount\x18\x01 \x01(\x05\x12\x14\n\x0crequestCount\x18\x02 \x01(\x05\x12\x11\n\tusedCount\x18\x03 \x01(\x05\x12\x13\n\x0bviewedCount\x18\x04 \x01(\x05\x12\x12\n\nerrorCount\x18\x05 \x01(\x05\x12\x15\n\rdurationSumMs\x18\x06 \x01(\x03\x12\x1a\n\x12\x65rrorDurationSumMs\x18\x07 \x01(\x03\"\"\n\nStatResult\x12\x14\n\x0c\x66\x65\x61tureCount\x18\x01 \x01(\x05\x32;\n\x05Usage\x12\x32\n\tSendStats\x12\x12.Usage.FeatureStat\x1a\x11.Usage.StatResultBKZIgithub.com/ops-ai/Toggly.FeatureManagement/toggly-go/toggly/usage/usagepbb\x06proto3"
12
12
 
13
13
  pool = ::Google::Protobuf::DescriptorPool.generated_pool
14
14
  pool.add_serialized_file(descriptor_data)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
- # Generated by the protocol buffer compiler. DO NOT EDIT!
2
+ # Generated by the protocol buffer compiler. DO NOT EDIT!
3
3
  # Source: usage.proto for package 'Usage'
4
4
 
5
5
  require "grpc"
@@ -120,6 +120,19 @@ module Toggly
120
120
  batcher&.record_view(feature, identity, variant: variant)
121
121
  end
122
122
 
123
+ def record_usage_scope_success(feature, duration_ms, variant: "enabled")
124
+ batcher = @mutex.synchronize { @usage_batcher }
125
+ batcher&.record_usage_duration(feature, duration_ms, variant: variant, failed: false)
126
+ end
127
+
128
+ def record_usage_scope_failure(feature, duration_ms, variant: "enabled")
129
+ batcher = @mutex.synchronize { @usage_batcher }
130
+ return unless batcher
131
+
132
+ batcher.record_usage_error(feature, variant: variant)
133
+ batcher.record_usage_duration(feature, duration_ms, variant: variant, failed: true)
134
+ end
135
+
123
136
  def record_definition_cache_hit
124
137
  batcher = @mutex.synchronize { @usage_batcher }
125
138
  batcher&.record_definition_cache_hit
@@ -4,8 +4,25 @@ module Toggly
4
4
  module Telemetry
5
5
  # In-memory feature usage aggregator (Usage.SendStats payload shape).
6
6
  class UsageBatcher
7
- VariantStatsAgg = Struct.new(:check_count, :request_count, :used_count, :viewed_count, keyword_init: true) do
8
- def initialize(check_count: 0, request_count: 0, used_count: 0, viewed_count: 0)
7
+ VariantStatsAgg = Struct.new(
8
+ :check_count,
9
+ :request_count,
10
+ :used_count,
11
+ :viewed_count,
12
+ :error_count,
13
+ :duration_sum_ms,
14
+ :error_duration_sum_ms,
15
+ keyword_init: true
16
+ ) do
17
+ def initialize(
18
+ check_count: 0,
19
+ request_count: 0,
20
+ used_count: 0,
21
+ viewed_count: 0,
22
+ error_count: 0,
23
+ duration_sum_ms: 0,
24
+ error_duration_sum_ms: 0
25
+ )
9
26
  super
10
27
  end
11
28
 
@@ -14,7 +31,10 @@ module Toggly
14
31
  checkCount: check_count,
15
32
  requestCount: request_count,
16
33
  usedCount: used_count,
17
- viewedCount: viewed_count
34
+ viewedCount: viewed_count,
35
+ errorCount: error_count,
36
+ durationSumMs: duration_sum_ms,
37
+ errorDurationSumMs: error_duration_sum_ms
18
38
  }
19
39
  end
20
40
 
@@ -23,7 +43,10 @@ module Toggly
23
43
  check_count: check_count,
24
44
  request_count: request_count,
25
45
  used_count: used_count,
26
- viewed_count: viewed_count
46
+ viewed_count: viewed_count,
47
+ error_count: error_count,
48
+ duration_sum_ms: duration_sum_ms,
49
+ error_duration_sum_ms: error_duration_sum_ms
27
50
  )
28
51
  end
29
52
  end
@@ -138,6 +161,27 @@ module Toggly
138
161
  end
139
162
  end
140
163
 
164
+ def record_usage_error(feature, variant: "enabled", count: 1)
165
+ return if count <= 0
166
+
167
+ @mutex.synchronize do
168
+ get_variant(get_feature(feature), variant.to_s).error_count += count
169
+ end
170
+ end
171
+
172
+ def record_usage_duration(feature, duration_ms, variant: "enabled", failed: false)
173
+ return if duration_ms <= 0
174
+
175
+ @mutex.synchronize do
176
+ stats = get_variant(get_feature(feature), variant.to_s)
177
+ if failed
178
+ stats.error_duration_sum_ms += duration_ms
179
+ else
180
+ stats.duration_sum_ms += duration_ms
181
+ end
182
+ end
183
+ end
184
+
141
185
  def empty?
142
186
  @mutex.synchronize { empty_unlocked? }
143
187
  end
@@ -180,6 +224,9 @@ module Toggly
180
224
  stats.request_count += snap_stats.request_count
181
225
  stats.used_count += snap_stats.used_count
182
226
  stats.viewed_count += snap_stats.viewed_count
227
+ stats.error_count += snap_stats.error_count
228
+ stats.duration_sum_ms += snap_stats.duration_sum_ms
229
+ stats.error_duration_sum_ms += snap_stats.error_duration_sum_ms
183
230
  end
184
231
  agg.unique_users_enabled.merge(snap_agg.unique_users_enabled)
185
232
  agg.unique_users_disabled.merge(snap_agg.unique_users_disabled)
@@ -250,7 +297,9 @@ module Toggly
250
297
  variant_stats = {}
251
298
  agg.variant_stats.each do |name, vs|
252
299
  next unless vs.check_count.positive? || vs.request_count.positive? ||
253
- vs.used_count.positive? || vs.viewed_count.positive?
300
+ vs.used_count.positive? || vs.viewed_count.positive? ||
301
+ vs.error_count.positive? || vs.duration_sum_ms.positive? ||
302
+ vs.error_duration_sum_ms.positive?
254
303
 
255
304
  variant_stats[name] = vs.to_wire
256
305
  end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "English"
4
+
5
+ module Toggly
6
+ module Telemetry
7
+ # Tracks one usage attempt; call {#fail} to mark failure without raising.
8
+ class FeatureUsageScope
9
+ def initialize
10
+ @failed = false
11
+ end
12
+
13
+ def fail
14
+ @failed = true
15
+ end
16
+
17
+ def failed?
18
+ @failed
19
+ end
20
+ end
21
+
22
+ # Runs a usage-scope block and records attempt, duration, and errors.
23
+ module UsageScopeRunner
24
+ module_function
25
+
26
+ def run(handlers, &block)
27
+ record_attempt, record_success, record_failure = handlers
28
+ record_attempt.call
29
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
30
+ scope = FeatureUsageScope.new
31
+ outcome_recorded = false
32
+ # $! is the nearest enclosing rescue, not this scope. A clean return
33
+ # inside an outer rescue must not inherit that exception as a failure.
34
+ enclosing_error = $ERROR_INFO
35
+ begin
36
+ result = block.call(scope)
37
+ duration_ms = elapsed_ms(start)
38
+ if scope.failed?
39
+ record_failure.call(duration_ms)
40
+ else
41
+ record_success.call(duration_ms)
42
+ end
43
+ outcome_recorded = true
44
+ result
45
+ rescue StandardError => e
46
+ record_failure.call(elapsed_ms(start))
47
+ outcome_recorded = true
48
+ raise e
49
+ ensure
50
+ # Non-local exits (return/break/throw) and Exception subclasses that
51
+ # bypass rescue StandardError still need an outcome. Only a $! raised
52
+ # by this scope (different from the enclosing rescue) is a failure.
53
+ # Interrupt / SystemExit / SignalException match Python: attempt only.
54
+ unless outcome_recorded
55
+ duration_ms = elapsed_ms(start)
56
+ raised_here = !$ERROR_INFO.equal?(enclosing_error)
57
+ if attempt_only_exception?($ERROR_INFO) && raised_here && !scope.failed?
58
+ # Attempt already recorded; process control exits are not failures.
59
+ elsif scope.failed? || raised_here
60
+ record_failure.call(duration_ms)
61
+ else
62
+ record_success.call(duration_ms)
63
+ end
64
+ end
65
+ end
66
+ end
67
+
68
+ def attempt_only_exception?(error)
69
+ error.is_a?(SystemExit) || error.is_a?(Interrupt) || error.is_a?(SignalException)
70
+ end
71
+
72
+ def elapsed_ms(start)
73
+ ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000).round
74
+ ms.negative? ? 0 : ms
75
+ end
76
+ end
77
+ end
78
+ end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative "telemetry/grpc_clients"
4
4
  require_relative "telemetry/usage_batcher"
5
+ require_relative "telemetry/usage_scope"
5
6
  require_relative "telemetry/metrics_batcher"
6
7
  require_relative "telemetry/runtime"
7
8
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Toggly
4
- VERSION = "1.2.1"
4
+ VERSION = "1.4.2"
5
5
  end
data/lib/toggly.rb CHANGED
@@ -37,6 +37,7 @@ require_relative "toggly/snapshot_providers/file"
37
37
  require_relative "toggly/definition_cache"
38
38
  require_relative "toggly/definitions_provider"
39
39
  require_relative "toggly/client"
40
+ require_relative "toggly/segment_membership"
40
41
 
41
42
  # Toggly - Feature Flag Management SDK for Ruby
42
43
  #
data/proto/usage.proto CHANGED
@@ -44,6 +44,9 @@ message VariantStats {
44
44
  int32 requestCount = 2;
45
45
  int32 usedCount = 3;
46
46
  int32 viewedCount = 4;
47
+ int32 errorCount = 5;
48
+ int64 durationSumMs = 6;
49
+ int64 errorDurationSumMs = 7;
47
50
  }
48
51
 
49
52
  message StatResult {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: toggly
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.4.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ops.ai
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-04 00:00:00.000000000 Z
11
+ date: 2026-10-17 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: High-performance Ruby SDK for Toggly feature flag management. Works with
14
14
  or without Toggly.io.
@@ -52,6 +52,7 @@ files:
52
52
  - lib/toggly/http_request_mapper.rb
53
53
  - lib/toggly/registry.rb
54
54
  - lib/toggly/request_context.rb
55
+ - lib/toggly/segment_membership.rb
55
56
  - lib/toggly/snapshot_providers/base.rb
56
57
  - lib/toggly/snapshot_providers/file.rb
57
58
  - lib/toggly/snapshot_providers/memory.rb
@@ -66,6 +67,7 @@ files:
66
67
  - lib/toggly/telemetry/runtime.rb
67
68
  - lib/toggly/telemetry/runtime_flush.rb
68
69
  - lib/toggly/telemetry/usage_batcher.rb
70
+ - lib/toggly/telemetry/usage_scope.rb
69
71
  - lib/toggly/user_agent_parser.rb
70
72
  - lib/toggly/variant_allocator.rb
71
73
  - lib/toggly/variant_result.rb