blackbeard 0.0.2.0 → 0.0.3.1
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/.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
|