toggly 0.2.1 → 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/CHANGELOG.md +57 -0
- data/README.md +34 -2
- data/lib/toggly/client/cache_telemetry.rb +36 -0
- data/lib/toggly/client/snapshot_support.rb +38 -0
- data/lib/toggly/client.rb +173 -51
- data/lib/toggly/config.rb +39 -1
- data/lib/toggly/context.rb +135 -9
- data/lib/toggly/definition_cache.rb +78 -0
- data/lib/toggly/definitions_provider.rb +72 -18
- data/lib/toggly/evaluation_engine.rb +4 -3
- data/lib/toggly/evaluators/always_off.rb +5 -1
- data/lib/toggly/evaluators/always_on.rb +5 -1
- data/lib/toggly/evaluators/base.rb +21 -3
- data/lib/toggly/evaluators/browser_family.rb +27 -0
- data/lib/toggly/evaluators/browser_language.rb +26 -0
- data/lib/toggly/evaluators/context_property.rb +5 -1
- data/lib/toggly/evaluators/contextual_targeting.rb +4 -0
- data/lib/toggly/evaluators/country.rb +30 -0
- data/lib/toggly/evaluators/device_type.rb +27 -0
- data/lib/toggly/evaluators/operating_system.rb +31 -0
- data/lib/toggly/evaluators/percentage.rb +11 -49
- data/lib/toggly/evaluators/segment_helpers.rb +98 -0
- data/lib/toggly/evaluators/targeting.rb +61 -14
- data/lib/toggly/evaluators/time_window.rb +14 -9
- data/lib/toggly/evaluators/user_claims.rb +27 -0
- data/lib/toggly/feature_definition.rb +1 -2
- data/lib/toggly/http_request_mapper.rb +64 -0
- data/lib/toggly/registry.rb +25 -6
- data/lib/toggly/request_context.rb +77 -0
- data/lib/toggly/sticky_hash.rb +39 -0
- data/lib/toggly/telemetry/grpc_clients.rb +333 -0
- data/lib/toggly/telemetry/metrics_batcher.rb +140 -0
- data/lib/toggly/telemetry/pb/metrics_pb.rb +28 -0
- data/lib/toggly/telemetry/pb/metrics_services_pb.rb +28 -0
- data/lib/toggly/telemetry/pb/usage_pb.rb +27 -0
- data/lib/toggly/telemetry/pb/usage_services_pb.rb +28 -0
- data/lib/toggly/telemetry/runtime.rb +262 -0
- data/lib/toggly/telemetry/runtime_flush.rb +89 -0
- data/lib/toggly/telemetry/usage_batcher.rb +277 -0
- data/lib/toggly/telemetry.rb +37 -0
- data/lib/toggly/user_agent_parser.rb +69 -0
- data/lib/toggly/version.rb +1 -1
- data/lib/toggly.rb +16 -1
- data/proto/metrics.proto +60 -0
- data/proto/usage.proto +55 -0
- metadata +29 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 62d54073d16c04726b00e66b33a8edc9081b9dc397a8f8a8e5aac5316c00bacd
|
|
4
|
+
data.tar.gz: 8687814d25dbba0b4353622aed082f508f5ce185107419614e018cd4ea047afe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6fae978a95aea14cccb5c639623ed55bc65559feab2bf3053fa9c785ddaee516dcffd10977afb58f2f78b353b80d55ef48cf7defd18b564a92d7c966abd4d8f4
|
|
7
|
+
data.tar.gz: 82b457f3764084effd4b0724a422dfabc69972aff30e6c0af7bff1b28ab215c287662026c86b4e36be657c7351001134093bfc7ca18aab326b547fa41842ca2e
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,63 @@ 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
|
+
## [0.5.0] - 2026-09-08
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Definition-refresh cache hit/miss counters on `Usage.SendStats`
|
|
13
|
+
(`definitionCacheHits` / `definitionCacheMisses`), counted once per refresh
|
|
14
|
+
attempt (poll skip, HTTP 304, matching ETag, network error keeping cache =
|
|
15
|
+
hit; new revision applied = miss). Concurrent in-flight skips are not counted.
|
|
16
|
+
- Soft-fail restore of the full usage batch (feature stats + hashes + cache
|
|
17
|
+
counters) when `SendStats` fails; cache-only batches still flush.
|
|
18
|
+
|
|
19
|
+
## [0.4.0] - 2026-09-06
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Batched usage (`Usage.SendStats`) and business metrics (`Metrics.SendMetrics`)
|
|
24
|
+
telemetry with ~1 minute flush and best-effort flush on `close` / `at_exit`.
|
|
25
|
+
- Public client APIs: `record_usage`, `record_view`, `measure`,
|
|
26
|
+
`increment_counter`, `observe`, `flush_telemetry`.
|
|
27
|
+
- Auto `record_check` from `enabled?` when usage tracking is enabled.
|
|
28
|
+
- Config: `enable_usage_tracking`, `enable_metrics`, `metrics_base_url`,
|
|
29
|
+
flush intervals; set `TOGGLY_DISABLE_TELEMETRY=1` to disable globally.
|
|
30
|
+
- Optional `grpc` + `google-protobuf` transport (core gem stays zero required
|
|
31
|
+
runtime deps); gRPC metadata `ua` = `toggly-ruby/{VERSION}`.
|
|
32
|
+
- FNV-1a UTF-8 signed int32 identity hashing with golden vectors matching
|
|
33
|
+
Go/Node/Python; `variantStats` / `variantValues` wire fields.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- Assign full `Google::Protobuf::Timestamp` objects for nested time fields
|
|
38
|
+
(FeatureStat/MetricStat/observations) instead of mutating nil fields.
|
|
39
|
+
- Cooperative flush-timer stop on `close` (condition wake + join timeout)
|
|
40
|
+
instead of `Thread#kill` during an in-flight send.
|
|
41
|
+
|
|
42
|
+
## [0.3.0] - 2026-09-04
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- EvalContext fields `claims` and `request` (`userAgent`, `acceptLanguage`,
|
|
47
|
+
`country`) plus `Toggly::RequestContext` and `Toggly::HttpRequestMapper`
|
|
48
|
+
(headers → request; country order `cf-ipcountry` → `x-vercel-ip-country` →
|
|
49
|
+
`cloudfront-viewer-country`).
|
|
50
|
+
- Segment filters: `BrowserFamily`, `BrowserLanguage`, `Country` /
|
|
51
|
+
`CountryFamily`, `DeviceType`, `OS` / `OperatingSystem` with indexed params
|
|
52
|
+
and Percentage fail-closed gating.
|
|
53
|
+
- `UserClaims` filter (`Claim` + `Value`).
|
|
54
|
+
- `Microsoft.Percentage`, `Microsoft.TimeWindow`, and `Microsoft.Targeting`
|
|
55
|
+
aliases; golden fixtures under `docs/filter-parity/fixtures/`.
|
|
56
|
+
|
|
57
|
+
### Changed
|
|
58
|
+
|
|
59
|
+
- Sticky percentage hashing now uses Definitions / toggly-eval SHA-256
|
|
60
|
+
(`featureKey + "\n" + userId`, little-endian uint32 / `0xFFFFFFFF * 100`)
|
|
61
|
+
instead of FNV-1a. Existing sticky cohorts shift when upgrading from 0.2.x.
|
|
62
|
+
- Unknown filter names fail closed.
|
|
63
|
+
- Percentage missing or `≤0` fails closed (aligned with filter-parity contract).
|
|
64
|
+
|
|
8
65
|
## [0.2.1] - 2026-09-03
|
|
9
66
|
|
|
10
67
|
### Changed
|
data/README.md
CHANGED
|
@@ -2,7 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Core Ruby SDK for [Toggly](https://toggly.io) feature flag management.
|
|
4
4
|
|
|
5
|
-
**Zero dependencies**
|
|
5
|
+
**Zero required runtime dependencies** — pure Ruby evaluation. Optional gems:
|
|
6
|
+
|
|
7
|
+
- `websocket-client-simple` — live definition updates
|
|
8
|
+
- `grpc` + `google-protobuf` — usage / business metrics telemetry send
|
|
9
|
+
|
|
10
|
+
The Gemfile `:development, :test` group also includes `grpc` and
|
|
11
|
+
`google-protobuf` so `bundle exec rspec` can exercise the native conversion
|
|
12
|
+
path without making them required runtime deps of the published gem.
|
|
6
13
|
|
|
7
14
|
Entity `ContextProperty` filters evaluate `Toggly::EntityContext` (`kind`, `key`, `attributes`) and are ANDed with user filters. `Toggly.register_context` optionally PUTs schemas to `sdk/{appKey}/contexts`.
|
|
8
15
|
|
|
@@ -10,6 +17,10 @@ Entity `ContextProperty` filters evaluate `Toggly::EntityContext` (`kind`, `key`
|
|
|
10
17
|
|
|
11
18
|
```ruby
|
|
12
19
|
gem 'toggly'
|
|
20
|
+
|
|
21
|
+
# Optional: send usage + business metrics over gRPC
|
|
22
|
+
gem 'grpc'
|
|
23
|
+
gem 'google-protobuf'
|
|
13
24
|
```
|
|
14
25
|
|
|
15
26
|
## Quick Start
|
|
@@ -23,10 +34,31 @@ client = Toggly::Client.new(
|
|
|
23
34
|
)
|
|
24
35
|
|
|
25
36
|
if client.enabled?(:my_feature)
|
|
26
|
-
# Feature is enabled
|
|
37
|
+
# Feature is enabled (also records a usage check when tracking is on)
|
|
27
38
|
end
|
|
28
39
|
```
|
|
29
40
|
|
|
41
|
+
## Usage & metrics telemetry
|
|
42
|
+
|
|
43
|
+
When `app_key` is set, usage tracking and metrics default **on** (disable with
|
|
44
|
+
`enable_usage_tracking: false` / `enable_metrics: false`, or
|
|
45
|
+
`TOGGLY_DISABLE_TELEMETRY=1`). Batches flush about every 60 seconds and on
|
|
46
|
+
`client.close`.
|
|
47
|
+
|
|
48
|
+
```ruby
|
|
49
|
+
client.record_usage('Checkout', identity: 'user-1')
|
|
50
|
+
client.record_view('Banner', identity: 'user-1')
|
|
51
|
+
client.measure('revenue', 12.5, feature: 'Checkout', variant: 'enabled')
|
|
52
|
+
client.increment_counter('clicks', 1, feature: 'Banner')
|
|
53
|
+
client.observe('latency_ms', 42.0)
|
|
54
|
+
client.flush_telemetry
|
|
55
|
+
client.close
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
gRPC metadata includes `ua` = `toggly-ruby/{VERSION}` against
|
|
59
|
+
`metrics_base_url` (default `https://app.toggly.io/`). Without the optional
|
|
60
|
+
`grpc` gem, evaluation still works; send is a no-op with a warning.
|
|
61
|
+
|
|
30
62
|
## Documentation
|
|
31
63
|
|
|
32
64
|
See the [main README](../README.md) for full documentation.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
class Client
|
|
5
|
+
# Definition-refresh cache hit/miss recording for usage telemetry.
|
|
6
|
+
module CacheTelemetry
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
def record_refresh_cache_outcome(outcome)
|
|
10
|
+
if outcome == :miss
|
|
11
|
+
record_definition_cache_miss
|
|
12
|
+
else
|
|
13
|
+
record_definition_cache_hit
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def record_definition_cache_hit
|
|
18
|
+
return unless @telemetry&.usage_enabled?
|
|
19
|
+
|
|
20
|
+
@telemetry.record_definition_cache_hit
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def record_definition_cache_miss
|
|
24
|
+
return unless @telemetry&.usage_enabled?
|
|
25
|
+
|
|
26
|
+
@telemetry.record_definition_cache_miss
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# True when a revision has been successfully loaded, including an empty
|
|
30
|
+
# feature set (valid last-good cache for error-path hits).
|
|
31
|
+
def definitions_cached?
|
|
32
|
+
@mutex.synchronize { @definitions_loaded }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toggly
|
|
4
|
+
class Client
|
|
5
|
+
# Durable snapshot load/save helpers for Client.
|
|
6
|
+
module SnapshotSupport
|
|
7
|
+
private
|
|
8
|
+
|
|
9
|
+
# @return [Boolean] true when a durable snapshot was applied into memory
|
|
10
|
+
def load_snapshot
|
|
11
|
+
return false unless @config.snapshot_provider
|
|
12
|
+
|
|
13
|
+
data = @config.snapshot_provider.load
|
|
14
|
+
return false unless data
|
|
15
|
+
|
|
16
|
+
@mutex.synchronize do
|
|
17
|
+
@definitions = data[:definitions]
|
|
18
|
+
@definitions_loaded = true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
log_debug("Loaded #{@definitions.size} features from snapshot")
|
|
22
|
+
true
|
|
23
|
+
rescue StandardError => e
|
|
24
|
+
log_warn("Failed to load snapshot: #{e.message}")
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def save_snapshot
|
|
29
|
+
return unless @config.snapshot_provider
|
|
30
|
+
|
|
31
|
+
@config.snapshot_provider.save(@definitions)
|
|
32
|
+
log_debug("Saved snapshot with #{@definitions.size} features")
|
|
33
|
+
rescue StandardError => e
|
|
34
|
+
log_warn("Failed to save snapshot: #{e.message}")
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
data/lib/toggly/client.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "client/cache_telemetry"
|
|
4
|
+
require_relative "client/snapshot_support"
|
|
5
|
+
|
|
3
6
|
module Toggly
|
|
4
7
|
# Main client for interacting with Toggly feature flags.
|
|
5
8
|
#
|
|
@@ -21,6 +24,9 @@ module Toggly
|
|
|
21
24
|
# )
|
|
22
25
|
# client = Toggly::Client.new(config)
|
|
23
26
|
class Client
|
|
27
|
+
include CacheTelemetry
|
|
28
|
+
include SnapshotSupport
|
|
29
|
+
|
|
24
30
|
# @return [Config] Client configuration
|
|
25
31
|
attr_reader :config
|
|
26
32
|
|
|
@@ -38,6 +44,8 @@ module Toggly
|
|
|
38
44
|
@config.validate!
|
|
39
45
|
|
|
40
46
|
@definitions = {}
|
|
47
|
+
# True once a revision (including empty) or durable snapshot was applied.
|
|
48
|
+
@definitions_loaded = false
|
|
41
49
|
@mutex = Mutex.new
|
|
42
50
|
@ready = false
|
|
43
51
|
@closed = false
|
|
@@ -46,11 +54,16 @@ module Toggly
|
|
|
46
54
|
@provider = DefinitionsProvider.new(
|
|
47
55
|
config: @config,
|
|
48
56
|
logger: @config.logger,
|
|
49
|
-
on_definitions_updated: -> { refresh }
|
|
57
|
+
on_definitions_updated: -> { refresh(force: true, from_websocket: true) }
|
|
50
58
|
)
|
|
51
59
|
|
|
52
60
|
@refresh_thread = nil
|
|
61
|
+
@refresh_in_flight = false
|
|
62
|
+
@pending_ws_refresh = false
|
|
63
|
+
@telemetry = nil
|
|
53
64
|
|
|
65
|
+
# Start telemetry before the first refresh so cache outcomes can be recorded.
|
|
66
|
+
start_telemetry
|
|
54
67
|
initialize_definitions
|
|
55
68
|
Toggly.register_entity_contexts_at_startup(@config) unless @config.disable_entity_context_registration
|
|
56
69
|
start_background_refresh unless @config.disable_background_refresh
|
|
@@ -68,15 +81,22 @@ module Toggly
|
|
|
68
81
|
definition = @mutex.synchronize { @definitions[key] }
|
|
69
82
|
|
|
70
83
|
# Check defaults if not found
|
|
71
|
-
if definition.nil?
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
84
|
+
result = if definition.nil?
|
|
85
|
+
if !default.nil?
|
|
86
|
+
default
|
|
87
|
+
elsif @config.defaults.key?(key)
|
|
88
|
+
@config.defaults[key]
|
|
89
|
+
elsif development?
|
|
90
|
+
@config.enable_undefined_in_dev
|
|
91
|
+
else
|
|
92
|
+
false
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
@engine.evaluate(definition, context)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
record_check(key, result, context)
|
|
99
|
+
result
|
|
80
100
|
end
|
|
81
101
|
|
|
82
102
|
# Check if a feature is disabled
|
|
@@ -126,27 +146,113 @@ module Toggly
|
|
|
126
146
|
# Manually refresh definitions
|
|
127
147
|
#
|
|
128
148
|
# @param force [Boolean] Force refresh even if not modified
|
|
149
|
+
# @param from_websocket [Boolean] True when triggered by a live WS notify
|
|
129
150
|
# @return [Boolean] Whether definitions were updated
|
|
130
|
-
def refresh(force: false)
|
|
151
|
+
def refresh(force: false, from_websocket: false)
|
|
131
152
|
return false if @config.offline_mode?
|
|
132
153
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
@ready = true
|
|
154
|
+
# Concurrent refresh skipped (in flight) — do not count.
|
|
155
|
+
@mutex.synchronize do
|
|
156
|
+
if @refresh_in_flight
|
|
157
|
+
@pending_ws_refresh = true if from_websocket
|
|
158
|
+
return false
|
|
139
159
|
end
|
|
160
|
+
@refresh_in_flight = true
|
|
161
|
+
end
|
|
140
162
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
163
|
+
begin
|
|
164
|
+
result = @provider.fetch(force: force)
|
|
165
|
+
record_refresh_cache_outcome(result.cache_outcome)
|
|
166
|
+
|
|
167
|
+
if result.definitions
|
|
168
|
+
@mutex.synchronize do
|
|
169
|
+
@definitions = result.definitions
|
|
170
|
+
@definitions_loaded = true
|
|
171
|
+
@ready = true
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
save_snapshot
|
|
175
|
+
log_info("Definitions refreshed (#{result.definitions.size} features)")
|
|
176
|
+
true
|
|
177
|
+
else
|
|
178
|
+
false
|
|
179
|
+
end
|
|
180
|
+
rescue StandardError => e
|
|
181
|
+
log_error("Failed to refresh definitions: #{e.message}")
|
|
182
|
+
# Network error / timeout keeping last-good revision (incl. empty) — hit.
|
|
183
|
+
record_definition_cache_hit if definitions_cached?
|
|
145
184
|
false
|
|
185
|
+
ensure
|
|
186
|
+
drain_pending = false
|
|
187
|
+
@mutex.synchronize do
|
|
188
|
+
@refresh_in_flight = false
|
|
189
|
+
if @pending_ws_refresh
|
|
190
|
+
@pending_ws_refresh = false
|
|
191
|
+
drain_pending = true
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
refresh(force: true, from_websocket: true) if drain_pending
|
|
146
195
|
end
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
# Record a feature used/interaction event
|
|
199
|
+
#
|
|
200
|
+
# @param feature_key [String, Symbol]
|
|
201
|
+
# @param identity [String, nil]
|
|
202
|
+
# @param variant [String]
|
|
203
|
+
def record_usage(feature_key, identity: nil, variant: "enabled")
|
|
204
|
+
return unless @telemetry&.usage_enabled?
|
|
205
|
+
|
|
206
|
+
@telemetry.record_usage(feature_key.to_s, identity, variant: variant)
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Record a feature viewed/rendered event
|
|
210
|
+
#
|
|
211
|
+
# @param feature_key [String, Symbol]
|
|
212
|
+
# @param identity [String, nil]
|
|
213
|
+
# @param variant [String]
|
|
214
|
+
def record_view(feature_key, identity: nil, variant: "enabled")
|
|
215
|
+
return unless @telemetry&.usage_enabled?
|
|
216
|
+
|
|
217
|
+
@telemetry.record_view(feature_key.to_s, identity, variant: variant)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# Aggregate a business measurement (sum over the flush window)
|
|
221
|
+
#
|
|
222
|
+
# @param metric [String]
|
|
223
|
+
# @param value [Numeric]
|
|
224
|
+
# @param options [Hash, Telemetry::MetricsFeatureOptions, nil]
|
|
225
|
+
def measure(metric, value, options = nil, **kwargs)
|
|
226
|
+
return unless @telemetry&.metrics_enabled?
|
|
227
|
+
|
|
228
|
+
@telemetry.measure(metric, value, options || (kwargs.empty? ? nil : kwargs))
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Increment a business counter
|
|
232
|
+
#
|
|
233
|
+
# @param metric [String]
|
|
234
|
+
# @param value [Numeric]
|
|
235
|
+
# @param options [Hash, Telemetry::MetricsFeatureOptions, nil]
|
|
236
|
+
def increment_counter(metric, value = 1.0, options = nil, **kwargs)
|
|
237
|
+
return unless @telemetry&.metrics_enabled?
|
|
238
|
+
|
|
239
|
+
@telemetry.increment_counter(metric, value, options || (kwargs.empty? ? nil : kwargs))
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
# Record a point-in-time business observation
|
|
243
|
+
#
|
|
244
|
+
# @param metric [String]
|
|
245
|
+
# @param value [Numeric]
|
|
246
|
+
# @param options [Hash, Telemetry::MetricsFeatureOptions, nil]
|
|
247
|
+
def observe(metric, value, options = nil, **kwargs)
|
|
248
|
+
return unless @telemetry&.metrics_enabled?
|
|
249
|
+
|
|
250
|
+
@telemetry.observe(metric, value, options || (kwargs.empty? ? nil : kwargs))
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Flush pending usage and metrics batches
|
|
254
|
+
def flush_telemetry
|
|
255
|
+
@telemetry&.flush_all
|
|
150
256
|
end
|
|
151
257
|
|
|
152
258
|
# Close the client and stop background refresh
|
|
@@ -155,6 +261,8 @@ module Toggly
|
|
|
155
261
|
@provider.stop_websocket
|
|
156
262
|
@refresh_thread&.kill
|
|
157
263
|
@refresh_thread = nil
|
|
264
|
+
@telemetry&.close
|
|
265
|
+
@telemetry = nil
|
|
158
266
|
end
|
|
159
267
|
|
|
160
268
|
# Check if client is closed
|
|
@@ -183,8 +291,10 @@ module Toggly
|
|
|
183
291
|
private
|
|
184
292
|
|
|
185
293
|
def initialize_definitions
|
|
186
|
-
#
|
|
187
|
-
|
|
294
|
+
# Startup served from durable snapshot before first network — cache hit.
|
|
295
|
+
# Distinct from the subsequent refresh() network outcome (no double-count
|
|
296
|
+
# inside one refresh invocation; snapshot load is its own attempt).
|
|
297
|
+
record_definition_cache_hit if load_snapshot
|
|
188
298
|
|
|
189
299
|
# Initialize with defaults if in offline mode
|
|
190
300
|
if @config.offline_mode?
|
|
@@ -220,8 +330,11 @@ module Toggly
|
|
|
220
330
|
break if @closed
|
|
221
331
|
|
|
222
332
|
# When WebSocket is connected, skip HTTP refresh unless
|
|
223
|
-
# the fallback interval has elapsed
|
|
224
|
-
|
|
333
|
+
# the fallback interval has elapsed. Skipped poll = cache hit.
|
|
334
|
+
if @provider.should_skip_refresh?
|
|
335
|
+
record_definition_cache_hit
|
|
336
|
+
next
|
|
337
|
+
end
|
|
225
338
|
|
|
226
339
|
refresh
|
|
227
340
|
end
|
|
@@ -240,33 +353,42 @@ module Toggly
|
|
|
240
353
|
@provider.start_websocket
|
|
241
354
|
end
|
|
242
355
|
|
|
243
|
-
def
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
data = @config.snapshot_provider.load
|
|
247
|
-
return unless data
|
|
248
|
-
|
|
249
|
-
@mutex.synchronize do
|
|
250
|
-
@definitions = data[:definitions]
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
log_debug("Loaded #{@definitions.size} features from snapshot")
|
|
254
|
-
rescue StandardError => e
|
|
255
|
-
log_warn("Failed to load snapshot: #{e.message}")
|
|
356
|
+
def development?
|
|
357
|
+
env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["APP_ENV"] || "development"
|
|
358
|
+
env.downcase == "development"
|
|
256
359
|
end
|
|
257
360
|
|
|
258
|
-
def
|
|
259
|
-
return
|
|
260
|
-
|
|
261
|
-
@config.
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
361
|
+
def start_telemetry
|
|
362
|
+
return if @config.app_key.nil? || @config.app_key.empty?
|
|
363
|
+
return if ENV["TOGGLY_DISABLE_TELEMETRY"] == "1"
|
|
364
|
+
return unless @config.enable_usage_tracking || @config.enable_metrics
|
|
365
|
+
|
|
366
|
+
usage_provided = !@config.usage_client.nil?
|
|
367
|
+
metrics_provided = !@config.metrics_client.nil?
|
|
368
|
+
@telemetry = Telemetry::Runtime.new(
|
|
369
|
+
app_key: @config.app_key,
|
|
370
|
+
environment: @config.environment,
|
|
371
|
+
metrics_base_url: @config.metrics_base_url,
|
|
372
|
+
enable_usage_tracking: @config.enable_usage_tracking,
|
|
373
|
+
enable_metrics: @config.enable_metrics,
|
|
374
|
+
usage_flush_interval: @config.usage_flush_interval,
|
|
375
|
+
metrics_flush_interval: @config.metrics_flush_interval,
|
|
376
|
+
instance_name: @config.instance_name,
|
|
377
|
+
app_version: @config.app_version,
|
|
378
|
+
usage_client: @config.usage_client,
|
|
379
|
+
metrics_client: @config.metrics_client,
|
|
380
|
+
usage_client_provided: usage_provided,
|
|
381
|
+
metrics_client_provided: metrics_provided,
|
|
382
|
+
logger: @config.logger
|
|
383
|
+
)
|
|
384
|
+
@telemetry.start
|
|
265
385
|
end
|
|
266
386
|
|
|
267
|
-
def
|
|
268
|
-
|
|
269
|
-
|
|
387
|
+
def record_check(feature_key, enabled, context)
|
|
388
|
+
return unless @telemetry&.usage_enabled?
|
|
389
|
+
|
|
390
|
+
identity = context&.identity
|
|
391
|
+
@telemetry.record_check(feature_key, enabled, identity)
|
|
270
392
|
end
|
|
271
393
|
|
|
272
394
|
def log_info(message)
|
data/lib/toggly/config.rb
CHANGED
|
@@ -58,11 +58,34 @@ module Toggly
|
|
|
58
58
|
# @return [Logger, nil] Logger instance
|
|
59
59
|
attr_accessor :logger
|
|
60
60
|
|
|
61
|
+
# @return [Boolean] Enable feature usage tracking (Usage.SendStats)
|
|
62
|
+
attr_accessor :enable_usage_tracking
|
|
63
|
+
|
|
64
|
+
# @return [Boolean] Enable business metrics (Metrics.SendMetrics)
|
|
65
|
+
attr_accessor :enable_metrics
|
|
66
|
+
|
|
67
|
+
# @return [String] Base URL for usage/metrics gRPC (default app.toggly.io)
|
|
68
|
+
attr_accessor :metrics_base_url
|
|
69
|
+
|
|
70
|
+
# @return [Numeric] Usage flush interval in seconds
|
|
71
|
+
attr_accessor :usage_flush_interval
|
|
72
|
+
|
|
73
|
+
# @return [Numeric] Metrics flush interval in seconds
|
|
74
|
+
attr_accessor :metrics_flush_interval
|
|
75
|
+
|
|
76
|
+
# Injectable usage transport (tests / custom senders). Must respond to +send_stats+.
|
|
77
|
+
attr_accessor :usage_client
|
|
78
|
+
|
|
79
|
+
# Injectable metrics transport (tests / custom senders). Must respond to +send_metrics+.
|
|
80
|
+
attr_accessor :metrics_client
|
|
81
|
+
|
|
61
82
|
# Default values
|
|
62
83
|
DEFAULT_BASE_URL = "https://definitions.toggly.io/"
|
|
63
84
|
DEFAULT_REFRESH_INTERVAL = 300 # 5 minutes
|
|
64
85
|
DEFAULT_HTTP_TIMEOUT = 10 # seconds
|
|
65
86
|
DEFAULT_ENVIRONMENT = "Production"
|
|
87
|
+
DEFAULT_METRICS_BASE_URL = "https://app.toggly.io/"
|
|
88
|
+
DEFAULT_TELEMETRY_FLUSH_SECONDS = 60.0
|
|
66
89
|
|
|
67
90
|
def initialize(**options)
|
|
68
91
|
@app_key = options[:app_key]
|
|
@@ -82,6 +105,16 @@ module Toggly
|
|
|
82
105
|
@use_signed_definitions = options[:use_signed_definitions] || false
|
|
83
106
|
@allowed_key_ids = options[:allowed_key_ids] || []
|
|
84
107
|
@logger = options[:logger]
|
|
108
|
+
|
|
109
|
+
telemetry_default = options.key?(:app_key) && !options[:app_key].to_s.empty? &&
|
|
110
|
+
ENV["TOGGLY_DISABLE_TELEMETRY"] != "1"
|
|
111
|
+
@enable_usage_tracking = options.fetch(:enable_usage_tracking, telemetry_default)
|
|
112
|
+
@enable_metrics = options.fetch(:enable_metrics, telemetry_default)
|
|
113
|
+
@metrics_base_url = normalize_url(options[:metrics_base_url] || DEFAULT_METRICS_BASE_URL)
|
|
114
|
+
@usage_flush_interval = options.fetch(:usage_flush_interval, DEFAULT_TELEMETRY_FLUSH_SECONDS)
|
|
115
|
+
@metrics_flush_interval = options.fetch(:metrics_flush_interval, DEFAULT_TELEMETRY_FLUSH_SECONDS)
|
|
116
|
+
@usage_client = options[:usage_client]
|
|
117
|
+
@metrics_client = options[:metrics_client]
|
|
85
118
|
end
|
|
86
119
|
|
|
87
120
|
# Get the definitions endpoint URL
|
|
@@ -126,7 +159,12 @@ module Toggly
|
|
|
126
159
|
enable_live_updates: @enable_live_updates,
|
|
127
160
|
app_version: @app_version,
|
|
128
161
|
instance_name: @instance_name,
|
|
129
|
-
use_signed_definitions: @use_signed_definitions
|
|
162
|
+
use_signed_definitions: @use_signed_definitions,
|
|
163
|
+
enable_usage_tracking: @enable_usage_tracking,
|
|
164
|
+
enable_metrics: @enable_metrics,
|
|
165
|
+
metrics_base_url: @metrics_base_url,
|
|
166
|
+
usage_flush_interval: @usage_flush_interval,
|
|
167
|
+
metrics_flush_interval: @metrics_flush_interval
|
|
130
168
|
}
|
|
131
169
|
end
|
|
132
170
|
|