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
@@ -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