modesty 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +13 -0
- data/Gemfile.lock +18 -0
- data/LICENSE +21 -0
- data/README.md +121 -0
- data/Rakefile +29 -0
- data/VERSION +1 -0
- data/init.rb +1 -0
- data/lib/modesty.rb +26 -0
- data/lib/modesty/api.rb +14 -0
- data/lib/modesty/core_ext.rb +5 -0
- data/lib/modesty/core_ext/array.rb +21 -0
- data/lib/modesty/core_ext/fixnum.rb +5 -0
- data/lib/modesty/core_ext/hash.rb +39 -0
- data/lib/modesty/core_ext/string.rb +9 -0
- data/lib/modesty/core_ext/symbol.rb +33 -0
- data/lib/modesty/datastore.rb +51 -0
- data/lib/modesty/datastore/redis.rb +180 -0
- data/lib/modesty/experiment.rb +87 -0
- data/lib/modesty/experiment/base.rb +47 -0
- data/lib/modesty/experiment/builder.rb +48 -0
- data/lib/modesty/experiment/console.rb +4 -0
- data/lib/modesty/experiment/data.rb +75 -0
- data/lib/modesty/experiment/interface.rb +29 -0
- data/lib/modesty/experiment/significance.rb +376 -0
- data/lib/modesty/experiment/stats.rb +163 -0
- data/lib/modesty/frameworks/rails.rb +27 -0
- data/lib/modesty/identity.rb +32 -0
- data/lib/modesty/load.rb +80 -0
- data/lib/modesty/load/load_experiments.rb +14 -0
- data/lib/modesty/load/load_metrics.rb +17 -0
- data/lib/modesty/metric.rb +56 -0
- data/lib/modesty/metric/base.rb +38 -0
- data/lib/modesty/metric/builder.rb +23 -0
- data/lib/modesty/metric/data.rb +133 -0
- data/modesty.gemspec +192 -0
- data/spec/core_ext_spec.rb +17 -0
- data/spec/experiment_spec.rb +239 -0
- data/spec/identity_spec.rb +161 -0
- data/spec/load_spec.rb +87 -0
- data/spec/metric_spec.rb +176 -0
- data/spec/rails_spec.rb +48 -0
- data/spec/redis_spec.rb +29 -0
- data/spec/significance_spec.rb +147 -0
- data/spec/spec.opts +1 -0
- data/test/myapp/config/modesty.yml +9 -0
- data/test/myapp/modesty/experiments/cookbook.rb +4 -0
- data/test/myapp/modesty/metrics/kitchen_metrics.rb +9 -0
- data/test/myapp/modesty/metrics/stove/burner_metrics.rb +2 -0
- data/vendor/.piston.yml +8 -0
- data/vendor/mock_redis/.gitignore +2 -0
- data/vendor/mock_redis/README +8 -0
- data/vendor/mock_redis/lib/mock_redis.rb +10 -0
- data/vendor/mock_redis/lib/mock_redis/hash.rb +61 -0
- data/vendor/mock_redis/lib/mock_redis/list.rb +6 -0
- data/vendor/mock_redis/lib/mock_redis/misc.rb +69 -0
- data/vendor/mock_redis/lib/mock_redis/set.rb +108 -0
- data/vendor/mock_redis/lib/mock_redis/string.rb +32 -0
- data/vendor/redis-rb/.gitignore +8 -0
- data/vendor/redis-rb/LICENSE +20 -0
- data/vendor/redis-rb/README.markdown +129 -0
- data/vendor/redis-rb/Rakefile +155 -0
- data/vendor/redis-rb/benchmarking/logging.rb +62 -0
- data/vendor/redis-rb/benchmarking/pipeline.rb +51 -0
- data/vendor/redis-rb/benchmarking/speed.rb +21 -0
- data/vendor/redis-rb/benchmarking/suite.rb +24 -0
- data/vendor/redis-rb/benchmarking/thread_safety.rb +38 -0
- data/vendor/redis-rb/benchmarking/worker.rb +71 -0
- data/vendor/redis-rb/examples/basic.rb +15 -0
- data/vendor/redis-rb/examples/dist_redis.rb +43 -0
- data/vendor/redis-rb/examples/incr-decr.rb +17 -0
- data/vendor/redis-rb/examples/list.rb +26 -0
- data/vendor/redis-rb/examples/pubsub.rb +31 -0
- data/vendor/redis-rb/examples/sets.rb +36 -0
- data/vendor/redis-rb/examples/unicorn/config.ru +3 -0
- data/vendor/redis-rb/examples/unicorn/unicorn.rb +20 -0
- data/vendor/redis-rb/lib/redis.rb +676 -0
- data/vendor/redis-rb/lib/redis/client.rb +201 -0
- data/vendor/redis-rb/lib/redis/compat.rb +21 -0
- data/vendor/redis-rb/lib/redis/connection.rb +134 -0
- data/vendor/redis-rb/lib/redis/distributed.rb +526 -0
- data/vendor/redis-rb/lib/redis/hash_ring.rb +131 -0
- data/vendor/redis-rb/lib/redis/pipeline.rb +13 -0
- data/vendor/redis-rb/lib/redis/subscribe.rb +79 -0
- data/vendor/redis-rb/redis.gemspec +29 -0
- data/vendor/redis-rb/test/commands_on_hashes_test.rb +46 -0
- data/vendor/redis-rb/test/commands_on_lists_test.rb +50 -0
- data/vendor/redis-rb/test/commands_on_sets_test.rb +78 -0
- data/vendor/redis-rb/test/commands_on_sorted_sets_test.rb +109 -0
- data/vendor/redis-rb/test/commands_on_strings_test.rb +70 -0
- data/vendor/redis-rb/test/commands_on_value_types_test.rb +88 -0
- data/vendor/redis-rb/test/connection_handling_test.rb +87 -0
- data/vendor/redis-rb/test/db/.gitignore +1 -0
- data/vendor/redis-rb/test/distributd_key_tags_test.rb +53 -0
- data/vendor/redis-rb/test/distributed_blocking_commands_test.rb +54 -0
- data/vendor/redis-rb/test/distributed_commands_on_hashes_test.rb +12 -0
- data/vendor/redis-rb/test/distributed_commands_on_lists_test.rb +18 -0
- data/vendor/redis-rb/test/distributed_commands_on_sets_test.rb +85 -0
- data/vendor/redis-rb/test/distributed_commands_on_strings_test.rb +50 -0
- data/vendor/redis-rb/test/distributed_commands_on_value_types_test.rb +73 -0
- data/vendor/redis-rb/test/distributed_commands_requiring_clustering_test.rb +141 -0
- data/vendor/redis-rb/test/distributed_connection_handling_test.rb +25 -0
- data/vendor/redis-rb/test/distributed_internals_test.rb +18 -0
- data/vendor/redis-rb/test/distributed_persistence_control_commands_test.rb +24 -0
- data/vendor/redis-rb/test/distributed_publish_subscribe_test.rb +90 -0
- data/vendor/redis-rb/test/distributed_remote_server_control_commands_test.rb +31 -0
- data/vendor/redis-rb/test/distributed_sorting_test.rb +21 -0
- data/vendor/redis-rb/test/distributed_test.rb +60 -0
- data/vendor/redis-rb/test/distributed_transactions_test.rb +34 -0
- data/vendor/redis-rb/test/encoding_test.rb +16 -0
- data/vendor/redis-rb/test/helper.rb +86 -0
- data/vendor/redis-rb/test/internals_test.rb +27 -0
- data/vendor/redis-rb/test/lint/hashes.rb +90 -0
- data/vendor/redis-rb/test/lint/internals.rb +53 -0
- data/vendor/redis-rb/test/lint/lists.rb +93 -0
- data/vendor/redis-rb/test/lint/sets.rb +66 -0
- data/vendor/redis-rb/test/lint/sorted_sets.rb +132 -0
- data/vendor/redis-rb/test/lint/strings.rb +98 -0
- data/vendor/redis-rb/test/lint/value_types.rb +84 -0
- data/vendor/redis-rb/test/persistence_control_commands_test.rb +22 -0
- data/vendor/redis-rb/test/pipelining_commands_test.rb +78 -0
- data/vendor/redis-rb/test/publish_subscribe_test.rb +151 -0
- data/vendor/redis-rb/test/redis_mock.rb +64 -0
- data/vendor/redis-rb/test/remote_server_control_commands_test.rb +56 -0
- data/vendor/redis-rb/test/sorting_test.rb +44 -0
- data/vendor/redis-rb/test/test.conf +8 -0
- data/vendor/redis-rb/test/thread_safety_test.rb +34 -0
- data/vendor/redis-rb/test/transactions_test.rb +91 -0
- data/vendor/redis-rb/test/unknown_commands_test.rb +14 -0
- data/vendor/redis-rb/test/url_param_test.rb +52 -0
- metadata +277 -0
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
#MIT license. See http://www.opensource.org/licenses/mit-license.php
|
2
|
+
|
3
|
+
Copyright (c) 2010 Philotic, Inc.
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,121 @@
|
|
1
|
+
# Modesty
|
2
|
+
|
3
|
+
Modesty is a really simple metrics and a/b testing framework that doesn't really do all that much. It was inspired by assaf's Vanity (github.com/assaf/vanity).
|
4
|
+
|
5
|
+
## Metrics
|
6
|
+
A metric keeps track of things that happen, and ties in and disaggregates many different types of data. Define a metric with:
|
7
|
+
|
8
|
+
Modesty.new_metric :foo
|
9
|
+
|
10
|
+
Your metric will now be available as `Modesty.metrics[:foo]`.
|
11
|
+
You can track it with
|
12
|
+
|
13
|
+
Modesty.track! :foo`,
|
14
|
+
|
15
|
+
You can get a raw count with
|
16
|
+
|
17
|
+
Modesty.metrics[:foo].count`.
|
18
|
+
|
19
|
+
You can track multiple counts with `Modesty.track! :foo, 7`,
|
20
|
+
or, if you prefer, `Modesty.track! :foo, :count => 7`.
|
21
|
+
|
22
|
+
Simple, huh?
|
23
|
+
|
24
|
+
You can also pass in any sort of data when you track your metric. For example,
|
25
|
+
|
26
|
+
Modesty.track! :product_page_viewed, :with => {:product_id => 500, :seller_id => 278}
|
27
|
+
|
28
|
+
This provides you with a few more granular methods.
|
29
|
+
|
30
|
+
m = Modesty.metrics[:product_page_viewed]
|
31
|
+
m.unique :product_ids # => number of unique product_ids that were tracked
|
32
|
+
m.all :product_ids # => the actual ids that were tracked
|
33
|
+
m.aggregate_by :product_ids # => a hash of {product_id => tracks for this product id}
|
34
|
+
m.distribution_by :product_ids # => equivalent to aggregate_by(:product_ids).values.histogram
|
35
|
+
|
36
|
+
TODO: submetrics
|
37
|
+
|
38
|
+
## Identity
|
39
|
+
To save you some hassle, Modesty keeps around a global identity in `Modesty.identity`.
|
40
|
+
You can set the identity with `Modesty.identify! id`,
|
41
|
+
where `id` is either an integer (i.e. the id of the current user) or `nil` for guests.
|
42
|
+
When the identity is present, all metrics tracked will get a `:user` parameter passed in.
|
43
|
+
This makes it really easy to call `m.unique :users` and such,
|
44
|
+
without having to pass it in every time.
|
45
|
+
To override this, just call `Modesty.track! :metric, :with => {:user => other_user}`.
|
46
|
+
|
47
|
+
If you're using Rails, I recommend putting a `before_filter` on `ApplicationController` that does something akin to `Modesty.identify! viewer.id`.
|
48
|
+
|
49
|
+
## Experiments
|
50
|
+
Experiments are really the point of Modesty.
|
51
|
+
With an experiment, you separate your users into experiment groups
|
52
|
+
and track how each group performs on a given set of metrics.
|
53
|
+
Modesty will ensure that
|
54
|
+
* each group contains roughly the same number of users
|
55
|
+
* Each user has a consistent experience
|
56
|
+
* All specified metrics can be disaggregated by experiment group.
|
57
|
+
|
58
|
+
Here's how you make an experiment:
|
59
|
+
|
60
|
+
Modesty.new_experiment :button_size do |e|
|
61
|
+
e.metrics :conversion, :view
|
62
|
+
e.alternatives :huge, :medium, :small
|
63
|
+
end
|
64
|
+
|
65
|
+
Then, you can do something like this (in a controller, say)
|
66
|
+
|
67
|
+
button_size = Modesty.experiment :button_size do |e|
|
68
|
+
e.group :huge do
|
69
|
+
9001
|
70
|
+
end
|
71
|
+
e.group :medium do
|
72
|
+
1337
|
73
|
+
end
|
74
|
+
e.group :small do
|
75
|
+
2
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
This code will use `Modesty.identity` to determine the appropriate experiment group,
|
80
|
+
and run the corresponding block.
|
81
|
+
|
82
|
+
All your tracking data will automagically be disaggregated into
|
83
|
+
|
84
|
+
Modesty.metrics[:conversion/:button_size/:huge]
|
85
|
+
Modesty.metrics[:conversion/:button_size/:medium]
|
86
|
+
Modesty.metrics[:conversion/:button_size/:small]
|
87
|
+
|
88
|
+
## Statistics and reporting
|
89
|
+
|
90
|
+
TODO.
|
91
|
+
|
92
|
+
##Datastores and config
|
93
|
+
|
94
|
+
Right now there are two datastores available: Redis and a sweet mock Redis for testing. To switch between them, use
|
95
|
+
|
96
|
+
Modesty.data = :redis
|
97
|
+
Modesty.data = :mock
|
98
|
+
|
99
|
+
If you need to pass in more options, use
|
100
|
+
|
101
|
+
Modesty.set_store :redis, :port => 8888, :host => '123.123.123.1'
|
102
|
+
|
103
|
+
## Rails
|
104
|
+
|
105
|
+
By default, Modesty looks in configy/modesty.yml for something like:
|
106
|
+
|
107
|
+
datastore:
|
108
|
+
type: redis
|
109
|
+
port: 6739
|
110
|
+
host: localhost
|
111
|
+
|
112
|
+
paths:
|
113
|
+
experiments: modesty/experiments
|
114
|
+
metrics: modesty/metrics
|
115
|
+
|
116
|
+
In this, the default setup, Modesty will look for experiments
|
117
|
+
in #{Rails.root}/modesty/experiments, and metrics in #{Rails.root}/modesty/metrics.
|
118
|
+
If no config file is found, or you omit something, Modesty will use these settings.
|
119
|
+
Everything in the `datastore:` stanza (sans type: redis) will be passed as options to Redis.new.
|
120
|
+
|
121
|
+
Have fun!
|
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'spec/rake/spectask'
|
4
|
+
|
5
|
+
begin
|
6
|
+
require 'jeweler'
|
7
|
+
Jeweler::Tasks.new do |gem|
|
8
|
+
gem.name = "modesty"
|
9
|
+
gem.summary = %Q{Modesty is simple and scalable split testing and event tracking framework.}
|
10
|
+
gem.description = %Q{Modesty is simple and scalable split testing and event tracking framework. It was inspired by assaf's Vanity (github.com/assaf/vanity).}
|
11
|
+
gem.email = "jay@causes.com"
|
12
|
+
gem.homepage = "http://github.com/causes/modesty"
|
13
|
+
gem.authors = ["Jay Adkisson", "Kevin Ball", "Kristján Pétursson"]
|
14
|
+
gem.add_development_dependency "rspec"
|
15
|
+
gem.add_runtime_dependency "redis"
|
16
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
17
|
+
end
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
Jeweler::RubygemsDotOrgTasks.new
|
23
|
+
|
24
|
+
|
25
|
+
Spec::Rake::SpecTask.new('spec') do |t|
|
26
|
+
t.spec_files = FileList['spec/**/*_spec.rb']
|
27
|
+
t.ruby_opts = ['-Ilib']
|
28
|
+
t.spec_opts = ['-O spec/spec.opts']
|
29
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/init.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'lib', 'modesty.rb')
|
data/lib/modesty.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'yaml'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
|
6
|
+
module Modesty
|
7
|
+
LIB = File.dirname(__FILE__)
|
8
|
+
ROOT = File.expand_path(File.join(LIB, '..'))
|
9
|
+
VENDOR = File.expand_path(File.join(ROOT, 'vendor'))
|
10
|
+
TEST = File.expand_path(File.join(ROOT, 'test'))
|
11
|
+
end
|
12
|
+
|
13
|
+
$:.unshift Modesty::LIB
|
14
|
+
require 'modesty/core_ext.rb'
|
15
|
+
require 'modesty/api.rb'
|
16
|
+
require 'modesty/datastore.rb'
|
17
|
+
require 'modesty/identity.rb'
|
18
|
+
require 'modesty/metric.rb'
|
19
|
+
require 'modesty/experiment.rb'
|
20
|
+
require 'modesty/load.rb'
|
21
|
+
|
22
|
+
if defined? Rails
|
23
|
+
require 'modesty/frameworks/rails'
|
24
|
+
end
|
25
|
+
|
26
|
+
$:.shift
|
data/lib/modesty/api.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
class Array
|
2
|
+
unless method_defined? :histogram
|
3
|
+
def histogram
|
4
|
+
hsh = Hash.new(0)
|
5
|
+
self.each do |e|
|
6
|
+
hsh[e] += 1
|
7
|
+
end
|
8
|
+
hsh
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_h
|
13
|
+
Hash[self]
|
14
|
+
end
|
15
|
+
|
16
|
+
def hashmap
|
17
|
+
self.map do |e|
|
18
|
+
[e, yield(e)]
|
19
|
+
end.to_h
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
class Hash
|
2
|
+
# addition for histogram aggregation
|
3
|
+
# >> {:a=>1, :b=>2} + {:a=>3, :c=>4}
|
4
|
+
# => {:a=>4, :b=>2, :c=>4}
|
5
|
+
def +(other)
|
6
|
+
hash = self.dup
|
7
|
+
other.each do |k, v|
|
8
|
+
if hash.include? k
|
9
|
+
hash[k] += v
|
10
|
+
else
|
11
|
+
hash[k] = v
|
12
|
+
end
|
13
|
+
end
|
14
|
+
hash
|
15
|
+
end
|
16
|
+
|
17
|
+
def map_values!(&blk)
|
18
|
+
self.each do |k,v|
|
19
|
+
self[k] = yield(v)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def map_values(&blk)
|
24
|
+
self.dup.map_values!(&blk)
|
25
|
+
end
|
26
|
+
|
27
|
+
def map!
|
28
|
+
self.keys.each do |k|
|
29
|
+
v = self.delete(k)
|
30
|
+
new_k, new_v = yield [k,v]
|
31
|
+
self[new_k] = new_v
|
32
|
+
end
|
33
|
+
self
|
34
|
+
end
|
35
|
+
|
36
|
+
def to_h
|
37
|
+
self
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
class Symbol
|
2
|
+
unless method_defined? :/
|
3
|
+
def /(other)
|
4
|
+
:"#{self}/#{other}"
|
5
|
+
end
|
6
|
+
end
|
7
|
+
|
8
|
+
alias __old_inspect inspect
|
9
|
+
def inspect
|
10
|
+
s = self.to_s
|
11
|
+
|
12
|
+
#some things should not use this.
|
13
|
+
if (
|
14
|
+
s[0..0] == '/' ||
|
15
|
+
s[-1..-1] == '/' ||
|
16
|
+
s.include?("//") ||
|
17
|
+
s.include?(":")
|
18
|
+
)
|
19
|
+
return self.__old_inspect
|
20
|
+
end
|
21
|
+
|
22
|
+
begin
|
23
|
+
inspected = self.to_s.split(/\//).map { |s| ":#{s}"}.join('/')
|
24
|
+
return inspected
|
25
|
+
rescue
|
26
|
+
return self.__old_inspect
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def <=>(other)
|
31
|
+
self.to_s <=> other.to_s
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Modesty
|
2
|
+
class Datastore
|
3
|
+
class ConnectionError < StandardError; end
|
4
|
+
def connected?
|
5
|
+
self.ping!
|
6
|
+
true
|
7
|
+
rescue ConnectionError
|
8
|
+
false
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :store
|
12
|
+
|
13
|
+
class MetricData
|
14
|
+
def initialize(metric)
|
15
|
+
@metric = metric
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class ExperimentData
|
20
|
+
def initialize(exp)
|
21
|
+
@experiment = exp
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module DatastoreMethods
|
27
|
+
def set_store(type, opts={})
|
28
|
+
@data = case type.to_s
|
29
|
+
when 'redis'
|
30
|
+
require File.join(Modesty::LIB, 'modesty', 'datastore', 'redis')
|
31
|
+
RedisData.new(opts)
|
32
|
+
else
|
33
|
+
puts "Unrecognized datastore #{type}. Defaulting to MockRedis."
|
34
|
+
self.set_store :redis, :mock => true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
alias data= set_store
|
38
|
+
|
39
|
+
def data
|
40
|
+
@data || set_store(:redis, :mock => true)
|
41
|
+
end
|
42
|
+
|
43
|
+
def handle_error(e)
|
44
|
+
raise e
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
class API
|
49
|
+
include DatastoreMethods
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,180 @@
|
|
1
|
+
module Modesty
|
2
|
+
class RedisData < Datastore
|
3
|
+
def self.date_key(date)
|
4
|
+
"%04d-%02d-%02d" % [date.year, date.month, date.day]
|
5
|
+
end
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
if options[:redis]
|
9
|
+
@store = options[:redis]
|
10
|
+
elsif options['mock'] || options[:mock]
|
11
|
+
require File.join(
|
12
|
+
Modesty::VENDOR, 'mock_redis', 'lib', 'mock_redis.rb'
|
13
|
+
)
|
14
|
+
@store = MockRedis.new
|
15
|
+
else
|
16
|
+
$:.push(File.join(Modesty::VENDOR, 'redis-rb', 'lib'))
|
17
|
+
require 'redis'
|
18
|
+
$:.pop
|
19
|
+
|
20
|
+
@store = Redis.new(options)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def ping!
|
25
|
+
self.ping
|
26
|
+
end
|
27
|
+
|
28
|
+
def method_missing(name, *args, &blk)
|
29
|
+
@store.send(name, *args, &blk)
|
30
|
+
rescue Exception => e
|
31
|
+
raise ConnectionError, e.to_s
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.keyify(*args)
|
35
|
+
args.map {|a| a.to_s}.map do |a|
|
36
|
+
(a.is_a?(Date)) ? date_key(a) : a
|
37
|
+
end.map do |k|
|
38
|
+
k.gsub(/[^\w\-\/]/,'_')
|
39
|
+
end.unshift('modesty').join(':')
|
40
|
+
end
|
41
|
+
|
42
|
+
class MetricData < Datastore::MetricData
|
43
|
+
def key(*args)
|
44
|
+
RedisData.keyify('metrics', @metric.slug, *args)
|
45
|
+
end
|
46
|
+
|
47
|
+
def data
|
48
|
+
Modesty.data
|
49
|
+
end
|
50
|
+
|
51
|
+
def key_for_with(*args)
|
52
|
+
key(:with, *args)
|
53
|
+
end
|
54
|
+
|
55
|
+
# -*- raw counts -*- #
|
56
|
+
def count_key(date)
|
57
|
+
key(date, :count)
|
58
|
+
end
|
59
|
+
|
60
|
+
def add_counts(date, count)
|
61
|
+
data.incrby(count_key(date), count)
|
62
|
+
end
|
63
|
+
|
64
|
+
def count(date)
|
65
|
+
data.get(count_key(date)).to_i
|
66
|
+
end
|
67
|
+
|
68
|
+
def count_range(range)
|
69
|
+
keys = range.map { |d| count_key(d) }
|
70
|
+
data.mget(keys).map(&:to_i?)
|
71
|
+
end
|
72
|
+
|
73
|
+
# -*- unidentified users -*- #
|
74
|
+
def count_unidentified_user(date)
|
75
|
+
data.incr(unidentified_users_key(date))
|
76
|
+
end
|
77
|
+
|
78
|
+
def unidentified_users_key(date)
|
79
|
+
key_for_with(:users, date, :unidentified)
|
80
|
+
end
|
81
|
+
|
82
|
+
def unidentified_users(date = :all)
|
83
|
+
data.get(unidentified_users_key(date)).to_i
|
84
|
+
end
|
85
|
+
|
86
|
+
def unidentified_users_range(range)
|
87
|
+
keys = range.map { |d| unidentified_users_key(d) }
|
88
|
+
data.mget(keys).map(&:to_i?)
|
89
|
+
end
|
90
|
+
|
91
|
+
# -*- :with => params -*- #
|
92
|
+
def add_param_counts(date, count, param, id)
|
93
|
+
#puts "data.hincrby(#{key_for_with(param, date).inspect}, #{id.inspect}, #{count.inspect})"
|
94
|
+
data.hincrby(key_for_with(param, date), id, count)
|
95
|
+
end
|
96
|
+
|
97
|
+
def unique(param, date)
|
98
|
+
data.hlen(key_for_with(param, date))
|
99
|
+
end
|
100
|
+
|
101
|
+
def all(param, date)
|
102
|
+
data.hkeys(key_for_with(param, date)).map(&:to_i?)
|
103
|
+
end
|
104
|
+
|
105
|
+
def distribution_by(param, date)
|
106
|
+
dist = data.hvals(key_for_with(param, date)).histogram
|
107
|
+
dist.map! { |k,v| [k,v].map(&:to_i?) }
|
108
|
+
dist
|
109
|
+
end
|
110
|
+
|
111
|
+
def aggregate_by(param, date)
|
112
|
+
agg = data.hgetall(key_for_with(param, date))
|
113
|
+
agg.map! { |k,v| [k,v].map(&:to_i?) }
|
114
|
+
agg
|
115
|
+
end
|
116
|
+
|
117
|
+
def track!(count, with_args)
|
118
|
+
data.pipelined do
|
119
|
+
[:all, Date.today].each do |date|
|
120
|
+
self.add_counts(date, count)
|
121
|
+
|
122
|
+
self.count_unidentified_user(date) unless with_args[:user]
|
123
|
+
|
124
|
+
with_args.each do |param, id|
|
125
|
+
self.add_param_counts(date, count, param, id)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
class ExperimentData < Datastore::ExperimentData
|
134
|
+
def data
|
135
|
+
Modesty.data
|
136
|
+
end
|
137
|
+
|
138
|
+
def key(*args)
|
139
|
+
RedisData.keyify(:experiments, @experiment.slug, *args)
|
140
|
+
end
|
141
|
+
|
142
|
+
def register!(alt, identity)
|
143
|
+
old_alt = self.get_cached_alternative(identity)
|
144
|
+
if old_alt
|
145
|
+
data.srem(self.key(old_alt), identity)
|
146
|
+
end
|
147
|
+
data.sadd(self.key(alt), identity)
|
148
|
+
return alt
|
149
|
+
end
|
150
|
+
|
151
|
+
def get_cached_alternative(identity)
|
152
|
+
@experiment.alternatives.each do |alt|
|
153
|
+
if data.sismember(self.key(alt), identity)
|
154
|
+
return alt
|
155
|
+
end
|
156
|
+
end
|
157
|
+
return nil
|
158
|
+
end
|
159
|
+
|
160
|
+
def users(alt=nil)
|
161
|
+
if alt.nil? #return the union
|
162
|
+
data.sunion(*@experiment.alternatives.map {|a| self.key(a) })
|
163
|
+
else
|
164
|
+
data.smembers(self.key(alt))
|
165
|
+
end.map(&:to_i)
|
166
|
+
end
|
167
|
+
|
168
|
+
def num_users(alt=nil)
|
169
|
+
if alt.nil?
|
170
|
+
@experiment.alternatives.map do |alt|
|
171
|
+
data.scard(self.key(alt)).to_i
|
172
|
+
end.sum
|
173
|
+
else
|
174
|
+
data.scard(self.key(alt)).to_i
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|