determinator 0.10.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ecd2a7d802e9ad2562fbdf9708ff34a84242b91e
4
- data.tar.gz: 5b1918806a440a52f4c337286164b79376059fef
3
+ metadata.gz: e34898ef5ebd55998c28389b7644266fff00ca90
4
+ data.tar.gz: 01497458ea7daf49b59fb70c9214e7a2ae4c14cf
5
5
  SHA512:
6
- metadata.gz: f1a2cf800a27d2b5bf007d50507bc8a1394c596cc0563b4b73fa6cb5dabe3ea43f8558b64840df9baad51ed571c2837409f20aef17e3a5ac4c895264326bb56f
7
- data.tar.gz: 64035bfedb2ed42cb72f27bf48cc09d875a1764f5ecec945f8a409a95c8aab09ac66893b0ef9432d4852689093ccc1c3d137e248285e00dfc27826d0479f4116
6
+ metadata.gz: 8ec544cbdd744dc830e2432ee98af864361427ea543883b4db6227d72fcdf61ab2345bba6abd36df5abccacd67f99f2f4524a85338e5eea8fac5dbca32f2afe5
7
+ data.tar.gz: c68d088da9eba87eba25e3ecc89ae20122801a9be93c183c18c96c2e4b71b0081fc2d2e1b4c450400dd8945d4f1f54c47b3c00a32f60ad31af04a8d7da6b5b22
@@ -1,3 +1,10 @@
1
+ # v0.11.0 (2017-10-13)
2
+
3
+ Bug fix:
4
+ - Ensure constraints and properties are string-keyed so that they match regardless of which are used (#33)
5
+ - Be more permissive with the situations where `Rspec::Determinator` can be used (#34)
6
+ - Swallow not found errors from routemaster so determinator isn't too shouty, allow them to be tracked. (#35)
7
+
1
8
  # v0.10.0 (2017-09-15)
2
9
 
3
10
  Feature:
data/README.md CHANGED
@@ -1,9 +1,15 @@
1
1
  # Determinator
2
2
 
3
- A gem that works with [Florence](https://github.com/deliveroo/actor-tracking) to deterministically calculate whether an actor (a customer, rider, restaurant or employee) should have a feature flag turned on or off, or which variant they should see in an experiment.
3
+ A gem that works with _Florence_ to deterministically calculate whether an **actor** should have a feature flag turned on or off, or which variant they should see in an experiment.
4
4
 
5
5
  ![Determinator](docs/img/determinator.jpg)
6
6
 
7
+ Useful documentation:
8
+
9
+ - [Terminology and Background](docs/background.md)
10
+ - [Local development](docs/local_development.md)
11
+ - [Example implemention in Rails](examples/determinator-rails)
12
+
7
13
  ## Usage
8
14
 
9
15
  Once [set up](#installation), determinator can be used to determine whether a **feature flag** or **experiment** is on or off for the current user and, for experiments, which **variant** they should see.
@@ -25,16 +31,14 @@ when 'velociraptors'
25
31
  end
26
32
  ```
27
33
 
28
- Feature flags and Experiments can be configured to have string based constraints. When the experiment's _constraints_ do not match the given actor's _properties_, the flag or experiment will always be off. When they match the rollout specified by the feature will be applied.
29
-
30
- Constraints must be strings, what matches and doesn't is configurable after-the-fact within Florence.
34
+ Feature flags and experiments can be targeted to specific actors by specifying actor properties (which must match the constraints defined in the feature).
31
35
 
32
36
  ```ruby
33
- # Constraints
37
+ # Targeting specific actors
34
38
  variant = determinator.which_variant(
35
39
  :my_experiment_name,
36
40
  properties: {
37
- country_of_first_order: current_user.orders.first.country.tld,
41
+ employee: current_user.employee?
38
42
  }
39
43
  )
40
44
  ```
@@ -53,10 +57,11 @@ Check the example Rails app in `examples` for more information on how to make us
53
57
  require 'determinator/retrieve/routemaster'
54
58
  Determinator.configure(
55
59
  retrieval: Determinator::Retrieve::Routemaster.new(
56
- discovery_url: 'https://florence.dev/'
60
+ discovery_url: 'https://flo.dev/'
57
61
  retrieval_cache: ActiveSupport::Cache::MemoryStore.new(expires_in: 1.minute)
58
62
  ),
59
- errors: -> error { NewRelic::Agent.notice_error(error) }
63
+ errors: -> error { NewRelic::Agent.notice_error(error) },
64
+ missing_features: -> feature_name { STATSD.increment 'determinator.missing_feature', tags: ["feature:#{name}"] }
60
65
  )
61
66
  ```
62
67
 
@@ -0,0 +1,42 @@
1
+ # Terminology & Background
2
+
3
+ **Florence** is a suite of tools which help run experiments and feature flags (_collectively called **features**_), **Determinator** is the client-side component which implements the algorithm for figuring out what to show to whom.
4
+
5
+ **Feature flags** are used as a way to switch on and off functionality for specific actors across an entire ecosystem, where an **actor** might be a customer, a rider, or any identifiable agent using those systems.
6
+
7
+ **Experiments** are, at this stage, really just [A/B tests](https://en.wikipedia.org/wiki/A/B_testing) where an actor is repeatably shown either one **variant** of the product or another. The activity of large numbers of actors can be analysed to determine if one variant was, statistically speaking, better than the other for a given metric.
8
+
9
+ ## Targeting actors
10
+
11
+ Florence also provides a very flexible way to target specific actors for new features or experiments. Every feature has one or more **target groups** associated with it each for which specifies a _rollout_ fraction and any number of _constraints_.
12
+
13
+ For a given feature an actor is part of a target group if the actor's **properties** are a match with the target group's **constraints** (ie. the feature's constraints are a subset of the actor's properties). For example, a customer may have a property `employee: 'false'`; this actor would _not_ be part of a target group with the constraint `employee: 'true'`, but _would_ be part of a target group with no constraints.
14
+
15
+ If an actor is in no target groups, then the feature (whether it is a feature flag or an experiment) will be off for that actor. If an actor is in more than one target group, then the most permissive target group is chosen.
16
+
17
+ Target groups also have a **rollout** fraction which represents how many actors should be included or excluded from the feature. For example this allows a feature to be on for 95% of people using it, or to be rolled out to 100% of employees, but only 5% of non-employees.
18
+
19
+ ## Experiments vs. Feature Flags
20
+
21
+ Experiments in Florence are Feature flags which also allocate an experimental **variant** to the actors invovled. The variants chosen are also chosen deterministically, so the same actor will always see the same experiment.
22
+
23
+ For example an experiment which tested whether the 🎉 or the 🙌 emoji was better in a given situation by showing 80% of all non-employees one or the other (in a 50/50 split) would have:
24
+
25
+ - One target group, with a rollout of 80% and a single constraint that `employee` must be `false`
26
+ - Two variants, one called `party popper` and one called `high ten`, with the same **weight** as each other (so the split is equal)
27
+
28
+ ## Determinism
29
+
30
+ Whether an actor is rolled out to or not and which variant they see is calculated [deterministically](https://en.wikipedia.org/wiki/Deterministic_system). This is so that two isolated systems, if they have the same list of experiments and feature flags, will have _the same outcomes_ for every actor, feature flag and experiment.
31
+
32
+ In order to ensure that the same actor sees the same things _every time_ a [cryptographically secure hashing function](https://en.wikipedia.org/wiki/Cryptographic_hash_function) is applied to a combination of an actor's identifier (eg. their ID or their anonymous ID) and the feature's name and the resulting information is used to determine what the specified actor will see.
33
+
34
+ This is so that there is no need for a centralised database of which actors should be shown which variants, and which actors should be in hold out groups for experiments (this removes the need for [locks](https://en.wikipedia.org/wiki/Lock_%28computer_science%29) which become very complex in distributed environments).
35
+
36
+ This algorithm is also organised so that, when increasing and lowering rollout fractions, the same actors will be included and excluded at each fraction.
37
+
38
+ ## Overrides
39
+
40
+ The determination algorithm also allows **overrides** which allow specified actors to receive the given outcome even if the algorithm would normally give them another. This is particularly helpful for testing in production environments, where a product manager might have a feature turned on for only them, or to switch themselves between the two variants of an experiment to ensure both work as expected.
41
+
42
+ Overrides should be used sparingly and only for temporary changes; for situations where even a small group of actors should see a specific feature consider whether the actor has an attribute which defines whether they should see it or not, and instead deliver that as a property so that a target group can specify just them. A simple example of this is 'VIPs', rather than specifying them as `override: true` for a feature flag, they should instead have the property `vip: true`, with an equivalent constraint on a 100% rollout target group.
@@ -0,0 +1,72 @@
1
+ # Local development
2
+
3
+ Because Determinator depends on features defined elsewhere, local development is supported by two pieces of software.
4
+
5
+ ## `RSpec::Determinator` for automated testing
6
+
7
+ When writing tests for your code that makes use of Determinator you can use some RSpec helpers:
8
+
9
+ ```ruby
10
+ require 'rspec/determinator'
11
+
12
+ RSpec.describe YourClass, :determinator_support do
13
+
14
+ context "when the actor is in variant_a" do
15
+ # This allows testing of the experiment being in a specific variant
16
+ forced_determination(:experiment_name, 'variant_a')
17
+
18
+ it "should respond in a way that is defined by variant_a"
19
+ end
20
+
21
+ context "when the actor is not in the experiment" do
22
+ # This allows testing of the experiment being off
23
+ forced_determination(:experiment_name, false)
24
+
25
+ it "should respond in a way that is defined by being out of the experiment"
26
+ end
27
+
28
+ context "when the actor is not from France" do
29
+ before { ensure_the_actor_is_not_from_france }
30
+ # This allows testing of target group constraint functionality
31
+ forced_determination(:experiment_name, 'variant_b', only_for: { country: 'fr' })
32
+
33
+ it "should respond in a way that is defined by being out of the experiment"
34
+ end
35
+
36
+ context "when the actor has a specified id" do
37
+ before { ensure_the_actor_has_id_123 }
38
+ # This allows testing of override functionality
39
+ forced_determination(:experiment_name, 'variant_b', only_for: { id: '123' })
40
+
41
+ it "should respond in a way that is defined by variant_b"
42
+ end
43
+ end
44
+ ```
45
+
46
+ ## Fake Florence for local execution
47
+
48
+ [Fake Florence](https://github.com/deliveroo/fake_florence) is a command line utility which operates a determinator compatible server and provides tooling for easy editing of feature flags and experiments.
49
+
50
+ ```bash
51
+ $ gem install fake_florence
52
+ Fake Florence has been installed. Run `flo help` for more information.
53
+ 1 gem installed
54
+
55
+ $ flo start
56
+ Flo is now running at https://flo.dev
57
+ Use other commands to create or edit Feature flags and Experiments.
58
+ See `flo help` for more information
59
+
60
+ $ flo create my_experiment
61
+ create ~/.flo/features/my_experiment.yaml
62
+ my_experiment created and opened for editing
63
+ ```
64
+
65
+ Then in your service, configured with `discovery_url: 'https://flo.dev'`, experiments and feature flags will retrieved and posted from Fake Florence:
66
+
67
+ ```ruby
68
+ determinator.which_variant(:my_experiment, id: 123)
69
+ "anchovy"
70
+ ```
71
+
72
+ More information can be found on the [Fake Florence](https://github.com/deliveroo/fake_florence) project page.
@@ -14,7 +14,12 @@ class ApplicationController < ActionController::API
14
14
  # which allows simple use throughout the app
15
15
  @_determinator ||= Determinator.instance.for_actor(
16
16
  id: current_user && current_user.id || nil,
17
- guid: guid
17
+ guid: guid,
18
+ default_properties: {
19
+ # Clearly this would return real information about whether the
20
+ # user is an employee.
21
+ employee: false
22
+ }
18
23
  )
19
24
  end
20
25
  end
@@ -7,9 +7,11 @@ require 'determinator/retrieve/null_retriever'
7
7
 
8
8
  module Determinator
9
9
  # @param :retrieval [Determinator::Retrieve::Routemaster] A retrieval instance for Features
10
- # @param :errors [Proc, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
11
- def self.configure(retrieval:, errors: nil)
10
+ # @param :errors [#call, nil] a proc, accepting an error, which will be called with any errors which occur while determinating
11
+ # @param :missing_feature [#call, nil] a proc, accepting a feature name, which will be called any time a feature is requested but isn't available
12
+ def self.configure(retrieval:, errors: nil, missing_feature: nil)
12
13
  @error_logger = errors if errors.respond_to?(:call)
14
+ @missing_feature_logger = missing_feature if missing_feature.respond_to?(:call)
13
15
  @instance = Control.new(retrieval: retrieval)
14
16
  end
15
17
 
@@ -23,4 +25,10 @@ module Determinator
23
25
 
24
26
  @error_logger.call(error)
25
27
  end
28
+
29
+ def self.missing_feature(name)
30
+ return unless @missing_feature_logger
31
+
32
+ @missing_feature_logger.call(name)
33
+ end
26
34
  end
@@ -57,7 +57,10 @@ module Determinator
57
57
 
58
58
  def determinate(name, id:, guid:, properties:)
59
59
  feature = retrieval.retrieve(name)
60
- return false unless feature
60
+ if feature.nil?
61
+ Determinator.missing_feature(name)
62
+ return false
63
+ end
61
64
 
62
65
  # Calling method can place constraints on the feature, eg. experiment only
63
66
  return false if block_given? && !yield(feature)
@@ -86,9 +89,14 @@ module Determinator
86
89
  end
87
90
 
88
91
  def choose_target_group(feature, properties)
92
+ # Keys must be strings
93
+ normalised_properties = properties.each_with_object({}) do |(k, v), h|
94
+ h[k.to_s] = v
95
+ end
96
+
89
97
  feature.target_groups.select { |tg|
90
98
  tg.constraints.reduce(true) do |fit, (scope, *required)|
91
- present = [*properties[scope]]
99
+ present = [*normalised_properties[scope.to_s]]
92
100
  fit && (required.flatten & present.flatten).any?
93
101
  end
94
102
  # Must choose target group deterministically, if more than one match
@@ -31,6 +31,9 @@ module Determinator
31
31
  cached_feature_lookup(feature_id) do
32
32
  @actor_service.feature.show(feature_id)
33
33
  end
34
+ rescue ::Routemaster::Errors::ResourceNotFound
35
+ # Don't be noisy
36
+ nil
34
37
  rescue => e
35
38
  Determinator.notice_error(e)
36
39
  nil
@@ -47,10 +50,6 @@ module Determinator
47
50
  @routemaster_app ||= ::Routemaster::Drain::Caching.new
48
51
  end
49
52
 
50
- def self.index_cache_key(feature_name)
51
- "determinator_index:#{feature_name}"
52
- end
53
-
54
53
  def self.lookup_cache_key(feature_name)
55
54
  "determinator_cache:#{feature_name}"
56
55
  end
@@ -1,3 +1,3 @@
1
1
  module Determinator
2
- VERSION = '0.10.0'
2
+ VERSION = '0.11.0'
3
3
  end
@@ -7,7 +7,7 @@ module RSpec
7
7
 
8
8
  by.let(:fake_determinator) { FakeControl.new }
9
9
  by.before do
10
- allow(::Determinator::Control).to receive(:new).and_return(fake_determinator)
10
+ allow(::Determinator).to receive(:instance).and_return(fake_determinator)
11
11
  end
12
12
  end
13
13
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: determinator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - JP Hastings-Spital
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-09-15 00:00:00.000000000 Z
11
+ date: 2017-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: routemaster-drain
@@ -143,7 +143,9 @@ files:
143
143
  - bin/setup
144
144
  - circle.yml
145
145
  - determinator.gemspec
146
+ - docs/background.md
146
147
  - docs/img/determinator.jpg
148
+ - docs/local_development.md
147
149
  - examples/determinator-rails/.env
148
150
  - examples/determinator-rails/.gitignore
149
151
  - examples/determinator-rails/Gemfile