lab_coat 0.1.4 → 0.1.5

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
  SHA256:
3
- metadata.gz: 0c1c5580cec0ec677137fcbf23a90291fab80564c955a1d8b10fb48903f1f43b
4
- data.tar.gz: 15540d166b6a0d6098c07979efc8b780ecebb26d5059ac3db1217489e2448e06
3
+ metadata.gz: 19e5e50fffea78d509d6c9e9aa7454460250a80917689f0651003a62ba43c7d9
4
+ data.tar.gz: e0f1a47e5cc78a8a3779dc17f53574b407b3191d870aadb28854ac58db567166
5
5
  SHA512:
6
- metadata.gz: c84bde55abd4974f068a802f181552111d847c3eeca07e28046fe3492919ba25a12fdefb10ac547bc22fbce90c1785fa01d73084db769f51ebe55993d52b2cc9
7
- data.tar.gz: 128f1a14f2a2ec4f370d40d5c5b147d35b26abd75f558d5f19755fb71e82cfe8aad4de8673835a82c9b12f21088642ea346aa3bba4d29cdcc7d334a15b4622db
6
+ metadata.gz: da1dcbed4d24ae2ec3e68ab82aa7eb97d6314fff05aa7746d6e7075548c70faae9f11e804af9ef69bd43185ca7d9a9fbe7bf7748472cae629db1e9ea27893cd7
7
+ data.tar.gz: 2f466dc529e11ae7967e93e72ff0b58d718e9b428891e913efe83b074ccb1e1d95ab50d1203e5e49e4a4c6ff8102145c630f422e742e4f736999f0dd6d495891
data/CHANGELOG.md CHANGED
@@ -1,4 +1,7 @@
1
- ## [0.1.4] - Unreleased
1
+ ## [0.1.5] - 2024-05-20
2
+ - Adds `select_observation` to allow users to control which observation value is returned by the experiment. This helps with controlled rollout.
3
+
4
+ ## [0.1.4] - 2024-04-19
2
5
  - Remove the arity check, it's not very intuitive
3
6
  - Adds a `@context` that gets set at runtime and reset after each run. This is a much simpler way for methods to access a shared runtime context that can be set per `run!`.
4
7
 
data/README.md CHANGED
@@ -9,6 +9,7 @@ This library is heavily inspired by [Scientist](https://github.com/github/scient
9
9
  - There is no app wide default experiment that gets magically set.
10
10
  - The `Result` only supports one comparison at a time, i.e. only 1 `candidate` is allowed per run.
11
11
  - The `duration` is measured using Ruby's `Benchmark`.
12
+ - The final return value of the `Experiment` run can be selected dynamically.
12
13
 
13
14
  ## Installation
14
15
 
@@ -54,7 +55,7 @@ See the [`Experiment`](lib/lab_coat/experiment.rb) class for more details.
54
55
  |`candidate`|The new behavior you want to test.|
55
56
  |`control`|The existing or default behavior. This will always be returned from `#run!`.|
56
57
  |`enabled?`|Returns a `Boolean` that controls whether or not the experiment runs.|
57
- |`publish!`|This is not _technically_ required, but `Experiments` are not useful unless you can analyze the results. Override this method to record the `Result` however you wish.|
58
+ |`publish!`|This is _technically_ not required, but `Experiments` are not useful unless you can analyze the results. Override this method to record the `Result` however you wish.|
58
59
 
59
60
  > [!IMPORTANT]
60
61
  > The `#run!` method accepts arbitrary key word arguments and stores them in an instance variable called `@context` in case you need to provide data at runtime. You can access the runtime context via `@context` or `context`. The runtime context **is reset** after each run.
@@ -63,9 +64,11 @@ See the [`Experiment`](lib/lab_coat/experiment.rb) class for more details.
63
64
 
64
65
  |Method|Description|
65
66
  |---|---|
66
- |`compare`|Whether or not the result is a match. This is how you can run complex/custom comparisons.|
67
- |`ignore?`|Whether or not the result should be ignored. Ignored `Results` are still passed to `#publish!`|
67
+ |`compare`|Whether or not the result is a match. This is how you can run complex/custom comparisons. Defaults to `control.value == candidate.value`.|
68
+ |`ignore?`|Whether or not the result should be ignored. Ignored `Results` are still passed to `#publish!`. Defaults to `false`, i.e. nothing is ignored.|
69
+ |`publishable_value`|The data to publish for a given `Observation`. This value is only for publishing and **is not** returned by `run!`. Defaults to `Observation#value`.|
68
70
  |`raised`|Callback method that's called when an `Observation` raises.|
71
+ |`select_observation`|Override this method to select which observation's `value` should be returned by the `Experiment`. Defaults to the control `Observation`.|
69
72
 
70
73
  > [!TIP]
71
74
  > You should create a shared base class(es) to maintain consistency across experiments within your app.
@@ -90,7 +93,7 @@ class ApplicationExperiment < LabCoat::Experiment
90
93
  def publish!(result)
91
94
  payload = result.to_h.merge(
92
95
  user_id: @user.id, # e.g. something from the `Experiment` state
93
- build_number: context.version # e.g. something from the runtime context
96
+ build_number: context[:version] # e.g. something from the runtime context
94
97
  )
95
98
  YourO11yService.track_experiment_result(payload)
96
99
  end
@@ -122,6 +125,21 @@ class ApplicationExperiment < LabCoat::Experiment
122
125
  end
123
126
  ```
124
127
 
128
+ You might want to rollout the new code path in certain cases.
129
+
130
+ ```ruby
131
+ # application_experiment.rb
132
+ class ApplicationExperiment < LabCoat::Experiment
133
+ def select_observation(result)
134
+ if result.matched? || YourFeatureFlagService.flag_enabled?(@user.id, @context[:rollout_flag_name])
135
+ candidate
136
+ else
137
+ super
138
+ end
139
+ end
140
+ end
141
+ ```
142
+
125
143
  ### Make some `Observations` via `run!`
126
144
 
127
145
  You don't have to create an `Observation` yourself; that happens automatically when you call `Experiment#run!`. The control and candidate `Observations` are packaged into a `Result` and [passed to `Experiment#publish!`](#publish-the-result).
@@ -151,8 +169,10 @@ def compare(control, candidate)
151
169
  end
152
170
 
153
171
  def ignore?(control, candidate)
172
+ # You might ignore runs that throw errors and handle them separately via `raised`.
154
173
  return true if control.raised? || candidate.raised?
155
- return true if candidate.value.some_guard?
174
+ # You might ignore runs where the candidate meets some condition.
175
+ return true if candidate.value.some_condition?
156
176
 
157
177
  false
158
178
  end
@@ -185,19 +205,19 @@ A `Result` represents a single run of an `Experiment`.
185
205
  |`matched?`|Whether or not the `control` and `candidate` match, as defined by `Experiment#compare`|
186
206
  |`to_h`|A hash representation of the `Result`. Useful for publishing and/or reporting.|
187
207
 
188
- The `Result` is passed to your implementation of `#publish!` when an `Experiment` is finished running. The `to_h` method on a Result is a good place to start and might be sufficient for most experiments. You might want to `merge` additional data such as the runtime `context` or other state if you find that relevant for analysis.
208
+ The `Result` is passed to your implementation of `#publish!` when an `Experiment` is finished running. The `to_h` method on a Result is a good place to start and might be sufficient for most experiments. You might want to include additional data such as the runtime `context` or other state if you find that relevant for analysis.
189
209
 
190
210
  ```ruby
191
211
  # your_experiment.rb
192
212
  def publish!(result)
193
213
  return if result.ignored?
194
214
 
195
- puts result.to_h.merge(run_context: context)
215
+ puts result.to_h.merge(context:)
196
216
  end
197
217
  ```
198
218
 
199
219
  > [!NOTE]
200
- > All `Results` are passed to `publish!`, **including ignored ones**. It is your responsibility to call the `ignored?` method and handle those as you wish.
220
+ > All `Results` are passed to `publish!`, **including ignored ones**. It is your responsibility to check the `ignored?` method and handle those as you wish.
201
221
 
202
222
  You can always access all of the attributes of the `Result` and its `Observations` directly to fully customize what your experiment publishing looks like.
203
223
 
@@ -12,7 +12,7 @@ module LabCoat
12
12
 
13
13
  # Override this method to control whether or not the experiment runs.
14
14
  # @return [TrueClass, FalseClass]
15
- def enabled?(...)
15
+ def enabled?
16
16
  raise InvalidExperimentError, "`#enabled?` must be implemented in your Experiment class."
17
17
  end
18
18
 
@@ -38,12 +38,16 @@ module LabCoat
38
38
  end
39
39
 
40
40
  # Override this method to define which results are ignored. Must return a boolean.
41
+ # @param control [LabCoat::Observation] The control `Observation`.
42
+ # @param candidate [LabCoat::Observation] The candidate `Observation`.
43
+ # @return [TrueClass, FalseClass]
41
44
  def ignore?(_control, _candidate)
42
45
  false
43
46
  end
44
47
 
45
48
  # Called when the control and/or candidate observations raise an error.
46
49
  # @param observation [LabCoat::Observation]
50
+ # @return [void]
47
51
  def raised(observation); end
48
52
 
49
53
  # Override this method to transform the value for publishing. This could mean turning the value into something
@@ -56,11 +60,23 @@ module LabCoat
56
60
  # Override this method to publish the `Result`. It's recommended to override this once in an application wide base
57
61
  # class.
58
62
  # @param result [LabCoat::Result] The result of this experiment.
63
+ # @return [void]
59
64
  def publish!(result); end
60
65
 
66
+ # Override this method to select which observation's `value` should be returned by the `Experiment`. Defaults to
67
+ # the control `Observation`. This method is only called if the `Experiment` is enabled. This is useful for rolling
68
+ # out new behavior in a controlled way.
69
+ # @param result [LabCoat::Result] The result of the experiment.
70
+ # @return [LabCoat::Observation] Either the control or candidate `Observation` from the given `Result`.
71
+ def select_observation(result)
72
+ result.control
73
+ end
74
+
61
75
  # Runs the control and candidate and publishes the result. Always returns the result of `control`.
76
+ # It's not recommended to override this method.
62
77
  # @param context [Hash] Any data needed at runtime.
63
- def run!(**context)
78
+ # @return [Object] An `Observation` value.
79
+ def run!(**context) # rubocop:disable Metrics/MethodLength
64
80
  # Set the context for this run.
65
81
  @context = context
66
82
 
@@ -69,6 +85,7 @@ module LabCoat
69
85
  raised(control_obs) if control_obs.raised?
70
86
  return control_obs.value unless enabled?
71
87
 
88
+ # Run the candidate.
72
89
  candidate_obs = Observation.new("candidate", self) { candidate }
73
90
  raised(candidate_obs) if candidate_obs.raised?
74
91
 
@@ -76,23 +93,11 @@ module LabCoat
76
93
  result = Result.new(self, control_obs, candidate_obs)
77
94
  publish!(result)
78
95
 
79
- # Reset the context for this run.
80
- @context = {}
81
-
82
96
  # Always return the control.
83
- control_obs.value
84
- end
85
-
86
- private
87
-
88
- # Because `run!` forwards arbitrary args to `#enabled?`, `control`, and `candidate`, the methods must have the same
89
- # arity. Otherwise
90
- def enforce_arity!
91
- return if %i[enabled? control candidate].map { |m| method(m).arity }.uniq.size == 1
92
-
93
- raise InvalidExperimentError,
94
- "The `#enabled?`, `#control` and `#candidate` methods must have the same arity. All runtime args passed " \
95
- "to `#run!` are forwarded to these methods."
97
+ select_observation(result).value.tap do
98
+ # Reset the context for this run. Done here so that `select_observation` has access to the runtime context.
99
+ @context = {}
100
+ end
96
101
  end
97
102
  end
98
103
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LabCoat
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lab_coat
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Omkar Moghe
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-04-19 00:00:00.000000000 Z
11
+ date: 2024-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest