blackbeard 0.0.2.0 → 0.0.3.1

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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +9 -0
  3. data/Guardfile +8 -0
  4. data/README.md +162 -20
  5. data/Rakefile +6 -0
  6. data/TODO.md +13 -34
  7. data/blackbeard.gemspec +5 -1
  8. data/console.rb +3 -0
  9. data/dashboard/public/bootstrap3-editable/css/bootstrap-editable.css +663 -0
  10. data/dashboard/public/bootstrap3-editable/img/clear.png +0 -0
  11. data/dashboard/public/bootstrap3-editable/img/loading.gif +0 -0
  12. data/dashboard/public/bootstrap3-editable/js/bootstrap-editable.min.js +7 -0
  13. data/dashboard/public/fonts/glyphicons-halflings-regular.eot +0 -0
  14. data/dashboard/public/fonts/glyphicons-halflings-regular.svg +229 -0
  15. data/dashboard/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  16. data/dashboard/public/fonts/glyphicons-halflings-regular.woff +0 -0
  17. data/dashboard/public/javascripts/bootstrap.min.js +7 -0
  18. data/dashboard/public/javascripts/jquery-1.10.2.min.js +6 -0
  19. data/dashboard/public/stylesheets/application.css +28 -0
  20. data/dashboard/public/stylesheets/bootstrap-theme.css +7 -0
  21. data/dashboard/public/stylesheets/bootstrap.css +7 -0
  22. data/dashboard/routes/base.rb +19 -0
  23. data/dashboard/routes/groups.rb +22 -0
  24. data/dashboard/routes/home.rb +11 -0
  25. data/dashboard/routes/metrics.rb +30 -0
  26. data/dashboard/routes/tests.rb +23 -0
  27. data/dashboard/views/groups/index.erb +22 -0
  28. data/dashboard/views/groups/show.erb +58 -0
  29. data/dashboard/views/index.erb +4 -0
  30. data/dashboard/views/layout.erb +48 -0
  31. data/dashboard/views/metrics/_metric_data.erb +59 -0
  32. data/dashboard/views/metrics/index.erb +23 -0
  33. data/dashboard/views/metrics/show.erb +73 -0
  34. data/dashboard/views/tests/index.erb +21 -0
  35. data/dashboard/views/tests/show.erb +58 -0
  36. data/lib/blackbeard/configuration.rb +8 -1
  37. data/lib/blackbeard/configuration_methods.rb +24 -0
  38. data/lib/blackbeard/context.rb +33 -21
  39. data/lib/blackbeard/dashboard.rb +17 -21
  40. data/lib/blackbeard/dashboard_helpers.rb +29 -0
  41. data/lib/blackbeard/errors.rb +2 -2
  42. data/lib/blackbeard/group.rb +35 -0
  43. data/lib/blackbeard/metric.rb +34 -32
  44. data/lib/blackbeard/metric_data/base.rb +101 -0
  45. data/lib/blackbeard/metric_data/total.rb +39 -0
  46. data/lib/blackbeard/metric_data/unique.rb +58 -0
  47. data/lib/blackbeard/metric_date.rb +11 -0
  48. data/lib/blackbeard/metric_hour.rb +17 -0
  49. data/lib/blackbeard/pirate.rb +33 -22
  50. data/lib/blackbeard/redis_store.rb +46 -2
  51. data/lib/blackbeard/selected_variation.rb +13 -0
  52. data/lib/blackbeard/storable.rb +39 -27
  53. data/lib/blackbeard/storable_attributes.rb +54 -0
  54. data/lib/blackbeard/storable_has_many.rb +60 -0
  55. data/lib/blackbeard/storable_has_set.rb +59 -0
  56. data/lib/blackbeard/test.rb +21 -0
  57. data/lib/blackbeard/version.rb +1 -1
  58. data/lib/blackbeard.rb +0 -8
  59. data/spec/configuration_spec.rb +15 -0
  60. data/spec/context_spec.rb +94 -19
  61. data/spec/dashboard/groups_spec.rb +50 -0
  62. data/spec/dashboard/home_spec.rb +20 -0
  63. data/spec/dashboard/metrics_spec.rb +57 -0
  64. data/spec/dashboard/tests_spec.rb +43 -0
  65. data/spec/group_spec.rb +36 -0
  66. data/spec/metric_data/base_spec.rb +57 -0
  67. data/spec/metric_data/total_spec.rb +116 -0
  68. data/spec/metric_data/unique_spec.rb +91 -0
  69. data/spec/metric_spec.rb +52 -44
  70. data/spec/pirate_spec.rb +32 -15
  71. data/spec/redis_store_spec.rb +121 -0
  72. data/spec/spec_helper.rb +13 -1
  73. data/spec/storable_attributes_spec.rb +47 -0
  74. data/spec/storable_has_many_spec.rb +49 -0
  75. data/spec/storable_has_set_spec.rb +39 -0
  76. data/spec/storable_spec.rb +33 -0
  77. data/spec/test_spec.rb +25 -0
  78. metadata +133 -17
  79. data/lib/blackbeard/dashboard/helpers.rb +0 -8
  80. data/lib/blackbeard/dashboard/views/layout.erb +0 -15
  81. data/lib/blackbeard/dashboard/views/metrics/index.erb +0 -10
  82. data/lib/blackbeard/dashboard/views/metrics/show.erb +0 -16
  83. data/lib/blackbeard/feature.rb +0 -13
  84. data/lib/blackbeard/metric/total.rb +0 -17
  85. data/lib/blackbeard/metric/unique.rb +0 -18
  86. data/spec/dashboard_spec.rb +0 -38
  87. data/spec/total_metric_spec.rb +0 -65
  88. data/spec/unique_metric_spec.rb +0 -60
@@ -0,0 +1,58 @@
1
+ require 'blackbeard/metric_data/base'
2
+
3
+ module Blackbeard
4
+ module MetricData
5
+ class Unique < Base
6
+
7
+ DEFAULT_SEGMENT = 'uniques'
8
+ def add(uid, amount = nil, segment = DEFAULT_SEGMENT)
9
+ add_at(tz.now, uid, amount, segment)
10
+ end
11
+
12
+ def add_at(time, uid, amount = nil, segment = DEFAULT_SEGMENT)
13
+ key = key_for_hour(time)
14
+ segment_key = segment_key(key, segment)
15
+
16
+ db.set_add_member(hours_set_key, key)
17
+ db.set_add_member(key, segment_key)
18
+ db.set_add_member(segment_key, uid)
19
+ #TODO: if not today, blow away rollup keys
20
+ end
21
+
22
+ def result_for_hour(time)
23
+ key = key_for_hour(time)
24
+ segment_keys = db.set_members(key)
25
+ result = {}
26
+ segment_keys.each do |segment_key|
27
+ segment = segment_key.split(/::/).last
28
+ result[segment] = db.set_count(segment_key)
29
+ end
30
+ result
31
+ end
32
+
33
+ def segment_key(key, segment)
34
+ "#{key}::#{segment}"
35
+ end
36
+
37
+ private
38
+
39
+ def merge_results(keys)
40
+ segments = {}
41
+ keys.each do |key|
42
+ segment_keys = db.set_members(key)
43
+ segment_keys.each do |segment_key|
44
+ segment = segment_key.split(/::/).last
45
+ segments[segment] ||= []
46
+ segments[segment].push(segment_key)
47
+ end
48
+ end
49
+ merged_results = {}
50
+ segments.each do |segment, segment_keys|
51
+ merged_results[segment] = db.set_union_count(segment_keys)
52
+ end
53
+ merged_results
54
+ end
55
+ end
56
+ end
57
+ end
58
+
@@ -0,0 +1,11 @@
1
+ module Blackbeard
2
+ class MetricDate
3
+ attr_reader :date, :result
4
+
5
+ def initialize(date, result)
6
+ @date = date
7
+ @result = result
8
+ end
9
+
10
+ end
11
+ end
@@ -0,0 +1,17 @@
1
+ module Blackbeard
2
+ class MetricHour
3
+ attr_reader :hour, :result
4
+
5
+ def initialize(time, result)
6
+ @hour = round_to_beginning_of_hour(time)
7
+ @result = result
8
+ end
9
+
10
+ private
11
+
12
+ def round_to_beginning_of_hour(t)
13
+ t - ((t.min * 60) + t.sec)
14
+ end
15
+
16
+ end
17
+ end
@@ -1,47 +1,58 @@
1
1
  require "blackbeard/context"
2
2
  require "blackbeard/metric"
3
- require "blackbeard/metric/unique"
4
- require "blackbeard/metric/total"
5
- require "blackbeard/feature"
3
+ require "blackbeard/metric_data/unique"
4
+ require "blackbeard/metric_data/total"
5
+ require "blackbeard/test"
6
6
  require "blackbeard/errors"
7
+ require "blackbeard/group"
7
8
 
8
9
  module Blackbeard
9
10
  class Pirate
10
11
  def initialize
11
- @total_metrics = {}
12
- @unique_metrics = {}
13
- @features = {}
12
+ @metrics = {}
13
+ @tests = {}
14
14
  end
15
15
 
16
- def total_metric(name)
17
- @total_metrics[name] ||= Metric::Total.new(name)
16
+ def metric(type, type_id)
17
+ @metrics["#{type}::#{type_id}"] ||= Metric.new(type, type_id)
18
18
  end
19
19
 
20
- def unique_metric(name)
21
- @unique_metrics[name] ||= Metric::Unique.new(name)
20
+ def test(id)
21
+ @tests[id] ||= Test.new(id)
22
22
  end
23
23
 
24
- def feature(name)
25
- @features[name] ||= Feature.new(name)
24
+ def context(*args)
25
+ Context.new(self, *args)
26
26
  end
27
27
 
28
- def context(options = {})
29
- Context.new(self, options)
28
+ def set_context(*args)
29
+ @set_context = context(*args)
30
30
  end
31
31
 
32
- def set_context(options = {})
33
- @set_context = Context.new(self, options)
32
+ def clear_context
33
+ @set_context = nil
34
34
  end
35
35
 
36
- def add_unique(name)
37
- raise MissingContextError unless @set_context
38
- @set_context.add_unique(name)
36
+ def add_unique(id)
37
+ return self unless @set_context
38
+ @set_context.add_unique(id)
39
39
  end
40
40
 
41
- def add_total(name, amount)
42
- raise MissingContextError unless @set_context
43
- @set_context.add_total(name, amount)
41
+ def add_total(id, amount)
42
+ return self unless @set_context
43
+ @set_context.add_total(id, amount)
44
44
  end
45
45
 
46
+ def ab_test(id, options)
47
+ return self unless @set_context
48
+ @set_context.ab_test(id, options)
49
+ end
50
+
51
+ def active?(id)
52
+ return self unless @set_context
53
+ @set_context.active?(id)
54
+ end
55
+
56
+
46
57
  end
47
58
  end
@@ -14,10 +14,20 @@ module Blackbeard
14
14
  redis.keys
15
15
  end
16
16
 
17
+
18
+ # Hash commands
17
19
  def hash_key_set_if_not_exists(hash_key, field, value)
18
20
  redis.hsetnx(hash_key, field, value)
19
21
  end
20
22
 
23
+ def hash_set(hash_key, field, value)
24
+ redis.hset(hash_key, field, value)
25
+ end
26
+
27
+ def hash_multi_set(hash_key, hash)
28
+ redis.mapped_hmset(hash_key, hash) unless hash.empty?
29
+ end
30
+
21
31
  def hash_length(hash_key)
22
32
  redis.hlen(hash_key)
23
33
  end
@@ -26,24 +36,50 @@ module Blackbeard
26
36
  redis.hkeys(hash_key)
27
37
  end
28
38
 
39
+ def hash_get(hash_key, field)
40
+ redis.hget(hash_key, field)
41
+ end
42
+
43
+ def hash_get_all(hash_key)
44
+ redis.hgetall(hash_key)
45
+ end
46
+
47
+ def hash_increment_by_float(hash_key, field, float)
48
+ redis.hincrbyfloat(hash_key, field, float)
49
+ end
50
+
51
+ # Set commands
29
52
  def set_members(set_key)
30
53
  redis.smembers(set_key)
31
54
  end
32
55
 
56
+ def set_remove_member(set_key, member)
57
+ redis.srem(set_key, member)
58
+ end
59
+
33
60
  def set_add_member(set_key, member)
34
61
  redis.sadd(set_key, member)
35
62
  end
36
63
 
64
+ def set_add_members(set_key, *members)
65
+ redis.sadd(set_key, members.flatten)
66
+ end
67
+
37
68
  def set_count(set_key)
38
69
  redis.scard(set_key)
39
70
  end
40
71
 
72
+ def set_union_count(*keys)
73
+ redis.sunionstore('set_union_count', keys.flatten)
74
+ end
75
+
76
+ # String commands
41
77
  def del(*keys)
42
78
  redis.del(*keys)
43
79
  end
44
80
 
45
- def increment_by_float(key, increment)
46
- redis.incrbyfloat(key, increment)
81
+ def increment_by_float(key, float)
82
+ redis.incrbyfloat(key, float)
47
83
  end
48
84
 
49
85
  def increment(key)
@@ -54,5 +90,13 @@ module Blackbeard
54
90
  redis.get(key)
55
91
  end
56
92
 
93
+ def multi_get(*keys)
94
+ redis.mget(*keys.flatten)
95
+ end
96
+
97
+ def set(key, value)
98
+ redis.set(key, value)
99
+ end
100
+
57
101
  end
58
102
  end
@@ -0,0 +1,13 @@
1
+ module Blackbeard
2
+ class SelectedVariation
3
+ def initialize(test, variation)
4
+ @test = test
5
+ @variation = variation
6
+ end
7
+
8
+ def ==(s)
9
+ @test.add_variation(s)
10
+ @variation == s.to_s
11
+ end
12
+ end
13
+ end
@@ -1,19 +1,32 @@
1
+ require 'blackbeard/configuration_methods'
2
+ require 'blackbeard/storable_attributes'
3
+ require 'blackbeard/storable_has_many'
4
+ require 'blackbeard/storable_has_set'
5
+
1
6
  module Blackbeard
2
7
  class Storable
3
- attr_reader :name
8
+ include ConfigurationMethods
9
+ include StorableHasMany
10
+ include StorableHasSet
11
+ include StorableAttributes
4
12
 
5
- def initialize(name)
6
- @name = name.to_s.downcase
7
- db.hash_key_set_if_not_exists(master_key, key, tz.now.to_date)
8
- end
13
+ class << self
14
+ def set_master_key(master_key)
15
+ @master_key = master_key.to_s
16
+ end
9
17
 
10
- def master_key
11
- raise "define master key in the class that inherits from storable"
18
+ def master_key
19
+ return @master_key if defined? @master_key
20
+ return self.superclass.master_key if self.superclass.respond_to?(:master_key)
21
+ raise StorableMasterKeyUndefined, "define master key in the class that inherits from storable"
22
+ end
12
23
  end
13
24
 
14
- def self.new_from_key(key)
15
- name = key.split('::').last
16
- self.new(name)
25
+ attr_reader :id
26
+
27
+ def initialize(id)
28
+ @id = id.to_s.downcase
29
+ db.hash_key_set_if_not_exists(master_key, key, tz.now.to_date)
17
30
  end
18
31
 
19
32
  def self.all_keys
@@ -24,34 +37,33 @@ module Blackbeard
24
37
  db.hash_length(master_key)
25
38
  end
26
39
 
27
- def self.all
28
- all_keys.map{ |key| new_from_key(key) }
40
+ def self.new_from_keys(*keys)
41
+ keys.flatten.map{ |key| new_from_key(key) }
29
42
  end
30
43
 
31
- protected
32
-
33
- def key
34
- "#{master_key}::#{ name }"
44
+ def self.new_from_key(key)
45
+ if key =~ /^#{master_key}::(.+)$/
46
+ new($1)
47
+ else
48
+ nil
49
+ end
35
50
  end
36
51
 
37
- def master_key
38
- self.class.master_key
52
+ def self.all
53
+ all_keys.map{ |key| new_from_key(key) }
39
54
  end
40
55
 
41
- def db
42
- self.class.db
56
+ def ==(o)
57
+ o.class == self.class && o.id == self.id
43
58
  end
44
59
 
45
- def self.db
46
- Blackbeard.db
60
+ def key
61
+ "#{master_key}::#{ id }"
47
62
  end
48
63
 
49
- def tz
50
- self.class.tz
64
+ def master_key
65
+ self.class.master_key
51
66
  end
52
67
 
53
- def self.tz
54
- Blackbeard.tz
55
- end
56
68
  end
57
69
  end
@@ -0,0 +1,54 @@
1
+ module Blackbeard
2
+ module StorableAttributes
3
+ def self.included(base)
4
+ base.send :include, InstanceMethods
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module ClassMethods
9
+ def storable_attributes=(x)
10
+ @storable_attributes = x
11
+ end
12
+
13
+ def storable_attributes
14
+ return @storable_attributes if defined? @storable_attributes
15
+ return self.superclass.storable_attributes if self.superclass.respond_to?(:storable_attributes)
16
+ []
17
+ end
18
+
19
+ def string_attributes(*attributes)
20
+ self.storable_attributes += attributes
21
+ attributes.each do |method_name|
22
+ method_name = method_name.to_sym
23
+ send :define_method, method_name do
24
+ storable_attributes_hash[method_name.to_s]
25
+ end
26
+ send :define_method, "#{method_name}=".to_sym do |value|
27
+ db.hash_set(attributes_hash_key, method_name, value)
28
+ storable_attributes_hash[method_name.to_s] = value
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ module InstanceMethods
35
+ def update_attributes(tainted_params)
36
+ attributes = self.class.storable_attributes
37
+ safe_attributes = tainted_params.keys.select{ |k| attributes.include?(k.to_sym) }
38
+ safe_attributes.each do |attribute|
39
+ self.send("#{attribute}=".to_sym, tainted_params[attribute])
40
+ end
41
+ end
42
+
43
+ def storable_attributes_hash
44
+ @storable_attributes ||= db.hash_get_all(attributes_hash_key)
45
+ end
46
+
47
+ def attributes_hash_key
48
+ "#{key}::attributes"
49
+ end
50
+
51
+ end
52
+
53
+ end
54
+ end
@@ -0,0 +1,60 @@
1
+ module Blackbeard
2
+ module StorableHasMany
3
+ def self.included(base)
4
+ base.send :include, InstanceMethods
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def has_many(options = {})
14
+ options.each_pair do |plural, klass|
15
+ _has_many(plural, klass)
16
+ end
17
+ end
18
+
19
+ def _has_many(plural, klass)
20
+ plural = plural.to_s.downcase
21
+ singular = klass.name.split('::').last.downcase
22
+
23
+ methods = <<-END_OF_RUBY
24
+
25
+ def has_#{singular}?(o)
26
+ #{singular}_ids.include?(o.id)
27
+ end
28
+
29
+ def add_#{singular}(o)
30
+ db.set_add_member(#{plural}_set_key, o.key) unless has_#{singular}?(o)
31
+ \@#{plural} = nil
32
+ end
33
+
34
+ def remove_#{singular}(o)
35
+ db.set_remove_member(#{plural}_set_key, o.key)
36
+ \@#{plural} = nil
37
+ end
38
+
39
+ def #{plural}
40
+ \@#{plural} ||= #{klass.name}.new_from_keys(#{singular}_keys)
41
+ end
42
+
43
+ def #{singular}_ids
44
+ #{plural}.map{ |g| g.id }
45
+ end
46
+
47
+ def #{singular}_keys
48
+ db.set_members(#{plural}_set_key)
49
+ end
50
+
51
+ def #{plural}_set_key
52
+ key+"::#{plural}"
53
+ end
54
+ END_OF_RUBY
55
+
56
+ class_eval(methods)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ module Blackbeard
2
+ module StorableHasSet
3
+ def self.included(base)
4
+ base.send :include, InstanceMethods
5
+ base.extend ClassMethods
6
+ end
7
+
8
+ module InstanceMethods
9
+ end
10
+
11
+ module ClassMethods
12
+
13
+ def has_set(options = {})
14
+ options.each_pair do |plural, singular|
15
+ _has_set(plural, singular)
16
+ end
17
+ end
18
+
19
+ def _has_set(plural, singular)
20
+ plural = plural.to_s.downcase
21
+ singular = singular.to_s.downcase
22
+
23
+ methods = <<-END_OF_RUBY
24
+
25
+ def has_#{singular}?(o)
26
+ #{plural}.include?(o)
27
+ end
28
+
29
+ def add_#{singular}(o)
30
+ db.set_add_member(#{plural}_key, o) unless has_#{singular}?(o)
31
+ \@#{plural} = nil
32
+ end
33
+
34
+ def add_#{plural}(*args)
35
+ args.flatten.reject!{|o| has_#{singular}?(o) }
36
+ db.set_add_members(#{plural}_key, args) unless args.empty?
37
+ \@#{plural} = nil
38
+ end
39
+
40
+ def remove_#{singular}(o)
41
+ db.set_remove_member(#{plural}_key, o)
42
+ \@#{plural} = nil
43
+ end
44
+
45
+ def #{plural}
46
+ \@#{plural} ||= db.set_members(#{plural}_key)
47
+ end
48
+
49
+ def #{plural}_key
50
+ key+"::#{plural}"
51
+ end
52
+
53
+ END_OF_RUBY
54
+
55
+ class_eval(methods)
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,21 @@
1
+ require 'blackbeard/storable'
2
+
3
+ module Blackbeard
4
+ class Test < Storable
5
+ set_master_key :tests
6
+ string_attributes :name, :description
7
+ has_set :variations => :variation
8
+
9
+ def select_variation
10
+ # add :off unless :off, :control, :default, or :inactive
11
+ :off
12
+ end
13
+
14
+ def name
15
+ storable_attributes_hash['name'] || id
16
+ end
17
+
18
+ private
19
+
20
+ end
21
+ end
@@ -1,3 +1,3 @@
1
1
  module Blackbeard
2
- VERSION = "0.0.2.0"
2
+ VERSION = "0.0.3.1"
3
3
  end
data/lib/blackbeard.rb CHANGED
@@ -5,14 +5,6 @@ module Blackbeard
5
5
  extend self
6
6
  attr_accessor :config
7
7
 
8
- def tz
9
- config.tz
10
- end
11
-
12
- def db
13
- config.db
14
- end
15
-
16
8
  def self.pirate
17
9
  @config ||= Configuration.new
18
10
  yield(config)
@@ -0,0 +1,15 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module Blackbeard
4
+ describe Configuration do
5
+ let(:config) { Configuration.new }
6
+ describe "define_group" do
7
+ it "should store the block in group_definiations" do
8
+ config.define_group(:hello) do |user,controller|
9
+ "world"
10
+ end
11
+ config.group_definitions[:hello].call.should == 'world'
12
+ end
13
+ end
14
+ end
15
+ end