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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +9 -0
  3. data/Guardfile +8 -0
  4. data/README.md +162 -20
  5. data/Rakefile +6 -0
  6. data/TODO.md +13 -34
  7. data/blackbeard.gemspec +5 -1
  8. data/console.rb +3 -0
  9. data/dashboard/public/bootstrap3-editable/css/bootstrap-editable.css +663 -0
  10. data/dashboard/public/bootstrap3-editable/img/clear.png +0 -0
  11. data/dashboard/public/bootstrap3-editable/img/loading.gif +0 -0
  12. data/dashboard/public/bootstrap3-editable/js/bootstrap-editable.min.js +7 -0
  13. data/dashboard/public/fonts/glyphicons-halflings-regular.eot +0 -0
  14. data/dashboard/public/fonts/glyphicons-halflings-regular.svg +229 -0
  15. data/dashboard/public/fonts/glyphicons-halflings-regular.ttf +0 -0
  16. data/dashboard/public/fonts/glyphicons-halflings-regular.woff +0 -0
  17. data/dashboard/public/javascripts/bootstrap.min.js +7 -0
  18. data/dashboard/public/javascripts/jquery-1.10.2.min.js +6 -0
  19. data/dashboard/public/stylesheets/application.css +28 -0
  20. data/dashboard/public/stylesheets/bootstrap-theme.css +7 -0
  21. data/dashboard/public/stylesheets/bootstrap.css +7 -0
  22. data/dashboard/routes/base.rb +19 -0
  23. data/dashboard/routes/groups.rb +22 -0
  24. data/dashboard/routes/home.rb +11 -0
  25. data/dashboard/routes/metrics.rb +30 -0
  26. data/dashboard/routes/tests.rb +23 -0
  27. data/dashboard/views/groups/index.erb +22 -0
  28. data/dashboard/views/groups/show.erb +58 -0
  29. data/dashboard/views/index.erb +4 -0
  30. data/dashboard/views/layout.erb +48 -0
  31. data/dashboard/views/metrics/_metric_data.erb +59 -0
  32. data/dashboard/views/metrics/index.erb +23 -0
  33. data/dashboard/views/metrics/show.erb +73 -0
  34. data/dashboard/views/tests/index.erb +21 -0
  35. data/dashboard/views/tests/show.erb +58 -0
  36. data/lib/blackbeard/configuration.rb +8 -1
  37. data/lib/blackbeard/configuration_methods.rb +24 -0
  38. data/lib/blackbeard/context.rb +33 -21
  39. data/lib/blackbeard/dashboard.rb +17 -21
  40. data/lib/blackbeard/dashboard_helpers.rb +29 -0
  41. data/lib/blackbeard/errors.rb +2 -2
  42. data/lib/blackbeard/group.rb +35 -0
  43. data/lib/blackbeard/metric.rb +34 -32
  44. data/lib/blackbeard/metric_data/base.rb +101 -0
  45. data/lib/blackbeard/metric_data/total.rb +39 -0
  46. data/lib/blackbeard/metric_data/unique.rb +58 -0
  47. data/lib/blackbeard/metric_date.rb +11 -0
  48. data/lib/blackbeard/metric_hour.rb +17 -0
  49. data/lib/blackbeard/pirate.rb +33 -22
  50. data/lib/blackbeard/redis_store.rb +46 -2
  51. data/lib/blackbeard/selected_variation.rb +13 -0
  52. data/lib/blackbeard/storable.rb +39 -27
  53. data/lib/blackbeard/storable_attributes.rb +54 -0
  54. data/lib/blackbeard/storable_has_many.rb +60 -0
  55. data/lib/blackbeard/storable_has_set.rb +59 -0
  56. data/lib/blackbeard/test.rb +21 -0
  57. data/lib/blackbeard/version.rb +1 -1
  58. data/lib/blackbeard.rb +0 -8
  59. data/spec/configuration_spec.rb +15 -0
  60. data/spec/context_spec.rb +94 -19
  61. data/spec/dashboard/groups_spec.rb +50 -0
  62. data/spec/dashboard/home_spec.rb +20 -0
  63. data/spec/dashboard/metrics_spec.rb +57 -0
  64. data/spec/dashboard/tests_spec.rb +43 -0
  65. data/spec/group_spec.rb +36 -0
  66. data/spec/metric_data/base_spec.rb +57 -0
  67. data/spec/metric_data/total_spec.rb +116 -0
  68. data/spec/metric_data/unique_spec.rb +91 -0
  69. data/spec/metric_spec.rb +52 -44
  70. data/spec/pirate_spec.rb +32 -15
  71. data/spec/redis_store_spec.rb +121 -0
  72. data/spec/spec_helper.rb +13 -1
  73. data/spec/storable_attributes_spec.rb +47 -0
  74. data/spec/storable_has_many_spec.rb +49 -0
  75. data/spec/storable_has_set_spec.rb +39 -0
  76. data/spec/storable_spec.rb +33 -0
  77. data/spec/test_spec.rb +25 -0
  78. metadata +133 -17
  79. data/lib/blackbeard/dashboard/helpers.rb +0 -8
  80. data/lib/blackbeard/dashboard/views/layout.erb +0 -15
  81. data/lib/blackbeard/dashboard/views/metrics/index.erb +0 -10
  82. data/lib/blackbeard/dashboard/views/metrics/show.erb +0 -16
  83. data/lib/blackbeard/feature.rb +0 -13
  84. data/lib/blackbeard/metric/total.rb +0 -17
  85. data/lib/blackbeard/metric/unique.rb +0 -18
  86. data/spec/dashboard_spec.rb +0 -38
  87. data/spec/total_metric_spec.rb +0 -65
  88. 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
@@ -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, options)
8
+ def initialize(pirate, user, controller = nil)
5
9
  @pirate = pirate
6
- @user_id = options[:user_id]
7
- @bot = options[:bot] || false
8
- @cookies = options[:cookies] || {}
9
- raise NonIdentifyingContextError unless @cookies || @user_id
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(name, amount = 1)
13
- @pirate.total_metric(name.to_s).add(unique_identifier, amount) unless bot?
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(name)
18
- @pirate.unique_metric(name.to_s).add(unique_identifier) unless bot?
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 feature(name, options = {})
23
- variations = options.keys
24
- variation_to_show = @pirate.feature(name.to_s).select_variation(variations, unique_identifier)
25
- options[:variation_to_show]
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 bot?
29
- @bot
40
+ def active?(id)
41
+ ab_test(id) == :active
30
42
  end
31
43
 
32
44
  def unique_identifier
33
- @user_id.nil? ? "b#{blackbeard_visitor_id}" : "a#{@user_id}"
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(cookies)
39
- @cookies[:bbd] ||= generate_blackbeard_visitor_id(cookies)
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(cookies)
43
- id = Blackbeard.db.increment("visitor_id")
44
- @cookies[:bbd] = { :value => id, :expires => Time.now + 31536000 }
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
 
@@ -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/dashboard/helpers'
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
- set :root, File.expand_path(File.dirname(__FILE__) + "/dashboard")
8
- set :public_folder, Proc.new { "#{root}/public" }
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
+
@@ -1,4 +1,4 @@
1
1
  module Blackbeard
2
- class MissingContextError < StandardError; end
3
- class NonIdentifyingContextError < StandardError; end
2
+ class GroupNotInMetric < StandardError; end
3
+ class StorableMasterKeyUndefined < StandardError; end
4
4
  end
@@ -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
@@ -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 self.new_from_type_name(type, name)
7
- self.const_get(type.capitalize).new(name)
8
- end
9
-
10
- def type
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 =~ /^metrics::(.+)::(.+)$/
20
- new_from_type_name($1, $2)
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 key
27
- "metrics::#{ type }::#{ name }"
27
+ def recent_hours
28
+ metric_data.recent_hours
28
29
  end
29
30
 
30
- def hours
31
- hour_keys.map do |hour_key|
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 result_for_hour(time)
40
- key = key_for_hour(time)
41
- result_for_hour_key(key)
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
- private
45
-
46
- def hour_keys
47
- db.set_members(hours_set_key)
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 hours_set_key
51
- "#{key}::hours"
51
+ def name
52
+ storable_attributes_hash['name'] || type_id
52
53
  end
53
54
 
54
- def key_for_hour(time)
55
- "#{key}::#{ time.strftime("%Y%m%d%H") }"
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