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 +4 -4
- data/CHANGELOG.md +30 -0
- data/lib/toggly/client.rb +18 -0
- data/lib/toggly/segment_membership.rb +68 -0
- data/lib/toggly/telemetry/grpc_clients.rb +4 -1
- data/lib/toggly/telemetry/pb/usage_pb.rb +1 -1
- data/lib/toggly/telemetry/pb/usage_services_pb.rb +1 -1
- data/lib/toggly/telemetry/runtime.rb +13 -0
- data/lib/toggly/telemetry/usage_batcher.rb +54 -5
- data/lib/toggly/telemetry/usage_scope.rb +78 -0
- data/lib/toggly/telemetry.rb +1 -0
- data/lib/toggly/version.rb +1 -1
- data/lib/toggly.rb +1 -0
- data/proto/usage.proto +3 -0
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d58d487720427aea654a419504c20f753656ac862aa355bf25532a97446be208
|
|
4
|
+
data.tar.gz: 60ff9681f37a920b85e25e316ffda754fb8bfe018fef1e53ea34ebbc144f437a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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\"
|
|
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)
|
|
@@ -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(
|
|
8
|
-
|
|
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
|
data/lib/toggly/telemetry.rb
CHANGED
data/lib/toggly/version.rb
CHANGED
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
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
|
|
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-
|
|
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
|