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