Skip to content

Commit 5098de6

Browse files
authored
Experience sampling weight (#80)
* upgrade robocop * Add SplitRegistry model with sampling weight support increase brittleness if env var retrieval impl changes * V2 split registries controller with sampling weight Document EXPERIENCE_SAMPLING_WEIGHT * Expand split representation * correct var name
1 parent 2ec7dd2 commit 5098de6

13 files changed

Lines changed: 184 additions & 19 deletions

File tree

Gemfile.lock

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -256,8 +256,7 @@ GEM
256256
method_source
257257
rake (>= 0.8.7)
258258
thor (>= 0.18.1, < 2.0)
259-
rainbow (2.2.2)
260-
rake
259+
rainbow (3.0.0)
261260
rake (12.3.1)
262261
rb-fsevent (0.10.2)
263262
rb-inotify (0.9.10)
@@ -294,15 +293,15 @@ GEM
294293
rspec-support (3.7.0)
295294
rspec_junit_formatter (0.3.0)
296295
rspec-core (>= 2, < 4, != 2.12.0)
297-
rubocop (0.51.0)
296+
rubocop (0.56.0)
298297
parallel (~> 1.10)
299-
parser (>= 2.3.3.1, < 3.0)
298+
parser (>= 2.5)
300299
powerpack (~> 0.1)
301-
rainbow (>= 2.2.2, < 3.0)
300+
rainbow (>= 2.2.2, < 4.0)
302301
ruby-progressbar (~> 1.7)
303302
unicode-display_width (~> 1.0, >= 1.0.1)
304-
rubocop-betterment (1.0.0)
305-
rubocop (= 0.51)
303+
rubocop-betterment (1.3.0)
304+
rubocop (= 0.56)
306305
ruby-progressbar (1.9.0)
307306
ruby-saml (1.5.0)
308307
nokogiri (>= 1.5.10)
@@ -375,7 +374,7 @@ GEM
375374
thread_safe (~> 0.1)
376375
uglifier (3.2.0)
377376
execjs (>= 0.3.0, < 3)
378-
unicode-display_width (1.3.2)
377+
unicode-display_width (1.4.0)
379378
warden (1.2.7)
380379
rack (>= 1.0)
381380
web-console (2.3.0)

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,20 @@ Variants can be associated with metadata to describe their effects in human-read
132132

133133
For more on how paths are constructed, see the [paperclip documentation](https://github.com/thoughtbot/paperclip).
134134

135+
## Advanced topics
136+
137+
### Feature Gate Experience Sampling Weight
138+
139+
Feature gates report to your analytics provider differently relative to experiments. Where an experiment is assigned once per visitor and the assignment is recorded on the server for later reuse, a feature gate assignment is recalculated on the client side at each interaction. As a result, the analytics provider receives a different kind of event - `feature_gate_experienced` instead of `split_assigned`. These experience events are much higher velocity than assignment events because they occur each time a customer or backend codepath encounters the feature gate.
140+
141+
While this experience event information is valuable to developers who are evaluating the number of customers who have experienced a feature, for example during a slow feature rollout to establish confidence that error rate remains low, there is little value in having comprehensive records. Sampling a fraction of customer interactions provides the same signal developers need at much lower cost.
142+
143+
To switch to a sampling strategy, set the `EXPERIENCE_SAMPLING_WEIGHT` env var to a non-negative integer value as follows:
144+
145+
* The default is `1`, which means that every experience event will be reported to analytics
146+
* To disable reporting of feature gate assignments altogether, set it to `0`
147+
* A value of `10` tells clients to report experience events to analytics probablistically one out of ten times. Conformant (>= v4) TestTrack clients will then reduce their reporting rate accordingly across all feature gates.
148+
135149
## Concepts
136150

137151
### App
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
class Api::V2::SplitRegistriesController < UnauthenticatedApiController
2+
include CorsSupport
3+
4+
def show
5+
@split_registry = SplitRegistry.instance
6+
end
7+
end

app/inputs/collection_select_input.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ def value_method
7575
end
7676

7777
def collection_methods
78-
@_collection_methods ||= detect_collection_methods
78+
@_collection_methods ||= detect_collection_methods # rubocop:disable Naming/MemoizedInstanceVariableName
7979
end
8080

8181
def extract_label(option)

app/inputs/grouped_collection_select_input.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ def selected(_wrapper_options)
1313

1414
def selected_option
1515
current_value = object.public_send(attribute_name)
16+
# rubocop:disable Naming/MemoizedInstanceVariableName
1617
@_selected ||= collections.find { |option| option_value(option) == current_value || option == current_value }
18+
# rubocop:enable Naming/MemoizedInstanceVariableName
1719
end
1820

1921
def collections
@@ -53,15 +55,15 @@ def group_dropdown(parent)
5355
end
5456

5557
def label_method
56-
@_label_method ||= collection_methods.first
58+
@_label_method ||= collection_methods.first # rubocop:disable Naming/MemoizedInstanceVariableName
5759
end
5860

5961
def value_method
60-
@_value_method ||= collection_methods.last
62+
@_value_method ||= collection_methods.last # rubocop:disable Naming/MemoizedInstanceVariableName
6163
end
6264

6365
def collection_methods
64-
@_collection_methods ||= detect_collection_methods
66+
@_collection_methods ||= detect_collection_methods # rubocop:disable Naming/MemoizedInstanceVariableName
6567
end
6668

6769
def option_label(option)

app/models/app.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ class App < ActiveRecord::Base
1010
private
1111

1212
def auth_secret_must_be_sufficiently_strong
13-
return if auth_secret && auth_secret.size >= 43 # rubocop:disable Style/SafeNavigation
13+
return if auth_secret && auth_secret.size >= 43
1414
errors.add(:auth_secret, "must be at least 32-bytes, Base64 encoded")
1515
end
1616
end

app/models/split.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def variants_must_be_snake_case
9494
end
9595

9696
def registry_weights_must_sum_to_100
97-
sum = registry && registry.values.sum # rubocop:disable Style/SafeNavigation
97+
sum = registry && registry.values.sum
9898
errors.add(:registry, "must contain weights that sum to 100% (got #{sum})") unless sum == 100
9999
end
100100

app/models/split_registry.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
class SplitRegistry
2+
include Singleton
3+
4+
def splits
5+
Split.active
6+
end
7+
8+
def experience_sampling_weight
9+
@experience_sampling_weight ||= _experience_sampling_weight
10+
end
11+
12+
private
13+
14+
def _experience_sampling_weight
15+
Integer(ENV.fetch('EXPERIENCE_SAMPLING_WEIGHT', '1')).tap do |weight|
16+
raise <<~TEXT if weight.negative?
17+
EXPERIENCE_SAMPLING_WEIGHT, if specified, must be greater than or equal to 0. Use 0 to disable experience events.
18+
TEXT
19+
end
20+
end
21+
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
json.splits do
2+
@split_registry.splits.each do |split|
3+
json.set! split.name do
4+
json.weights split.registry
5+
json.feature_gate split.feature_gate?
6+
end
7+
end
8+
json.merge!({})
9+
end
10+
json.(@split_registry, :experience_sampling_weight)

config/routes.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@
3333
resources :split_configs, only: [:create, :destroy]
3434
resource :identifier_type, only: :create
3535
end
36+
37+
namespace :v2 do
38+
resource :split_registry, only: :show
39+
end
3640
end
3741

3842
if ENV['SAML_ISSUER'].present?

0 commit comments

Comments
 (0)