blackbeard 0.0.4.0 → 0.0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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