blackbeard 0.0.4.0 → 0.0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +44 -16
  3. data/TODO.md +3 -1
  4. data/dashboard/routes/cohorts.rb +25 -0
  5. data/dashboard/routes/features.rb +7 -4
  6. data/dashboard/routes/groups.rb +6 -3
  7. data/dashboard/routes/metrics.rb +25 -5
  8. data/dashboard/routes/tests.rb +6 -3
  9. data/dashboard/views/cohorts/index.erb +22 -0
  10. data/dashboard/views/cohorts/show.erb +41 -0
  11. data/dashboard/views/groups/show.erb +2 -2
  12. data/dashboard/views/layout.erb +1 -0
  13. data/dashboard/views/metrics/show.erb +37 -10
  14. data/dashboard/views/shared/_charts.erb +20 -0
  15. data/lib/blackbeard/chart.rb +65 -0
  16. data/lib/blackbeard/chartable.rb +47 -0
  17. data/lib/blackbeard/cohort.rb +48 -0
  18. data/lib/blackbeard/cohort_data.rb +72 -0
  19. data/lib/blackbeard/cohort_metric.rb +47 -0
  20. data/lib/blackbeard/context.rb +11 -2
  21. data/lib/blackbeard/dashboard.rb +2 -3
  22. data/lib/blackbeard/dashboard_helpers.rb +0 -22
  23. data/lib/blackbeard/errors.rb +2 -0
  24. data/lib/blackbeard/group.rb +3 -0
  25. data/lib/blackbeard/group_metric.rb +39 -0
  26. data/lib/blackbeard/metric.rb +30 -14
  27. data/lib/blackbeard/metric_data/base.rb +18 -35
  28. data/lib/blackbeard/metric_data/total.rb +2 -1
  29. data/lib/blackbeard/metric_data/uid_generator.rb +38 -0
  30. data/lib/blackbeard/metric_data/unique.rb +1 -1
  31. data/lib/blackbeard/metric_date.rb +10 -0
  32. data/lib/blackbeard/metric_hour.rb +8 -0
  33. data/lib/blackbeard/pirate.rb +18 -0
  34. data/lib/blackbeard/redis_store.rb +10 -1
  35. data/lib/blackbeard/storable.rb +1 -0
  36. data/lib/blackbeard/version.rb +1 -1
  37. data/spec/chart_spec.rb +38 -0
  38. data/spec/chartable_spec.rb +56 -0
  39. data/spec/cohort_data_spec.rb +142 -0
  40. data/spec/cohort_metric_spec.rb +26 -0
  41. data/spec/cohort_spec.rb +31 -0
  42. data/spec/context_spec.rb +9 -1
  43. data/spec/dashboard/cohorts_spec.rb +43 -0
  44. data/spec/dashboard/groups_spec.rb +0 -7
  45. data/spec/dashboard/metrics_spec.rb +35 -0
  46. data/spec/group_metric_spec.rb +26 -0
  47. data/spec/metric_data/base_spec.rb +0 -16
  48. data/spec/metric_data/uid_generator_spec.rb +40 -0
  49. data/spec/metric_spec.rb +23 -12
  50. data/spec/pirate_spec.rb +22 -1
  51. data/spec/redis_store_spec.rb +8 -2
  52. data/spec/storable_spec.rb +3 -0
  53. metadata +29 -3
  54. data/dashboard/views/metrics/_metric_data.erb +0 -59
@@ -1,32 +1,22 @@
1
1
  require 'blackbeard/metric_hour'
2
2
  require 'blackbeard/metric_date'
3
3
  require 'date'
4
+ require 'blackbeard/chart'
5
+ require 'blackbeard/metric_data/uid_generator'
6
+ require 'blackbeard/chartable'
4
7
 
5
8
  module Blackbeard
6
9
  module MetricData
7
10
  class Base
8
11
  include ConfigurationMethods
9
- attr_reader :metric, :group
10
12
 
11
- def initialize(metric, group = nil)
13
+ attr_reader :metric, :group, :cohort
14
+
15
+ # TODO: refactor so you pass group and cohort in as options
16
+ def initialize(metric, group = nil, cohort = nil)
12
17
  @metric = metric
13
18
  @group = group
14
- end
15
-
16
- def recent_days(count=28, starting_on = tz.now.to_date)
17
- Array(0..count-1).map do |offset|
18
- date = starting_on - offset
19
- result = result_for_day(date)
20
- Blackbeard::MetricDate.new(date, result)
21
- end
22
- end
23
-
24
- def recent_hours(count = 24, starting_at = tz.now)
25
- Array(0..count-1).map do |offset|
26
- hour = starting_at - (offset * 3600)
27
- result = result_for_hour(hour)
28
- Blackbeard::MetricHour.new(hour, result)
29
- end
19
+ @cohort = cohort
30
20
  end
31
21
 
32
22
  def hour_keys_for_day(date)
@@ -44,27 +34,18 @@ module Blackbeard
44
34
 
45
35
  def key
46
36
  @key ||= begin
47
- lookup_hash = "metric_data_keys"
48
- lookup_field = "metric-#{metric.id}"
49
- lookup_field += "::group-#{group.id}" if group
50
- uid = db.hash_get(lookup_hash, lookup_field)
51
- if uid.nil?
52
- uid = db.increment("metric_data_next_uid")
53
- # write and read to avoid race conditional writes
54
- db.hash_key_set_if_not_exists(lookup_hash, lookup_field, uid)
55
- uid = db.hash_get(lookup_hash, lookup_field)
56
- end
57
37
  "data::#{uid}"
58
38
  end
59
39
  end
60
40
 
41
+ def uid
42
+ uid = UidGenerator.new(self).uid
43
+ end
44
+
61
45
  def segments
62
- if group && group.segments.any?
63
- group.segments
64
- else
65
- [self.class::DEFAULT_SEGMENT]
66
- end
46
+ [self.class::DEFAULT_SEGMENT]
67
47
  end
48
+
68
49
  private
69
50
 
70
51
  def generate_result_for_day(date)
@@ -75,7 +56,6 @@ module Blackbeard
75
56
  result
76
57
  end
77
58
 
78
-
79
59
  def hour_keys
80
60
  db.set_members(hours_set_key)
81
61
  end
@@ -93,7 +73,10 @@ module Blackbeard
93
73
  end
94
74
 
95
75
  def key_for_hour(time)
96
- "#{key}::#{ time.strftime("%Y%m%d%H") }"
76
+ if time.kind_of?(Time)
77
+ time = time.strftime("%Y%m%d%H")
78
+ end
79
+ "#{key}::#{ time }"
97
80
  end
98
81
 
99
82
  end
@@ -11,10 +11,11 @@ module Blackbeard
11
11
  end
12
12
 
13
13
  def add_at(time, uid, amount = 1, segment = DEFAULT_SEGMENT)
14
+ # TODO: ensure time is in correct timezone
14
15
  key = key_for_hour(time)
15
16
  db.set_add_member(hours_set_key, key)
16
17
  db.hash_increment_by_float(key, segment, amount.to_f)
17
- #TODO: if not today, blow away rollup keys
18
+ # TODO: if not today, blow away rollup keys
18
19
  end
19
20
 
20
21
  def result_for_hour(time)
@@ -0,0 +1,38 @@
1
+ module Blackbeard
2
+ module MetricData
3
+ class UidGenerator
4
+ include ConfigurationMethods
5
+
6
+ def initialize(metric_data)
7
+ @metric = metric_data.metric
8
+ @group = metric_data.group
9
+ @cohort = metric_data.cohort
10
+ end
11
+
12
+ def uid
13
+ db.hash_get(lookup_hash, lookup_field) || generate_uid
14
+ end
15
+
16
+ private
17
+
18
+ def lookup_hash
19
+ "metric_data_keys"
20
+ end
21
+
22
+ def lookup_field
23
+ lookup_field = "metric-#{@metric.id}"
24
+ lookup_field += "::group-#{@group.id}" if @group
25
+ lookup_field += "::group-#{@cohort.id}" if @cohort
26
+ lookup_field
27
+ end
28
+
29
+ def generate_uid
30
+ uid = db.increment("metric_data_next_uid")
31
+ # write and read to avoid race conditional writes
32
+ db.hash_key_set_if_not_exists(lookup_hash, lookup_field, uid)
33
+ db.hash_get(lookup_hash, lookup_field)
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -10,6 +10,7 @@ module Blackbeard
10
10
  end
11
11
 
12
12
  def add_at(time, uid, amount = nil, segment = DEFAULT_SEGMENT)
13
+ #TODO: unsure time is in proper timezone
13
14
  key = key_for_hour(time)
14
15
  segment_key = segment_key(key, segment)
15
16
 
@@ -55,4 +56,3 @@ module Blackbeard
55
56
  end
56
57
  end
57
58
  end
58
-
@@ -1,5 +1,6 @@
1
1
  module Blackbeard
2
2
  class MetricDate
3
+ #TODO refactor with MetricHour to be compaosed
3
4
  attr_reader :date, :result
4
5
 
5
6
  def initialize(date, result)
@@ -7,5 +8,14 @@ module Blackbeard
7
8
  @result = result
8
9
  end
9
10
 
11
+ def results_for(segments)
12
+ segments.map{|s| result[s].to_f }
13
+ end
14
+
15
+ def result_rows(segments)
16
+ [@date] + results_for(segments)
17
+ end
18
+
19
+
10
20
  end
11
21
  end
@@ -7,6 +7,14 @@ module Blackbeard
7
7
  @result = result
8
8
  end
9
9
 
10
+ def results_for(segments)
11
+ segments.map{|s| result[s].to_f }
12
+ end
13
+
14
+ def result_rows(segments)
15
+ [@hours] + results_for(segments)
16
+ end
17
+
10
18
  private
11
19
 
12
20
  def round_to_beginning_of_hour(t)
@@ -6,6 +6,7 @@ require "blackbeard/test"
6
6
  require "blackbeard/errors"
7
7
  require "blackbeard/group"
8
8
  require "blackbeard/feature"
9
+ require "blackbeard/cohort"
9
10
 
10
11
  module Blackbeard
11
12
  class Pirate
@@ -13,16 +14,22 @@ module Blackbeard
13
14
  @metrics = {}
14
15
  @tests = {}
15
16
  @features = {}
17
+ @cohorts = {}
16
18
  end
17
19
 
18
20
  def metric(type, type_id)
19
21
  @metrics["#{type}::#{type_id}"] ||= Metric.find_or_create(type, type_id)
20
22
  end
21
23
 
24
+ # TODO: abstract out memoization to a cache class
22
25
  def test(id)
23
26
  @tests[id] ||= Test.find_or_create(id)
24
27
  end
25
28
 
29
+ def cohort(id)
30
+ @cohorts[id] ||= Cohort.find_or_create(id)
31
+ end
32
+
26
33
  def feature(id)
27
34
  @features[id] ||= Feature.find_or_create(id)
28
35
  end
@@ -39,6 +46,7 @@ module Blackbeard
39
46
  @set_context = nil
40
47
  end
41
48
 
49
+ # TODO: metaprogram all the context delegators
42
50
  def add_unique(id)
43
51
  return self unless @set_context
44
52
  @set_context.add_unique(id)
@@ -49,6 +57,16 @@ module Blackbeard
49
57
  @set_context.add_total(id, amount)
50
58
  end
51
59
 
60
+ def add_to_cohort(id, timestamp = nil)
61
+ return self unless @set_context
62
+ @set_context.add_to_cohort(id, timestamp)
63
+ end
64
+
65
+ def add_to_cohort!(id, timestamp = nil)
66
+ return self unless @set_context
67
+ @set_context.add_to_cohort!(id, timestamp)
68
+ end
69
+
52
70
  def ab_test(id, options)
53
71
  return self unless @set_context
54
72
  @set_context.ab_test(id, options)
@@ -16,6 +16,7 @@ module Blackbeard
16
16
 
17
17
 
18
18
  # Hash commands
19
+ # TODO: rename to hash_set_if_not_exisits
19
20
  def hash_key_set_if_not_exists(hash_key, field, value)
20
21
  redis.hsetnx(hash_key, field, value)
21
22
  end
@@ -28,6 +29,10 @@ module Blackbeard
28
29
  redis.mapped_hmset(hash_key, hash) unless hash.empty?
29
30
  end
30
31
 
32
+ def hash_multi_get(hash_key, *fields)
33
+ redis.hmget(hash_key, *fields) unless fields.empty?
34
+ end
35
+
31
36
  def hash_length(hash_key)
32
37
  redis.hlen(hash_key)
33
38
  end
@@ -44,8 +49,12 @@ module Blackbeard
44
49
  redis.hgetall(hash_key)
45
50
  end
46
51
 
52
+ def hash_increment_by(hash_key, field, int)
53
+ redis.hincrby(hash_key, field, int.to_i)
54
+ end
55
+
47
56
  def hash_increment_by_float(hash_key, field, float)
48
- redis.hincrbyfloat(hash_key, field, float)
57
+ redis.hincrbyfloat(hash_key, field, float.to_f)
49
58
  end
50
59
 
51
60
  def hash_field_exists(hash_key, field)
@@ -104,6 +104,7 @@ module Blackbeard
104
104
  @new_record = false
105
105
  end
106
106
  self.class.on_save_methods.each{ |m| self.send(m) }
107
+ true
107
108
  end
108
109
 
109
110
  def reload
@@ -1,3 +1,3 @@
1
1
  module Blackbeard
2
- VERSION = "0.0.4.0"
2
+ VERSION = "0.0.5.0"
3
3
  end
@@ -0,0 +1,38 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module Blackbeard
4
+ describe Chart do
5
+ let(:title) { 'This is a title' }
6
+ let(:chart){
7
+ Chart.new(
8
+ :title => title,
9
+ :columns=>["Date","Total","Uniques"],
10
+ :rows=>[
11
+ [Date.today, 10.5, 12],
12
+ [Date.today+1, 11.5, 13],
13
+ [Date.today+2, 0, 1],
14
+ ])
15
+ }
16
+ it "should put title and height in options" do
17
+ chart.options.should == {:title => title, :height => 300}
18
+ end
19
+
20
+ describe "data" do
21
+ it "should have cols" do
22
+ chart.data[:cols].should == [
23
+ {:label=>"Date", :type=>"string"},
24
+ {:label=>"Total", :type=>"number"},
25
+ {:label=>"Uniques", :type=>"number"}
26
+ ]
27
+ end
28
+ it "should have rows" do
29
+ chart.data[:rows].should == [
30
+ {:c=>[{:v=>Date.today}, {:v=>10.5}, {:v=>12}]},
31
+ {:c=>[{:v=>Date.today+1}, {:v=>11.5}, {:v=>13}]},
32
+ {:c=>[{:v=>Date.today+2}, {:v=>0}, {:v=>1}]}
33
+ ]
34
+ end
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,56 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module Blackbeard
4
+ module MetricData
5
+ describe Chartable do
6
+
7
+ class ExampleChartable
8
+ include Chartable
9
+
10
+ def chartable_result_for_day(day)
11
+ {'segment1' => 0}
12
+ end
13
+
14
+ def chartable_result_for_hour(hour)
15
+ {'segment1' => 0}
16
+ end
17
+
18
+ def chartable_segments
19
+ ['segment1']
20
+ end
21
+
22
+ end
23
+
24
+ let(:example){ ExampleChartable.new }
25
+
26
+ describe "recent_hours" do
27
+ let(:start_at) { Time.new(2014,1,1,12,0,0) }
28
+
29
+ it "should return results for recent hours" do
30
+ example.recent_hours(3, start_at).should have(3).metric_hours
31
+ end
32
+ end
33
+
34
+ describe "recent_days" do
35
+ let(:start_on) { Date.new(2014,1,3) }
36
+
37
+ it "should return results for recent days" do
38
+ example.recent_days(3, start_on).should have(3).metric_days
39
+ end
40
+ end
41
+
42
+ describe "recent_hours_chart" do
43
+ it "should return a chart obj" do
44
+ example.recent_hours_chart.should be_a(Chart)
45
+ end
46
+ end
47
+
48
+ describe "recent_days_chart" do
49
+ it "should return a chart obj" do
50
+ example.recent_days_chart.should be_a(Chart)
51
+ end
52
+ end
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,142 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ module Blackbeard
4
+ describe Cohort do
5
+ let(:uid){ "an id" }
6
+ let(:hour){ Time.new(2014,3,5,1) }
7
+ let(:hour_id){ data.send(:hour_id, hour)}
8
+ let(:different_hour){ Time.new(2014,3,5,2) }
9
+ let(:different_hour_id){ data.send(:hour_id, different_hour)}
10
+ let(:cohort) { Cohort.new(:happy) }
11
+ let(:data) { cohort.data }
12
+
13
+ describe "#add with force" do
14
+ context "no pre-existing cohort" do
15
+ it "should call add without force" do
16
+ data.should_receive(:add_without_force).with(uid, hour)
17
+ data.add_with_force(uid, hour)
18
+ end
19
+ it "should return true" do
20
+ data.add_with_force(uid, hour).should be_true
21
+ end
22
+ end
23
+
24
+ context "already in same cohort" do
25
+ before :each do
26
+ data.add_without_force(uid, hour)
27
+ end
28
+ it "should not increment the hours" do
29
+ expect{
30
+ data.add_with_force(uid, hour)
31
+ }.to_not change{ data.participants_for_hour(hour) }
32
+ end
33
+ it "should not update the participants" do
34
+ expect{
35
+ data.add_with_force(uid, hour)
36
+ }.to_not change{ data.hour_id_for_participant(uid) }
37
+ end
38
+ it "should return true" do
39
+ data.add_with_force(uid, hour).should be_true
40
+ end
41
+ end
42
+
43
+ context "already in different cohort" do
44
+ before :each do
45
+ data.add_without_force(uid, different_hour)
46
+ end
47
+ it "should de-increment the existing hour field" do
48
+ expect{
49
+ data.add_with_force(uid, hour)
50
+ }.to change{ data.participants_for_hour(different_hour) }.by(-1)
51
+ end
52
+ it "should increment the new hour field" do
53
+ expect{
54
+ data.add_with_force(uid, hour)
55
+ }.to change{ data.participants_for_hour(hour) }.by(1)
56
+ end
57
+ it "should update the participant to the current hour" do
58
+ expect{
59
+ data.add_with_force(uid, hour)
60
+ }.to change{ data.hour_id_for_participant(uid) }.from(different_hour_id).to(hour_id)
61
+ end
62
+ end
63
+ end
64
+
65
+ describe "#add without force" do
66
+ context "already in cohort" do
67
+ before :each do
68
+ data.add_without_force(uid, different_hour)
69
+ end
70
+ it "should not update the paricipant to the current hour" do
71
+ expect{
72
+ data.add_without_force(uid, hour)
73
+ }.to_not change{ data.hour_id_for_participant(uid) }
74
+ end
75
+ it "should not increment the new hour field" do
76
+ expect{
77
+ data.add_without_force(uid, hour)
78
+ }.to_not change{ data.participants_for_hour(hour) }
79
+ end
80
+ it "should return false" do
81
+ data.add_without_force(uid, hour).should be_false
82
+ end
83
+ end
84
+
85
+ context "not in cohort" do
86
+ it "should update the participant to the current hour" do
87
+ expect{
88
+ data.add_without_force(uid, hour)
89
+ }.to change{ data.hour_id_for_participant(uid) }.from(nil).to(hour_id)
90
+ end
91
+ it "should increment the new hour field" do
92
+ expect{
93
+ data.add_without_force(uid, hour)
94
+ }.to change{ data.participants_for_hour(hour) }.by(1)
95
+ end
96
+ it "should return true" do
97
+ data.add_without_force(uid, hour).should be_true
98
+ end
99
+ end
100
+ end
101
+
102
+ describe "countint participants" do
103
+ let(:aug22) { Date.new(2003,8,22) }
104
+ let(:aug22_1pm) { Time.new(2003,8,22,13) }
105
+ let(:aug22_11am) { Time.new(2003,8,22,11) }
106
+ let(:aug22_9am) { Time.new(2003,8,22,9) }
107
+ let(:aug23_3pm) { Time.new(2003,8,23,15) }
108
+ let(:context1) { double :unique_identifier => '1' }
109
+ let(:context2) { double :unique_identifier => '2' }
110
+ let(:context3) { double :unique_identifier => '3' }
111
+ let(:context4) { double :unique_identifier => '4' }
112
+
113
+ before :each do
114
+ cohort.add(context1, aug22_1pm)
115
+ cohort.add(context2, aug22_1pm)
116
+ cohort.add(context3, aug22_11am)
117
+ cohort.add(context4, aug23_3pm)
118
+ end
119
+
120
+ describe "participants for hour" do
121
+ it "should return the count for each hour" do
122
+ data.participants_for_hour(aug22_1pm).should == 2
123
+ data.participants_for_hour(aug22_11am).should == 1
124
+ data.participants_for_hour(aug22_9am).should == 0
125
+ end
126
+ end
127
+
128
+ describe "participants for hours" do
129
+ it "should return the count for each hour" do
130
+ data.participants_for_hours([aug22_9am, aug22_11am, aug22_1pm]).should == [0,1,2]
131
+ end
132
+ end
133
+
134
+ describe "participants for day" do
135
+ it "should sum the day" do
136
+ data.participants_for_day(aug22).should == 3
137
+ end
138
+ end
139
+
140
+ end
141
+ end
142
+ end