dat-analysis 1.2.0

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.
Files changed (33) hide show
  1. data/.gitignore +4 -0
  2. data/Gemfile +2 -0
  3. data/LICENSE.txt +22 -0
  4. data/README.md +423 -0
  5. data/dat-analysis.gemspec +17 -0
  6. data/lib/dat/analysis.rb +446 -0
  7. data/lib/dat/analysis/library.rb +30 -0
  8. data/lib/dat/analysis/matcher.rb +43 -0
  9. data/lib/dat/analysis/registry.rb +50 -0
  10. data/lib/dat/analysis/result.rb +78 -0
  11. data/lib/dat/analysis/tally.rb +59 -0
  12. data/script/bootstrap +9 -0
  13. data/script/release +38 -0
  14. data/script/test +9 -0
  15. data/test/dat_analysis_subclassing_test.rb +119 -0
  16. data/test/dat_analysis_test.rb +822 -0
  17. data/test/fixtures/analysis/test-suite-experiment/matcher.rb +7 -0
  18. data/test/fixtures/experiment-with-classes/matcher_a.rb +5 -0
  19. data/test/fixtures/experiment-with-classes/matcher_b.rb +11 -0
  20. data/test/fixtures/experiment-with-classes/wrapper_a.rb +5 -0
  21. data/test/fixtures/experiment-with-classes/wrapper_b.rb +11 -0
  22. data/test/fixtures/experiment-with-good-and-extraneous-classes/matcher_w.rb +5 -0
  23. data/test/fixtures/experiment-with-good-and-extraneous-classes/matcher_y.rb +11 -0
  24. data/test/fixtures/experiment-with-good-and-extraneous-classes/matcher_z.rb +11 -0
  25. data/test/fixtures/experiment-with-good-and-extraneous-classes/wrapper_w.rb +5 -0
  26. data/test/fixtures/experiment-with-good-and-extraneous-classes/wrapper_y.rb +11 -0
  27. data/test/fixtures/experiment-with-good-and-extraneous-classes/wrapper_z.rb +11 -0
  28. data/test/fixtures/initialize-classes/matcher_m.rb +5 -0
  29. data/test/fixtures/initialize-classes/matcher_n.rb +11 -0
  30. data/test/fixtures/initialize-classes/wrapper_m.rb +5 -0
  31. data/test/fixtures/initialize-classes/wrapper_n.rb +11 -0
  32. data/test/fixtures/invalid-matcher/matcher.rb +1 -0
  33. metadata +128 -0
@@ -0,0 +1,4 @@
1
+ /*.gem
2
+ /.bundle
3
+ /.ruby-version
4
+ /Gemfile.lock
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 GitHub, Inc.
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,423 @@
1
+ # Dat-analysis
2
+
3
+ A Ruby library for analyzing the results of [dat-science][dsc] experiments. For
4
+ the motivation behind this library, and documentation on setting up experiments,
5
+ go check out [dat-science][dsc]'s documentation.
6
+
7
+ [dsc]: https://github.com/github/dat-science/
8
+
9
+ ## What do I do with all these experiment results?
10
+
11
+ Once you've started a `dat-science` experiment and published some results,
12
+ you'll want to analyze the mismatches from your experiment. In `dat-analysis`
13
+ you'll find an analysis toolkit to help understand experiment results.
14
+
15
+ We designed the analysis tools to be run from your ruby console (`irb` or
16
+ `script/console` if you're doing science on a Rails app). You create an analyzer
17
+ and then interactively fetch experiment results and study them to determine the
18
+ reason the control method's results differ from the candidate method's results.
19
+
20
+ ### Your very own analyzer
21
+
22
+ The `Dat::Analysis` base class provides a number of tools for analysis. Since
23
+ the process of retrieving your experiment results depends on how you used
24
+ `publish` in your experiment, you'll need to create a subclass of `Dat::Analysis`
25
+ which implements methods to handle reading and processing results.
26
+
27
+ You will need to define `read` and `count` to return the next published experiment
28
+ result, and the count of remaining published experiment results, respectively.
29
+ You can optionally define `cook` to do any decoding, un-marshalling, or whatever
30
+ other pre-processing you desire on the raw experiment result returned by `read`.
31
+
32
+
33
+ ``` ruby
34
+ require 'dat/analysis'
35
+
36
+ module MyApp
37
+ # Public: Perform dat analysis on a dat-science experiment.
38
+ #
39
+ # This is a subclass of Dat::Analysis which provides the concrete implementation
40
+ # of the `#read`, `#count`, and `#cook` methods to interact with our Redis data
41
+ # store, and decodes our science mismatch results from JSON.
42
+ class Analysis < Dat::Analysis
43
+ # Public: Read the next available science mismatch result.
44
+ #
45
+ # Returns the next raw science mismatch result from Redis.
46
+ def read
47
+ Redis.rpop "dat-science.#{experiment_name}.results"
48
+ end
49
+
50
+ # Public: Get the number of pending science mismatch results.
51
+ #
52
+ # Returns the number of pending science mismatch results from redis.
53
+ def count
54
+ Redis.llen "dat-science.#{experiment_name}.results"
55
+ end
56
+
57
+ # Public: "Cook" a raw science mismatch result.
58
+ #
59
+ # raw_result - a raw science mismatch result
60
+ #
61
+ # Returns nil if raw_result is nil.
62
+ # Returns the JSON-parsed raw_result.
63
+ def cook(raw_result)
64
+ return nil unless raw_result
65
+ JSON.parse(raw_result)
66
+ end
67
+ end
68
+ end
69
+
70
+ ```
71
+
72
+ #### Instantiating the analyzer
73
+
74
+ This analyzer can be used with many experiments, so you'll need to instantiate an
75
+ analyzer instance for your current experiment:
76
+
77
+ ``` ruby
78
+ irb> a = MyApp::Analysis.new('widget-permissions')
79
+ => #<MyApp::Analysis:0x007fae4a0101f8 ...>
80
+ ```
81
+
82
+ ### Working with individual results
83
+
84
+ First, let's look at how you can work with single experiment mismatch results.
85
+ The `#result` method (also available as `#current`) will show you the most
86
+ recently fetched experiment result. Before you've fetched any results, this
87
+ will be empty:
88
+
89
+ ``` ruby
90
+ irb> a.result
91
+ => nil
92
+ irb> a.current
93
+ => nil
94
+ ```
95
+
96
+ We can use the `#more?` predicate method to see if there are experiment results
97
+ pending, and `#count` to see just how many results are available:
98
+
99
+ ``` ruby
100
+ irb> a.more?
101
+ => true
102
+ irb> a.count
103
+ => 103
104
+ ```
105
+
106
+ Let's fetch a result:
107
+
108
+ ``` ruby
109
+ irb> a.fetch
110
+ => {"experiment"=>"widget-permissions", "user"=>{ ... } .... }
111
+ irb> a.result
112
+ => {"experiment"=>"widget-permissions", "user"=>{ ... } .... }
113
+ irb> a.result.keys
114
+ => ["experiment", "user", "timestamp", "candidate", "control", "first"]
115
+ irb> a.result.experiment_name
116
+ => "widget-permissions"
117
+ irb> a.result['first']
118
+ => "candidate"
119
+ irb> a.result.first
120
+ => "candidate"
121
+ irb> a.result['control']
122
+ => {"duration"=>12.307, "exception"=>nil, "value"=>false}
123
+ irb> a.result.control
124
+ => {"duration"=>12.307, "exception"=>nil, "value"=>false}
125
+ irb> a.result['candidate']
126
+ => {"duration"=>12.366999999999999, "exception"=>nil, "value"=>true}
127
+ irb> a.result.candidate
128
+ => {"duration"=>12.366999999999999, "exception"=>nil, "value"=>true}
129
+ irb> a.result['first']
130
+ => "control"
131
+ irb> a.result['timestamp']
132
+ => "2013-04-22T13:31:32-05:00"
133
+ irb> a.result.timestamp
134
+ => 2013-04-22 13:31:32 -0500
135
+ irb> a.result.timestamp.class
136
+ => Time
137
+ irb> a.result.timestamp.to_i
138
+ => 1366655492
139
+ irb> a.result['user']
140
+ => {"login"=>"somed00d", ... }
141
+ ```
142
+
143
+ Results will contain entries for the duration (in milliseconds), exceptions,
144
+ and values returned by both the candidate and control methods for the experiment;
145
+ the time when the result was recorded; whether the candidate or the control method
146
+ was run first; and an entry for every object saved via a `context` call during
147
+ the experiment.
148
+
149
+ Note that the `#result` method will continue to return the previously fetched
150
+ result, until we overwrite it with another `#fetch`, `#skip`, or `#analyze`
151
+ (see below).
152
+
153
+ #### Skipping results
154
+
155
+ Sometimes we make changes to the code we're running experiments against, and
156
+ sometimes those changes cause experiment results to be out of date -- if we've
157
+ fixed a bug we found via science, it's not much point in looking at results
158
+ generated while our code still had that bug. To jump past a batch of results,
159
+ use `#skip`, giving it a block to test for the condition we want to skip
160
+ past:
161
+
162
+ ``` ruby
163
+ irb> a.skip {|r| 5.minutes.ago < a.result.timestamp }
164
+ => 43
165
+ irb> a.skip {|r| true }
166
+ => nil
167
+ ```
168
+
169
+ ### Batch analysis of results
170
+
171
+ After sifting through a handful of results from an experiment, it usually
172
+ becomes obvious that a single behavior in our studied code is often responsible
173
+ for many results published in an experiment. If a behavior difference can be
174
+ easily fixed by improving the candidate code, and your production release cycle
175
+ is short, then you just update the candidate method and continuing running your
176
+ experiment.
177
+
178
+ It's often the case that the relevant code can't be changed that quickly.
179
+ Perhaps the assumptions made when writing the candidate code were wrong in a way
180
+ that requires deeper consideration and discussion with your team. It could be
181
+ that the experiment results actually turn up bugs in the implementation of the
182
+ control method -- in which case there will likely be even more discussion
183
+ needed, and possibly a fairly long cycle to get production behaving properly.
184
+
185
+ That doesn't mean that analysis can't continue, but it could well be that a
186
+ majority of the experimental results to analyze are already examples of already
187
+ known behaviors. In this case, it's useful to be able to identify these results
188
+ and skip over them, to find results which can't be accounted for by any
189
+ currently known explanation.
190
+
191
+ The `#analyze` method, in conjunction with "matcher classes", makes this possible.
192
+
193
+ ### `#analyze`
194
+
195
+ You can run `#analyze` to automate the fetching of pending results. If a result
196
+ is identifiable by a matcher class, then a summary of the identified result will
197
+ be printed and that result will skipped. This process continues until either an
198
+ unidentifiable result is found, or there are no more results available. When an
199
+ unidentifiable result is found, a summary of the identified results is output,
200
+ and then the first unidentified result is displayed in detail.
201
+
202
+ ```
203
+ irb> a.analyze
204
+ User [somed00d] is staff (see http://github.com/our/project/issues/123)
205
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
206
+ User [somed00d] is staff (see http://github.com/our/project/issues/123)
207
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
208
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
209
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
210
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
211
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
212
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
213
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
214
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
215
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
216
+ User [somed00d] is staff (see http://github.com/our/project/issues/123)
217
+ User [somed00d] is staff (see http://github.com/our/project/issues/123)
218
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
219
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
220
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
221
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
222
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
223
+ User [somed00d] is staff (see http://github.com/our/project/issues/123)
224
+ User [somed00d] is staff (see http://github.com/our/project/issues/123)
225
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
226
+ User [0th3rd00d] is staff (see http://github.com/our/project/issues/123)
227
+ Permission [totesadmin] is obsolete (see http://github.com/dat/thing/issues/5234)
228
+
229
+ Summary of identified results:
230
+
231
+ StaffFunninessMatcher: 14
232
+ ZOMGIssue5423Matcher: 10
233
+ TOTAL: 24
234
+
235
+ First unidentifiable result:
236
+
237
+ Experiment [widget-permissions] first: candidate @ 2013-04-19T18:55:23-05:00
238
+ Duration: control ( 0.01) | candidate ( 1.36)
239
+
240
+ Control value: [false]
241
+ Candidate value: [true]
242
+
243
+ user => {
244
+ id => 1234876
245
+ login => "somed00d"
246
+ [...]
247
+ }
248
+ => 32
249
+ ```
250
+
251
+ Note that the number of pending results is returned as the result of the
252
+ analysis.
253
+
254
+
255
+ ### Matcher classes
256
+
257
+ The purpose of a matcher class is to identify a behavior which results in
258
+ mismatches in your experiment. For example, if permissions for staff users are
259
+ not implemented properly by your candidate code, you might create a matcher that
260
+ recognizes when the user involved is a staff user.
261
+
262
+ You create a matcher class by subclassing `Dat::Analysis::Matcher` and writing a
263
+ `#match?` method that returns true if the experiment result (available as
264
+ `result`) is an example of the behavior we know about:
265
+
266
+ ``` ruby
267
+ class StaffFunninessMatcher < Dat::Analysis::Matcher
268
+ # our staff role permissions are just soooo busted
269
+ def match?
270
+ User.find_by_login(result['user']['login']).staff?
271
+ end
272
+
273
+ def readable
274
+ "User [#{result['user']['login']}] is staff (see http://github.com/our/project/issues/123)"
275
+ end
276
+ end
277
+ ```
278
+
279
+ If you create a matcher class in the console, use `#add_matcher` to let your
280
+ analyzer know about it:
281
+
282
+ ``` ruby
283
+ irb> a.add_matcher StaffFunninessMatcher
284
+ Loading matcher class [StaffFunninessMatcher]
285
+ => [StaffFunninessMatcher]
286
+ ```
287
+
288
+ Now, when you run `#analyze`, all the results with staff users recorded in the
289
+ `user` context will be tallied and skipped.
290
+
291
+ See "Maintaining a library of matchers and wrappers" below for a more durable
292
+ way to let your analyzers keep track of your helper classes.
293
+
294
+ #### Getting a summary of an identified result
295
+
296
+ The `#summary` method on the analyzer will return a readable version of the
297
+ current result. This is by default a fairly voluminous output (it's what you saw
298
+ at the end of an `#analyze` run above), but if your matcher defines a
299
+ `#readable` method.
300
+
301
+ ``` ruby
302
+ irb> a.summary
303
+ => "User [somed00d] is staff (see http://github.com/our/project/issues/123)"
304
+ ```
305
+
306
+ The `#analyze` method uses these `#readable` methods to produce a more succinct
307
+ summary of identified results, like we showed above.
308
+
309
+ **Define a `#readable` method for cleaner `#analyze` output!**
310
+
311
+ ### Adding methods to results (wrappers)
312
+
313
+ For many experiments there is information in the results which is used often
314
+ enough that you'll get tired of doing repetitive lookups in the results hash.
315
+ When this happens, you can create result wrapper classes for your experiment
316
+ which can add methods to every result returned. Simply subclass
317
+ `Dat::Analysis::Result` and define the instance methods you want:
318
+
319
+ ``` ruby
320
+ class PermissionsWrapper < Dat::Analysis::Result
321
+ def user
322
+ User.find_by_login!(result['user']['login'])
323
+ rescue
324
+ "Could not find user, id=[#{result['actor']['id']}]"
325
+ end
326
+
327
+ def permission
328
+ Permission.find_by_handle!(result['permission']['handle'])
329
+ rescue
330
+ "Could not find permission, handle=[#{result['permission']['handle']}]"
331
+ end
332
+ alias_method :perm, :permission
333
+ end
334
+ ```
335
+
336
+ Then, add the wrapper to your analyzer:
337
+
338
+ ``` ruby
339
+ irb> a.add_wrapper(PermissionsWrapper)
340
+ => [PermissionsWrapper]
341
+ irb> a.result.user
342
+ => #<User id: 1234876, login: "somed00d", ...>
343
+ ```
344
+
345
+ These wrappers can also be used in your matchers classes:
346
+
347
+ ``` ruby
348
+ class StaffFunninessMatcher < Dat::Analysis::Matcher
349
+ # our staff role permissions are just soooo busted
350
+ def match?
351
+ result.user.staff?
352
+ end
353
+
354
+ def readable
355
+ "User [#{result.user.login}] is staff (see http://github.com/our/project/issues/123)"
356
+ end
357
+ end
358
+ ```
359
+
360
+ #### Skipping class naming
361
+
362
+ Inventing new non-conflicting class names for matcher and wrapper classes is a
363
+ bit of a pain. Often we just declare an anonymous class and skip the naming
364
+ altogether. If you do this, you'll probably want to define a readable `.name`
365
+ method for your class, so that `#analyze` summaries are readable:
366
+
367
+ ``` ruby
368
+ Class.new(Dat::Analysis::Matcher) do
369
+ def self.name
370
+ "Staff Permission Silliness"
371
+ end
372
+
373
+ def match?
374
+ result.user.staff?
375
+ end
376
+
377
+ def readable
378
+ "User [#{result.user.login}] is staff (see http://github.com/our/project/issues/123)"
379
+ end
380
+ end
381
+ ```
382
+
383
+ ### Maintaining a library of matchers and result wrappers
384
+
385
+ Being able to add matchers and result wrappers to an analyzer during a console
386
+ session is a fast way to iteratively identify problems and work through a batch of
387
+ results. Keeping those matchers around for the next session is usually in order.
388
+ Your `Dat::Analysis` subclass can define a `#path` instance method, which points
389
+ to the place on the filesystem where your matcher and wrapper classes live. The
390
+ analyzer will look here, in a sub-directory named for your experiment, and load
391
+ any ruby files it finds there:
392
+
393
+ ``` ruby
394
+ require 'dat/analysis'
395
+
396
+ module MyApp
397
+ # Public: Perform dat analysis on a dat-science experiment.
398
+ #
399
+ # This is a subclass of Dat::Analysis which provides the concrete implementation
400
+ # of the `#read`, `#count`, and `#cook` methods to interact with our Redis data
401
+ # store, and decodes our science mismatch results from JSON.
402
+ class Analysis < Dat::Analysis
403
+ def path
404
+ '/path/to/dat-science/experiments/'
405
+ end
406
+ end
407
+ end
408
+ ```
409
+
410
+ In this example, the analyzer for the `widget-permissions` experiment will look
411
+ in `/path/to/dat-science/experiments/widget-permissions/` for matcher and
412
+ wrapper classes.
413
+
414
+ ## Hacking on dat-analysis
415
+
416
+ Be on a Unixy box. Make sure a modern Bundler is available. `script/test` runs
417
+ the unit tests. All development dependencies will be installed automatically if
418
+ they're not available. Dat science happens primarily on Ruby 1.9.3 and 1.8.7,
419
+ but science should be universal.
420
+
421
+ ## Maintainers
422
+
423
+ [@jbarnette](https://github.com/jbarnette) and [@rick](https://github.com/rick)