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
@@ -0,0 +1,65 @@
1
+ module Blackbeard
2
+ class Chart
3
+ attr_reader :dom_id
4
+
5
+ def initialize(options)
6
+ [:dom_id, :height, :title].each{|m| instance_variable_set("@#{m}", options[m]) }
7
+ [:rows, :columns].each{|m| instance_variable_set("@#{m}", options[m] || []) }
8
+ end
9
+
10
+ def options
11
+ {:title => @title, :height => height}
12
+ end
13
+
14
+ def data
15
+ {:rows => rows, :cols => columns}
16
+ end
17
+
18
+ private
19
+
20
+ def height
21
+ @height || 300
22
+ end
23
+
24
+ def rows
25
+ @rows.map{ |r| row(r) }
26
+ end
27
+
28
+ def row(r)
29
+ { :c => r.map{ |c| row_cell(c) } }
30
+ end
31
+
32
+ def row_cell(cell_value)
33
+ { :v => cell_value }
34
+ end
35
+
36
+ def columns
37
+ types = column_types
38
+ @columns.map{ |label| column(label,types.shift) }
39
+ end
40
+
41
+ def column_types
42
+ @rows.first.map{ |cell_value| cell_type(cell_value) }
43
+ end
44
+
45
+ def cell_type(value)
46
+ case value.class.name
47
+ when 'Fixnum'
48
+ 'number'
49
+ when 'Float'
50
+ 'number'
51
+ when 'String'
52
+ 'string'
53
+ when 'Date'
54
+ 'string'
55
+ else
56
+ 'string'
57
+ end
58
+ end
59
+
60
+ def column(label, type)
61
+ {:label => label, :type => type}
62
+ end
63
+
64
+ end
65
+ end
@@ -0,0 +1,47 @@
1
+ module Blackbeard
2
+ module Chartable
3
+
4
+ # In your class define 3 methods:
5
+ # * chartable_segments
6
+ # * chartable_result_for_hour
7
+ # * chartable_result_for_day
8
+
9
+ def recent_days(count=28, starting_on = tz.now.to_date)
10
+ Array(0..count-1).map do |offset|
11
+ date = starting_on - offset
12
+ result = chartable_result_for_day(date)
13
+ Blackbeard::MetricDate.new(date, result)
14
+ end
15
+ end
16
+
17
+ def recent_hours(count = 24, starting_at = tz.now)
18
+ Array(0..count-1).map do |offset|
19
+ hour = starting_at - (offset * 3600)
20
+ result = chartable_result_for_hour(hour)
21
+ Blackbeard::MetricHour.new(hour, result)
22
+ end
23
+ end
24
+
25
+ def recent_hours_chart(count = 24, starting_at = tz.now)
26
+ data = recent_hours(count, starting_at)
27
+ title = "Last #{count} Hours"
28
+ Chart.new(
29
+ :dom_id => 'recent_hour_chart',
30
+ :title => title,
31
+ :columns => ['Hour']+chartable_segments,
32
+ :rows => data.map{ |metric_hour| metric_hour.result_rows(chartable_segments) }
33
+ )
34
+ end
35
+
36
+ def recent_days_chart(count = 28, starting_on = tz.now.to_date)
37
+ data = recent_days(count, starting_on)
38
+ Chart.new(
39
+ :dom_id => 'recent_days_chart',
40
+ :title => "Last #{count} Days",
41
+ :columns => ['Day']+chartable_segments,
42
+ :rows => data.map{ |metric_date| metric_date.result_rows(chartable_segments) }
43
+ )
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,48 @@
1
+ require 'blackbeard/storable'
2
+ require 'blackbeard/cohort_data'
3
+
4
+ module Blackbeard
5
+ class Cohort < Storable
6
+ include Chartable
7
+
8
+ set_master_key :cohorts
9
+ string_attributes :name, :description
10
+
11
+ def add(context, timestamp = nil, force = false)
12
+ save if new_record?
13
+ uid = context.unique_identifier
14
+ #TODO: Make sure timestamp is in correct tz
15
+ timestamp ||= tz.now
16
+ return (force) ? data.add_with_force(uid, timestamp) : data.add_without_force(uid, timestamp)
17
+ end
18
+
19
+ def data
20
+ @data ||= CohortData.new(self)
21
+ end
22
+
23
+ def name
24
+ storable_attributes_hash['name'] || id
25
+ end
26
+
27
+ def metric_data(metric)
28
+ CohortMetric.new(self,metric).metric_data
29
+ end
30
+
31
+ def hour_id_for_participant(uid)
32
+ data.hour_id_for_participant(uid)
33
+ end
34
+
35
+ def chartable_segments
36
+ ['participants']
37
+ end
38
+
39
+ def chartable_result_for_hour(hour)
40
+ {'participants' => data.participants_for_hour(hour) }
41
+ end
42
+
43
+ def chartable_result_for_day(date)
44
+ {'participants' => data.participants_for_day(date) }
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,72 @@
1
+ require 'date'
2
+
3
+ module Blackbeard
4
+ class CohortData
5
+ include ConfigurationMethods
6
+
7
+ def initialize(cohort)
8
+ @cohort = cohort
9
+ end
10
+
11
+ def hour_id_for_participant(uid)
12
+ db.hash_get(participants_hash_key, uid)
13
+ end
14
+
15
+ def participants_for_hour(time)
16
+ db.hash_get(hours_hash_key, hour_id(time)).to_i
17
+ end
18
+
19
+ def participants_for_day(date)
20
+ start_of_day = date.to_time
21
+ hours_in_day = Array(0..23).map{|x| start_of_day + (3600 * x) }
22
+ participants_by_hour = participants_for_hours(hours_in_day)
23
+ participants_by_hour.reduce(:+)
24
+ end
25
+
26
+ def participants_for_hours(hours)
27
+ hour_ids = hours.map{ |hour| hour_id(hour) }
28
+ db.hash_multi_get(hours_hash_key, *hour_ids).map{|s| s.to_i }
29
+ end
30
+
31
+ def add_with_force(uid, hour)
32
+ prior_hour_id = db.hash_get(participants_hash_key, uid)
33
+
34
+ # Force not necessary
35
+ return add_without_force(uid, hour) unless prior_hour_id
36
+
37
+ hour_id = hour_id(hour)
38
+ # No change in cohort status
39
+ return true if prior_hour_id == hour_id
40
+
41
+ # Deincrement old, increment new
42
+ db.hash_increment_by(hours_hash_key, prior_hour_id, -1)
43
+ db.hash_increment_by(hours_hash_key, hour_id, 1)
44
+ db.hash_set(participants_hash_key, uid, hour_id)
45
+ true
46
+ end
47
+
48
+ def add_without_force(uid, hour)
49
+ hour_id = hour_id(hour)
50
+ # Check if uid is alreaty in cohort
51
+ return false unless db.hash_key_set_if_not_exists(participants_hash_key, uid, hour_id)
52
+ db.hash_increment_by(hours_hash_key, hour_id, 1)
53
+ true
54
+ end
55
+
56
+ private
57
+
58
+ def hour_id(time)
59
+ time.strftime("%Y%m%d%H")
60
+ end
61
+
62
+ def hours_hash_key
63
+ "#{@cohort.key}::hours"
64
+ end
65
+
66
+ def participants_hash_key
67
+ "#{@cohort.key}::participants"
68
+ end
69
+
70
+
71
+ end
72
+ end
@@ -0,0 +1,47 @@
1
+ module Blackbeard
2
+ class CohortMetric
3
+ include Chartable
4
+
5
+ attr_reader :cohort, :metric
6
+
7
+ def initialize(cohort, metric)
8
+ @cohort = cohort
9
+ @metric = metric
10
+ end
11
+
12
+ def type
13
+ metric.type
14
+ end
15
+
16
+ def add(context, amount)
17
+ uid = context.unique_identifier
18
+ hour_id = cohort.hour_id_for_participant(uid)
19
+ metric_data.add_at(hour_id, uid, amount) unless hour_id.nil?
20
+ end
21
+
22
+ def metric_data
23
+ @metric_data ||= MetricData.const_get(type.capitalize).new(metric, nil, cohort)
24
+ end
25
+
26
+ def chartable_segments
27
+ metric_data.map{|s| "avg #{s}" }
28
+ end
29
+
30
+ def chartable_result_for_hour(hour)
31
+ participants = cohort.data.participants_for_hour(hour)
32
+ result_per_participant( metric_data.result_for_hour(hour), participants)
33
+ end
34
+
35
+ def chartable_result_for_day(date)
36
+ participants = cohort.data.participants_for_day(date)
37
+ result_per_participant( metric_data.result_for_day(date), participants)
38
+ end
39
+
40
+ def result_per_participant(result, participants)
41
+ participants = participants.to_f
42
+ result.keys.each{|k| result[k] = result[k].to_f / participants }
43
+ result
44
+ end
45
+
46
+ end
47
+ end
@@ -6,6 +6,7 @@ module Blackbeard
6
6
  attr_reader :controller, :user
7
7
 
8
8
  def initialize(pirate, user, controller = nil)
9
+ # TODO: remove pirate. access cache via separate cache class
9
10
  @pirate = pirate
10
11
  @controller = controller
11
12
  @user = user
@@ -37,12 +38,20 @@ module Blackbeard
37
38
  end
38
39
  end
39
40
 
41
+ def add_to_cohort(id, timestamp = nil, force = false)
42
+ @pirate.cohort(id.to_s).add(self, timestamp, force)
43
+ end
44
+
45
+ def add_to_cohort!(id, timestamp = nil)
46
+ add_to_cohort(id, timestamp, true)
47
+ end
48
+
40
49
  def feature_active?(id)
41
50
  @pirate.feature(id.to_s).reload.active_for?(self)
42
51
  end
43
52
 
44
53
  def unique_identifier
45
- @user.nil? ? "b#{visitor_id}" : "a#{@user.id}"
54
+ @unique_identifier ||= @user.nil? ? "b#{visitor_id}" : "a#{@user.id}"
46
55
  end
47
56
 
48
57
  def user_id
@@ -50,7 +59,7 @@ module Blackbeard
50
59
  end
51
60
 
52
61
  def visitor_id
53
- controller.request.cookies[:bbd] ||= generate_visitor_id
62
+ @visitor_id ||= controller.request.cookies[:bbd] ||= generate_visitor_id
54
63
  end
55
64
 
56
65
  private
@@ -9,6 +9,7 @@ require 'routes/groups'
9
9
  require 'routes/metrics'
10
10
  require 'routes/tests'
11
11
  require 'routes/features'
12
+ require 'routes/cohorts'
12
13
 
13
14
  module Blackbeard
14
15
  class Dashboard < Sinatra::Base
@@ -21,8 +22,6 @@ module Blackbeard
21
22
  use DashboardRoutes::Tests
22
23
  use DashboardRoutes::Groups
23
24
  use DashboardRoutes::Features
25
+ use DashboardRoutes::Cohorts
24
26
  end
25
27
  end
26
-
27
-
28
-
@@ -3,27 +3,5 @@ module Blackbeard
3
3
  def url(path = '')
4
4
  env['SCRIPT_NAME'].to_s + '/' + path
5
5
  end
6
-
7
- def js_date(date)
8
- "new Date(#{ date.year }, #{ date.month - 1}, #{ date.day } )"
9
- end
10
-
11
- def js_hour(hour)
12
- "new Date(#{ hour.year}, #{hour.month - 1 }, #{hour.day}, #{hour.hour})"
13
- end
14
-
15
- def js_metric_date(segments, d)
16
- row = [js_date(d.date)]
17
- segments.each{|s| row.push d.result[s].to_f }
18
- "[" + row.join(',') + "]"
19
- end
20
-
21
- def js_metric_hour(segments, h)
22
- row = [js_hour(h.hour)]
23
- segments.each{|s| row.push h.result[s].to_f }
24
- "[" + row.join(',') + "]"
25
- end
26
-
27
6
  end
28
7
  end
29
-
@@ -1,8 +1,10 @@
1
1
  module Blackbeard
2
2
  class GroupNotInMetric < StandardError; end
3
+ class CohortNotInMetric < StandardError; end
3
4
  class StorableMasterKeyUndefined < StandardError; end
4
5
  class StorableNotFound < StandardError; end
5
6
  class StorableDuplicateKey < StandardError; end
6
7
  class StorableNotSaved < StandardError; end
7
8
  class UserIdNotDivisable < StandardError; end
9
+ class InvalidMetricData < StandardError; end
8
10
  end
@@ -31,5 +31,8 @@ module Blackbeard
31
31
  config.group_definitions[self.id.to_sym]
32
32
  end
33
33
 
34
+ def metric_data(metric)
35
+ GroupMetric.new(self,metric).metric_data
36
+ end
34
37
  end
35
38
  end
@@ -0,0 +1,39 @@
1
+ module Blackbeard
2
+ class GroupMetric
3
+ include Chartable
4
+
5
+ attr_reader :group, :metric
6
+
7
+ def initialize(group, metric)
8
+ @group = group
9
+ @metric = metric
10
+ end
11
+
12
+ def type
13
+ metric.type
14
+ end
15
+
16
+ def add(context, amount)
17
+ uid = context.unique_identifier
18
+ segment = group.segment_for(context)
19
+ metric_data.add(uid, amount, segment) unless segment.nil?
20
+ end
21
+
22
+ def metric_data
23
+ @metric_data ||= MetricData.const_get(type.capitalize).new(metric, group, nil)
24
+ end
25
+
26
+ def chartable_segments
27
+ group.segments
28
+ end
29
+
30
+ def chartable_result_for_hour(hour)
31
+ metric_data.result_for_hour(hour)
32
+ end
33
+
34
+ def chartable_result_for_day(date)
35
+ metric_data.result_for_day(date)
36
+ end
37
+
38
+ end
39
+ end
@@ -1,13 +1,20 @@
1
1
  require "blackbeard/storable"
2
2
  require 'blackbeard/metric_data/total'
3
3
  require 'blackbeard/metric_data/unique'
4
+ require 'blackbeard/cohort'
5
+ require 'blackbeard/group'
6
+ require 'blackbeard/group_metric'
7
+ require 'blackbeard/cohort_metric'
4
8
 
5
9
  module Blackbeard
6
10
  class Metric < Storable
11
+ include Chartable
12
+
7
13
  attr_reader :type, :type_id
8
14
  set_master_key :metrics
9
15
  string_attributes :name, :description
10
16
  has_many :groups => Group
17
+ has_many :cohorts => Cohort
11
18
 
12
19
  def self.create(type, type_id, options = {})
13
20
  super("#{type}::#{type_id}", options)
@@ -41,29 +48,23 @@ module Blackbeard
41
48
  end
42
49
  end
43
50
 
44
- def recent_hours
45
- metric_data.recent_hours
51
+ def group_metrics
52
+ groups.map{ |g| GroupMetric.new(g, self) }
46
53
  end
47
54
 
48
- def recent_days
49
- metric_data.recent_days
55
+ def cohort_metrics
56
+ cohorts.map{ |c| CohortMetric.new(c, self) }
50
57
  end
51
58
 
52
59
  def add(context, amount)
53
60
  uid = context.unique_identifier
54
61
  metric_data.add(uid, amount)
55
- groups.each do |group|
56
- segment = group.segment_for(context)
57
- metric_data(group).add(uid, amount, segment) unless segment.nil?
58
- end
62
+ group_metrics.each { |gm| gm.add(context, amount) }
63
+ cohort_metrics.each { |cm| cm.add(context, amount) }
59
64
  end
60
65
 
61
- def metric_data(group = nil)
62
- @metric_data ||= {}
63
- @metric_data[group] ||= begin
64
- raise GroupNotInMetric unless group.nil? || has_group?(group)
65
- MetricData.const_get(type.capitalize).new(self, group)
66
- end
66
+ def metric_data
67
+ @metric_data ||= MetricData.const_get(type.capitalize).new(self, nil, nil)
67
68
  end
68
69
 
69
70
  def name
@@ -74,6 +75,21 @@ module Blackbeard
74
75
  Group.all.reject{ |g| group_ids.include?(g.id) }
75
76
  end
76
77
 
78
+ def addable_cohorts
79
+ Cohort.all.reject{ |c| cohort_ids.include?(c.id) }
80
+ end
81
+
82
+ def chartable_segments
83
+ metric_data.segments
84
+ end
85
+
86
+ def chartable_result_for_hour(hour)
87
+ metric_data.result_for_hour(hour)
88
+ end
89
+
90
+ def chartable_result_for_day(date)
91
+ metric_data.result_for_day(date)
92
+ end
77
93
 
78
94
  end
79
95
  end