dat-science 0.0.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.
- data/.gitignore +4 -0
- data/Gemfile +2 -0
- data/LICENSE.txt +22 -0
- data/README.md +106 -0
- data/dat-science.gemspec +17 -0
- data/lib/dat/science.rb +33 -0
- data/lib/dat/science/experiment.rb +135 -0
- data/lib/dat/science/result.rb +44 -0
- data/script/bootstrap +9 -0
- data/script/test +9 -0
- data/test/dat_science_experiment_test.rb +8 -0
- data/test/dat_science_test.rb +32 -0
- metadata +92 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
# Dat Science!
|
2
|
+
|
3
|
+
A Ruby library for carefully refactoring critical paths. Science isn't
|
4
|
+
a feature flipper or an A/B testing tool, it's a pattern that helps
|
5
|
+
measure and validate large code changes without altering behavior.
|
6
|
+
|
7
|
+
## How do I do science?
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
require "dat/science"
|
11
|
+
|
12
|
+
include Dat::Science
|
13
|
+
|
14
|
+
science "user-permissions" do |experiment|
|
15
|
+
experiment.control { model.check_user(user).valid? }
|
16
|
+
experiment.candidate { user.can? :read, model }
|
17
|
+
end
|
18
|
+
```
|
19
|
+
|
20
|
+
Wrap a `control` block around the code's original behavior, and wrap
|
21
|
+
`candidate` around the new behavior. The `science` block will return
|
22
|
+
whatever the `control` block returns, but it does a bunch of stuff
|
23
|
+
behind the scenes:
|
24
|
+
|
25
|
+
* Decides whether or not to run `candidate`,
|
26
|
+
* Runs `candidate` before `control` 50% of the time,
|
27
|
+
* Measures the duration of both behaviors,
|
28
|
+
* Compares the results of both behaviors,
|
29
|
+
* Swallows any exceptions raised by the candidate behavior, and
|
30
|
+
* Publishes all this information for tracking and reporting.
|
31
|
+
|
32
|
+
## Making Science Useful
|
33
|
+
|
34
|
+
(Talk about subclassing `Dat::Science::Experiment` and setting
|
35
|
+
`Dat::Science.experiment`)
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require "dat/science"
|
39
|
+
|
40
|
+
module FooCorp
|
41
|
+
class Experiment < Dat::Science::Experiment
|
42
|
+
def enabled?
|
43
|
+
# See "Ramping up Experiments" below.
|
44
|
+
end
|
45
|
+
|
46
|
+
def publish(name, payload)
|
47
|
+
# See "Publishing Results" below.
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
```
|
52
|
+
|
53
|
+
```ruby
|
54
|
+
Dat::Science.experiment = FooCorp::Experiment
|
55
|
+
```
|
56
|
+
|
57
|
+
### Ramping up Experiments
|
58
|
+
|
59
|
+
```ruby
|
60
|
+
def enabled?
|
61
|
+
rand(100) < 10
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
def enabled?
|
67
|
+
Flipper[name].enabled?
|
68
|
+
end
|
69
|
+
```
|
70
|
+
|
71
|
+
### Publishing Results
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
def publish(name, payload)
|
75
|
+
FooCorp.instrument "science.#{name}", payload
|
76
|
+
end
|
77
|
+
```
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
{
|
81
|
+
:candidate => {
|
82
|
+
:duration => 2.5,
|
83
|
+
:exception => nil,
|
84
|
+
:value => 42
|
85
|
+
},
|
86
|
+
|
87
|
+
:control => {
|
88
|
+
:duration => 25.0,
|
89
|
+
:exception => nil,
|
90
|
+
:value => 24
|
91
|
+
},
|
92
|
+
|
93
|
+
:first => :control
|
94
|
+
}
|
95
|
+
```
|
96
|
+
|
97
|
+
#### Adding Context
|
98
|
+
|
99
|
+
(using `e.context`)
|
100
|
+
|
101
|
+
## Hacking on Science
|
102
|
+
|
103
|
+
Make sure a modern Bundler is available.`script/test` runs the unit
|
104
|
+
tests. All development dependencies will be installed automatically if
|
105
|
+
they're not available. Dat science happens primarily on Ruby 1.9.3 and
|
106
|
+
1.8.7, but science should be universal.
|
data/dat-science.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
Gem::Specification.new do |gem|
|
2
|
+
gem.name = "dat-science"
|
3
|
+
gem.version = "0.0.0"
|
4
|
+
gem.authors = ["John Barnette", "Rick Bradley"]
|
5
|
+
gem.email = ["jbarnette@github.com"]
|
6
|
+
gem.description = "Gradually test, measure, and track refactored code."
|
7
|
+
gem.summary = "SO BRAVE WITH SCIENCE."
|
8
|
+
gem.homepage = "https://github.com/github/dat-science"
|
9
|
+
|
10
|
+
gem.files = `git ls-files`.split $/
|
11
|
+
gem.executables = []
|
12
|
+
gem.test_files = gem.files.grep /^test/
|
13
|
+
gem.require_paths = ["lib"]
|
14
|
+
|
15
|
+
gem.add_development_dependency "minitest"
|
16
|
+
gem.add_development_dependency "mocha"
|
17
|
+
end
|
data/lib/dat/science.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require "dat/science/experiment"
|
2
|
+
|
3
|
+
module Dat
|
4
|
+
|
5
|
+
# Public: Include this module if you like science.
|
6
|
+
module Science
|
7
|
+
|
8
|
+
# Public: Do some science.
|
9
|
+
def science(name, &block)
|
10
|
+
Science.experiment.new(name, &block).run
|
11
|
+
end
|
12
|
+
|
13
|
+
# Public: The Class to use for all `science` experiments. Default is
|
14
|
+
# `Dat::Science::Experiment`.
|
15
|
+
def self.experiment
|
16
|
+
@experiment ||= Dat::Science::Experiment
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: Set the Class to use for all `science` experiments.
|
20
|
+
# Returns `klass`.
|
21
|
+
def self.experiment=(klass)
|
22
|
+
@experiment = klass
|
23
|
+
end
|
24
|
+
|
25
|
+
# Internal: Reset any static configuration (primarily
|
26
|
+
# `Dat::Science.experiment`. Returns `self`.
|
27
|
+
def self.reset
|
28
|
+
@experiment = nil
|
29
|
+
|
30
|
+
self
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require "dat/science/result"
|
2
|
+
|
3
|
+
module Dat
|
4
|
+
module Science
|
5
|
+
|
6
|
+
# Public: Try things in code.
|
7
|
+
class Experiment
|
8
|
+
|
9
|
+
# Public: The name of this experiment.
|
10
|
+
attr_reader :name
|
11
|
+
|
12
|
+
# Public: Create a new experiment instance. `self` is yielded to
|
13
|
+
# an optional `block` if it's provided.
|
14
|
+
def initialize(name, &block)
|
15
|
+
@candidate = nil
|
16
|
+
@context = { :experiment => name }
|
17
|
+
@control = nil
|
18
|
+
@name = name
|
19
|
+
|
20
|
+
yield self if block_given?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Public: Add a Hash of `payload` data to be included when events
|
24
|
+
# are published.
|
25
|
+
def context(payload)
|
26
|
+
@context.merge! payload
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Declare the control behavior `block` for this
|
30
|
+
# experiment. Returns `block`.
|
31
|
+
def control(&block)
|
32
|
+
@control = block
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Declare the candidate behavior `block` for this
|
36
|
+
# experiment. Returns `block`.
|
37
|
+
def candidate(&block)
|
38
|
+
@candidate = block
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Run the control and candidate behaviors, timing each and
|
42
|
+
# comparing the results. The run order is randomized. Returns the
|
43
|
+
# control behavior's result.
|
44
|
+
#
|
45
|
+
# If the experiment is disabled or candidate behavior isn't
|
46
|
+
# provided the control behavior's result will be returned
|
47
|
+
# immediately.
|
48
|
+
def run
|
49
|
+
return run_control unless candidate? && enabled?
|
50
|
+
|
51
|
+
control_goes_first = rand(2) == 0
|
52
|
+
|
53
|
+
if control_goes_first
|
54
|
+
control = observe_control
|
55
|
+
candidate = observe_candidate
|
56
|
+
else
|
57
|
+
candidate = observe_candidate
|
58
|
+
control = observe_control
|
59
|
+
end
|
60
|
+
|
61
|
+
payload = {
|
62
|
+
:candidate => candidate.payload,
|
63
|
+
:control => control.payload,
|
64
|
+
:first => control_goes_first ? :control : :candidate
|
65
|
+
}
|
66
|
+
|
67
|
+
kind = control == candidate ? "match" : "mismatch"
|
68
|
+
publish_with_context kind, payload
|
69
|
+
|
70
|
+
raise control.exception if control.raised?
|
71
|
+
|
72
|
+
control.value
|
73
|
+
end
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
# Internal: Does this experiment have candidate behavior?
|
78
|
+
def candidate?
|
79
|
+
!!@candidate
|
80
|
+
end
|
81
|
+
|
82
|
+
# Internal: Is this experiment enabled? More specifically, should
|
83
|
+
# the candidate behavior be run and compared to the control
|
84
|
+
# behavior? The default implementation returns `true`.
|
85
|
+
def enabled?
|
86
|
+
true
|
87
|
+
end
|
88
|
+
|
89
|
+
# Internal: Run `block`, measuring the duration and rescuing any
|
90
|
+
# raised exceptions. Returns a Dat::Science::Result.
|
91
|
+
def observe(&block)
|
92
|
+
start = Time.now
|
93
|
+
|
94
|
+
begin
|
95
|
+
value = block.call
|
96
|
+
rescue => ex
|
97
|
+
raised = ex
|
98
|
+
end
|
99
|
+
|
100
|
+
duration = (Time.now - start) * 1000
|
101
|
+
Science::Result.new value, duration, raised
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
# Internal. Returns a Dat::Science::Result for `candidate`.
|
106
|
+
def observe_candidate
|
107
|
+
observe { run_candidate }
|
108
|
+
end
|
109
|
+
|
110
|
+
# Internal. Returns a Dat::Science::Result for `control`.
|
111
|
+
def observe_control
|
112
|
+
observe { run_control }
|
113
|
+
end
|
114
|
+
|
115
|
+
# Internal: Broadcast an event `name` and `payload` Hash. The
|
116
|
+
# default implementation is a no-op. Returns nothing.
|
117
|
+
def publish(name, payload)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Internal: Call `publish`, merging the `payload` with `context`.
|
121
|
+
def publish_with_context(name, payload)
|
122
|
+
publish name, @context.merge(payload)
|
123
|
+
end
|
124
|
+
# Internal: Run the candidate behavior and return its result.
|
125
|
+
def run_candidate
|
126
|
+
@candidate.call
|
127
|
+
end
|
128
|
+
|
129
|
+
# Internal: Run the control behavior and return its result.
|
130
|
+
def run_control
|
131
|
+
@control.call
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Dat
|
2
|
+
|
3
|
+
# Internal. The value of a watched behavior.
|
4
|
+
module Science
|
5
|
+
class Result
|
6
|
+
attr_reader :duration
|
7
|
+
attr_reader :exception
|
8
|
+
attr_reader :value
|
9
|
+
|
10
|
+
def initialize(value, duration, exception)
|
11
|
+
@duration = duration
|
12
|
+
@exception = exception
|
13
|
+
@value = value
|
14
|
+
end
|
15
|
+
|
16
|
+
def ==(other)
|
17
|
+
return false unless other.is_a? Science::Result
|
18
|
+
|
19
|
+
values_are_equal = other.value == value
|
20
|
+
both_raised = other.raised? && raised?
|
21
|
+
neither_raised = !other.raised? && !raised?
|
22
|
+
|
23
|
+
exceptions_are_equivalent =
|
24
|
+
both_raised && other.exception.class == self.exception.class &&
|
25
|
+
other.exception.message == self.exception.message
|
26
|
+
|
27
|
+
(values_are_equal && neither_raised) ||
|
28
|
+
(both_raised && exceptions_are_equivalent)
|
29
|
+
end
|
30
|
+
|
31
|
+
def hash
|
32
|
+
exception ^ value
|
33
|
+
end
|
34
|
+
|
35
|
+
def payload
|
36
|
+
{ :duration => duration, :exception => exception, :value => value }
|
37
|
+
end
|
38
|
+
|
39
|
+
def raised?
|
40
|
+
!!exception
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/script/bootstrap
ADDED
data/script/test
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require "minitest/autorun"
|
2
|
+
require "mocha/setup"
|
3
|
+
require "dat/science"
|
4
|
+
|
5
|
+
class DatScienceTest < MiniTest::Unit::TestCase
|
6
|
+
def teardown
|
7
|
+
Dat::Science.reset
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_experiment_default
|
11
|
+
assert_equal Dat::Science::Experiment, Dat::Science.experiment
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_experiment
|
15
|
+
Dat::Science.experiment = :foo
|
16
|
+
assert_equal :foo, Dat::Science.experiment
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_science
|
20
|
+
experiment = mock do
|
21
|
+
expects(:run).returns 42
|
22
|
+
end
|
23
|
+
|
24
|
+
Dat::Science.experiment.expects(:new).with("foo").returns experiment
|
25
|
+
|
26
|
+
obj = Object.new
|
27
|
+
obj.extend Dat::Science
|
28
|
+
|
29
|
+
ret = obj.science "foo"
|
30
|
+
assert_equal 42, ret
|
31
|
+
end
|
32
|
+
end
|
metadata
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: dat-science
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- John Barnette
|
9
|
+
- Rick Bradley
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2013-02-26 00:00:00.000000000 Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: minitest
|
17
|
+
requirement: !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: !ruby/object:Gem::Requirement
|
26
|
+
none: false
|
27
|
+
requirements:
|
28
|
+
- - ! '>='
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
version: '0'
|
31
|
+
- !ruby/object:Gem::Dependency
|
32
|
+
name: mocha
|
33
|
+
requirement: !ruby/object:Gem::Requirement
|
34
|
+
none: false
|
35
|
+
requirements:
|
36
|
+
- - ! '>='
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
type: :development
|
40
|
+
prerelease: false
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ! '>='
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '0'
|
47
|
+
description: Gradually test, measure, and track refactored code.
|
48
|
+
email:
|
49
|
+
- jbarnette@github.com
|
50
|
+
executables: []
|
51
|
+
extensions: []
|
52
|
+
extra_rdoc_files: []
|
53
|
+
files:
|
54
|
+
- .gitignore
|
55
|
+
- Gemfile
|
56
|
+
- LICENSE.txt
|
57
|
+
- README.md
|
58
|
+
- dat-science.gemspec
|
59
|
+
- lib/dat/science.rb
|
60
|
+
- lib/dat/science/experiment.rb
|
61
|
+
- lib/dat/science/result.rb
|
62
|
+
- script/bootstrap
|
63
|
+
- script/test
|
64
|
+
- test/dat_science_experiment_test.rb
|
65
|
+
- test/dat_science_test.rb
|
66
|
+
homepage: https://github.com/github/dat-science
|
67
|
+
licenses: []
|
68
|
+
post_install_message:
|
69
|
+
rdoc_options: []
|
70
|
+
require_paths:
|
71
|
+
- lib
|
72
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
79
|
+
none: false
|
80
|
+
requirements:
|
81
|
+
- - ! '>='
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0'
|
84
|
+
requirements: []
|
85
|
+
rubyforge_project:
|
86
|
+
rubygems_version: 1.8.23
|
87
|
+
signing_key:
|
88
|
+
specification_version: 3
|
89
|
+
summary: SO BRAVE WITH SCIENCE.
|
90
|
+
test_files:
|
91
|
+
- test/dat_science_experiment_test.rb
|
92
|
+
- test/dat_science_test.rb
|