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 +4 -4
- data/CHANGELOG.md +4 -1
- data/README.md +28 -8
- data/lib/lab_coat/experiment.rb +23 -18
- data/lib/lab_coat/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 19e5e50fffea78d509d6c9e9aa7454460250a80917689f0651003a62ba43c7d9
|
4
|
+
data.tar.gz: e0f1a47e5cc78a8a3779dc17f53574b407b3191d870aadb28854ac58db567166
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: da1dcbed4d24ae2ec3e68ab82aa7eb97d6314fff05aa7746d6e7075548c70faae9f11e804af9ef69bd43185ca7d9a9fbe7bf7748472cae629db1e9ea27893cd7
|
7
|
+
data.tar.gz: 2f466dc529e11ae7967e93e72ff0b58d718e9b428891e913efe83b074ccb1e1d95ab50d1203e5e49e4a4c6ff8102145c630f422e742e4f736999f0dd6d495891
|
data/CHANGELOG.md
CHANGED
@@ -1,4 +1,7 @@
|
|
1
|
-
## [0.1.
|
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
|
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
|
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
|
-
|
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
|
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(
|
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
|
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
|
|
data/lib/lab_coat/experiment.rb
CHANGED
@@ -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
|
-
|
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
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
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
|
data/lib/lab_coat/version.rb
CHANGED
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
|
+
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-
|
11
|
+
date: 2024-05-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|