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