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