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.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +57 -0
  3. data/README.md +34 -2
  4. data/lib/toggly/client/cache_telemetry.rb +36 -0
  5. data/lib/toggly/client/snapshot_support.rb +38 -0
  6. data/lib/toggly/client.rb +173 -51
  7. data/lib/toggly/config.rb +39 -1
  8. data/lib/toggly/context.rb +135 -9
  9. data/lib/toggly/definition_cache.rb +78 -0
  10. data/lib/toggly/definitions_provider.rb +72 -18
  11. data/lib/toggly/evaluation_engine.rb +4 -3
  12. data/lib/toggly/evaluators/always_off.rb +5 -1
  13. data/lib/toggly/evaluators/always_on.rb +5 -1
  14. data/lib/toggly/evaluators/base.rb +21 -3
  15. data/lib/toggly/evaluators/browser_family.rb +27 -0
  16. data/lib/toggly/evaluators/browser_language.rb +26 -0
  17. data/lib/toggly/evaluators/context_property.rb +5 -1
  18. data/lib/toggly/evaluators/contextual_targeting.rb +4 -0
  19. data/lib/toggly/evaluators/country.rb +30 -0
  20. data/lib/toggly/evaluators/device_type.rb +27 -0
  21. data/lib/toggly/evaluators/operating_system.rb +31 -0
  22. data/lib/toggly/evaluators/percentage.rb +11 -49
  23. data/lib/toggly/evaluators/segment_helpers.rb +98 -0
  24. data/lib/toggly/evaluators/targeting.rb +61 -14
  25. data/lib/toggly/evaluators/time_window.rb +14 -9
  26. data/lib/toggly/evaluators/user_claims.rb +27 -0
  27. data/lib/toggly/feature_definition.rb +1 -2
  28. data/lib/toggly/http_request_mapper.rb +64 -0
  29. data/lib/toggly/registry.rb +25 -6
  30. data/lib/toggly/request_context.rb +77 -0
  31. data/lib/toggly/sticky_hash.rb +39 -0
  32. data/lib/toggly/telemetry/grpc_clients.rb +333 -0
  33. data/lib/toggly/telemetry/metrics_batcher.rb +140 -0
  34. data/lib/toggly/telemetry/pb/metrics_pb.rb +28 -0
  35. data/lib/toggly/telemetry/pb/metrics_services_pb.rb +28 -0
  36. data/lib/toggly/telemetry/pb/usage_pb.rb +27 -0
  37. data/lib/toggly/telemetry/pb/usage_services_pb.rb +28 -0
  38. data/lib/toggly/telemetry/runtime.rb +262 -0
  39. data/lib/toggly/telemetry/runtime_flush.rb +89 -0
  40. data/lib/toggly/telemetry/usage_batcher.rb +277 -0
  41. data/lib/toggly/telemetry.rb +37 -0
  42. data/lib/toggly/user_agent_parser.rb +69 -0
  43. data/lib/toggly/version.rb +1 -1
  44. data/lib/toggly.rb +16 -1
  45. data/proto/metrics.proto +60 -0
  46. data/proto/usage.proto +55 -0
  47. metadata +29 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 661719eeb6fb2dec747b0f3bfd1dfa08349702573d583f72919abb6c5b6ae8d0
4
- data.tar.gz: f1fdf90d3fa48390637f5ea000c21a6002f6764623ed562b822569f3e0e10862
3
+ metadata.gz: 62d54073d16c04726b00e66b33a8edc9081b9dc397a8f8a8e5aac5316c00bacd
4
+ data.tar.gz: 8687814d25dbba0b4353622aed082f508f5ce185107419614e018cd4ea047afe
5
5
  SHA512:
6
- metadata.gz: 4e6045eb73a91c81ccb3d9e7f3b4096958453f825025b18be81707beb6d96a6ca8f636ab1ee2e6b2852ccca5c7461d93009906de1967bf3a04a44e08425ce70d
7
- data.tar.gz: c19d57ca41c649c359c29a0c4d6b5c8377d8cdcb03c4fdd1ff8020559d5d9788929aa9ee88e8281226e5e3175c1d8bb5647184c8d3d3c307a21ad6306c7f7546
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** - pure Ruby implementation.
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
- return default unless default.nil?
73
- return @config.defaults[key] if @config.defaults.key?(key)
74
- return @config.enable_undefined_in_dev if development?
75
-
76
- return false
77
- end
78
-
79
- @engine.evaluate(definition, context)
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
- new_definitions = @provider.fetch(force: force)
134
-
135
- if new_definitions
136
- @mutex.synchronize do
137
- @definitions = new_definitions
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
- save_snapshot
142
- log_info("Definitions refreshed (#{new_definitions.size} features)")
143
- true
144
- else
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
- rescue StandardError => e
148
- log_error("Failed to refresh definitions: #{e.message}")
149
- false
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
- # Try to load from snapshot first
187
- load_snapshot if @config.snapshot_provider
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
- next if @provider.should_skip_refresh?
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 load_snapshot
244
- return unless @config.snapshot_provider
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 save_snapshot
259
- return unless @config.snapshot_provider
260
-
261
- @config.snapshot_provider.save(@definitions)
262
- log_debug("Saved snapshot with #{@definitions.size} features")
263
- rescue StandardError => e
264
- log_warn("Failed to save snapshot: #{e.message}")
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 development?
268
- env = ENV["RACK_ENV"] || ENV["RAILS_ENV"] || ENV["APP_ENV"] || "development"
269
- env.downcase == "development"
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