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.
- checksums.yaml +4 -4
- data/README.md +44 -16
- data/TODO.md +3 -1
- data/dashboard/routes/cohorts.rb +25 -0
- data/dashboard/routes/features.rb +7 -4
- data/dashboard/routes/groups.rb +6 -3
- data/dashboard/routes/metrics.rb +25 -5
- data/dashboard/routes/tests.rb +6 -3
- data/dashboard/views/cohorts/index.erb +22 -0
- data/dashboard/views/cohorts/show.erb +41 -0
- data/dashboard/views/groups/show.erb +2 -2
- data/dashboard/views/layout.erb +1 -0
- data/dashboard/views/metrics/show.erb +37 -10
- data/dashboard/views/shared/_charts.erb +20 -0
- data/lib/blackbeard/chart.rb +65 -0
- data/lib/blackbeard/chartable.rb +47 -0
- data/lib/blackbeard/cohort.rb +48 -0
- data/lib/blackbeard/cohort_data.rb +72 -0
- data/lib/blackbeard/cohort_metric.rb +47 -0
- data/lib/blackbeard/context.rb +11 -2
- data/lib/blackbeard/dashboard.rb +2 -3
- data/lib/blackbeard/dashboard_helpers.rb +0 -22
- data/lib/blackbeard/errors.rb +2 -0
- data/lib/blackbeard/group.rb +3 -0
- data/lib/blackbeard/group_metric.rb +39 -0
- data/lib/blackbeard/metric.rb +30 -14
- data/lib/blackbeard/metric_data/base.rb +18 -35
- data/lib/blackbeard/metric_data/total.rb +2 -1
- data/lib/blackbeard/metric_data/uid_generator.rb +38 -0
- data/lib/blackbeard/metric_data/unique.rb +1 -1
- data/lib/blackbeard/metric_date.rb +10 -0
- data/lib/blackbeard/metric_hour.rb +8 -0
- data/lib/blackbeard/pirate.rb +18 -0
- data/lib/blackbeard/redis_store.rb +10 -1
- data/lib/blackbeard/storable.rb +1 -0
- data/lib/blackbeard/version.rb +1 -1
- data/spec/chart_spec.rb +38 -0
- data/spec/chartable_spec.rb +56 -0
- data/spec/cohort_data_spec.rb +142 -0
- data/spec/cohort_metric_spec.rb +26 -0
- data/spec/cohort_spec.rb +31 -0
- data/spec/context_spec.rb +9 -1
- data/spec/dashboard/cohorts_spec.rb +43 -0
- data/spec/dashboard/groups_spec.rb +0 -7
- data/spec/dashboard/metrics_spec.rb +35 -0
- data/spec/group_metric_spec.rb +26 -0
- data/spec/metric_data/base_spec.rb +0 -16
- data/spec/metric_data/uid_generator_spec.rb +40 -0
- data/spec/metric_spec.rb +23 -12
- data/spec/pirate_spec.rb +22 -1
- data/spec/redis_store_spec.rb +8 -2
- data/spec/storable_spec.rb +3 -0
- metadata +29 -3
- 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
|
data/lib/blackbeard/context.rb
CHANGED
@@ -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
|
data/lib/blackbeard/dashboard.rb
CHANGED
@@ -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
|
-
|
data/lib/blackbeard/errors.rb
CHANGED
@@ -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
|
data/lib/blackbeard/group.rb
CHANGED
@@ -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
|
data/lib/blackbeard/metric.rb
CHANGED
@@ -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
|
45
|
-
|
51
|
+
def group_metrics
|
52
|
+
groups.map{ |g| GroupMetric.new(g, self) }
|
46
53
|
end
|
47
54
|
|
48
|
-
def
|
49
|
-
|
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
|
-
|
56
|
-
|
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
|
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
|