lab_coat 0.1.1 → 0.1.2

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: d819fd05745d82590e7d0f8c2e05f415b3ec6bea1cdc5def244fcfee17b969c7
4
- data.tar.gz: 27819f2af7099608845196b00ae89a0ba59e9bdb8224814585a659601a261e76
3
+ metadata.gz: 22451390278d5dff8546f421b305107ea5e6d6636ea288eb674ab980353dcd20
4
+ data.tar.gz: 0a97027504a75312e42501283898021c9417b6e283e22cb5d86a614ebf939ae8
5
5
  SHA512:
6
- metadata.gz: 711d1510f5d1f4d59a57830567988d74fad50bfe7c4e548cd76e67d3f46bb2dfbbff503914e6129f7023032f7986c2153a1359e600d1f7d2e6aa443d98f27c0d
7
- data.tar.gz: 66ed5879e1e803045806464c6b708b1a355c0d997965e1a6ea43e42b789d4f2904b7c39223b60101c08a1857c35dba72003d9463102962da27d9f39b181c1636
6
+ metadata.gz: d59adedb051274080954bd28c23b0fbbdc4b6914d90a19f45e5a4c8d3d8f46a06b334e7a2c1e5698eee9adbc9bdfb0f8d4270d37c37430978d952635f4186711
7
+ data.tar.gz: eb3f5aadd5a01813521b81941052a699ce5ced2b7e9f8d0964942d4a419524e001a0664cd55170069a9fff149eddc6f36885d704e035cc139309115f35c8d856
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ ## [0.1.2] - 2024-04-15
2
+ - use `Benchmark` to capture the duration with more details
3
+ - add `to_h` methods to `Result` and `Observation` for convenience
4
+
1
5
  ## [0.1.1] - 2024-04-08
2
6
  - add `#slug` method to `Observation`
3
7
 
data/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ![Gem Version](https://img.shields.io/gem/v/lab_coat) ![Gem Total Downloads](https://img.shields.io/gem/dt/lab_coat)
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 may 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.
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 likely 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.
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
- YourO11yService.track_experiment_result(
93
- name: result.experiment.name,
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 also 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.
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 in `float` seconds.|
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
- error_class: observation.error.class.name,
151
- error_message: observation.error.message
152
- }
153
- else
154
- {
155
- type: observation.name,
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
- The `Result` is passed to your implementation of `#publish!` when an `Experiment` is finished running.
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 <<~MISMATCH
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
- MISMATCH
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 "duration: #{observation.duration}"
242
- puts "succeeded: #{!observation.raised?}"
269
+ puts <<~MSG
270
+ duration: #{observation.duration.real}
271
+ succeeded: #{!observation.raised?}
272
+ MSG
243
273
  end
244
274
  end
245
275
  ```
@@ -9,13 +9,10 @@ module LabCoat
9
9
  @name = name
10
10
  @experiment = experiment
11
11
 
12
- start_at = Process.clock_gettime(Process::CLOCK_MONOTONIC, :float_second)
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module LabCoat
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
data/lib/lab_coat.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "benchmark"
4
+
3
5
  require_relative "lab_coat/version"
4
6
  require_relative "lab_coat/observation"
5
7
  require_relative "lab_coat/result"
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.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-09 00:00:00.000000000 Z
11
+ date: 2024-04-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest