blackbeard 0.0.2.0 → 0.0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +9 -0
- data/Guardfile +8 -0
- data/README.md +162 -20
- data/Rakefile +6 -0
- data/TODO.md +13 -34
- data/blackbeard.gemspec +5 -1
- data/console.rb +3 -0
- data/dashboard/public/bootstrap3-editable/css/bootstrap-editable.css +663 -0
- data/dashboard/public/bootstrap3-editable/img/clear.png +0 -0
- data/dashboard/public/bootstrap3-editable/img/loading.gif +0 -0
- data/dashboard/public/bootstrap3-editable/js/bootstrap-editable.min.js +7 -0
- data/dashboard/public/fonts/glyphicons-halflings-regular.eot +0 -0
- data/dashboard/public/fonts/glyphicons-halflings-regular.svg +229 -0
- data/dashboard/public/fonts/glyphicons-halflings-regular.ttf +0 -0
- data/dashboard/public/fonts/glyphicons-halflings-regular.woff +0 -0
- data/dashboard/public/javascripts/bootstrap.min.js +7 -0
- data/dashboard/public/javascripts/jquery-1.10.2.min.js +6 -0
- data/dashboard/public/stylesheets/application.css +28 -0
- data/dashboard/public/stylesheets/bootstrap-theme.css +7 -0
- data/dashboard/public/stylesheets/bootstrap.css +7 -0
- data/dashboard/routes/base.rb +19 -0
- data/dashboard/routes/groups.rb +22 -0
- data/dashboard/routes/home.rb +11 -0
- data/dashboard/routes/metrics.rb +30 -0
- data/dashboard/routes/tests.rb +23 -0
- data/dashboard/views/groups/index.erb +22 -0
- data/dashboard/views/groups/show.erb +58 -0
- data/dashboard/views/index.erb +4 -0
- data/dashboard/views/layout.erb +48 -0
- data/dashboard/views/metrics/_metric_data.erb +59 -0
- data/dashboard/views/metrics/index.erb +23 -0
- data/dashboard/views/metrics/show.erb +73 -0
- data/dashboard/views/tests/index.erb +21 -0
- data/dashboard/views/tests/show.erb +58 -0
- data/lib/blackbeard/configuration.rb +8 -1
- data/lib/blackbeard/configuration_methods.rb +24 -0
- data/lib/blackbeard/context.rb +33 -21
- data/lib/blackbeard/dashboard.rb +17 -21
- data/lib/blackbeard/dashboard_helpers.rb +29 -0
- data/lib/blackbeard/errors.rb +2 -2
- data/lib/blackbeard/group.rb +35 -0
- data/lib/blackbeard/metric.rb +34 -32
- data/lib/blackbeard/metric_data/base.rb +101 -0
- data/lib/blackbeard/metric_data/total.rb +39 -0
- data/lib/blackbeard/metric_data/unique.rb +58 -0
- data/lib/blackbeard/metric_date.rb +11 -0
- data/lib/blackbeard/metric_hour.rb +17 -0
- data/lib/blackbeard/pirate.rb +33 -22
- data/lib/blackbeard/redis_store.rb +46 -2
- data/lib/blackbeard/selected_variation.rb +13 -0
- data/lib/blackbeard/storable.rb +39 -27
- data/lib/blackbeard/storable_attributes.rb +54 -0
- data/lib/blackbeard/storable_has_many.rb +60 -0
- data/lib/blackbeard/storable_has_set.rb +59 -0
- data/lib/blackbeard/test.rb +21 -0
- data/lib/blackbeard/version.rb +1 -1
- data/lib/blackbeard.rb +0 -8
- data/spec/configuration_spec.rb +15 -0
- data/spec/context_spec.rb +94 -19
- data/spec/dashboard/groups_spec.rb +50 -0
- data/spec/dashboard/home_spec.rb +20 -0
- data/spec/dashboard/metrics_spec.rb +57 -0
- data/spec/dashboard/tests_spec.rb +43 -0
- data/spec/group_spec.rb +36 -0
- data/spec/metric_data/base_spec.rb +57 -0
- data/spec/metric_data/total_spec.rb +116 -0
- data/spec/metric_data/unique_spec.rb +91 -0
- data/spec/metric_spec.rb +52 -44
- data/spec/pirate_spec.rb +32 -15
- data/spec/redis_store_spec.rb +121 -0
- data/spec/spec_helper.rb +13 -1
- data/spec/storable_attributes_spec.rb +47 -0
- data/spec/storable_has_many_spec.rb +49 -0
- data/spec/storable_has_set_spec.rb +39 -0
- data/spec/storable_spec.rb +33 -0
- data/spec/test_spec.rb +25 -0
- metadata +133 -17
- data/lib/blackbeard/dashboard/helpers.rb +0 -8
- data/lib/blackbeard/dashboard/views/layout.erb +0 -15
- data/lib/blackbeard/dashboard/views/metrics/index.erb +0 -10
- data/lib/blackbeard/dashboard/views/metrics/show.erb +0 -16
- data/lib/blackbeard/feature.rb +0 -13
- data/lib/blackbeard/metric/total.rb +0 -17
- data/lib/blackbeard/metric/unique.rb +0 -18
- data/spec/dashboard_spec.rb +0 -38
- data/spec/total_metric_spec.rb +0 -65
- data/spec/unique_metric_spec.rb +0 -60
@@ -0,0 +1,73 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1><span id="editable-name"><%= @metric.name %></span> <small><%= @metric.type %></small></h1>
|
3
|
+
<p id="editable-description"><%= @metric.description %></p>
|
4
|
+
</div>
|
5
|
+
<form id="add_group" action="<%= url("/metrics/#{@metric.type}/#{@metric.type_id}/groups") %>" method="post"><input id="add_group_input" type="hidden" name="group_id"></form>
|
6
|
+
<ul class="nav nav-tabs">
|
7
|
+
<% if @group.nil? %><li class="active"><% else %><li><% end %><a href="?">All</a></li>
|
8
|
+
<% @metric.groups.each do |group| %>
|
9
|
+
<% if @group == group %><li class="active"><% else %><li><% end %><a href="?group_id=<%= group.id %>"><%= group.name %></a></li>
|
10
|
+
<% end %>
|
11
|
+
<% if @metric.addable_groups.any? %>
|
12
|
+
<li class="dropdown">
|
13
|
+
<a id="drop5" role="button" data-toggle="dropdown" href="#">Segment by Group <b class="caret"></b></a>
|
14
|
+
<ul id="menu2" class="dropdown-menu" role="menu" aria-labelledby="drop5">
|
15
|
+
<% @metric.addable_groups.each do |group| %>
|
16
|
+
<li role="presentation" data-group-id="<%= group.id %>" data-group-name="<%= group.name %>" class="add-group"><a role="menuitem" tabindex="-1" href="#"><%= group.name %></a></li>
|
17
|
+
<% end %>
|
18
|
+
</ul>
|
19
|
+
</li>
|
20
|
+
<% end %>
|
21
|
+
</ul>
|
22
|
+
|
23
|
+
<!--Load the AJAX API-->
|
24
|
+
<script src="<%= url('javascripts/jquery-1.10.2.min.js') %>"></script>
|
25
|
+
<script src="<%= url('javascripts/bootstrap.min.js') %>"></script>
|
26
|
+
<script src="<%= url('bootstrap3-editable/js/bootstrap-editable.min.js') %>"></script>
|
27
|
+
<script type="text/javascript" src="https://www.google.com/jsapi"></script>
|
28
|
+
<script type="text/javascript">
|
29
|
+
$('ul.nav-tabs').on("click", ".add-group", function (e) {
|
30
|
+
e.preventDefault();
|
31
|
+
var groupId = $(this).data("group-id");
|
32
|
+
var groupName = $(this).data("group-name");
|
33
|
+
|
34
|
+
if(confirm("Start collecting data by group "+groupName+"?")) {
|
35
|
+
$('#add_group_input').val(groupId);
|
36
|
+
$('#add_group').submit();
|
37
|
+
}
|
38
|
+
});
|
39
|
+
|
40
|
+
//turn to inline mode
|
41
|
+
$.fn.editable.defaults.send = 'always';
|
42
|
+
$.fn.editable.defaults.params = function(params) {
|
43
|
+
var obj = {};
|
44
|
+
obj[params.name] = params.value;
|
45
|
+
return obj;
|
46
|
+
}
|
47
|
+
|
48
|
+
$(document).ready(function() {
|
49
|
+
$('#editable-name').editable({
|
50
|
+
placement: 'right',
|
51
|
+
type: 'text',
|
52
|
+
url: '<%= url("metrics/#{@metric.type}/#{@metric.type_id}") %>',
|
53
|
+
name: 'name',
|
54
|
+
title: 'Edit metric name',
|
55
|
+
validate: function(value) {
|
56
|
+
if($.trim(value) == '') {
|
57
|
+
return 'This field is required';
|
58
|
+
}
|
59
|
+
}
|
60
|
+
});
|
61
|
+
$('#editable-description').editable({
|
62
|
+
placement: 'bottom',
|
63
|
+
type: 'textarea',
|
64
|
+
url: '<%= url("metrics/#{@metric.type}/#{@metric.type_id}") %>',
|
65
|
+
name: 'description',
|
66
|
+
title: 'Edit metric description'
|
67
|
+
});
|
68
|
+
});
|
69
|
+
</script>
|
70
|
+
<%= partial :'metrics/metric_data', :locals => {:metric_data => @metric.metric_data(@group) } %>
|
71
|
+
|
72
|
+
|
73
|
+
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1>Tests</h1>
|
3
|
+
<p>Tests are elements or features that you can change or turn off.</p>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<% if @tests.any? %>
|
7
|
+
<div class="panel panel-primary">
|
8
|
+
<div class="panel-heading">
|
9
|
+
<h3 class="panel-title">Active Tests</h3>
|
10
|
+
</div>
|
11
|
+
<ul class="list-group">
|
12
|
+
<% @tests.each do |test| %>
|
13
|
+
<li class="list-group-item"><a href="<%= url("tests/#{test.id}") %>"><%= test.name %></a></li>
|
14
|
+
<% end %>
|
15
|
+
</ul>
|
16
|
+
</div>
|
17
|
+
<% else %>
|
18
|
+
<div class="alert alert-danger">
|
19
|
+
<strong>No tests!</strong> Something may be wrong or you may just need to define some tests.
|
20
|
+
</div>
|
21
|
+
<% end %>
|
@@ -0,0 +1,58 @@
|
|
1
|
+
<div class="page-header">
|
2
|
+
<h1><span id="editable-name"><%= @test.name %></span></h1>
|
3
|
+
<p id="editable-description"><%= @test.description %></p>
|
4
|
+
</div>
|
5
|
+
|
6
|
+
<% if @test.variations.any? %>
|
7
|
+
<div class="panel panel-primary">
|
8
|
+
<div class="panel-heading">
|
9
|
+
<h3 class="panel-title">Known Variations</h3>
|
10
|
+
</div>
|
11
|
+
<ul class="list-group">
|
12
|
+
<% @test.variations.each do |variant| %>
|
13
|
+
<li class="list-group-item"><%= variant %></li>
|
14
|
+
<% end %>
|
15
|
+
</ul>
|
16
|
+
</div>
|
17
|
+
|
18
|
+
<% else %>
|
19
|
+
<div class="alert alert-danger">
|
20
|
+
<strong>No variations!</strong> Something may be wrong or maybe this test not been called yet.
|
21
|
+
</div>
|
22
|
+
<% end %>
|
23
|
+
|
24
|
+
<!--Load the AJAX API-->
|
25
|
+
<script src="<%= url('javascripts/jquery-1.10.2.min.js') %>"></script>
|
26
|
+
<script src="<%= url('javascripts/bootstrap.min.js') %>"></script>
|
27
|
+
<script src="<%= url('bootstrap3-editable/js/bootstrap-editable.min.js') %>"></script>
|
28
|
+
<script type="text/javascript">
|
29
|
+
//turn to inline mode
|
30
|
+
$.fn.editable.defaults.send = 'always';
|
31
|
+
$.fn.editable.defaults.params = function(params) {
|
32
|
+
var obj = {};
|
33
|
+
obj[params.name] = params.value;
|
34
|
+
return obj;
|
35
|
+
}
|
36
|
+
|
37
|
+
$(document).ready(function() {
|
38
|
+
$('#editable-name').editable({
|
39
|
+
placement: 'right',
|
40
|
+
type: 'text',
|
41
|
+
url: '<%= url("tests/#{@test.id}") %>',
|
42
|
+
name: 'name',
|
43
|
+
title: 'Edit test name',
|
44
|
+
validate: function(value) {
|
45
|
+
if($.trim(value) == '') {
|
46
|
+
return 'This field is required';
|
47
|
+
}
|
48
|
+
}
|
49
|
+
});
|
50
|
+
$('#editable-description').editable({
|
51
|
+
placement: 'bottom',
|
52
|
+
type: 'textarea',
|
53
|
+
url: '<%= url("tests/#{@test.id}") %>',
|
54
|
+
name: 'description',
|
55
|
+
title: 'Edit test description'
|
56
|
+
});
|
57
|
+
});
|
58
|
+
</script>
|
@@ -1,13 +1,16 @@
|
|
1
1
|
require 'tzinfo'
|
2
2
|
require "blackbeard/redis_store"
|
3
|
+
require "blackbeard/group"
|
3
4
|
|
4
5
|
module Blackbeard
|
5
6
|
class Configuration
|
6
|
-
attr_accessor :timezone, :namespace, :redis
|
7
|
+
attr_accessor :timezone, :namespace, :redis, :guest_method
|
8
|
+
attr_reader :group_definitions
|
7
9
|
|
8
10
|
def initialize
|
9
11
|
@timezone = 'America/Los_Angeles'
|
10
12
|
@namespace = 'Blackbeard'
|
13
|
+
@group_definitions = {}
|
11
14
|
@redis = nil
|
12
15
|
end
|
13
16
|
|
@@ -19,5 +22,9 @@ module Blackbeard
|
|
19
22
|
@tz ||= TZInfo::Timezone.get(@timezone)
|
20
23
|
end
|
21
24
|
|
25
|
+
def define_group(id, &block)
|
26
|
+
@group_definitions[id.to_sym] = block
|
27
|
+
Group.new(id)
|
28
|
+
end
|
22
29
|
end
|
23
30
|
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Blackbeard
|
2
|
+
module ConfigurationMethods
|
3
|
+
def self.included(base)
|
4
|
+
base.extend(self)
|
5
|
+
end
|
6
|
+
|
7
|
+
def config
|
8
|
+
Blackbeard.config
|
9
|
+
end
|
10
|
+
|
11
|
+
def db
|
12
|
+
config.db
|
13
|
+
end
|
14
|
+
|
15
|
+
def tz
|
16
|
+
config.tz
|
17
|
+
end
|
18
|
+
|
19
|
+
def guest_method
|
20
|
+
config.guest_method
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
data/lib/blackbeard/context.rb
CHANGED
@@ -1,47 +1,59 @@
|
|
1
|
+
require 'blackbeard/selected_variation'
|
2
|
+
|
1
3
|
module Blackbeard
|
2
4
|
class Context
|
5
|
+
include ConfigurationMethods
|
6
|
+
attr_reader :controller, :user
|
3
7
|
|
4
|
-
def initialize(pirate,
|
8
|
+
def initialize(pirate, user, controller = nil)
|
5
9
|
@pirate = pirate
|
6
|
-
@
|
7
|
-
@
|
8
|
-
|
9
|
-
|
10
|
+
@controller = controller
|
11
|
+
@user = user
|
12
|
+
|
13
|
+
if (@user == false) || (@user && guest_method && @user.send(guest_method) == false)
|
14
|
+
@user = nil
|
15
|
+
end
|
10
16
|
end
|
11
17
|
|
12
|
-
def add_total(
|
13
|
-
@pirate.
|
18
|
+
def add_total(id, amount = 1)
|
19
|
+
@pirate.metric(:total, id.to_s).add(self, amount)
|
14
20
|
self
|
15
21
|
end
|
16
22
|
|
17
|
-
def add_unique(
|
18
|
-
@pirate.
|
23
|
+
def add_unique(id)
|
24
|
+
@pirate.metric(:unique, id.to_s).add(self, 1)
|
19
25
|
self
|
20
26
|
end
|
21
27
|
|
22
|
-
def
|
23
|
-
|
24
|
-
|
25
|
-
|
28
|
+
def ab_test(id, options = nil)
|
29
|
+
test = @pirate.test(id.to_s)
|
30
|
+
if options.is_a? Hash
|
31
|
+
test.add_variations(options.keys)
|
32
|
+
variation = test.select_variation
|
33
|
+
options[variation.to_sym]
|
34
|
+
else
|
35
|
+
variation = test.select_variation
|
36
|
+
SelectedVariation.new(test, variation)
|
37
|
+
end
|
26
38
|
end
|
27
39
|
|
28
|
-
def
|
29
|
-
|
40
|
+
def active?(id)
|
41
|
+
ab_test(id) == :active
|
30
42
|
end
|
31
43
|
|
32
44
|
def unique_identifier
|
33
|
-
@
|
45
|
+
@user.nil? ? "b#{blackbeard_visitor_id}" : "a#{@user.id}"
|
34
46
|
end
|
35
47
|
|
36
48
|
private
|
37
49
|
|
38
|
-
def blackbeard_visitor_id
|
39
|
-
|
50
|
+
def blackbeard_visitor_id
|
51
|
+
controller.request.cookies[:bbd] ||= generate_blackbeard_visitor_id
|
40
52
|
end
|
41
53
|
|
42
|
-
def generate_blackbeard_visitor_id
|
43
|
-
id =
|
44
|
-
|
54
|
+
def generate_blackbeard_visitor_id
|
55
|
+
id = db.increment("visitor_id")
|
56
|
+
controller.request.cookies[:bbd] = { :value => id, :expires => Time.now + 31536000 }
|
45
57
|
id
|
46
58
|
end
|
47
59
|
|
data/lib/blackbeard/dashboard.rb
CHANGED
@@ -1,30 +1,26 @@
|
|
1
|
+
$LOAD_PATH << File.expand_path('../../../dashboard', __FILE__)
|
2
|
+
|
1
3
|
require 'sinatra/base'
|
2
4
|
require 'blackbeard'
|
3
|
-
require 'blackbeard/
|
5
|
+
require 'blackbeard/dashboard_helpers'
|
6
|
+
require 'routes/base'
|
7
|
+
require 'routes/home'
|
8
|
+
require 'routes/groups'
|
9
|
+
require 'routes/metrics'
|
10
|
+
require 'routes/tests'
|
4
11
|
|
5
12
|
module Blackbeard
|
6
13
|
class Dashboard < Sinatra::Base
|
7
|
-
|
8
|
-
|
9
|
-
set :views, Proc.new { "#{root}/views" }
|
10
|
-
set :raise_errors, true
|
11
|
-
set :show_exceptions, false
|
12
|
-
|
13
|
-
helpers Blackbeard::DashboardHelpers
|
14
|
-
|
15
|
-
get '/' do
|
16
|
-
redirect url('/metrics')
|
17
|
-
end
|
18
|
-
|
19
|
-
get '/metrics' do
|
20
|
-
@metrics = Blackbeard::Metric.all
|
21
|
-
erb 'metrics/index'.to_sym
|
22
|
-
end
|
23
|
-
|
24
|
-
get "/metrics/:type/:name" do
|
25
|
-
@metric = Blackbeard::Metric.new_from_type_name(params[:type], params[:name])
|
26
|
-
erb 'metrics/show'.to_sym
|
14
|
+
configure do
|
15
|
+
set :public_folder, Proc.new { "#{root}/public" }
|
27
16
|
end
|
28
17
|
|
18
|
+
use DashboardRoutes::Home
|
19
|
+
use DashboardRoutes::Metrics
|
20
|
+
use DashboardRoutes::Tests
|
21
|
+
use DashboardRoutes::Groups
|
29
22
|
end
|
30
23
|
end
|
24
|
+
|
25
|
+
|
26
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Blackbeard
|
2
|
+
module DashboardHelpers
|
3
|
+
def url(path = '')
|
4
|
+
env['SCRIPT_NAME'].to_s + '/' + path
|
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
|
+
end
|
28
|
+
end
|
29
|
+
|
data/lib/blackbeard/errors.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'blackbeard/storable'
|
2
|
+
|
3
|
+
module Blackbeard
|
4
|
+
class Group < Storable
|
5
|
+
set_master_key :groups
|
6
|
+
string_attributes :name, :description
|
7
|
+
has_set :segments => :segment
|
8
|
+
|
9
|
+
def name
|
10
|
+
storable_attributes_hash['name'] || id
|
11
|
+
end
|
12
|
+
|
13
|
+
def segment_for(context)
|
14
|
+
return nil unless definition
|
15
|
+
segment = definition.call(context.user, context.controller)
|
16
|
+
segment_id = case segment
|
17
|
+
when false
|
18
|
+
nil
|
19
|
+
when nil
|
20
|
+
nil
|
21
|
+
when true
|
22
|
+
self.id
|
23
|
+
else
|
24
|
+
segment.to_s
|
25
|
+
end
|
26
|
+
add_segment(segment_id) unless segment_id.nil?
|
27
|
+
segment_id
|
28
|
+
end
|
29
|
+
|
30
|
+
def definition
|
31
|
+
config.group_definitions[self.id.to_sym]
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/blackbeard/metric.rb
CHANGED
@@ -1,59 +1,61 @@
|
|
1
1
|
require "blackbeard/storable"
|
2
|
+
require 'blackbeard/metric_data/total'
|
3
|
+
require 'blackbeard/metric_data/unique'
|
2
4
|
|
3
5
|
module Blackbeard
|
4
6
|
class Metric < Storable
|
7
|
+
attr_reader :type, :type_id
|
8
|
+
set_master_key :metrics
|
9
|
+
string_attributes :name, :description
|
10
|
+
has_many :groups => Group
|
5
11
|
|
6
|
-
def
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
self.class.name.split("::").last.downcase
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.master_key
|
15
|
-
"metrics"
|
12
|
+
def initialize(type, type_id)
|
13
|
+
@type = type
|
14
|
+
@type_id = type_id
|
15
|
+
@metric_data = {}
|
16
|
+
super("#{type}::#{type_id}")
|
16
17
|
end
|
17
18
|
|
18
19
|
def self.new_from_key(key)
|
19
|
-
if key =~
|
20
|
-
|
20
|
+
if key =~ /^#{master_key}::(.+)::(.+)$/
|
21
|
+
new($1,$2)
|
21
22
|
else
|
22
23
|
nil
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
26
|
-
def
|
27
|
-
|
27
|
+
def recent_hours
|
28
|
+
metric_data.recent_hours
|
28
29
|
end
|
29
30
|
|
30
|
-
def
|
31
|
-
|
32
|
-
{
|
33
|
-
:hour => hour_key.split("::").last,
|
34
|
-
:result => result_for_hour_key(hour_key)
|
35
|
-
}
|
36
|
-
end
|
31
|
+
def recent_days
|
32
|
+
metric_data.recent_days
|
37
33
|
end
|
38
34
|
|
39
|
-
def
|
40
|
-
|
41
|
-
|
35
|
+
def add(context, amount)
|
36
|
+
uid = context.unique_identifier
|
37
|
+
metric_data.add(uid, amount)
|
38
|
+
groups.each do |group|
|
39
|
+
segment = group.segment_for(context)
|
40
|
+
metric_data(group).add(uid, amount, segment) unless segment.nil?
|
41
|
+
end
|
42
42
|
end
|
43
43
|
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
44
|
+
def metric_data(group = nil)
|
45
|
+
@metric_data[group] ||= begin
|
46
|
+
raise GroupNotInMetric unless group.nil? || has_group?(group)
|
47
|
+
MetricData.const_get(type.capitalize).new(self, group)
|
48
|
+
end
|
48
49
|
end
|
49
50
|
|
50
|
-
def
|
51
|
-
|
51
|
+
def name
|
52
|
+
storable_attributes_hash['name'] || type_id
|
52
53
|
end
|
53
54
|
|
54
|
-
def
|
55
|
-
|
55
|
+
def addable_groups
|
56
|
+
Group.all.reject{ |g| group_ids.include?(g.id) }
|
56
57
|
end
|
57
58
|
|
59
|
+
|
58
60
|
end
|
59
61
|
end
|
@@ -0,0 +1,101 @@
|
|
1
|
+
require 'blackbeard/metric_hour'
|
2
|
+
require 'blackbeard/metric_date'
|
3
|
+
require 'date'
|
4
|
+
|
5
|
+
module Blackbeard
|
6
|
+
module MetricData
|
7
|
+
class Base
|
8
|
+
include ConfigurationMethods
|
9
|
+
attr_reader :metric, :group
|
10
|
+
|
11
|
+
def initialize(metric, group = nil)
|
12
|
+
@metric = metric
|
13
|
+
@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
|
30
|
+
end
|
31
|
+
|
32
|
+
def hour_keys_for_day(date)
|
33
|
+
start_of_day = date.to_time
|
34
|
+
Array(0..23).map{|x| start_of_day + (3600 * x) }.map{|t| key_for_hour(t) }
|
35
|
+
end
|
36
|
+
|
37
|
+
def result_for_day(date)
|
38
|
+
key = key_for_date(date)
|
39
|
+
result = db.hash_get_all(key)
|
40
|
+
result = generate_result_for_day(date) if result.empty?
|
41
|
+
result.each { |k,v| result[k] = v.to_f }
|
42
|
+
result
|
43
|
+
end
|
44
|
+
|
45
|
+
def key
|
46
|
+
@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
|
+
"data::#{uid}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def segments
|
62
|
+
if group && group.segments.any?
|
63
|
+
group.segments
|
64
|
+
else
|
65
|
+
[self.class::DEFAULT_SEGMENT]
|
66
|
+
end
|
67
|
+
end
|
68
|
+
private
|
69
|
+
|
70
|
+
def generate_result_for_day(date)
|
71
|
+
date_key = key_for_date(date)
|
72
|
+
hours_keys = hour_keys_for_day(date)
|
73
|
+
result = merge_results(hours_keys)
|
74
|
+
db.hash_multi_set(date_key, result) unless date == tz.now.to_date
|
75
|
+
result
|
76
|
+
end
|
77
|
+
|
78
|
+
|
79
|
+
def hour_keys
|
80
|
+
db.set_members(hours_set_key)
|
81
|
+
end
|
82
|
+
|
83
|
+
def hours_set_key
|
84
|
+
"#{key}::hours"
|
85
|
+
end
|
86
|
+
|
87
|
+
def days_set_key
|
88
|
+
"#{key}::days"
|
89
|
+
end
|
90
|
+
|
91
|
+
def key_for_date(date)
|
92
|
+
"#{key}::#{ date.strftime("%Y%m%d") }"
|
93
|
+
end
|
94
|
+
|
95
|
+
def key_for_hour(time)
|
96
|
+
"#{key}::#{ time.strftime("%Y%m%d%H") }"
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
end
|
101
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'blackbeard/metric_data/base'
|
2
|
+
|
3
|
+
module Blackbeard
|
4
|
+
module MetricData
|
5
|
+
class Total < Base
|
6
|
+
|
7
|
+
DEFAULT_SEGMENT = 'total'
|
8
|
+
|
9
|
+
def add(uid, amount = 1, segment = DEFAULT_SEGMENT)
|
10
|
+
add_at(tz.now, uid, amount, segment)
|
11
|
+
end
|
12
|
+
|
13
|
+
def add_at(time, uid, amount = 1, segment = DEFAULT_SEGMENT)
|
14
|
+
key = key_for_hour(time)
|
15
|
+
db.set_add_member(hours_set_key, key)
|
16
|
+
db.hash_increment_by_float(key, segment, amount.to_f)
|
17
|
+
#TODO: if not today, blow away rollup keys
|
18
|
+
end
|
19
|
+
|
20
|
+
def result_for_hour(time)
|
21
|
+
key = key_for_hour(time)
|
22
|
+
result = db.hash_get_all(key)
|
23
|
+
result.each{ |k,v| result[k] = v.to_f }
|
24
|
+
result
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def merge_results(keys)
|
30
|
+
merged_results = {}
|
31
|
+
keys.each do |key|
|
32
|
+
result = db.hash_get_all(key)
|
33
|
+
result.each{ |k,v| merged_results[k] ||= 0; merged_results[k] += v.to_f}
|
34
|
+
end
|
35
|
+
merged_results
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|