scientist 0.0.0 → 0.0.1

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
  SHA1:
3
- metadata.gz: 97114552c98c00982ddd484d3b150fd0c636b446
4
- data.tar.gz: 93c7f89162716794316df3929ce43dc3af1087d0
3
+ metadata.gz: 7b1ce13f8a2483a8027f3252d79a962dd116733c
4
+ data.tar.gz: 54e81bb6123073b95c9bec0b1e751f88519fd498
5
5
  SHA512:
6
- metadata.gz: c109b14c1a7cd08f8a42c6861193f7165d54419de6fc23265065a9858306478c2e4e8b901e9dc221738eb94e7f36f5dac8eef42596a2e885564b6238405e6b2a
7
- data.tar.gz: 449aa244f546d2de87c66d4cde26775a638aff8fdd8c3e7f7fe49ef7053728c3f46b41d07cdfb1729e50288106f08aded08ef7ed00c76bf7d2e30b3b0288ce42
6
+ metadata.gz: 5e97d816e8b340701d2a6488799f899ae74480992e223258615c96c6f19042d53a867680aada5cc010c5718fa499d31889056f111b9476b810ac74af2023b4d7
7
+ data.tar.gz: c65a04f83ef07d9c770354fc5a1dcfa1a76199f150152c0c39735839eec2cf975f333779418827c201d03b4e9047d9afcb740d1ac735a69004e2a945afafb5de
data/CLA.md ADDED
@@ -0,0 +1,54 @@
1
+ GitHub CLA
2
+ ===============
3
+
4
+ ## Don't give up - please go ahead and create this PR.
5
+
6
+ We welcome you to follow, fork, and work on, our open source projects. If you want to contribute back to this project, or any other GitHub project, we need to ask you to complete the Contributor License Agreement (CLA) below. If you are contributing on behalf of your employer, or as part of your role as an employee, remember that you are signing in the name of your employer and you have to make sure that that's okay before you sign.
7
+
8
+ ## What is this?
9
+
10
+ This is GitHub Inc.’s Contributor License Agreement. If you’ve worked in the technology space before, contributed or maintained an open source project, there’s a good chance that you’ve run across one or more of these in the past. What CLAs aim to do is make sure the project is able to merge contributions from multiple contributors without getting itself into different types of trouble. This one is no different in that sense.
11
+
12
+ ## Why is this?
13
+
14
+ The answer is that we need to protect the open source projects that we maintain, their users and their contributors (including Hubber contributors, of course, but not just). Why? Just imagine a case when a contributor is making a contribution to a project and that contribution is subsequently merged and becomes an integral part of the project. Now, go on to imagine that our contributor copied the code, or that this contributor works for a company that doesn't want its employees to make contributions to this project or any project. What then? Well, the person the code was copied from or the company can do whatever the hell they like, including to come after the project and its users if using it, make them stop or even sue. If either has patent rights in the code, the project is in even deeper trouble.
15
+
16
+
17
+ ## So.
18
+
19
+ Please read the following terms, make sure you understand, and that if you agree, that you sign. Then, your pull request would be created and the project and the other contributors would be safe. It's important to us that you remember that except for the license you grant by signing this document - to GitHub, to your fellow contributors and to the project, you reserve all right, title, and interest in your contributions.
20
+
21
+ ### 1. Definitions.
22
+
23
+ *You*, *you* (*Your*, or *your*) means the copyright owner or legal entity authorized by the copyright owner to sign this agreement.
24
+
25
+ *Contribution* or *contribution* means any original work of authorship, including any modifications or additions to an existing work, that is submitted to a GitHub project. "Submitted" means via a Pull Request, an issue or any form of electronic, verbal, or written communication sent to GitHub.
26
+
27
+ ### 2. Grant of Copyright License.
28
+
29
+ Subject to the terms and conditions of this agreement, you grant to GitHub, to fellow contributors and to the project a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your contributions and such derivative works.
30
+
31
+ ### 3. Grant of Patent License.
32
+
33
+ Subject to the terms and conditions of this agreement, You hereby grant to GitHub, to fellow contributors to the project, and to its users a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer your contribution as part of the project, where such license applies only to those patent claims licensable by you that are necessarily infringed by your contribution or by combination of your contribution with the project to which this contribution was submitted. If any entity institutes patent litigation against you or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that your contribution, or the project to which you have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this agreement for that contribution shall terminate as of the date such litigation is filed.
34
+
35
+ ### 4. You Can Grant this License.
36
+
37
+ Signing would mean that you are legally entitled to grant the above license. If your employer(s) has rights to intellectual property that you create that includes your contributions, you have verified and are certain that you have received permission to make your contribution on behalf of that employer, that your employer has waived such rights for your contribution, or that your employer has executed a separate license with GitHub or the project.
38
+
39
+ ### 5. Your Contribution is Yours.
40
+
41
+ Signing doesn't change the fact that your contribution is your original creation (see section 7 for submissions on behalf of others) and that they include complete details of any third-party license or other restriction (including related patents and trademarks) of which you are personally aware and which are associated with any part of your contributions.
42
+
43
+ ### 6. You Provide Your Contribution "as is".
44
+
45
+ Signing this won't mean anybody will argue otherwise. In other words, your contributions are made without warranties or conditions of any kind.
46
+
47
+ ### 7. If Some or All Your Contributions Is Not Yours.
48
+
49
+ That's fine but you need to identify the source or sources of the contribution and any license or other restriction (like related patents, trademarks, and license agreements) of which you are personally aware, and conspicuously marking the contribution as "Submitted on behalf of a third-party or third parties: [named here]". A place where you can do this is in a commit comment to the PR.
50
+
51
+ ### 8. If Any Circumstances of Your Contribution change.
52
+ You agree to notify the project and GitHub of any facts or circumstances of which you become aware.
53
+
54
+ ### 9. That's it!
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,3 @@
1
+ Hi there! We're thrilled that you'd like to contribute to this project. Before you do, would you mind reading [this license agreement](CLA.md)? If you open a PR, we'll assume you agree to it. If you have any hesitation or disagreement, please do open a PR still, but note your concerns as well.
2
+
3
+ Thanks!
data/Gemfile CHANGED
@@ -1,4 +1,3 @@
1
- ruby "2.1.0"
2
1
  source "https://rubygems.org"
3
2
 
4
3
  gemspec
data/README.md CHANGED
@@ -1,11 +1,420 @@
1
- # Science!
1
+ # Scientist!
2
2
 
3
- A Ruby library for carefully refactoring critical paths. Scientist provides a pattern a pattern for measuring and validating large code changes without altering behavior.
3
+ A Ruby library for carefully refactoring critical paths.
4
+
5
+ ## How do I do science?
6
+
7
+ Let's pretend you're changing the way you handle permissions in a large web app. Tests can help guide your refactoring, but you really want to compare the current and refactored behaviors under load.
8
+
9
+ ```ruby
10
+ require "scientist"
11
+
12
+ class MyWidget
13
+ def allows?(user)
14
+ experiment = Scientist::Default.new "widget-permissions" do |e|
15
+ e.use { model.check_user?(user).valid? } # old way
16
+ e.try { user.can?(:read, model) } # new way
17
+ end
18
+
19
+ experiment.run
20
+ end
21
+ end
22
+ ```
23
+
24
+ Wrap a `use` block around the code's original behavior, and wrap `try` around the new behavior. `experiment.run` will always return whatever the `use` block returns, but it does a bunch of stuff behind the scenes:
25
+
26
+ * It decides whether or not to run the `try` block,
27
+ * Randomizes the order in which `use` and `try` blocks are run,
28
+ * Measures the durations of all behaviors,
29
+ * Compares the result of `try` to the result of `use`,
30
+ * Swallows (but records) any exceptions raise in the `try` block, and
31
+ * Publishes all this information.
32
+
33
+ The `use` block is called the **control**. The `try` block is called the **candidate**.
34
+
35
+ Creating an experiment is wordy, but when you include the `Scientist` module, the `science` helper will instantiate an experiment and call `run` for you:
36
+
37
+ ```ruby
38
+ require "scientist"
39
+
40
+ class MyWidget
41
+ include Scientist
42
+
43
+ def allows?(user)
44
+ science "widget-permissions" do |e|
45
+ e.use { model.check_user(user).valid? } # old way
46
+ e.try { user.can?(:read, model) } # new way
47
+ end # returns the control value
48
+ end
49
+ end
50
+ ```
51
+
52
+ If you don't declare any `try` blocks, none of the Scientist machinery is invoked and the control value is always returned.
53
+
54
+ ## Making science useful
55
+
56
+ The examples above will run, but they're not really *doing* anything. The `try` blocks run every time and none of the results get published. Replace the default experiment implementation to control execution and reporting:
57
+
58
+ ```ruby
59
+ require "scientist"
60
+
61
+ class MyExperiment < ActiveRecord::Base
62
+ include Scientist::Experiment
63
+
64
+ def enabled?
65
+ # see "Ramping up experiments" below
66
+ super
67
+ end
68
+
69
+ def publish(result)
70
+ # see "Publishing results" below
71
+ super
72
+ end
73
+ end
74
+
75
+ # replace `Scientist::Default` as the default implementation
76
+ def Scientist::Experiment.new(name)
77
+ MyExperiment.find_or_initialize_by(name: name)
78
+ end
79
+ ```
80
+
81
+ Now calls to the `science` helper will load instances of `MyExperiment`.
82
+
83
+ ### Controlling comparison
84
+
85
+ Scientist compares control and candidate values using `==`. To override this behavior, use `compare` to define how to compare observed values instead:
86
+
87
+ ```ruby
88
+ class MyWidget
89
+ include Scientist
90
+
91
+ def users
92
+ science "users" do |e|
93
+ e.use { User.all } # returns User instances
94
+ e.try { UserService.list } # returns UserService::User instances
95
+
96
+ e.compare do |control, candidate|
97
+ control.map(&:login) == candidate.map(&:login)
98
+ end
99
+ end
100
+ end
101
+ end
102
+ ```
103
+
104
+ ### Adding context
105
+
106
+ Results aren't very useful without some way to identify them. Use the `context` method to add to or retrieve the context for an experiment:
107
+
108
+ ```ruby
109
+ science "widget-permissions" do |e|
110
+ e.context :user => user
111
+
112
+ e.use { model.check_user(user).valid? }
113
+ e.try { user.can?(:read, model) }
114
+ end
115
+ ```
116
+
117
+ `context` takes a Symbol-keyed Hash of extra data. The data is available in `Experiment#publish` via the `context` method. If you're using the `science` helper a lot in a class, you can provide a default context:
118
+
119
+ ```ruby
120
+ class MyWidget
121
+ include Scientist
122
+
123
+ def allows?(user)
124
+ science "widget-permissions" do |e|
125
+ e.context :user => user
126
+
127
+ e.use { model.check_user(user).valid? }
128
+ e.try { user.can?(:read, model) }
129
+ end
130
+ end
131
+
132
+ def destroy
133
+ science "widget-destruction" do |e|
134
+ e.use { old_scary_destroy }
135
+ e.try { new_safe_destroy }
136
+ end
137
+ end
138
+
139
+ def default_scientist_context
140
+ { :widget => self }
141
+ end
142
+ end
143
+ ```
144
+
145
+ The `widget-permissions` and `widget-destruction` experiments will both have a `:widget` key in their contexts.
146
+
147
+ ### Keeping it clean
148
+
149
+ Sometimes you don't want to store the full value for later analysis. For example, an experiment may return `User` instances, but when researching a mismatch, all you care about is the logins. You can define how to clean these values in an experiment:
150
+
151
+ ```ruby
152
+ class MyWidget
153
+ include Scientist
154
+
155
+ def users
156
+ science "users" do |e|
157
+ e.use { User.all }
158
+ e.try { UserService.list }
159
+
160
+ e.clean do |value|
161
+ value.map(&:login).sort
162
+ end
163
+ end
164
+ end
165
+ end
166
+ ```
167
+
168
+ And this cleaned value is available in observations in the final published result:
169
+
170
+ ```ruby
171
+ class MyExperiment < ActiveRecord::Base
172
+ include Scientist::Experiment
173
+
174
+ def publish(result)
175
+ result.control.value # [<User alice>, <User bob>, <User carol>]
176
+ result.control.cleaned_value # ["alice", "bob", "carol"]
177
+ end
178
+ end
179
+ ```
180
+
181
+ ### Ignoring mismatches
182
+
183
+ During the early stages of an experiment, it's possible that some of your code will always generate a mismatch for reasons you know and understand but haven't yet fixed. Instead of these known cases always showing up as mismatches in your metrics or analysis, you can tell an experiment whether or not to ignore a mismatch using the `ignore` method. You may include more than one block if needed:
184
+
185
+ ```ruby
186
+ def admin?(user)
187
+ science "widget-permissions" do |e|
188
+ e.use { model.check_user(user).admin? }
189
+ e.try { user.can?(:admin, model) }
190
+
191
+ e.ignore { user.staff? } # user is staff, always an admin in the new system
192
+ e.ignore do |control, candidate|
193
+ # new system doesn't handle unconfirmed users yet:
194
+ control && !candidate && !user.confirmed_email?
195
+ end
196
+ end
197
+ end
198
+ ```
199
+
200
+ The ignore blocks are only called if the *values* don't match. If one observation raises an exception and the other doesn't, it's always considered a mismatch. If both observations raise different exceptions, that is also considered a mismatch.
201
+
202
+ ### Enabling/disabling experiments
203
+
204
+ Sometimes you don't want an experiment to run. Say, disabling a new codepath for anyone who isn't staff. You can disable an experiment by setting a `run_if` block. If this returns `false`, the experiment will merely return the control value. Otherwise, it defers to the experiment's configured `enabled?` method.
205
+
206
+ ```ruby
207
+ class DashboardController
208
+ include Scientist
209
+
210
+ def dashboard_items
211
+ science "dashboard-items" do |e|
212
+ # only run this experiment for staff members
213
+ e.run_if { current_user.staff? }
214
+ # ...
215
+ end
216
+ end
217
+ ```
218
+
219
+ ### Ramping up experiments
220
+
221
+ As a scientist, you know it's always important to be able to turn your experiment off, lest it run amok and result in villagers with pitchforks on your doorstep. In order to control whether or not an experiment is enabled, you must include the `enabled?` method in your `Scientist::Experiment` implementation.
222
+
223
+ ```ruby
224
+ class MyExperiment < ActiveRecord::Base
225
+ include Scientist::Experiment
226
+ def enabled?
227
+ percent_enabled > 0 && rand(100) < percent_enabled
228
+ end
229
+ end
230
+ ```
231
+
232
+ This code will be invoked for every method with an experiment every time, so be sensitive about its performance. For example, you can store an experiment in the database but wrap it in various levels of caching such as memcache or per-request thread-locals.
233
+
234
+ ### Publishing results
235
+
236
+ What good is science if you can't publish your results?
237
+
238
+ You must implement the `publish(result)` method, and can publish data however you like. For example, timing data can be sent to graphite, and mismatches can be placed in a capped collection in redis for debugging later.
239
+
240
+ The `publish` method is given a `Scientist::Result` instance with its associated `Scientist::Observation`s:
241
+
242
+ ```ruby
243
+ class MyExperiment
244
+ include Scientist::Experiment
245
+
246
+ # ...
247
+
248
+ def publish(result)
249
+
250
+ # Store the timing for the control value,
251
+ $statsd.timing "science.#{name}.control", result.control.duration
252
+ # for the candidate (only the first, see "Breaking the rules" below,
253
+ $statsd.timing "science.#{name}.candidate", result.candidates.first.duration
254
+
255
+ # and counts for match/ignore/mismatch:
256
+ if result.matched?
257
+ $statsd.increment "science.#{name}.matched"
258
+ elsif result.ignored?
259
+ $statsd.increment "science.#{name}.ignored"
260
+ else
261
+ $statsd.increment "science.#{name}.mismatched"
262
+ # Finally, store mismatches in redis so they can be retrieved and examined
263
+ # later on, for debugging and research.
264
+ store_mismatch_data(result)
265
+ end
266
+ end
267
+
268
+ def store_mismatch_data(result)
269
+ payload = {
270
+ :name => name,
271
+ :context => context,
272
+ :control => observation_payload(result.control),
273
+ :candidate => observation_payload(result.candidates.first)
274
+ :execution_order => result.observations.map(&:name),
275
+ }
276
+
277
+ key = "science.#{name}.mismatch"
278
+ $redis.lpush key, payload
279
+ $redis.ltrim key, 0, 1000
280
+ end
281
+
282
+ def observation_payload(observation)
283
+ if observation.raised?
284
+ {
285
+ :exception => observation.exeception.class,
286
+ :message => observation.exeception.message,
287
+ :backtrace => observation.exception.backtrace
288
+ }
289
+ else
290
+ {
291
+ # see "Keeping it clean" below
292
+ :value => observation.cleaned_value
293
+ }
294
+ end
295
+ end
296
+ end
297
+ ```
298
+
299
+ ### Testing
300
+
301
+ When running your test suite, it's helpful to know that the experimental results always match. To help with testing, Scientist defines a `raise_on_mismatches` class attribute when you include `Scientist::Experiment`. Only do this in your test suite!
302
+
303
+ To raise on mismatches:
304
+
305
+ ```ruby
306
+ class MyExperiment
307
+ include Scientist::Experiment
308
+ # ... implementation
309
+ end
310
+
311
+ MyExperiment.raise_on_mismatches = true
312
+ ```
313
+
314
+ Scientist will raise a `Scientist::Experiment::MismatchError` exception if any observations don't match.
315
+
316
+ ### Handling errors
317
+
318
+ If an exception is raised within any of scientist's internal helpers, like `publish`, `compare`, or `clean`, the `raised` method is called with the symbol name of the internal operation that failed and the exception that was raised. The default behavior of `Scientist::Default` is to simply re-raise the exception. Since this halts the experiment entirely, it's often a better idea to handle this error and continue so the experiment as a whole isn't canceled entirely:
319
+
320
+ ```ruby
321
+ class MyExperiment
322
+ include Scientist::Experiment
323
+
324
+ # ...
325
+
326
+ def raised(operation, error)
327
+ InternalErrorTracker.track! "science failure in #{name}: #{operation}", error
328
+ end
329
+ end
330
+ ```
331
+
332
+ The operations that may be handled here are:
333
+
334
+ * `:clean` - an exception is raised in a `clean` block
335
+ * `:compare` - an exception is raised in a `compare` block
336
+ * `:enabled` - an exception is raised in the `enabled?` method
337
+ * `:ignore` - an exception is raised in an `ignore` block
338
+ * `:publish` - an exception is raised in the `publish` method
339
+ * `:run_if` - an exception is raised in a `run_if` block
340
+
341
+ ### Designing an experiment
342
+
343
+ Because `enabled?` and `run_if` determine when a candidate runs, it's impossible to guarantee that it will run every time. For this reason, Scientist is only safe for wrapping methods that aren't changing data.
344
+
345
+ When using Scientist, we've found it most useful to modify both the existing and new systems simultaneously anywhere writes happen, and verify the results at read time with `science`. `raise_on_mismatches` has also been useful to ensure that the correct data was written during tests, and reviewing published mismatches has helped us find any situations we overlooked with our production data at runtime. When writing to and reading from two systems, it's also useful to write some data reconciliation scripts to verify and clean up production data alongside any running experiments.
346
+
347
+ ### Finishing an experiment
348
+
349
+ As your candidate behavior converges on the controls, you'll start thinking about removing an experiment and using the new behavior.
350
+
351
+ * If there are any ignore blocks, the candidate behavior is *guaranteed* to be different. If this is unacceptable, you'll need to remove the ignore blocks and resolve any ongoing mismatches in behavior until the observations match perfectly every time.
352
+ * When removing a read-behavior experiment, it's a good idea to keep any write-side duplication between an old and new system in place until well after the new behavior has been in production, in case you need to roll back.
353
+
354
+ ## Breaking the rules
355
+
356
+ Sometimes scientists just gotta do weird stuff. We understand.
357
+
358
+ ### Ignoring results entirely
359
+
360
+ Science is useful even when all you care about is the timing data or even whether or not a new code path blew up. If you have the ability to incrementally control how often an experiment runs via your `enabled?` method, you can use it to silently and carefully test new code paths and ignore the results altogether. You can do this by setting `ignore { true }`, or for greater efficiency, `compare { true }`.
361
+
362
+ This will still log mismatches if any exceptions are raised, but will disregard the values entirely.
363
+
364
+ ### Trying more than one thing
365
+
366
+ It's not usually a good idea to try more than one alternative simultaneously. Behavior isn't guaranteed to be isolated and reporting + visualization get quite a bit harder. Still, it's sometimes useful.
367
+
368
+ To try more than one alternative at once, add names to some `try` blocks:
369
+
370
+ ```ruby
371
+ require "scientist"
372
+
373
+ class MyWidget
374
+ include Scientist
375
+
376
+ def allows?(user)
377
+ science "widget-permissions" do |e|
378
+ e.use { model.check_user(user).valid? } # old way
379
+
380
+ e.try("api") { user.can?(:read, model) } # new service API
381
+ e.try("raw-sql") { user.can_sql?(:read, model) } # raw query
382
+ end
383
+ end
384
+ end
385
+ ```
386
+
387
+ When the experiment runs, all candidate behaviors are tested and each candidate observation is compared with the control in turn.
388
+
389
+ ### No control, just candidates
390
+
391
+ Define the candidates with named `try` blocks, omit a `use`, and pass a candidate name to `run`:
392
+
393
+ ```ruby
394
+ experiment = MyExperiment.new("various-ways") do |e|
395
+ e.try("first-way") { ... }
396
+ e.try("second-way") { ... }
397
+ end
398
+
399
+ experiment.run("second-way")
400
+ ```
401
+
402
+ The `science` helper also knows this trick:
403
+
404
+ ```ruby
405
+ science "various-ways", run: "first-way" do |e|
406
+ e.try("first-way") { ... }
407
+ e.try("second-way") { ... }
408
+ end
409
+ ```
4
410
 
5
411
  ## Hacking
6
412
 
7
- Be on a Unixy box. Make sure a modern Bundler is available. script/test runs the unit tests. All development dependencies will be installed automatically if they're not available. Dat science happens primarily on Ruby 2.1.0, but science should be universal.
413
+ Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs the unit tests. All development dependencies are installed automatically. Science requires Ruby 2.1.
8
414
 
9
415
  ## Maintainers
10
416
 
11
- [@jbarnette](https://github.com/jbarnette) and [@rick](https://github.com/rick)
417
+ [@jbarnette](https://github.com/jbarnette),
418
+ [@jesseplusplus](https://github.com/jesseplusplus),
419
+ [@rick](https://github.com/rick),
420
+ and [@zerowidth](https://github.com/zerowidth)
@@ -0,0 +1,21 @@
1
+ require "scientist/experiment"
2
+
3
+ # A null experiment.
4
+ class Scientist::Default
5
+ include Scientist::Experiment
6
+
7
+ attr_reader :name
8
+
9
+ def initialize(name)
10
+ @name = name
11
+ end
12
+
13
+ # Run everything every time.
14
+ def enabled?
15
+ true
16
+ end
17
+
18
+ # Don't publish anything.
19
+ def publish(result)
20
+ end
21
+ end
@@ -0,0 +1,38 @@
1
+ module Scientist
2
+
3
+ # Smoking in the bathroom and/or sassing.
4
+ class BadBehavior < StandardError
5
+ attr_reader :experiment
6
+ attr_reader :name
7
+
8
+ def initialize(experiment, name, message)
9
+ @experiment = experiment
10
+ @name = name
11
+
12
+ super message
13
+ end
14
+ end
15
+
16
+ class BehaviorMissing < BadBehavior
17
+ def initialize(experiment, name)
18
+ super experiment, name,
19
+ "#{experiment.name} missing #{name} behavior"
20
+ end
21
+ end
22
+
23
+ class BehaviorNotUnique < BadBehavior
24
+ def initialize(experiment, name)
25
+ super experiment, name,
26
+ "#{experiment.name} alread has #{name} behavior"
27
+ end
28
+ end
29
+
30
+ class NoValue < StandardError
31
+ attr_reader :observation
32
+
33
+ def initialize(observation)
34
+ @observation = observation
35
+ super "#{observation.name} didn't return a value"
36
+ end
37
+ end
38
+ end