absurdity 0.2.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +6 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/README.rdoc +117 -0
- data/Rakefile +15 -0
- data/absurdity.gemspec +28 -0
- data/app/controllers/absurdities_controller.rb +18 -0
- data/app/helpers/absurdity_helper.rb +31 -0
- data/app/views/absurdities/index.html.erb +12 -0
- data/app/views/absurdities/show.html.erb +22 -0
- data/app/views/layouts/absurdities.html.erb +17 -0
- data/config/routes.rb +7 -0
- data/lib/absurdity.rb +26 -0
- data/lib/absurdity/config.rb +24 -0
- data/lib/absurdity/datastore.rb +172 -0
- data/lib/absurdity/datastore/redis.rb +5 -0
- data/lib/absurdity/engine.rb +9 -0
- data/lib/absurdity/experiment.rb +135 -0
- data/lib/absurdity/metric.rb +46 -0
- data/lib/absurdity/railtie.rb +32 -0
- data/lib/absurdity/variant.rb +20 -0
- data/lib/absurdity/version.rb +3 -0
- data/public/stylesheets/absurdity.css +96 -0
- data/test/absurdity_test.rb +96 -0
- data/test/config_test.rb +13 -0
- data/test/experiment_test.rb +36 -0
- data/test/metric_test.rb +83 -0
- data/test/test_helper.rb +15 -0
- metadata +140 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm ruby-1.9.2@absurdity
|
data/Gemfile
ADDED
data/README.rdoc
ADDED
@@ -0,0 +1,117 @@
|
|
1
|
+
=Absurdity
|
2
|
+
|
3
|
+
A rails gem for absurdly simple a/b testing
|
4
|
+
|
5
|
+
http://github.com/xing/absurdity
|
6
|
+
|
7
|
+
==Prerequisites
|
8
|
+
|
9
|
+
Redis (tested with 1.2.6, 2.2.2)
|
10
|
+
Rails (tested with 3.0.10, 3.1.0)
|
11
|
+
|
12
|
+
==Installation
|
13
|
+
|
14
|
+
gem "absurdity"
|
15
|
+
|
16
|
+
==Usage
|
17
|
+
|
18
|
+
===Instantiate a Redis client
|
19
|
+
|
20
|
+
Put this in an initializer, for expample config/initializers/redis_absurdity.rb:
|
21
|
+
|
22
|
+
Absurdity.redis = Redis.new(:host => "localhost", :port => 6379)
|
23
|
+
|
24
|
+
Note: More often you will use a redis.conf to set up your redis client, the important part is to assign your
|
25
|
+
redis client instance to Absurdity.redis .
|
26
|
+
Don't forget to start your Redis server.
|
27
|
+
|
28
|
+
===Define Experiments
|
29
|
+
|
30
|
+
mkdir "absurdity" in your rails app root directory
|
31
|
+
touch "absurdity/experiments.yml"
|
32
|
+
|
33
|
+
define your experiments in this file:
|
34
|
+
:experiments:
|
35
|
+
:wild_side:
|
36
|
+
:metrics:
|
37
|
+
- :fun
|
38
|
+
- :hangover
|
39
|
+
:variants:
|
40
|
+
- :with_music
|
41
|
+
- :without_music
|
42
|
+
:walk:
|
43
|
+
:metrics:
|
44
|
+
- :sun
|
45
|
+
- :rain
|
46
|
+
:variants:
|
47
|
+
- :slow
|
48
|
+
- :fast
|
49
|
+
:completed:
|
50
|
+
:fast
|
51
|
+
|
52
|
+
:wild_side will be the name of your experiment
|
53
|
+
[:fun, :hangover] will be the metrics you wish to track
|
54
|
+
[:with_music, :without_music] will be the variants you wish to vary
|
55
|
+
|
56
|
+
the :walk has been marked as completed.
|
57
|
+
You should identify which variant to show here (:fast).
|
58
|
+
Now everyone will only see the :fast variant.
|
59
|
+
And, no one will produce any metric tracking.
|
60
|
+
|
61
|
+
Note: You can't use the shortform for declaring an array of symbols in ruby 1.9.2 and Psych.
|
62
|
+
So instead of using :metrics: [:fun, :hangover] use the longer form shown above.
|
63
|
+
|
64
|
+
===Usage in a controller
|
65
|
+
|
66
|
+
def create
|
67
|
+
Absurdity.track! :fun, :wild_side, current_user.id
|
68
|
+
end
|
69
|
+
|
70
|
+
===Usage in view/helpers
|
71
|
+
|
72
|
+
def my_helper
|
73
|
+
variant = Absurdity.variant :wild_side, current_user.id
|
74
|
+
if variant == :with_music
|
75
|
+
Play.music
|
76
|
+
else
|
77
|
+
Play.nothing
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
===View Metrics
|
82
|
+
|
83
|
+
/absurdities => linked list of experiments
|
84
|
+
/absurdities/experiment_name => show experiment
|
85
|
+
|
86
|
+
==Authors
|
87
|
+
|
88
|
+
{Tim Payton}[http://github.com/dizzy42]
|
89
|
+
{Ömür Özkir}[http://github.com/oem]
|
90
|
+
|
91
|
+
Please find out more about our work in our
|
92
|
+
{tech blog}[http://blog.xing.com/category/english/tech-blog].
|
93
|
+
|
94
|
+
|
95
|
+
==License
|
96
|
+
|
97
|
+
The MIT License
|
98
|
+
|
99
|
+
Copyright (c) 2011 {XING AG}[http://www.xing.com/]
|
100
|
+
|
101
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
102
|
+
of this software and associated documentation files (the "Software"), to deal
|
103
|
+
in the Software without restriction, including without limitation the rights
|
104
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
105
|
+
copies of the Software, and to permit persons to whom the Software is
|
106
|
+
furnished to do so, subject to the following conditions:
|
107
|
+
|
108
|
+
The above copyright notice and this permission notice shall be included in
|
109
|
+
all copies or substantial portions of the Software.
|
110
|
+
|
111
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
112
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
113
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
114
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
115
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
116
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
117
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.libs << 'lib'
|
8
|
+
t.libs << 'test'
|
9
|
+
t.pattern = 'test/**/*_test.rb'
|
10
|
+
t.verbose = false
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Run tests"
|
14
|
+
task :default => :test
|
15
|
+
|
data/absurdity.gemspec
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "absurdity/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "absurdity"
|
7
|
+
s.version = Absurdity::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Tim Payton", "Ömür Özkir"]
|
10
|
+
s.email = "opensource@xing.com"
|
11
|
+
s.homepage = "http://xing.github.com/absurdity/"
|
12
|
+
s.summary = %q{Absurdly simple a/b testing}
|
13
|
+
s.description = %q{See summary}
|
14
|
+
|
15
|
+
s.rubyforge_project = "absurdity"
|
16
|
+
|
17
|
+
s.add_dependency("redis", ">= 0")
|
18
|
+
s.add_dependency("rake", ">= 0.8.7")
|
19
|
+
|
20
|
+
s.add_development_dependency("mock_redis", ">= 0")
|
21
|
+
s.add_development_dependency("mocha", ">= 0")
|
22
|
+
s.add_development_dependency("minitest", ">= 0")
|
23
|
+
|
24
|
+
s.files = `git ls-files`.split("\n")
|
25
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
26
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
27
|
+
s.require_paths = ["lib"]
|
28
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
class AbsurditiesController < ApplicationController
|
2
|
+
helper :absurdity
|
3
|
+
layout "absurdities"
|
4
|
+
|
5
|
+
def index
|
6
|
+
@reports = Absurdity::Experiment.reports
|
7
|
+
end
|
8
|
+
|
9
|
+
def show
|
10
|
+
@report = Absurdity::Experiment.find(params[:id].to_sym).report
|
11
|
+
end
|
12
|
+
|
13
|
+
def create
|
14
|
+
Absurdity.track!(params[:metric], params[:experiment], params[:identity_id])
|
15
|
+
render nothing: true
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module AbsurdityHelper
|
2
|
+
|
3
|
+
def metric_info(metrics, metric, count)
|
4
|
+
content_tag(:span, "#{metric.to_s.humanize}: ", class: "metric")
|
5
|
+
.concat "#{count} "
|
6
|
+
# str += metric_ratios(metrics, metric, count)
|
7
|
+
end
|
8
|
+
|
9
|
+
def completed_text(completed)
|
10
|
+
if completed == :completed
|
11
|
+
content_tag(:span, "Completed")
|
12
|
+
elsif completed
|
13
|
+
content_tag(:span, "Completed: #{completed}")
|
14
|
+
else
|
15
|
+
""
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def metric_ratios(metrics, metric, count)
|
22
|
+
ratios = metrics.map do |other_metric, other_count|
|
23
|
+
if metric != other_metric && count != 0 && other_count != 0
|
24
|
+
str = "#{metric.to_s.humanize}/#{other_metric.to_s.humanize} :"
|
25
|
+
str += " #{number_to_percentage((count.to_f/other_count.to_f) * 100, precision: 3)}"
|
26
|
+
str
|
27
|
+
end
|
28
|
+
end.compact
|
29
|
+
ratios.present? ? "(" + ratios.join(",") + ")" : ""
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
<header>
|
2
|
+
<h1>Experiments</h1>
|
3
|
+
</header>
|
4
|
+
|
5
|
+
<ul class="reports">
|
6
|
+
<% @reports.each do |report| %>
|
7
|
+
<li>
|
8
|
+
<%= link_to report[:name].to_s.humanize, absurdity_path(report[:name]) %>
|
9
|
+
<%= completed_text(report[:completed]) %>
|
10
|
+
</li>
|
11
|
+
<% end %>
|
12
|
+
</ul>
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<header>
|
2
|
+
<h1>Experiment</h1>
|
3
|
+
|
4
|
+
<h2>
|
5
|
+
<%= @report[:name].to_s.humanize %>
|
6
|
+
</h2>
|
7
|
+
|
8
|
+
<h3>
|
9
|
+
<%= completed_text(@report[:completed]) %>
|
10
|
+
</h3>
|
11
|
+
</header>
|
12
|
+
|
13
|
+
<ul>
|
14
|
+
<% @report[:data].each do |variant, metrics| %>
|
15
|
+
<li class="variant">
|
16
|
+
<h3><%= variant.to_s.humanize %></h3>
|
17
|
+
<% metrics.each do |metric, count| %>
|
18
|
+
<p><%= metric_info(metrics, metric, count) %></p>
|
19
|
+
<% end %>
|
20
|
+
</li>
|
21
|
+
<% end %>
|
22
|
+
</ul>
|
@@ -0,0 +1,17 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="<%= I18n.locale %>">
|
3
|
+
<head>
|
4
|
+
<title>Absurdity - A/B Testing</title>
|
5
|
+
<meta charset="utf-8">
|
6
|
+
<meta name="viewport" content="width=device-width, minimum-scale=1, maximum-scale=1, user-scalable=0">
|
7
|
+
<link rel="apple-touch-icon" href="/images/iphone_favicon.png">
|
8
|
+
<link rel="prev" href="/">
|
9
|
+
|
10
|
+
<link rel="stylesheet" href="/stylesheets/absurdity.css" media="screen">
|
11
|
+
</head>
|
12
|
+
|
13
|
+
<body>
|
14
|
+
<%= yield %>
|
15
|
+
</body>
|
16
|
+
|
17
|
+
</html>
|
data/config/routes.rb
ADDED
data/lib/absurdity.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'absurdity/railtie.rb' if defined?(Rails)
|
3
|
+
require 'absurdity/engine' if defined?(Rails)
|
4
|
+
|
5
|
+
module Absurdity
|
6
|
+
class MissingIdentityIDError < RuntimeError; end
|
7
|
+
|
8
|
+
autoload :Config, "absurdity/config"
|
9
|
+
autoload :Experiment, "absurdity/experiment"
|
10
|
+
autoload :Metric, "absurdity/metric"
|
11
|
+
autoload :Variant, "absurdity/variant"
|
12
|
+
autoload :Datastore, "absurdity/datastore"
|
13
|
+
|
14
|
+
def self.redis=(redis)
|
15
|
+
Config.instance.redis = redis
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.track!(metric_slug, experiment_slug, identity_id=nil)
|
19
|
+
Experiment.find(experiment_slug).track!(metric_slug, identity_id)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.variant(experiment_slug, identity_id)
|
23
|
+
Experiment.find(experiment_slug).variant_for(identity_id)
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'singleton'
|
2
|
+
require 'logger'
|
3
|
+
|
4
|
+
module Absurdity
|
5
|
+
class Config
|
6
|
+
include Singleton
|
7
|
+
|
8
|
+
def redis
|
9
|
+
@redis
|
10
|
+
end
|
11
|
+
|
12
|
+
def redis=(redis)
|
13
|
+
@redis = redis
|
14
|
+
end
|
15
|
+
|
16
|
+
def logger
|
17
|
+
@logger ||= ::Logger.new(STDOUT)
|
18
|
+
end
|
19
|
+
|
20
|
+
def logger=(logger)
|
21
|
+
@logger = logger
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,172 @@
|
|
1
|
+
module Absurdity
|
2
|
+
class Datastore
|
3
|
+
PARSE_AS_JSON = [
|
4
|
+
:metrics_list,
|
5
|
+
:variants_list,
|
6
|
+
:experiments_list
|
7
|
+
]
|
8
|
+
|
9
|
+
def self.save(object)
|
10
|
+
klass = object.class
|
11
|
+
if klass == Absurdity::Experiment
|
12
|
+
save_experiment(object)
|
13
|
+
elsif klass == Absurdity::Metric
|
14
|
+
save_metric(object)
|
15
|
+
elsif klass == Absurdity::Variant
|
16
|
+
save_variant(object)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.all(klass)
|
21
|
+
if klass == Absurdity::Experiment
|
22
|
+
all_experiments
|
23
|
+
elsif klass == Absurdity::Metric
|
24
|
+
all_metrics
|
25
|
+
elsif klass == Absurdity::Variant
|
26
|
+
all_variants
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find(klass, options)
|
31
|
+
if klass == Absurdity::Experiment
|
32
|
+
find_experiment(options[:slug])
|
33
|
+
elsif klass == Absurdity::Metric
|
34
|
+
find_metric(options[:slug], options[:experiment_slug], options[:variant_slug])
|
35
|
+
elsif klass == Absurdity::Variant
|
36
|
+
find_variant(options[:identity_id], options[:experiment_slug])
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def self.redis
|
43
|
+
Config.instance.redis
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
# FINDERS
|
48
|
+
|
49
|
+
def self.find_experiment(experiment_slug)
|
50
|
+
return nil unless slug = experiments_list.find { |e| e == experiment_slug }
|
51
|
+
experiment = Experiment.new(slug)
|
52
|
+
experiment.attributes[:metrics_list] = get(:metrics_list, experiment: experiment)
|
53
|
+
experiment.attributes[:variants_list] = get(:variants_list, experiment: experiment)
|
54
|
+
experiment.attributes[:completed] = get(:completed, experiment: experiment)
|
55
|
+
experiment
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.find_metric(metric_slug, experiment_slug, variant_slug)
|
59
|
+
experiment = find_experiment(experiment_slug)
|
60
|
+
return nil unless experiment && experiment.metrics_list.find { |sl| sl == metric_slug }
|
61
|
+
metric = Metric.new(metric_slug, experiment_slug, variant_slug)
|
62
|
+
metric.instance_variable_set(:@count, metric_count(metric))
|
63
|
+
metric
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.find_variant(identity_id, experiment_slug)
|
67
|
+
experiment = find_experiment(experiment_slug)
|
68
|
+
return nil unless slug = get(variant_key(identity_id), experiment: experiment)
|
69
|
+
Variant.new(slug, experiment_slug, identity_id)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.all_experiments
|
73
|
+
experiments_list.map { |exp| find_experiment(exp) }
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
# SAVERS
|
78
|
+
|
79
|
+
def self.save_metric(metric)
|
80
|
+
set(metric_key(metric), metric.count.to_i, experiment: find_experiment(metric.experiment_slug))
|
81
|
+
end
|
82
|
+
|
83
|
+
def self.save_experiment(experiment)
|
84
|
+
unless find_experiment(experiment.slug)
|
85
|
+
add_to_experiments_list(experiment.slug)
|
86
|
+
create_variants(experiment)
|
87
|
+
create_metrics(experiment)
|
88
|
+
end
|
89
|
+
mark_completed(experiment) if experiment.completed
|
90
|
+
end
|
91
|
+
|
92
|
+
def self.save_variant(variant)
|
93
|
+
set(variant_key(variant.identity_id), variant.slug, experiment: find_experiment(variant.experiment_slug))
|
94
|
+
end
|
95
|
+
|
96
|
+
def self.get(key, options={})
|
97
|
+
if experiment = options[:experiment]
|
98
|
+
store_key = "experiments:#{experiment.slug}:#{key}"
|
99
|
+
else
|
100
|
+
store_key = "experiments:#{key}"
|
101
|
+
end
|
102
|
+
string = redis.get(store_key)
|
103
|
+
if string.to_i.to_s == string
|
104
|
+
string.to_i
|
105
|
+
elsif PARSE_AS_JSON.include?(key)
|
106
|
+
string && JSON.parse(string).map { |v| v.to_sym }
|
107
|
+
else
|
108
|
+
string && string.to_sym
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.set(key, value, options={})
|
113
|
+
if experiment = options[:experiment]
|
114
|
+
store_key = "experiments:#{experiment.slug}:#{key}"
|
115
|
+
else
|
116
|
+
store_key = "experiments:#{key}"
|
117
|
+
end
|
118
|
+
redis.set(store_key, value)
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.metric_count(metric)
|
122
|
+
experiment = find_experiment(metric.experiment_slug)
|
123
|
+
get(metric_key(metric), experiment: experiment).to_i
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.metric_key(metric)
|
127
|
+
key = metric.variant_slug ? "#{metric.variant_slug}" : ""
|
128
|
+
key += ":#{metric.slug}:count"
|
129
|
+
key
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.variant_key(identity_id)
|
133
|
+
"identity_id:#{identity_id}:variant"
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.experiments_list
|
137
|
+
get(:experiments_list) || []
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.add_to_experiments_list(slug)
|
141
|
+
set(:experiments_list, (experiments_list << slug).to_json)
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.create_metrics(experiment)
|
145
|
+
set(:metrics_list, experiment.metrics_list.to_json, experiment: experiment)
|
146
|
+
experiment.metrics_list.each do |metric_slug|
|
147
|
+
if experiment.variants?
|
148
|
+
experiment.variants_list.each do |variant_slug|
|
149
|
+
Metric.create(metric_slug, experiment.slug, variant_slug)
|
150
|
+
end
|
151
|
+
else
|
152
|
+
Metric.create(metric_slug, experiment.slug)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def self.create_variants(experiment)
|
158
|
+
if !experiment.variants_list.nil?
|
159
|
+
set(:variants_list, experiment.variants_list.to_json, experiment: experiment)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def self.mark_completed(experiment)
|
164
|
+
set(:completed, experiment.completed, experiment: experiment)
|
165
|
+
end
|
166
|
+
|
167
|
+
def self.logger
|
168
|
+
Config.instance.logger
|
169
|
+
end
|
170
|
+
|
171
|
+
end
|
172
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module Absurdity
|
2
|
+
class Experiment
|
3
|
+
class NotFoundError < RuntimeError; end
|
4
|
+
class FoundError < RuntimeError; end
|
5
|
+
|
6
|
+
def self.create(slug, metrics_list, variants_list=nil)
|
7
|
+
raise FoundError if Datastore.find(self, slug: slug)
|
8
|
+
experiment = new(slug, metrics_list: metrics_list, variants_list: variants_list)
|
9
|
+
experiment.save
|
10
|
+
experiment
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.find(slug)
|
14
|
+
raise NotFoundError unless experiment = Datastore.find(self, slug: slug)
|
15
|
+
experiment
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.reports
|
19
|
+
all.map { |exp| exp.report }
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.all
|
23
|
+
Datastore.all_experiments
|
24
|
+
end
|
25
|
+
|
26
|
+
attr_reader :slug, :attributes
|
27
|
+
def initialize(slug, attributes = {})
|
28
|
+
@slug = slug
|
29
|
+
@attributes = attributes
|
30
|
+
end
|
31
|
+
|
32
|
+
def report
|
33
|
+
report = { name: slug, completed: completed, data: {} }
|
34
|
+
if variants?
|
35
|
+
variants_list.each do |variant|
|
36
|
+
report[:data][variant] = {}
|
37
|
+
metrics_list.each do |metric_slug|
|
38
|
+
report[:data][variant][metric_slug] = metric(metric_slug, variant).count
|
39
|
+
end
|
40
|
+
end
|
41
|
+
else
|
42
|
+
metrics_list.each do |metric_slug|
|
43
|
+
report[:data][metric_slug] = metric(metric_slug).count
|
44
|
+
end
|
45
|
+
end
|
46
|
+
report
|
47
|
+
end
|
48
|
+
|
49
|
+
def save
|
50
|
+
Datastore.save_experiment(self)
|
51
|
+
end
|
52
|
+
|
53
|
+
def track!(metric_slug, identity_id=nil)
|
54
|
+
raise Absurdity::MissingIdentityIDError if variants? && identity_id.nil?
|
55
|
+
variant = variants? ? variant_for(identity_id) : nil
|
56
|
+
metric(metric_slug, variant).track! unless completed
|
57
|
+
end
|
58
|
+
|
59
|
+
def complete(variant_slug)
|
60
|
+
@attributes[:completed] = variant_slug
|
61
|
+
save
|
62
|
+
end
|
63
|
+
|
64
|
+
def completed
|
65
|
+
@attributes[:completed]
|
66
|
+
end
|
67
|
+
|
68
|
+
def count(metric_slug)
|
69
|
+
if variants?
|
70
|
+
count = {}
|
71
|
+
variants_list.each do |variant|
|
72
|
+
count[variant] = metric(metric_slug, variant).count
|
73
|
+
end
|
74
|
+
else
|
75
|
+
count = metric(metric_slug).count
|
76
|
+
end
|
77
|
+
count
|
78
|
+
end
|
79
|
+
|
80
|
+
def metric(metric_slug, variant=nil)
|
81
|
+
Metric.find(metric_slug, slug, variant)
|
82
|
+
end
|
83
|
+
|
84
|
+
def ==(other_experiment)
|
85
|
+
slug == other_experiment.slug &&
|
86
|
+
metrics_list == other_experiment.metrics_list &&
|
87
|
+
variants_list == other_experiment.variants_list
|
88
|
+
end
|
89
|
+
|
90
|
+
def metrics
|
91
|
+
return @metrics unless @metrics.nil?
|
92
|
+
@metrics = []
|
93
|
+
metrics_list.each do |metric_slug|
|
94
|
+
if variants?
|
95
|
+
variants_list.each { |variant| @metrics << metric(metric_slug, variant) }
|
96
|
+
else
|
97
|
+
@metrics << metric(metric_slug)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
@metrics
|
101
|
+
end
|
102
|
+
|
103
|
+
def variant_for(identity_id)
|
104
|
+
if completed
|
105
|
+
variant = Variant.new(completed, slug, identity_id)
|
106
|
+
else
|
107
|
+
variant = Variant.find(identity_id, slug)
|
108
|
+
if variant.nil?
|
109
|
+
variant = Variant.new(random_variant, slug, identity_id)
|
110
|
+
variant.save
|
111
|
+
end
|
112
|
+
end
|
113
|
+
variant.slug
|
114
|
+
end
|
115
|
+
|
116
|
+
def metrics_list
|
117
|
+
attributes[:metrics_list]
|
118
|
+
end
|
119
|
+
|
120
|
+
def variants_list
|
121
|
+
attributes[:variants_list]
|
122
|
+
end
|
123
|
+
|
124
|
+
def variants?
|
125
|
+
variants_list
|
126
|
+
end
|
127
|
+
|
128
|
+
private
|
129
|
+
|
130
|
+
def random_variant
|
131
|
+
variants_list.sort_by { rand }[0]
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
module Absurdity
|
2
|
+
class Metric
|
3
|
+
class NotFoundError < RuntimeError; end
|
4
|
+
|
5
|
+
def self.create(slug, experiment_slug, variant_slug=nil)
|
6
|
+
metric = new(slug, experiment_slug, variant_slug)
|
7
|
+
metric.save
|
8
|
+
metric
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.find(slug, experiment_slug, variant_slug=nil)
|
12
|
+
raise NotFoundError unless metric = Datastore.find(self,
|
13
|
+
slug: slug,
|
14
|
+
experiment_slug: experiment_slug,
|
15
|
+
variant_slug: variant_slug)
|
16
|
+
metric
|
17
|
+
end
|
18
|
+
|
19
|
+
attr_reader :slug, :experiment_slug, :variant_slug
|
20
|
+
def initialize(slug, experiment_slug, variant_slug=nil)
|
21
|
+
@slug = slug
|
22
|
+
@experiment_slug = experiment_slug
|
23
|
+
@variant_slug = variant_slug
|
24
|
+
end
|
25
|
+
|
26
|
+
def save
|
27
|
+
Datastore.save(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
def track!
|
31
|
+
@count += 1
|
32
|
+
save
|
33
|
+
end
|
34
|
+
|
35
|
+
def count
|
36
|
+
@count ||= 0
|
37
|
+
end
|
38
|
+
|
39
|
+
def ==(other_metric)
|
40
|
+
slug == other_metric.slug &&
|
41
|
+
experiment_slug == other_metric.experiment_slug &&
|
42
|
+
variant_slug == other_metric.variant_slug
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module Absurdity
|
2
|
+
class Railtie < ::Rails::Railtie
|
3
|
+
config.after_initialize do
|
4
|
+
Config.instance.logger = ::Rails.logger
|
5
|
+
load_experiments
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.load_experiments
|
9
|
+
experiments_to_create = YAML.load_file("absurdity/experiments.yml")[:experiments]
|
10
|
+
experiments_to_create.each do |experiment_slug, values|
|
11
|
+
metrics_list = values[:metrics]
|
12
|
+
variants_list = values[:variants]
|
13
|
+
experiment = new_experiment(experiment_slug, metrics_list, variants_list)
|
14
|
+
complete(experiment_slug, values[:completed]) if values[:completed] && !experiment.completed
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.new_experiment(experiment_slug, metrics_list, variants_list=nil)
|
19
|
+
begin
|
20
|
+
experiment = Experiment.find(experiment_slug)
|
21
|
+
rescue Experiment::NotFoundError
|
22
|
+
Experiment.create(experiment_slug, metrics_list, variants_list)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.complete(experiment_slug, variant_slug)
|
27
|
+
experiment = Experiment.find(experiment_slug)
|
28
|
+
experiment.complete(variant_slug)
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
module Absurdity
|
2
|
+
class Variant
|
3
|
+
|
4
|
+
def self.find(identity_id, experiment_slug)
|
5
|
+
Datastore.find(self, experiment_slug: experiment_slug, identity_id: identity_id)
|
6
|
+
end
|
7
|
+
|
8
|
+
attr_reader :slug, :experiment_slug, :identity_id
|
9
|
+
def initialize(slug, experiment_slug, identity_id)
|
10
|
+
@slug = slug
|
11
|
+
@experiment_slug = experiment_slug
|
12
|
+
@identity_id = identity_id
|
13
|
+
end
|
14
|
+
|
15
|
+
def save
|
16
|
+
Datastore.save(self)
|
17
|
+
end
|
18
|
+
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
body {
|
2
|
+
background-color: #fff;
|
3
|
+
font: 12px Helvetica, Arial, sans-serif;
|
4
|
+
margin: 0 auto;
|
5
|
+
padding: 12px 0;
|
6
|
+
width: 1000px;
|
7
|
+
}
|
8
|
+
|
9
|
+
h1, h2, h3 {
|
10
|
+
color: #bbb;
|
11
|
+
display: inline;
|
12
|
+
font-size: 2em;
|
13
|
+
font-weight: normal;
|
14
|
+
margin: 0;
|
15
|
+
padding: 0;
|
16
|
+
text-transform: uppercase;
|
17
|
+
white-space: nowrap;
|
18
|
+
overflow: hidden;
|
19
|
+
text-overflow: ellipsis;
|
20
|
+
}
|
21
|
+
|
22
|
+
h1 {
|
23
|
+
color: #333;
|
24
|
+
font-weight: bold;
|
25
|
+
padding-right: 12px;
|
26
|
+
}
|
27
|
+
|
28
|
+
h2 {
|
29
|
+
font-size: 2em;
|
30
|
+
font-weight: normal;
|
31
|
+
}
|
32
|
+
|
33
|
+
h3 {
|
34
|
+
font-size: 1.8em;
|
35
|
+
display: block;
|
36
|
+
}
|
37
|
+
|
38
|
+
ul {
|
39
|
+
list-style: none;
|
40
|
+
padding: 0;
|
41
|
+
}
|
42
|
+
|
43
|
+
.reports li {
|
44
|
+
color: #aaa;
|
45
|
+
background-color: #333;
|
46
|
+
border-radius: 10px;
|
47
|
+
-moz-box-shadow: 0 5px 5px #999;
|
48
|
+
-webkit-box-shadow: 0 5px 5px #999;
|
49
|
+
box-shadow: 0 5px 5px #999;
|
50
|
+
display: inline-block;
|
51
|
+
font-weight: bold;
|
52
|
+
padding: 10px 10px;
|
53
|
+
margin: 10px 10px 10px 0;
|
54
|
+
position: relative;
|
55
|
+
height: 80px;
|
56
|
+
width: 170px;
|
57
|
+
}
|
58
|
+
|
59
|
+
.reports li span {
|
60
|
+
font-weight: normal;
|
61
|
+
position: absolute;
|
62
|
+
bottom: 10px;
|
63
|
+
left: 10px;
|
64
|
+
width: 160px;
|
65
|
+
overflow: hidden;
|
66
|
+
text-overflow: ellipsis;
|
67
|
+
}
|
68
|
+
|
69
|
+
.reports li a, .reports li a:visited {
|
70
|
+
color: #f8f8f8;
|
71
|
+
display: inline-block;
|
72
|
+
text-decoration: none;
|
73
|
+
position: absolute;
|
74
|
+
top: 10px;
|
75
|
+
height: 80px;
|
76
|
+
width: 170px;
|
77
|
+
}
|
78
|
+
|
79
|
+
li.variant {
|
80
|
+
border-left: 3px solid #eee;
|
81
|
+
color: #333;
|
82
|
+
display: inline-block;
|
83
|
+
margin-top: 12px;
|
84
|
+
padding-left: 12px;
|
85
|
+
width: 25%;
|
86
|
+
}
|
87
|
+
|
88
|
+
li.variant:first-child {
|
89
|
+
border-left: none;
|
90
|
+
padding: 0 12px 0 0;
|
91
|
+
}
|
92
|
+
|
93
|
+
.metric {
|
94
|
+
font-weight: bold;
|
95
|
+
}
|
96
|
+
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AbsurdityTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_redis_setter_and_getter
|
6
|
+
a_redis = MockRedis.new
|
7
|
+
Absurdity.redis = a_redis
|
8
|
+
assert_equal a_redis, Absurdity.redis
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_track_missing_experiment
|
12
|
+
Absurdity.redis = MockRedis.new
|
13
|
+
|
14
|
+
assert_raises Absurdity::Experiment::NotFoundError do
|
15
|
+
Absurdity.track! :clicked, :shared_contacts_link
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_track_missing_metric
|
20
|
+
Absurdity.redis = MockRedis.new
|
21
|
+
Absurdity::Experiment.create(:shared_contacts_link,
|
22
|
+
[:clicked])
|
23
|
+
|
24
|
+
assert_raises Absurdity::Metric::NotFoundError do
|
25
|
+
Absurdity.track! :seen, :shared_contacts_link
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_track_missing_identity_id
|
30
|
+
Absurdity.redis = MockRedis.new
|
31
|
+
Absurdity::Experiment.create(:shared_contacts_link,
|
32
|
+
[:clicked],
|
33
|
+
[:with_photos, :without_photos])
|
34
|
+
|
35
|
+
assert_raises Absurdity::MissingIdentityIDError do
|
36
|
+
Absurdity.track! :seen, :shared_contacts_link
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_track_experiment_metric_without_variants
|
41
|
+
Absurdity.redis = MockRedis.new
|
42
|
+
Absurdity::Experiment.create(:shared_contacts_link,
|
43
|
+
[:clicked])
|
44
|
+
|
45
|
+
Absurdity.track! :clicked, :shared_contacts_link
|
46
|
+
assert_equal 1, Absurdity::Experiment.find(:shared_contacts_link).count(:clicked)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_track_experiment_metric_with_variants
|
50
|
+
Absurdity.redis = MockRedis.new
|
51
|
+
Absurdity::Experiment.any_instance.expects(:random_variant).returns(:with_photos)
|
52
|
+
|
53
|
+
Absurdity::Experiment.create(:shared_contacts_link,
|
54
|
+
[:clicked],
|
55
|
+
[:with_photos, :without_photos])
|
56
|
+
|
57
|
+
Absurdity.track! :clicked, :shared_contacts_link, 1
|
58
|
+
count = Absurdity::Experiment.find(:shared_contacts_link).count(:clicked)
|
59
|
+
|
60
|
+
assert_equal 1, count[:with_photos]
|
61
|
+
assert_equal 0, count[:without_photos]
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_track_experiment_with_multiple_metrics_with_variants
|
65
|
+
Absurdity.redis = MockRedis.new
|
66
|
+
Absurdity::Experiment.any_instance.expects(:random_variant).returns(:with_photos)
|
67
|
+
|
68
|
+
Absurdity::Experiment.create(:shared_contacts_link,
|
69
|
+
[:clicked, :seen],
|
70
|
+
[:with_photos, :without_photos])
|
71
|
+
|
72
|
+
Absurdity.track! :clicked, :shared_contacts_link, 1
|
73
|
+
Absurdity.track! :seen, :shared_contacts_link, 1
|
74
|
+
count = Absurdity::Experiment.find(:shared_contacts_link).count(:clicked)
|
75
|
+
|
76
|
+
assert_equal 1, count[:with_photos]
|
77
|
+
assert_equal 0, count[:without_photos]
|
78
|
+
|
79
|
+
count = Absurdity::Experiment.find(:shared_contacts_link).count(:seen)
|
80
|
+
assert_equal 1, count[:with_photos]
|
81
|
+
assert_equal 0, count[:without_photos]
|
82
|
+
end
|
83
|
+
|
84
|
+
def test_variant
|
85
|
+
Absurdity.redis = MockRedis.new
|
86
|
+
Absurdity::Experiment.any_instance.expects(:random_variant).returns(:with_photos)
|
87
|
+
|
88
|
+
Absurdity::Experiment.create(:shared_contacts_link,
|
89
|
+
[:clicked, :seen],
|
90
|
+
[:with_photos, :without_photos])
|
91
|
+
|
92
|
+
assert_equal :with_photos, Absurdity.variant(:shared_contacts_link, 1)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class AbsurdityTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def test_redis_setter_and_getter
|
6
|
+
a_redis = MockRedis.new
|
7
|
+
Absurdity::Config.instance.redis = a_redis
|
8
|
+
|
9
|
+
assert_equal a_redis, Absurdity::Config.instance.redis
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ExperimentTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@experiment_slug = :shared_contacts_link
|
7
|
+
@metrics_list = [:clicked, :seen]
|
8
|
+
@variants_list = [:with_photos, :without_photos]
|
9
|
+
Absurdity.redis = MockRedis.new
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_find_and_test_create
|
13
|
+
experiment = Absurdity::Experiment.create(@experiment_slug,
|
14
|
+
@metrics_list,
|
15
|
+
@variants_list)
|
16
|
+
|
17
|
+
assert_equal experiment, Absurdity::Experiment.find(@experiment_slug)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_initialize
|
21
|
+
# crappy test
|
22
|
+
experiment = Absurdity::Experiment.new(@experiment_slug,
|
23
|
+
metrics_list: @metrics_list,
|
24
|
+
variants_list: @variants_list)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_save
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_metric
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
|
data/test/metric_test.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class MetricTest < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
Absurdity.redis = MockRedis.new
|
7
|
+
end
|
8
|
+
|
9
|
+
def test_find_and_create
|
10
|
+
experiment_slug = :shared_contacts_link
|
11
|
+
metric_slug = :clicked
|
12
|
+
experiment = stub(slug: experiment_slug, metrics_list: [metric_slug])
|
13
|
+
Absurdity::Datastore.stubs(:find_experiment).returns(experiment)
|
14
|
+
|
15
|
+
metric = Absurdity::Metric.create(metric_slug, experiment_slug)
|
16
|
+
|
17
|
+
assert_equal metric, Absurdity::Metric.find(metric_slug, experiment_slug)
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_find_not_found
|
21
|
+
assert_raises Absurdity::Metric::NotFoundError do
|
22
|
+
Absurdity::Metric.find(:blah, :boo)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_save_first_time
|
27
|
+
experiment_slug = :shared_contacts_link
|
28
|
+
metric_slug = :clicked
|
29
|
+
experiment = stub(slug: experiment_slug, metrics_list: [metric_slug])
|
30
|
+
Absurdity::Datastore.stubs(:find_experiment).returns(experiment)
|
31
|
+
|
32
|
+
metric = Absurdity::Metric.new(metric_slug, experiment_slug)
|
33
|
+
|
34
|
+
metric.save
|
35
|
+
assert_equal metric, Absurdity::Metric.find(metric_slug, experiment_slug)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_save_after_first_time
|
39
|
+
experiment_slug = :shared_contacts_link
|
40
|
+
metric_slug = :clicked
|
41
|
+
metric = Absurdity::Metric.create(metric_slug, experiment_slug)
|
42
|
+
|
43
|
+
assert_equal 0, metric.count
|
44
|
+
metric.track!
|
45
|
+
assert_equal 1, metric.count
|
46
|
+
metric.save
|
47
|
+
assert_equal 1, metric.count
|
48
|
+
end
|
49
|
+
|
50
|
+
def test_count
|
51
|
+
experiment_slug = :shared_contacts_link
|
52
|
+
metric_slug = :clicked
|
53
|
+
metric = Absurdity::Metric.create(metric_slug, experiment_slug)
|
54
|
+
|
55
|
+
assert_equal 0, metric.count
|
56
|
+
metric.track!
|
57
|
+
assert_equal 1, metric.count
|
58
|
+
end
|
59
|
+
|
60
|
+
def test_track_metric
|
61
|
+
experiment_slug = :shared_contacts_link
|
62
|
+
metric_slug = :clicked
|
63
|
+
|
64
|
+
metric = Absurdity::Metric.new(metric_slug, experiment_slug)
|
65
|
+
|
66
|
+
assert_equal 0, metric.count
|
67
|
+
metric.track!
|
68
|
+
assert_equal 1, metric.count
|
69
|
+
end
|
70
|
+
|
71
|
+
def test_track_variant_metric
|
72
|
+
experiment_slug = :shared_contacts_link
|
73
|
+
metric_slug = :clicked
|
74
|
+
variant_slug = :with_photos
|
75
|
+
metric = Absurdity::Metric.new(metric_slug, experiment_slug, variant_slug)
|
76
|
+
|
77
|
+
assert_equal 0, metric.count
|
78
|
+
metric.track!
|
79
|
+
assert_equal 1, metric.count
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'mock_redis'
|
3
|
+
require 'absurdity'
|
4
|
+
require 'redis'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
class MiniTest::Unit::TestCase
|
8
|
+
|
9
|
+
def teardown
|
10
|
+
config = Absurdity::Config.instance
|
11
|
+
config.redis && config.redis.flushdb
|
12
|
+
config.instance_variable_set(:@redis, nil)
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
metadata
ADDED
@@ -0,0 +1,140 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: absurdity
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.2.5
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Tim Payton
|
9
|
+
- "\xC3\x96m\xC3\xBCr \xC3\x96zkir"
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
|
14
|
+
date: 2011-10-21 00:00:00 Z
|
15
|
+
dependencies:
|
16
|
+
- !ruby/object:Gem::Dependency
|
17
|
+
name: redis
|
18
|
+
prerelease: false
|
19
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
20
|
+
none: false
|
21
|
+
requirements:
|
22
|
+
- - ">="
|
23
|
+
- !ruby/object:Gem::Version
|
24
|
+
version: "0"
|
25
|
+
type: :runtime
|
26
|
+
version_requirements: *id001
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
prerelease: false
|
30
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
31
|
+
none: false
|
32
|
+
requirements:
|
33
|
+
- - ">="
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: 0.8.7
|
36
|
+
type: :runtime
|
37
|
+
version_requirements: *id002
|
38
|
+
- !ruby/object:Gem::Dependency
|
39
|
+
name: mock_redis
|
40
|
+
prerelease: false
|
41
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
42
|
+
none: false
|
43
|
+
requirements:
|
44
|
+
- - ">="
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id003
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: mocha
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id004
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: minitest
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: "0"
|
69
|
+
type: :development
|
70
|
+
version_requirements: *id005
|
71
|
+
description: See summary
|
72
|
+
email: opensource@xing.com
|
73
|
+
executables: []
|
74
|
+
|
75
|
+
extensions: []
|
76
|
+
|
77
|
+
extra_rdoc_files: []
|
78
|
+
|
79
|
+
files:
|
80
|
+
- .gitignore
|
81
|
+
- .rvmrc
|
82
|
+
- Gemfile
|
83
|
+
- README.rdoc
|
84
|
+
- Rakefile
|
85
|
+
- absurdity.gemspec
|
86
|
+
- app/controllers/absurdities_controller.rb
|
87
|
+
- app/helpers/absurdity_helper.rb
|
88
|
+
- app/views/absurdities/index.html.erb
|
89
|
+
- app/views/absurdities/show.html.erb
|
90
|
+
- app/views/layouts/absurdities.html.erb
|
91
|
+
- config/routes.rb
|
92
|
+
- lib/absurdity.rb
|
93
|
+
- lib/absurdity/config.rb
|
94
|
+
- lib/absurdity/datastore.rb
|
95
|
+
- lib/absurdity/datastore/redis.rb
|
96
|
+
- lib/absurdity/engine.rb
|
97
|
+
- lib/absurdity/experiment.rb
|
98
|
+
- lib/absurdity/metric.rb
|
99
|
+
- lib/absurdity/railtie.rb
|
100
|
+
- lib/absurdity/variant.rb
|
101
|
+
- lib/absurdity/version.rb
|
102
|
+
- public/stylesheets/absurdity.css
|
103
|
+
- test/absurdity_test.rb
|
104
|
+
- test/config_test.rb
|
105
|
+
- test/experiment_test.rb
|
106
|
+
- test/metric_test.rb
|
107
|
+
- test/test_helper.rb
|
108
|
+
homepage: http://xing.github.com/absurdity/
|
109
|
+
licenses: []
|
110
|
+
|
111
|
+
post_install_message:
|
112
|
+
rdoc_options: []
|
113
|
+
|
114
|
+
require_paths:
|
115
|
+
- lib
|
116
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
117
|
+
none: false
|
118
|
+
requirements:
|
119
|
+
- - ">="
|
120
|
+
- !ruby/object:Gem::Version
|
121
|
+
version: "0"
|
122
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ">="
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: "0"
|
128
|
+
requirements: []
|
129
|
+
|
130
|
+
rubyforge_project: absurdity
|
131
|
+
rubygems_version: 1.8.10
|
132
|
+
signing_key:
|
133
|
+
specification_version: 3
|
134
|
+
summary: Absurdly simple a/b testing
|
135
|
+
test_files:
|
136
|
+
- test/absurdity_test.rb
|
137
|
+
- test/config_test.rb
|
138
|
+
- test/experiment_test.rb
|
139
|
+
- test/metric_test.rb
|
140
|
+
- test/test_helper.rb
|