featureflow 0.7.0 → 0.8.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2865ebc3fed1dfffb613a9a459ce6e0ac8d6e5cd05d758b4e93944cbc8d8d372
4
- data.tar.gz: 1b5a7d4b01a2ff98501ca2818e3d00a4d4523525e9e71cd4cda1a847ef009867
3
+ metadata.gz: d5b0b34f2239d222c1899b7b7af000ab6ee80dc31c481076c73a3220a8f6e76b
4
+ data.tar.gz: fc7e045b04d49220ed536cd885aa7ecdcd63f85d22feed2b41fab7ca6ea434bf
5
5
  SHA512:
6
- metadata.gz: 8c1fb252dcd6beac50457f7d8328198b7512d4bad761824d665490dc0a8fa59026aa55901adf0555dbfce12599ecbf1b611695801d150b29a76ee747abb1aadd
7
- data.tar.gz: 712e342d59b58327b4ec1bf98770946ef255f39dbecc2093dc50fa786252b82f6aed33b06a1faacbd5c7e9f60aa65f750a37e454c4ec6f03f8a8cb9f96bb87c2
6
+ metadata.gz: 7c7834c0bd38d21c36bdbfe588491916b4d3f99c50bfce8ab33a654849fb3d58920918eada5616fc168373792c8373e86f5fbb239c210d1ce213bdd1c643002d
7
+ data.tar.gz: 362fae11135765cb677f26ee3619b57b499bc569d4af61119add4410704dfdcebae7e79d4652314f9d8d185f34dff8502383b0cc9ebf4b81f2d6003f3ea1fb8f
data/CHANGELOG.md CHANGED
@@ -1,4 +1,12 @@
1
1
  # Change log
2
+ ## [0.8.0] - 2026-08-13
3
+ ### Added
4
+ - `application` configuration option (falling back to the `FEATUREFLOW_APPLICATION`
5
+ environment variable): names this workload, sent as `X-Featureflow-Application` on
6
+ every request so the dashboard can attribute usage and flag evaluations per
7
+ application. Slug-validated client-side (case forgiven; an invalid value is dropped
8
+ with a warning and no header is sent).
9
+
2
10
  ## [0.7.0] - 2026-08-11
3
11
  ### Fixed
4
12
  - Condition operators fail closed instead of raising into the host application: an
data/README.md CHANGED
@@ -153,6 +153,22 @@ featureflow.evaluate('key-three', user).is? 'custom' # == true
153
153
 
154
154
  ```
155
155
 
156
+ #### Naming your application
157
+
158
+ Optionally tag this workload with an application name so the Featureflow dashboard can
159
+ attribute SDK usage and flag evaluations to it (Admin → SDKs, and the "Evaluated by"
160
+ panel on each feature's statistics tab):
161
+
162
+ ```ruby
163
+ featureflow = Featureflow::Client.new(api_key: FEATUREFLOW_SERVER_KEY,
164
+ application: 'checkout-api')
165
+ ```
166
+
167
+ The name is a slug — lowercase letters, numbers, `.`, `_` and `-`, at most 64
168
+ characters. An invalid value is dropped with a warning and no tag is sent. The
169
+ `FEATUREFLOW_APPLICATION` environment variable is used when the option is not set in
170
+ code.
171
+
156
172
  #### Further documentation
157
173
  Further documentation can be found [here](http://docs.featureflow.io/docs)
158
174
 
@@ -0,0 +1,28 @@
1
+ module Featureflow
2
+ # Validates the configured application tag, sent as the X-Featureflow-Application
3
+ # header on every request so the dashboard can attribute usage per workload.
4
+ #
5
+ # Contract slug (featureflow-client-sdk-testbed/CONTRACT.md): lowercase [a-z0-9._-],
6
+ # max 64 chars. The server sanitises defensively; the SDK validates strictly so a
7
+ # typo is a visible warning here rather than a silently mangled tag there.
8
+ class Application
9
+ PATTERN = /\A[a-z0-9._-]{1,64}\z/.freeze
10
+
11
+ # Case is forgiven (lowercased); anything else invalid is dropped with a warning
12
+ # and no X-Featureflow-Application header is sent.
13
+ def self.sanitise(raw)
14
+ return nil if raw.nil? || raw == ''
15
+ unless raw.is_a?(String)
16
+ Featureflow.logger.warn "ignoring application #{raw.inspect} - must be a string"
17
+ return nil
18
+ end
19
+ value = raw.strip.downcase
20
+ unless PATTERN.match?(value)
21
+ Featureflow.logger.warn "ignoring application #{raw.inspect} - must be lowercase " \
22
+ 'a-z, 0-9, dot, underscore or hyphen, max 64 chars'
23
+ return nil
24
+ end
25
+ value
26
+ end
27
+ end
28
+ end
@@ -1,4 +1,5 @@
1
1
  require 'time'
2
+ require 'featureflow/application'
2
3
  require 'featureflow/evaluate'
3
4
  require 'featureflow/polling_client'
4
5
  require 'featureflow/events_client'
@@ -10,6 +11,10 @@ module Featureflow
10
11
  @configuration.validate!
11
12
  @features = {}
12
13
 
14
+ # Optional workload name sent as X-Featureflow-Application on every request so
15
+ # usage and flag evaluations are attributable per application in the dashboard.
16
+ @application = Application.sanitise(@configuration.application)
17
+
13
18
  Featureflow.logger.info 'initializing client'
14
19
 
15
20
  @failover_variants = {}
@@ -20,7 +25,7 @@ module Featureflow
20
25
  end
21
26
 
22
27
  unless @configuration.disable_events
23
- @events_client = EventsClient.new @configuration.event_endpoint, @configuration.api_key
28
+ @events_client = EventsClient.new @configuration.event_endpoint, @configuration.api_key, @application
24
29
  @events_client.register_features @configuration.with_features
25
30
  end
26
31
 
@@ -39,7 +44,8 @@ module Featureflow
39
44
  @configuration.endpoint,
40
45
  @configuration.api_key,
41
46
  poll_interval: 10,
42
- timeout: 30
47
+ timeout: 30,
48
+ application: @application
43
49
  )
44
50
  end
45
51
 
@@ -5,12 +5,16 @@ class Featureflow::Configuration
5
5
  attr_accessor :disable_events
6
6
  attr_accessor :with_features
7
7
  attr_accessor :logger
8
+ # Optional workload name for per-application usage attribution in the dashboard;
9
+ # sent as X-Featureflow-Application on every request. See Featureflow::Application.
10
+ attr_accessor :application
8
11
 
9
12
  DEFAULT_ENDPOINT = 'https://app.featureflow.io'
10
13
  DEFAULT_EVENT_ENDPOINT = 'https://events.featureflow.io'
11
14
 
12
15
  def initialize
13
16
  self.api_key = ENV["FEATUREFLOW_SERVER_KEY"]
17
+ self.application = ENV["FEATUREFLOW_APPLICATION"]
14
18
  self.endpoint = DEFAULT_ENDPOINT
15
19
  self.event_endpoint = DEFAULT_EVENT_ENDPOINT
16
20
  self.disable_events = false
@@ -7,9 +7,11 @@ module Featureflow
7
7
  #LOCK = Mutex.new
8
8
 
9
9
  class EventsClient
10
- def initialize(url, api_key)
10
+ def initialize(url, api_key, application = nil)
11
11
  @url = url
12
12
  @api_key = api_key
13
+ # Already sanitised by the Client (see lib/featureflow/application.rb).
14
+ @application = application
13
15
  @eventsQueue = Queue.new
14
16
  @scheduler = start_scheduler
15
17
  end
@@ -104,15 +106,17 @@ module Featureflow
104
106
  =end
105
107
 
106
108
  private def send_event(event_name, method, path, body)
109
+ headers = {
110
+ 'Authorization' => "Bearer #{@api_key}",
111
+ 'Content-Type' => 'application/json;charset=UTF-8',
112
+ 'Accept' => 'Application/Json',
113
+ 'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
114
+ }
115
+ headers['X-Featureflow-Application'] = @application if @application
107
116
  connection = Excon.new(@url)
108
117
  response = connection.request(method: method,
109
118
  path: path,
110
- headers: {
111
- 'Authorization' => "Bearer #{@api_key}",
112
- 'Content-Type' => 'application/json;charset=UTF-8',
113
- 'Accept' => 'Application/Json',
114
- 'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
115
- },
119
+ headers: headers,
116
120
  omit_default_port: true,
117
121
  body: JSON.generate(body))
118
122
  if response.status >= 400
@@ -5,7 +5,8 @@ module Featureflow
5
5
  class PollingClient
6
6
  DEFAULT_OPTIONS = {
7
7
  poll_interval: 30,
8
- timeout: 30
8
+ timeout: 30,
9
+ application: nil
9
10
  }.freeze
10
11
  LOCK = Mutex.new
11
12
 
@@ -35,12 +36,16 @@ module Featureflow
35
36
  end
36
37
 
37
38
  def load_features
38
- response = Excon.get(@url + + '/api/sdk/v1/features', headers: {
39
+ headers = {
39
40
  'Authorization' => "Bearer #{@api_key}",
40
41
  'Accept' => 'Application/Json',
41
42
  'If-None-Match' => @etag,
42
43
  'X-Featureflow-Client' => 'RubyClient/' + Featureflow::VERSION
43
- }, omit_default_port: true, read_timeout: @options[:timeout])
44
+ }
45
+ # Write-only telemetry: never affects the response, so it cannot fragment the CDN cache.
46
+ headers['X-Featureflow-Application'] = @options[:application] if @options[:application]
47
+ response = Excon.get(@url + + '/api/sdk/v1/features', headers: headers,
48
+ omit_default_port: true, read_timeout: @options[:timeout])
44
49
 
45
50
  if response.status == 200
46
51
  Featureflow.logger.debug "updating features"
@@ -1,3 +1,3 @@
1
1
  module Featureflow
2
- VERSION = "0.7.0"
2
+ VERSION = "0.8.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: featureflow
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oliver Oldfield-Hodge
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-11 00:00:00.000000000 Z
11
+ date: 2026-08-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -105,6 +105,7 @@ files:
105
105
  - LICENSE
106
106
  - README.md
107
107
  - lib/featureflow.rb
108
+ - lib/featureflow/application.rb
108
109
  - lib/featureflow/client.rb
109
110
  - lib/featureflow/conditions.rb
110
111
  - lib/featureflow/configuration.rb