lab_coat 0.1.4 → 0.1.5

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: 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