lab_coat 0.1.1 → 0.1.2
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 -0
- data/README.md +66 -36
- data/lib/lab_coat/observation.rb +15 -5
- data/lib/lab_coat/result.rb +11 -0
- data/lib/lab_coat/version.rb +1 -1
- data/lib/lab_coat.rb +2 -0
- 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: 22451390278d5dff8546f421b305107ea5e6d6636ea288eb674ab980353dcd20
|
4
|
+
data.tar.gz: 0a97027504a75312e42501283898021c9417b6e283e22cb5d86a614ebf939ae8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d59adedb051274080954bd28c23b0fbbdc4b6914d90a19f45e5a4c8d3d8f46a06b334e7a2c1e5698eee9adbc9bdfb0f8d4270d37c37430978d952635f4186711
|
7
|
+
data.tar.gz: eb3f5aadd5a01813521b81941052a699ce5ced2b7e9f8d0964942d4a419524e001a0664cd55170069a9fff149eddc6f36885d704e035cc139309115f35c8d856
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
 
|
4
4
|
|
5
|
-
A simple experiment library to safely test new code paths.
|
5
|
+
A simple experiment library to safely test new code paths. `LabCoat` is designed to be highly customizable and play nice with your existing tools/services.
|
6
6
|
|
7
7
|
This library is heavily inspired by [Scientist](https://github.com/github/scientist), with some key differences:
|
8
8
|
- `Experiments` are `classes`, not `modules` which means they are stateful by default.
|
@@ -69,13 +69,11 @@ See the [`Experiment`](lib/lab_coat/experiment.rb) class for more details.
|
|
69
69
|
> [!TIP]
|
70
70
|
> You should create a shared base class(es) to maintain consistency across experiments within your app.
|
71
71
|
|
72
|
-
You
|
72
|
+
You might want to give your experiment some context, or state. You can do this via an initializer or writer methods just like any other Ruby class.
|
73
73
|
|
74
74
|
```ruby
|
75
75
|
# application_experiment.rb
|
76
76
|
class ApplicationExperiment < LabCoat::Experiment
|
77
|
-
attr_reader :user, :is_admin
|
78
|
-
|
79
77
|
def initialize(user)
|
80
78
|
@user = user
|
81
79
|
@is_admin = user.admin?
|
@@ -83,25 +81,19 @@ class ApplicationExperiment < LabCoat::Experiment
|
|
83
81
|
end
|
84
82
|
```
|
85
83
|
|
86
|
-
You
|
84
|
+
You might want to `publish!` all experiments in a consistent way so that you can analyze the data and make decisions. New `Experiment` authors should not have to redo the "plumbing" between your experimentation framework (e.g. `LabCoat`) and your observability (o11y) process.
|
87
85
|
|
88
86
|
```ruby
|
89
87
|
# application_experiment.rb
|
90
88
|
class ApplicationExperiment < LabCoat::Experiment
|
91
89
|
def publish!(result)
|
92
|
-
|
93
|
-
|
94
|
-
matched: result.matched?,
|
95
|
-
observations: {
|
96
|
-
control: result.control.publishable_value,
|
97
|
-
candidate: result.candidate.publishable_value,
|
98
|
-
}
|
99
|
-
)
|
90
|
+
payload = result.to_h.merge(user_id: @user.id)
|
91
|
+
YourO11yService.track_experiment_result(payload)
|
100
92
|
end
|
101
93
|
end
|
102
94
|
```
|
103
95
|
|
104
|
-
You might
|
96
|
+
You might have a common way to enable experiments such as a feature flag system and/or common guards you want to enforce application wide. These might come from a mix of services and the `Experiment`'s state.
|
105
97
|
|
106
98
|
```ruby
|
107
99
|
# application_experiment.rb
|
@@ -112,19 +104,34 @@ class ApplicationExperiment < LabCoat::Experiment
|
|
112
104
|
end
|
113
105
|
```
|
114
106
|
|
107
|
+
You might want to track any errors thrown from all your experiments and route them to some service, or log them.
|
108
|
+
|
109
|
+
```ruby
|
110
|
+
# application_experiment.rb
|
111
|
+
class ApplicationExperiment < LabCoat::Experiment
|
112
|
+
def raised(observation)
|
113
|
+
YourErrorService.report_error(
|
114
|
+
observation.error,
|
115
|
+
tags: observation.to_h
|
116
|
+
)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
```
|
120
|
+
|
115
121
|
### Make some `Observations` via `run!`
|
116
122
|
|
117
123
|
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).
|
118
124
|
|
119
125
|
|Attribute|Description|
|
120
126
|
|---|---|
|
121
|
-
|`duration`|The duration of the run
|
127
|
+
|`duration`|The duration of the run represented as a `Benchmark::Tms` object.|
|
122
128
|
|`error`|If the code path raised, the thrown exception is stored here.|
|
123
129
|
|`experiment`|The `Experiment` instance this `Result` is for.|
|
124
130
|
|`name`|Either `"control"` or `"candidate"`.|
|
125
131
|
|`publishable_value`|A publishable representation of the `value`, as defined by `Experiment#publishable_value`.|
|
126
132
|
|`raised?`|Whether or not the code path raised.|
|
127
133
|
|`slug`|A combination of the `Experiment#name` and `Observation#name`, e.g. `"experiment_name.control"`|
|
134
|
+
|`to_h`|A hash representation of the `Observation`. Useful for publishing and/or reporting.|
|
128
135
|
|`value`|The return value of the observed code path.|
|
129
136
|
|
130
137
|
`Observation` instances are passed to many of the `Experiment` methods that you may override.
|
@@ -145,18 +152,14 @@ def ignore?(control, candidate)
|
|
145
152
|
end
|
146
153
|
|
147
154
|
def publishable_value(observation)
|
148
|
-
if observation.raised?
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
value: observation.publishable_value,
|
157
|
-
duration: observation.duration
|
158
|
-
}
|
159
|
-
end
|
155
|
+
return nil if observation.raised?
|
156
|
+
|
157
|
+
# Let's say your control and candidate blocks return objects that don't serialize nicely.
|
158
|
+
{
|
159
|
+
some_attribute: observation.value.some_attribute,
|
160
|
+
some_other_attribute: observation.value.some_other_attribute,
|
161
|
+
some_count: observation.value.some_array.count
|
162
|
+
}
|
160
163
|
end
|
161
164
|
|
162
165
|
# Elsewhere...
|
@@ -174,10 +177,26 @@ A `Result` represents a single run of an `Experiment`.
|
|
174
177
|
|`experiment`|The `Experiment` instance this `Result` is for.|
|
175
178
|
|`ignored?`|Whether or not the result should be ignored, as defined by `Experiment#ignore?`|
|
176
179
|
|`matched?`|Whether or not the `control` and `candidate` match, as defined by `Experiment#compare`|
|
180
|
+
|`to_h`|A hash representation of the `Result`. Useful for publishing and/or reporting.|
|
181
|
+
|
182
|
+
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.
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
# your_experiment.rb
|
186
|
+
def publish!(result)
|
187
|
+
return if result.ignored?
|
177
188
|
|
178
|
-
|
189
|
+
puts result.to_h
|
190
|
+
end
|
191
|
+
```
|
192
|
+
|
193
|
+
> ![NOTE]
|
194
|
+
> All `Results` are passed to `publish!`, **including ignored ones**. It is your responsibility to call the `ignored?` method and handle those as you wish.
|
195
|
+
|
196
|
+
You can always access all of the attributes of the `Result` and its `Observations` directly to fully customize what your experiment publishing looks like.
|
179
197
|
|
180
198
|
```ruby
|
199
|
+
# your_experiment.rb
|
181
200
|
def publish!(result)
|
182
201
|
if result.ignored?
|
183
202
|
puts "🙈"
|
@@ -189,22 +208,27 @@ def publish!(result)
|
|
189
208
|
else
|
190
209
|
control = result.control
|
191
210
|
candidate = result.candidate
|
192
|
-
puts <<~
|
211
|
+
puts <<~MSG
|
193
212
|
😮
|
194
213
|
|
195
214
|
#{control.slug}
|
196
215
|
Value: #{control.publishable_value}
|
197
|
-
Duration: #{control.duration}
|
216
|
+
Duration Real: #{control.duration.real}
|
217
|
+
Duration System: #{control.duration.stime}
|
218
|
+
Duration User: #{control.duration.utime}
|
198
219
|
Error: #{control.error&.message}
|
199
220
|
|
200
221
|
#{candidate.slug}
|
201
222
|
Value: #{candidate.publishable_value}
|
202
|
-
Duration: #{candidate.duration}
|
223
|
+
Duration: #{candidate.duration.real}
|
224
|
+
Duration System: #{candidate.duration.stime}
|
225
|
+
Duration User: #{candidate.duration.utime}
|
203
226
|
Error: #{candidate.error&.message}
|
204
|
-
|
227
|
+
MSG
|
205
228
|
end
|
206
229
|
end
|
207
230
|
```
|
231
|
+
|
208
232
|
Running a mismatched experiment with this implementation of `publish!` would produce:
|
209
233
|
|
210
234
|
```
|
@@ -212,12 +236,16 @@ Running a mismatched experiment with this implementation of `publish!` would pro
|
|
212
236
|
|
213
237
|
my_experiment.control
|
214
238
|
Value: 420
|
215
|
-
Duration: 12.934
|
239
|
+
Duration Real: 12.934
|
240
|
+
Duration System: 2.134
|
241
|
+
Duration User: 10.800
|
216
242
|
Error:
|
217
243
|
|
218
244
|
my_experiment.candidate
|
219
245
|
Value: 69
|
220
|
-
Duration: 9.702
|
246
|
+
Duration Real: 9.702
|
247
|
+
Duration System: 1.002
|
248
|
+
Duration User: 8.700
|
221
249
|
Error:
|
222
250
|
```
|
223
251
|
|
@@ -238,8 +266,10 @@ The `Observation` class can be used as a standalone wrapper for any code that yo
|
|
238
266
|
if observation.raised?
|
239
267
|
puts "error: #{observation.error.message}"
|
240
268
|
else
|
241
|
-
puts
|
242
|
-
|
269
|
+
puts <<~MSG
|
270
|
+
duration: #{observation.duration.real}
|
271
|
+
succeeded: #{!observation.raised?}
|
272
|
+
MSG
|
243
273
|
end
|
244
274
|
end
|
245
275
|
```
|
data/lib/lab_coat/observation.rb
CHANGED
@@ -9,13 +9,10 @@ module LabCoat
|
|
9
9
|
@name = name
|
10
10
|
@experiment = experiment
|
11
11
|
|
12
|
-
|
13
|
-
begin
|
12
|
+
@duration = Benchmark.measure(name) do
|
14
13
|
@value = block.call
|
15
14
|
rescue StandardError => e
|
16
15
|
@error = e
|
17
|
-
ensure
|
18
|
-
@duration = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second) - start_at
|
19
16
|
end
|
20
17
|
end
|
21
18
|
|
@@ -28,9 +25,22 @@ module LabCoat
|
|
28
25
|
!error.nil?
|
29
26
|
end
|
30
27
|
|
31
|
-
# @return [String] String representing this Observation
|
28
|
+
# @return [String] String representing this `Observation`.
|
32
29
|
def slug
|
33
30
|
"#{experiment.name}.#{name}"
|
34
31
|
end
|
32
|
+
|
33
|
+
# @return [Hash] A hash representation of this `Observation`. Useful when publishing `Results`.
|
34
|
+
def to_h
|
35
|
+
{
|
36
|
+
name: name,
|
37
|
+
experiment: experiment.name,
|
38
|
+
slug: slug,
|
39
|
+
value: publishable_value,
|
40
|
+
duration: duration.to_h,
|
41
|
+
error_class: error&.class&.name,
|
42
|
+
error_message: error&.message
|
43
|
+
}.compact
|
44
|
+
end
|
35
45
|
end
|
36
46
|
end
|
data/lib/lab_coat/result.rb
CHANGED
@@ -26,5 +26,16 @@ module LabCoat
|
|
26
26
|
def ignored?
|
27
27
|
@ignored
|
28
28
|
end
|
29
|
+
|
30
|
+
# @return [Hash] A hash representation of this `Result`. Useful when publishing.
|
31
|
+
def to_h
|
32
|
+
{
|
33
|
+
experiment: experiment.name,
|
34
|
+
matched: matched?,
|
35
|
+
ignored: ignored?,
|
36
|
+
control: control.to_h,
|
37
|
+
candidate: candidate.to_h
|
38
|
+
}
|
39
|
+
end
|
29
40
|
end
|
30
41
|
end
|
data/lib/lab_coat/version.rb
CHANGED
data/lib/lab_coat.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.2
|
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-
|
11
|
+
date: 2024-04-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|