mountain-goat 0.1.8 → 1.0.0

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 (135) hide show
  1. data/README.md +119 -48
  2. data/generators/mg/mg_generator.rb +17 -9
  3. data/generators/mg/templates/create_mountain_goat_tables.rb +61 -25
  4. data/generators/mg/templates/mg.rb +1 -0
  5. data/generators/mg/templates/mountain-goat.yml +22 -3
  6. data/generators/mg/templates/mountain_goat_reports.rake +17 -0
  7. data/generators/mg/templates/update_mountain_goat_tables.rb +104 -0
  8. data/lib/mountain-goat.rb +21 -12
  9. data/lib/mountain-goat/analytics.rb +134 -0
  10. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_converts_controller.rb → mg/converts_controller.rb} +12 -11
  11. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_metric_variants_controller.rb → mg/metric_variants_controller.rb} +6 -5
  12. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_metrics_controller.rb → mg/metrics_controller.rb} +8 -6
  13. data/lib/mountain-goat/controllers/mg/mg.rb +47 -0
  14. data/lib/mountain-goat/controllers/{mountain_goat → mg}/mountain_goat_controller.rb +5 -42
  15. data/lib/mountain-goat/controllers/mg/playground_controller.rb +8 -0
  16. data/lib/mountain-goat/controllers/{mountain_goat/mountain_goat_rallies_controller.rb → mg/rallies_controller.rb} +4 -26
  17. data/lib/mountain-goat/controllers/mg/report_items_controller.rb +82 -0
  18. data/lib/mountain-goat/controllers/mg/reports_controller.rb +90 -0
  19. data/lib/mountain-goat/m_g.rb +64 -0
  20. data/lib/mountain-goat/metric_tracking.rb +192 -74
  21. data/lib/mountain-goat/models/mg/ci_meta.rb +10 -0
  22. data/lib/mountain-goat/models/mg/convert.rb +147 -0
  23. data/lib/mountain-goat/models/mg/convert_meta_type.rb +20 -0
  24. data/lib/mountain-goat/models/mg/cs_meta.rb +10 -0
  25. data/lib/mountain-goat/models/{metric.rb → mg/metric.rb} +3 -5
  26. data/lib/mountain-goat/models/mg/metric_variant.rb +25 -0
  27. data/lib/mountain-goat/models/mg/mountain_goat.rb +3 -0
  28. data/lib/mountain-goat/models/{rally.rb → mg/rally.rb} +4 -3
  29. data/lib/mountain-goat/models/mg/report.rb +11 -0
  30. data/lib/mountain-goat/models/mg/report_item.rb +24 -0
  31. data/lib/mountain-goat/models/mg/report_mailer.rb +18 -0
  32. data/lib/mountain-goat/public/g-bar-min.js +7 -0
  33. data/lib/mountain-goat/public/g-dot-min.js +7 -0
  34. data/lib/mountain-goat/public/g-line-min.js +7 -0
  35. data/lib/mountain-goat/public/g-pie-min.js +1 -0
  36. data/lib/mountain-goat/public/g-raphael-min.js +7 -0
  37. data/lib/mountain-goat/public/jqModel.css +41 -0
  38. data/lib/mountain-goat/public/jqModel.js +69 -0
  39. data/lib/mountain-goat/public/jquery.raphael.js +208 -0
  40. data/lib/mountain-goat/public/mg.css +135 -26
  41. data/lib/mountain-goat/public/mg.js +53 -1
  42. data/lib/mountain-goat/public/raphael-min.js +7 -0
  43. data/lib/mountain-goat/public/utils.js +520 -0
  44. data/lib/mountain-goat/switch_variant.rb +4 -4
  45. data/lib/mountain-goat/version.rb +1 -1
  46. data/lib/mountain-goat/views/mountain_goat/layouts/_pdf.html.erb +15 -0
  47. data/lib/mountain-goat/views/mountain_goat/layouts/mountain_goat.html.erb +17 -5
  48. data/lib/mountain-goat/views/mountain_goat/layouts/xhr.html.erb +2 -0
  49. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/.tmp_show.html.erb.4433~ +0 -0
  50. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/_convert_form.html.erb +0 -0
  51. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/_convert_meta_type_form.html.erb +0 -0
  52. data/lib/mountain-goat/views/mountain_goat/mg/converts/edit.html.erb +13 -0
  53. data/lib/mountain-goat/views/mountain_goat/mg/converts/index.html.erb +25 -0
  54. data/lib/mountain-goat/views/mountain_goat/mg/converts/new.html.erb +13 -0
  55. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_converts → mg/converts}/show.html.erb +3 -28
  56. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metric_variants → mg/metric_variants}/_metric_variant_form.html.erb +0 -4
  57. data/lib/mountain-goat/views/mountain_goat/mg/metric_variants/edit.html.erb +13 -0
  58. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metric_variants → mg/metric_variants}/index.html.erb +3 -3
  59. data/lib/mountain-goat/views/mountain_goat/mg/metric_variants/new.html.erb +15 -0
  60. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metric_variants → mg/metric_variants}/show.html.erb +4 -5
  61. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metrics → mg/metrics}/.tmp_show.html.erb.21270~ +0 -0
  62. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metrics → mg/metrics}/_metric_form.html.erb +0 -2
  63. data/lib/mountain-goat/views/mountain_goat/mg/metrics/edit.html.erb +13 -0
  64. data/lib/mountain-goat/views/mountain_goat/mg/metrics/index.html.erb +14 -0
  65. data/lib/mountain-goat/views/mountain_goat/mg/metrics/new.html.erb +14 -0
  66. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_metrics → mg/metrics}/show.html.erb +7 -8
  67. data/lib/mountain-goat/views/mountain_goat/{mountain_goat → mg/mountain_goat}/login.html.erb +0 -0
  68. data/lib/mountain-goat/views/mountain_goat/mg/playground/test.html.erb +14 -0
  69. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/.tmp__rally.html.erb.40484~ +0 -0
  70. data/lib/mountain-goat/views/mountain_goat/mg/rallies/_rallies.html.erb +5 -0
  71. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/_rallies_form.html.erb +0 -0
  72. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/_rally.html.erb +0 -0
  73. data/lib/mountain-goat/views/mountain_goat/mg/rallies/edit.html.erb +13 -0
  74. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/index.html.erb +1 -1
  75. data/lib/mountain-goat/views/mountain_goat/mg/rallies/new.html.erb +13 -0
  76. data/lib/mountain-goat/views/mountain_goat/{mountain_goat_rallies → mg/rallies}/show.html.erb +1 -1
  77. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_chart.html.erb +18 -0
  78. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_report_item_form.html.erb +10 -0
  79. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_report_item_pivot_form.html.erb +14 -0
  80. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_show.html.erb +29 -0
  81. data/lib/mountain-goat/views/mountain_goat/mg/report_items/_svg_chart.html.erb +4 -0
  82. data/lib/mountain-goat/views/mountain_goat/mg/report_items/edit.html.erb +19 -0
  83. data/lib/mountain-goat/views/mountain_goat/mg/report_items/new.html.erb +17 -0
  84. data/lib/mountain-goat/views/mountain_goat/mg/report_mailer/report.html.erb +27 -0
  85. data/lib/mountain-goat/views/mountain_goat/mg/reports/_report.html.erb +22 -0
  86. data/lib/mountain-goat/views/mountain_goat/mg/reports/_report_form.html.erb +21 -0
  87. data/lib/mountain-goat/views/mountain_goat/mg/reports/_report_report_items.html.erb +5 -0
  88. data/lib/mountain-goat/views/mountain_goat/mg/reports/edit.html.erb +36 -0
  89. data/lib/mountain-goat/views/mountain_goat/mg/reports/index.html.erb +26 -0
  90. data/lib/mountain-goat/views/mountain_goat/mg/reports/new.html.erb +17 -0
  91. data/lib/mountain-goat/views/mountain_goat/mg/reports/show.html.erb +21 -0
  92. data/test/fixtures/{ci_metas.yml → mg_ci_metas.yml} +0 -0
  93. data/test/fixtures/{convert_meta_types.yml → mg_convert_meta_types.yml} +0 -0
  94. data/test/fixtures/{converts.yml → mg_converts.yml} +5 -0
  95. data/test/fixtures/{cs_metas.yml → mg_cs_metas.yml} +0 -0
  96. data/test/fixtures/mg_deliveries.yml +9 -0
  97. data/test/fixtures/{metric_variants.yml → mg_metric_variants.yml} +0 -0
  98. data/test/fixtures/{metrics.yml → mg_metrics.yml} +2 -3
  99. data/test/fixtures/{rallies.yml → mg_rallies.yml} +6 -2
  100. data/test/fixtures/mg_report_items.yml +13 -0
  101. data/test/fixtures/mg_reports.yml +15 -0
  102. data/test/mg_convert_test.rb +32 -0
  103. data/test/mg_converts_controller_test.rb +47 -0
  104. data/test/mg_metric_variants_controller_test.rb +46 -0
  105. data/test/mg_metrics_controller_test.rb +52 -0
  106. data/test/mg_mountain_goat_controller_test.rb +45 -0
  107. data/test/mg_mountain_goat_test.rb +392 -0
  108. data/test/mg_playground_controller_test.rb +11 -0
  109. data/test/mg_rallies_controller_test.rb +36 -0
  110. data/test/mg_report_item_test.rb +7 -0
  111. data/test/mg_report_items_controller_test.rb +31 -0
  112. data/test/mg_report_test.rb +18 -0
  113. data/test/mg_reports_controller_test.rb +50 -0
  114. data/test/test_helper.rb +203 -0
  115. metadata +108 -55
  116. data/lib/mountain-goat/models/ci_meta.rb +0 -9
  117. data/lib/mountain-goat/models/convert.rb +0 -70
  118. data/lib/mountain-goat/models/convert_meta_type.rb +0 -19
  119. data/lib/mountain-goat/models/cs_meta.rb +0 -9
  120. data/lib/mountain-goat/models/metric_variant.rb +0 -22
  121. data/lib/mountain-goat/views/mountain_goat/mountain_goat_converts/edit.html.erb +0 -13
  122. data/lib/mountain-goat/views/mountain_goat/mountain_goat_converts/index.html.erb +0 -48
  123. data/lib/mountain-goat/views/mountain_goat/mountain_goat_converts/new.html.erb +0 -13
  124. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metric_variants/edit.html.erb +0 -13
  125. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metric_variants/new.html.erb +0 -15
  126. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metrics/edit.html.erb +0 -13
  127. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metrics/index.html.erb +0 -14
  128. data/lib/mountain-goat/views/mountain_goat/mountain_goat_metrics/new.html.erb +0 -14
  129. data/lib/mountain-goat/views/mountain_goat/mountain_goat_rallies/_rallies.html.erb +0 -5
  130. data/lib/mountain-goat/views/mountain_goat/mountain_goat_rallies/edit.html.erb +0 -13
  131. data/lib/mountain-goat/views/mountain_goat/mountain_goat_rallies/new.html.erb +0 -13
  132. data/test/ocelot_converts_controller_test.rb +0 -45
  133. data/test/ocelot_metric_variants_controller_test.rb +0 -45
  134. data/test/ocelot_metrics_controller_test.rb +0 -45
  135. data/test/ocelot_rallies_controller_test.rb +0 -8
@@ -0,0 +1,47 @@
1
+
2
+ class Mg < ActionController::Base
3
+
4
+ layout 'mountain_goat'
5
+
6
+ before_filter :verify_access
7
+
8
+ self.prepend_view_path File.join([File.dirname(__FILE__), '../../views/mountain_goat/'])
9
+
10
+ private
11
+
12
+ def store_location(url = nil)
13
+ if url.nil?
14
+ if request.method == :post && request.params.count > 0
15
+ session[:mg_return_to] = "#{request.request_uri}?" + encode_parameters(request.params)
16
+ else
17
+ session[:mg_return_to] = request.request_uri
18
+ end
19
+ else
20
+ session[:mg_return_to] = url
21
+ end
22
+
23
+ logger.warn "Storing location: #{session[:mg_return_to]}"
24
+ end
25
+
26
+ def redirect_back_or_default(default)
27
+ redirect_to(session[:mg_return_to] || default)
28
+ session[:mg_return_to] = nil
29
+ end
30
+
31
+ def verify_access
32
+ return if session[:mg_access] == true
33
+ store_location
34
+ redirect_to mg_login_url and return
35
+ end
36
+
37
+ def self.password_digest(password, salt)
38
+ site_key = '1e9532ea39233e1e2786d80fde90d708c0918d2d'
39
+ stretches = 10
40
+ digest = site_key
41
+ stretches.times do
42
+ digest = secure_digest(digest, salt, password, site_key)
43
+ end
44
+ digest
45
+ end
46
+
47
+ end
@@ -1,15 +1,14 @@
1
1
 
2
- class MountainGoatController < ActionController::Base
3
-
4
- layout 'mountain_goat'
2
+ class Mg::MountainGoatController < Mg
5
3
 
6
4
  before_filter :verify_access, :except => [ :login, :login_create, :fetch ]
7
-
8
- self.prepend_view_path File.join([File.dirname(__FILE__), '../../views/mountain_goat/'])
9
5
 
10
6
  def fetch
11
7
  ct = { :png => 'image/png', :css => 'text/css', :html => 'text/html', :js => 'text/javascript' }
12
8
 
9
+ raise ArgumentError, "Invalid fetch file" if params[:file].match(/(([_][_])|([^a-z0-9_]))/ix) #extra security
10
+
11
+ #We will only serve files located in the public directory for security reasons
13
12
  Dir.open(File.join([File.dirname(__FILE__), '../../public/'])).each do |file|
14
13
  if file == params[:file].gsub('_','.')
15
14
  if file =~ /[.]([a-z0-9]+)$/
@@ -21,13 +20,7 @@ class MountainGoatController < ActionController::Base
21
20
  end
22
21
  end
23
22
 
24
- render :file => "#{Rails.root}/public/404.html", :status => :not_found
25
- end
26
-
27
- def verify_access
28
- return if session.has_key?(:mg_access) && session[:mg_access] == true
29
- store_location
30
- redirect_to '/mg/login' and return
23
+ render :file => "#{RAILS_ROOT}/public/404.html", :status => :not_found
31
24
  end
32
25
 
33
26
  def login
@@ -70,34 +63,4 @@ class MountainGoatController < ActionController::Base
70
63
  end
71
64
  end
72
65
 
73
- def store_location(url = nil)
74
- if url.nil?
75
- if request.method == :post && request.params.count > 0
76
- session[:mg_return_to] = "#{request.request_uri}?" + encode_parameters(request.params)
77
- else
78
- session[:mg_return_to] = request.request_uri
79
- end
80
- else
81
- session[:mg_return_to] = url
82
- end
83
-
84
- logger.warn "Storing location: #{session[:mg_return_to]}"
85
- end
86
-
87
- def redirect_back_or_default(default)
88
- redirect_to(session[:mg_return_to] || default)
89
- session[:mg_return_to] = nil
90
- end
91
-
92
- private
93
-
94
- def self.password_digest(password, salt)
95
- site_key = '1e9532ea39233e1e2786d80fde90d708c0918d2d'
96
- stretches = 10
97
- digest = site_key
98
- stretches.times do
99
- digest = secure_digest(digest, salt, password, site_key)
100
- end
101
- digest
102
- end
103
66
  end
@@ -0,0 +1,8 @@
1
+
2
+ class Mg::PlaygroundController < Mg
3
+
4
+ def test
5
+
6
+ end
7
+
8
+ end
@@ -1,9 +1,9 @@
1
- class MountainGoatRalliesController < MountainGoatController
1
+
2
+ class Mg::RalliesController < Mg
2
3
 
3
4
  def index
4
-
5
5
  @page = !params[:page].nil? ? params[:page].to_i : 1
6
- @convert = Convert.find(params[:mountain_goat_convert_id]) if !params[:mountain_goat_convert_id].nil?
6
+ @convert = Convert.find(params[:convert_id]) if !params[:convert_id].nil?
7
7
 
8
8
  if @convert
9
9
  @rallies = @convert.rallies.find(:all, :conditions => { }, :order => "created_at DESC", :limit => 100, :offset => ( @page - 1 ) * 100 )
@@ -28,7 +28,7 @@ class MountainGoatRalliesController < MountainGoatController
28
28
 
29
29
  if @rallies.count > 0
30
30
  render :json => { :success => true,
31
- :result => render_to_string(:partial => 'mountain_goat_rallies/rallies', :locals => { :rallies => @rallies } ),
31
+ :result => render_to_string(:partial => 'mg/rallies/rallies', :locals => { :rallies => @rallies } ),
32
32
  :recent_rally_id => @rallies.first.id }
33
33
  else
34
34
  render :json => { :success => false }
@@ -40,26 +40,4 @@ class MountainGoatRalliesController < MountainGoatController
40
40
  @rally = Rally.find(params[:id])
41
41
  end
42
42
 
43
- def new
44
- if params[:convert_id]
45
- @rally = Rally.new(:convert_id => params[:convert_id])
46
- else
47
- @rally = Rally.new
48
- end
49
-
50
- end
51
-
52
- def create
53
-
54
- end
55
-
56
- def edit
57
- @rally = Rally.find(params[:id])
58
- end
59
-
60
- def update
61
-
62
- end
63
-
64
-
65
43
  end
@@ -0,0 +1,82 @@
1
+ class Mg::ReportItemsController < Mg
2
+
3
+ def get_extra
4
+ ( render :json => { :success => true, :result => "<span></span>" } and return ) if params[:value].blank?
5
+ id, model = params[:value].split('-')
6
+ reportable = model.constantize.find(id)
7
+ render :json => { :success => true, :result => render_to_string( :partial => 'mg/report_items/report_item_pivot_form', :locals => { :reportable => reportable } ) }
8
+ end
9
+
10
+ def new
11
+ @report = Mg::Report.find(params[:report_id])
12
+ raise ArgumentError, "Invalid report" if @report.nil?
13
+
14
+ @report_item = Mg::ReportItem.new
15
+
16
+ render :json => { :success => true,
17
+ :result => render_to_string(:action => :new, :layout => 'xhr') }
18
+ end
19
+
20
+ def create
21
+ @report = Mg::Report.find(params[:report_id])
22
+ raise ArgumentError, "Invalid report" if @report.nil?
23
+
24
+ @report_item = @report.report_items.new(params[:report_item].clone.delete_if { |k, v| k.intern == :reportable || k.intern == :pivot } )
25
+ @report_item.order = @report.report_items.to_a.map { |ri| ri.order }.push(0).max + 1#@report.report_items.maximum(:order) + 1 -- weird sqlite3 bugs
26
+
27
+ if !params[:report_item][:reportable].blank?
28
+ id, model = params[:report_item][:reportable].split('-')
29
+ @report_item.reportable = model.constantize.find(id)
30
+ end
31
+
32
+ if !params[:report_item][:pivot].blank?
33
+ id, model = params[:report_item][:pivot].split('-')
34
+ @report_item.pivot = model.constantize.find(id)
35
+ end
36
+
37
+ if @report_item.save
38
+ render :json => { :success => true,
39
+ :close_dialog => true,
40
+ :result => "<span>Successfully added report item</span>",
41
+ :also => [ { :item => ".report-report-items", :result => render_to_string( :partial => "mg/reports/report_report_items", :locals => { :report => @report_item.report } ) } ] }
42
+ else
43
+ render :json => { :success => true,
44
+ :result => render_to_string(:action => :new, :layout => 'xhr') }
45
+ end
46
+ end
47
+
48
+ def edit
49
+ @report_item = Mg::ReportItem.find(params[:id])
50
+ @report = @report_item.report
51
+
52
+ render :json => { :success => true,
53
+ :result => render_to_string(:action => :edit, :layout => 'xhr') }
54
+ end
55
+
56
+ def update
57
+ @report_item = Mg::ReportItem.find(params[:id])
58
+ @report_item.update_attributes(params[:report_item].clone.delete_if { |k, v| k.intern == :reportable || k.intern == :pivot } )
59
+
60
+ if !params[:report_item][:reportable].blank?
61
+ id, model = params[:report_item][:reportable].split('-')
62
+ @report_item.reportable = model.constantize.find(id)
63
+ end
64
+
65
+ if !params[:report_item][:pivot].blank?
66
+ id, model = params[:report_item][:pivot].split('-')
67
+ @report_item.pivot = model.constantize.find(id)
68
+ end
69
+
70
+ if @report_item.save
71
+ render :json => { :success => true,
72
+ :close_dialog => true,
73
+ :result => "<span>Successfully updated report item</span>",
74
+ :also => [ { :item => ".report-report-items", :result => render_to_string( :partial => "mg/reports/report_report_items", :locals => { :report => @report_item.report }) } ] }
75
+ else
76
+ render :json => { :success => true,
77
+ :result => render_to_string(:action => :edit, :layout => 'xhr') }
78
+ end
79
+ end
80
+
81
+ #TODO: Destroy
82
+ end
@@ -0,0 +1,90 @@
1
+ class Mg::ReportsController < Mg
2
+ # GET /mg_reports
3
+ # GET /mg_reports.xml
4
+ def index
5
+ @reports = Mg::Report.all
6
+
7
+ respond_to do |format|
8
+ format.html # index.html.erb
9
+ format.xml { render :xml => @reports }
10
+ end
11
+ end
12
+
13
+ # GET /mg_reports/1
14
+ # GET /mg_reports/1.xml
15
+ def show
16
+ @report = Mg::Report.find(params[:id])
17
+
18
+ respond_to do |format|
19
+ format.html # show.html.erb
20
+ format.xml { render :xml => @report }
21
+ end
22
+ end
23
+
24
+ def show_svg
25
+ @report = Mg::Report.find(params[:id])
26
+
27
+ render :text => render_to_string(:partial => "mg/reports/report", :layout => '_pdf', :locals => { :report => @report } )
28
+ response.content_type = 'application/xhtml+xml'
29
+ end
30
+
31
+ # GET /mg_reports/new
32
+ # GET /mg_reports/new.xml
33
+ def new
34
+ @report = Mg::Report.new
35
+
36
+ respond_to do |format|
37
+ format.html # new.html.erb
38
+ format.xml { render :xml => @report }
39
+ end
40
+ end
41
+
42
+ # GET /mg_reports/1/edit
43
+ def edit
44
+ @report = Mg::Report.find(params[:id])
45
+ end
46
+
47
+ # POST /mg_reports
48
+ # POST /mg_reports.xml
49
+ def create
50
+ @report = Mg::Report.new(params[:report])
51
+
52
+ respond_to do |format|
53
+ if @report.save
54
+ format.html { flash[:notice] = 'Your report was successfully created, now add some report items.'; redirect_to(edit_mg_report_url @report) }
55
+ format.xml { render :xml => @report, :status => :created, :location => @report }
56
+ else
57
+ format.html { render :action => "new" }
58
+ format.xml { render :xml => @report.errors, :status => :unprocessable_entity }
59
+ end
60
+ end
61
+ end
62
+
63
+ # PUT /mg_reports/1
64
+ # PUT /mg_reports/1.xml
65
+ def update
66
+ @report = Mg::Report.find(params[:id])
67
+
68
+ respond_to do |format|
69
+ if @report.update_attributes(params[:report])
70
+ format.html { redirect_to(@report, :notice => 'Mg::Report was successfully updated.') }
71
+ format.xml { head :ok }
72
+ else
73
+ format.html { render :action => "edit" }
74
+ format.xml { render :xml => @report.errors, :status => :unprocessable_entity }
75
+ end
76
+ end
77
+ end
78
+
79
+ # DELETE /mg_reports/1
80
+ # DELETE /mg_reports/1.xml
81
+ def destroy
82
+ @report = Mg::Report.find(params[:id])
83
+ @report.destroy
84
+
85
+ respond_to do |format|
86
+ format.html { redirect_to(mg_reports_url) }
87
+ format.xml { head :ok }
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,64 @@
1
+
2
+ ################
3
+ # Setup PDFKit #
4
+ ################
5
+
6
+ begin
7
+ require 'pdfkit'
8
+ rescue LoadError
9
+ raise "Mountain Goat Reports will not work without the 'pdfkit' gem (please run `gem install pdfkit`)"
10
+ end
11
+
12
+ ######################
13
+ # Setup svg-graph #
14
+ ######################
15
+
16
+ begin
17
+ require 'SVG/Graph/TimeSeries'
18
+ rescue LoadError
19
+ raise "Mountain Goat Reports will not work without the 'svg-graph' gem (please run `gem install svg-graph`)"
20
+ end
21
+
22
+ ############################
23
+ # TODO: Verify Email Setup #
24
+ ############################
25
+
26
+ class MG
27
+ def self.deliver(delivery_set = nil)
28
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
29
+
30
+ if mg_yml.blank? || mg_yml[RAILS_ENV].blank? || mg_yml[RAILS_ENV]['wkhtmltopdf'].blank?
31
+ raise "Please configure wkhtmltopdf settings in #{RAILS_ROOT}/config/mountain-goat.yml"
32
+ end
33
+
34
+ PDFKit.configure do |cz|
35
+ cz.wkhtmltopdf = mg_yml[RAILS_ENV]['wkhtmltopdf']
36
+ cz.default_options = { :'custom-header' => "'Content-Type' 'application/xhtml+xml'" }
37
+ end
38
+
39
+ if delivery_set.nil?
40
+ reports = Mg::Report.all
41
+ else
42
+ reports = Mg::Report.find(:all, :conditions => { :delivery_set => delivery_set.to_s } )
43
+ end
44
+
45
+ #We need to render the report report_show
46
+ reports.each do |report|
47
+ puts "Delivering report: #{report.title}"
48
+ av = ActionView::Base.new
49
+ av.view_paths = File.join([File.dirname(__FILE__), 'views/mountain_goat/'])
50
+ data = av.render(:partial => 'mg/reports/report', :layout => 'layouts/pdf', :locals => { :report => report } )
51
+
52
+ #Oddly, the file extension matters most here
53
+ tmp = Tempfile.new(['chart', '.xhtml'])
54
+ tmp << data
55
+ tmp.flush
56
+
57
+ kit = PDFKit.new(File.new(tmp.path), :page_size => 'Letter')
58
+ pdf = kit.to_pdf
59
+ tmp.close
60
+
61
+ Mg::ReportMailer.deliver_report(report, pdf)
62
+ end
63
+ end
64
+ end
@@ -7,49 +7,165 @@ module MetricTracking
7
7
  define_method :clear!, lambda {}
8
8
  end
9
9
 
10
- #TODO: Namespace?
10
+ Mime::Type.register "application/xhtml+xml", :xhtml
11
+
11
12
  ActionController::Routing::Routes.draw do |map|
12
- map.mg '/mg', :controller => :mountain_goat_converts, :action => :index
13
- map.mg_login '/mg/login', :controller => :mountain_goat, :action => :login
14
- map.mg_login_create '/mg/login/create', :controller => :mountain_goat, :action => :login_create
15
- map.resources :mountain_goat_metric_variants
16
- map.resources :mountain_goat_converts, :has_many => [ :mountain_goat_metrics, :mountain_goat_rallies ]
17
- map.resources :mountain_goat_metrics, :has_many => :mountain_goat_metric_variants
18
- map.resources :mountain_goat_rallies
19
- map.new_rallies '/mg/rallies/new', :controller => :mountain_goat_rallies, :action => :new_rallies
20
- map.fresh_metrics '/fresh-metrics', :controller => :mountain_goat_metrics, :action => :fresh_metrics
21
- map.connect '/mg/public/:file', :controller => :mountain_goat, :action => :fetch
13
+ map.namespace :mg do |mg|
14
+ mg.mg '/mg', :controller => :converts, :action => :index, :path_prefix => ""
15
+ mg.login '/login', :controller => :mountain_goat, :action => :login
16
+ mg.login_create '/login/create', :controller => :mountain_goat, :action => :login_create
17
+ mg.resources :metric_variants
18
+ mg.resources :converts, :has_many => [ :rallies ]
19
+ mg.resources :metrics, :has_many => :metric_variants
20
+ mg.resources :rallies, :collection => { :new_rallies => :get }
21
+ mg.resources :reports, :has_many => :report_items, :member => { :show_svg => :get }
22
+ mg.resources :report_items, :member => { :destroy => :get, :update => :post }, :collection => { :get_extra => :get }
23
+ mg.resources :playground, :collection => { :test => :get }
24
+ mg.new_rallies '/rallies/new', :controller => :rallies, :action => :new_rallies
25
+ mg.fresh_metrics '/fresh-metrics', :controller => :metrics, :action => :fresh_metrics
26
+ mg.connect '/public/:file', :controller => :mountain_goat, :action => :fetch
27
+ end
22
28
  end
23
29
 
24
30
  module Controller
25
31
 
32
+ #This is just for testing
33
+ def mg_rand
34
+ return "(SELECT #{@mg_i.nil? ? 1 : @mg_i.to_f})" if defined?(MOUNTAIN_GOAT_TEST) && MOUNTAIN_GOAT_TEST
35
+ "RAND()"
36
+ end
37
+
38
+ def mg_epsilon
39
+ if @mg_epsilon.nil?
40
+ @mg_epsilon = 0.1 #default
41
+ mg_yml = nil
42
+ begin
43
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
44
+ rescue
45
+ end
46
+ if mg_yml
47
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('epsilon')
48
+ @mg_epsilon = mg_yml[RAILS_ENV]['epsilon'].to_f
49
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('epsilon')
50
+ @mg_epsilon = mg_yml['settings']['epsilon'].to_f
51
+ end
52
+ end
53
+ end
54
+ return @mg_epsilon
55
+ end
56
+
57
+ def mg_strategy
58
+ if @mg_strategy.nil?
59
+ @mg_strategy = 'e-greedy' #default
60
+ mg_yml = nil
61
+ begin
62
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
63
+ rescue
64
+ end
65
+ if mg_yml
66
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('strategy')
67
+ @mg_strategy = mg_yml[RAILS_ENV]['strategy']
68
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('strategy')
69
+ @mg_strategy = mg_yml['settings']['strategy']
70
+ end
71
+ end
72
+ end
73
+ return @mg_strategy
74
+ end
75
+
76
+ def mg_apply_strategy(metric)
77
+ case mg_strategy.downcase
78
+ when 'e-greedy'
79
+ logger.warn Mg::MetricVariant.all(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :metric_id => metric.id } )
80
+ return Mg::MetricVariant.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC, CASE WHEN #{mg_rand} < #{mg_epsilon.to_f} THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC, #{mg_rand} DESC", :conditions => { :metric_id => metric.id } )
81
+ when 'e-greedy-decreasing'
82
+ return Mg::MetricVariant.first(:order => "CASE WHEN served = 0 THEN 1 ELSE 0 END DESC,
83
+ CASE WHEN #{mg_rand} < #{mg_epsilon.to_f} / ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) THEN #{mg_rand} ELSE CASE WHEN served = 0 THEN -1 ELSE reward / served END END DESC,
84
+ #{mg_rand} DESC", :conditions => { :metric_id => metric.id } ) # * log( ( select sum(served) from mg_metric_variants where metric_id = #{ metric.id.to_i } ) )
85
+ when 'a/b'
86
+ return Mg::MetricVariant.first(:order => "#{mg_rand} DESC", :conditions => { :metric_id => metric.id } )
87
+ else
88
+ raise "Invalid strategy #{mg_strategy}"
89
+ end
90
+ end
91
+
92
+ def mg_storage
93
+ if @mg_storage.nil?
94
+ @mg_storage = defined?(cookies) ? cookies : nil
95
+
96
+ mg_yml = nil
97
+ begin
98
+ mg_yml = YAML::load(File.open("#{RAILS_ROOT}/config/mountain-goat.yml"))
99
+ rescue
100
+ end
101
+ if mg_yml
102
+ if mg_yml.has_key?(RAILS_ENV) && mg_yml[RAILS_ENV].has_key?('storage')
103
+ uc = mg_yml[RAILS_ENV]['storage'].strip
104
+ @mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
105
+ elsif mg_yml.has_key?('settings') && mg_yml['settings'].has_key?('storage')
106
+ uc = mg_yml['settings']['storage'].strip
107
+ @mg_storage = ( uc == "cookies" && defined?(cookies) ) ? cookies : ( uc == "session" && defined?(session) ) ? session : nil
108
+ end
109
+ end
110
+ end
111
+ @mg_storage = {} if @mg_storage.nil? #'none'
112
+ return @mg_storage
113
+ end
114
+
26
115
  ######################
27
116
  # Metric Tracking #
28
117
  ######################
29
118
 
30
- def sv(metric_type, convert_type, &block)
119
+ def bds(metric_type, &block)
31
120
  raise ArgumentError, "Switch variant needs block" if !block_given?
32
- metric, convert = get_metric_convert( metric_type, convert_type, true )
33
- block.call(SwitchVariant.new( logger, metric, convert, nil ) )
121
+ metric = get_metric( metric_type, true )
122
+ block.call(SwitchVariant.new( logger, metric, nil ) )
34
123
 
35
- var = get_switch_metric_variant(metric_type, convert_type)
36
- block.call(SwitchVariant.new( logger, metric, convert, var ) )
124
+ var = get_switch_metric_variant( metric_type )
125
+ block.call(SwitchVariant.new( logger, metric, var ) )
126
+ end
127
+
128
+ def bd(metric_type, default, opts = {}, opt = nil)
129
+ return get_metric_variant(metric_type, default, opts, opt)[:value]
130
+ end
131
+
132
+ def bdd(metric_type, default, opts = {}, opt = nil)
133
+ return get_metric_variant(metric_type, default, opts, opt)
134
+ end
135
+
136
+ #Legacy
137
+ def sv(metric_type, convert_type, &block)
138
+ bds(metric_type, &block)
37
139
  end
38
140
 
39
141
  def mv(metric_type, convert_type, default, opts = {}, opt = nil)
40
- return get_metric_variant(metric_type, convert_type, default, opts, opt)[:value]
142
+ bd(metric_type, default, opts, opt)
41
143
  end
42
144
 
43
145
  def mv_detailed(metric_type, convert_type, default, opts = {}, opt = nil)
44
- return get_metric_variant(metric_type, convert_type, default, opts, opt)
146
+ bdd(metric_type, default, opts, opt)
45
147
  end
46
148
 
47
149
  #shorthand
150
+ def rw(convert_type, reward, options = {})
151
+ self.bandit_reward(convert_type, reward, options)
152
+ end
153
+
48
154
  def rc(convert_type, options = {})
49
- self.record_conversion(convert_type, options)
155
+ self.bandit_reward(convert_type, 1, options)
50
156
  end
51
157
 
52
158
  def record_conversion(convert_type, options = {})
159
+ self.bandit_reward(convert_type, 1, options)
160
+ end
161
+
162
+ #allows bandit_reward(convert, options)
163
+ def bandit_reward(convert_type, reward, options = {})
164
+
165
+ if reward.is_a?(Hash) #allow arguments bandit_reward(convert, options)
166
+ options = reward
167
+ reward = 0
168
+ end
53
169
 
54
170
  metrics = {} #for user-defined metrics
55
171
  options = options.with_indifferent_access
@@ -71,18 +187,18 @@ module MetricTracking
71
187
 
72
188
  logger.warn "Recording conversion #{convert_type.to_s} with options #{options.inspect}"
73
189
 
74
- convert = Convert.first( :conditions => { :convert_type => convert_type.to_s } )
190
+ convert = Mg::Convert.first( :conditions => { :convert_type => convert_type.to_s } )
75
191
 
76
192
  #now, we just create the convert if we don't have one
77
- convert = Convert.create!( :convert_type => convert_type.to_s, :name => convert_type.to_s ) if convert.nil?
78
-
193
+ convert = Mg::Convert.create!( :convert_type => convert_type.to_s, :name => convert_type.to_s, :reward => reward ) if convert.nil?
194
+
79
195
  #first, let's tally for the conversion itself
80
196
  #we need to see what meta information we should fill based on the conversion type
81
- Rally.create!( { :convert_id => convert.id } ).set_meta_data(options)
197
+ Mg::Rally.create!( { :convert_id => convert.id } ).set_meta_data(options)
82
198
 
83
199
  #User-defined metric tallies
84
200
  metrics.each do |metric_type, variant_id|
85
- m = Metric.find_by_metric_type(metric_type)
201
+ m = Mg::Metric.find_by_metric_type(metric_type)
86
202
  if m.nil?
87
203
  logger.warn "Missing user-defined metric #{metric_type}"
88
204
  next
@@ -96,27 +212,27 @@ module MetricTracking
96
212
  end
97
213
 
98
214
  logger.warn "Tallying conversion #{convert.name} for #{m.title} - #{v.name} (#{v.value} - #{v.id})"
99
- v.tally_convert
215
+ v.tally_convert(convert, reward)
100
216
  end
101
217
 
102
- if defined?(cookies)
103
- #we just converted, let's tally each of our metrics (from cookies)
104
- convert.metrics.each do |metric|
218
+ if !mg_storage.nil?
219
+ #we just converted, let's tally each of our metrics (from cookies or session)
220
+ Mg::Metric.all.each do |metric|
105
221
  metric_sym = "metric_#{metric.metric_type}".to_sym
106
222
  metric_variant_sym = "metric_#{metric.metric_type}_variant".to_sym
107
223
 
108
- value = cookies[metric_sym]
109
- variant_id = cookies[metric_variant_sym]
224
+ value = mg_storage[metric_sym]
225
+ variant_id = mg_storage[metric_variant_sym]
110
226
 
111
227
  #logger.warn "Value: #{metric_sym} - #{value}"
112
228
  #logger.warn "Value: #{metric_variant_sym} - #{variant_id}"
113
229
 
114
230
  if variant_id.blank? #the user just doesn't have this set
115
- logger.error "No variant found for #{metric.title}"
231
+ #This is now common-case
116
232
  next
117
233
  end
118
234
 
119
- variant = MetricVariant.first(:conditions => { :id => variant_id.to_i } )
235
+ variant = Mg::MetricVariant.first(:conditions => { :id => variant_id.to_i } )
120
236
 
121
237
  if variant.nil?
122
238
  logger.error "Variant #{variant_id} not in metric variants for #{metric.title}"
@@ -128,7 +244,7 @@ module MetricTracking
128
244
  end
129
245
 
130
246
  logger.warn "Tallying conversion #{convert.name} for #{metric.title} - #{variant.name} (#{variant.value} - #{variant.id})"
131
- variant.tally_convert
247
+ variant.tally_convert(convert, reward)
132
248
  end
133
249
  end
134
250
  end
@@ -136,15 +252,15 @@ module MetricTracking
136
252
  private
137
253
 
138
254
  #returns a map { :value => value, :variant_id => id }
139
- def get_metric_variant(metric_type, convert_type, default, opts = {}, opt = nil)
255
+ def get_metric_variant(metric_type, default, opts = {}, opt = nil)
140
256
  metric_sym = "metric_#{metric_type}#{ opt.nil? ? "" : '_' + opt.to_s }".to_sym
141
257
  metric_variant_sym = "metric_#{metric_type}_variant".to_sym
142
258
 
143
259
  #first, we'll check for a cookie value
144
- if defined?(cookies) && cookies[metric_sym] && !cookies[metric_sym].blank?
260
+ if !mg_storage.nil? && mg_storage[metric_sym] && !mg_storage[metric_sym].blank?
145
261
  #we have the cookie
146
- variant_id = cookies[metric_variant_sym]
147
- variant = MetricVariant.first(:conditions => { :id => variant_id.to_i } )
262
+ variant_id = mg_storage[metric_variant_sym]
263
+ variant = Mg::MetricVariant.first(:conditions => { :id => variant_id.to_i } )
148
264
  if !variant.nil?
149
265
  if variant.metric.tally_each_serve
150
266
  variant.tally_serve
@@ -153,46 +269,44 @@ module MetricTracking
153
269
  logger.warn "Serving metric #{metric_type} #{ opt.nil? ? "" : opt.to_s } without finding / tallying variant."
154
270
  end
155
271
 
156
- return { :value => cookies[metric_sym], :variant_id => cookies[metric_variant_sym] } #it's the best we can do
272
+ return { :value => mg_storage[metric_sym], :variant_id => mg_storage[metric_variant_sym] } #it's the best we can do
157
273
  else
158
274
  #we don't have the cookie, let's find a value to set
159
- metric, convert = get_metric_convert( metric_type, convert_type, false )
275
+ metric = get_metric( metric_type, false )
160
276
 
161
- #to use RAND(), let's not use metrics.metric_variants
162
- sum_priority = MetricVariant.first(:select => "SUM(priority) as sum_priority", :conditions => { :metric_id => metric.id } ).sum_priority.to_f
163
-
164
- if sum_priority > 0.0
165
- metric_variant = MetricVariant.first(:order => "RAND() * ( priority / #{sum_priority.to_f} ) DESC", :conditions => { :metric_id => metric.id } )
166
- end
277
+ metric_variant = mg_apply_strategy(metric)
167
278
 
168
279
  if metric_variant.nil?
169
280
  logger.warn "Missing metric variants for #{metric_type}"
170
- metric_variant = MetricVariant.create!( { :metric_id => metric.id, :value => default, :name => default }.merge(opts) )
281
+ metric_variant = Mg::MetricVariant.create!( { :metric_id => metric.id, :value => default, :name => default }.merge(opts) )
282
+ end
283
+
284
+ if metric_variant.metric.tally_each_serve
285
+ metric_variant.tally_serve #donate we served this to a user
171
286
  end
172
287
 
173
- metric_variant.tally_serve #donate we served this to a user
174
288
  value = metric_variant.read_attribute( opt.nil? ? :value : opt )
175
289
  logger.debug "Serving #{metric_variant.name} (#{value}) for #{metric_sym}"
176
290
  #good, we have a variant, let's store it in session
177
291
 
178
- if defined?(cookies)
179
- cookies[metric_sym] = { :value => value } #, :domain => WILD_DOMAIN
180
- cookies[metric_variant_sym] = { :value => metric_variant.id } #, :domain => WILD_DOMAIN
292
+ if !mg_storage.nil?
293
+ mg_storage[metric_sym] = value #, :domain => WILD_DOMAIN
294
+ mg_storage[metric_variant_sym] = metric_variant.id #, :domain => WILD_DOMAIN
181
295
  end
182
296
 
183
297
  return { :value => value, :variant_id => metric_variant.id }
184
298
  end
185
299
  end
186
300
 
187
- def get_switch_metric_variant(metric_type, convert_type)
301
+ def get_switch_metric_variant(metric_type)
188
302
  metric_variant_sym = "metric_#{metric_type}_variant".to_sym
189
303
 
190
304
  #first, we'll check for a cookie selection
191
- if defined?(cookies) && cookies[metric_variant_sym] && !cookies[metric_variant_sym].blank?
305
+ if !mg_storage.nil? && mg_storage[metric_variant_sym] && !mg_storage[metric_variant_sym].blank?
192
306
  #we have the cookie
193
307
 
194
- variant_id = cookies[metric_variant_sym]
195
- variant = MetricVariant.first(:conditions => { :id => variant_id.to_i } )
308
+ variant_id = mg_storage[metric_variant_sym]
309
+ variant = Mg::MetricVariant.first(:conditions => { :id => variant_id.to_i } )
196
310
  if !variant.nil?
197
311
 
198
312
  if variant.metric.tally_each_serve
@@ -208,46 +322,38 @@ module MetricTracking
208
322
  end
209
323
 
210
324
  #we don't have the cookie, let's find a value to set
211
- metric, convert = get_metric_convert( metric_type, convert_type, true )
325
+ metric = get_metric( metric_type, true )
212
326
 
213
- #to use RAND(), let's not use metrics.metric_variants
214
- sum_priority = MetricVariant.first(:select => "SUM(priority) as sum_priority", :conditions => { :metric_id => metric.id } ).sum_priority.to_f
215
-
216
- if sum_priority > 0.0
217
- metric_variant = MetricVariant.first(:order => "RAND() * ( priority / #{sum_priority.to_f} ) DESC", :conditions => { :metric_id => metric.id } )
218
- end
327
+ metric_variant = mg_apply_strategy(metric)
219
328
 
220
329
  if metric_variant.nil?
221
330
  logger.warn "Missing metric variants for #{metric_type}"
222
331
  raise ArgumentError, "Missing variants for switch-type #{metric_type}"
223
332
  end
224
333
 
225
- metric_variant.tally_serve #donate we served this to a user
334
+ if metric_variant.metric.tally_each_serve
335
+ metric_variant.tally_serve #donate we served this to a user
336
+ end
337
+
226
338
  logger.debug "Serving #{metric_variant.name} (#{metric_variant.switch_type}) for #{metric.title} (switch-type)"
227
339
  #good, we have a variant, let's store it in session (not the value, just the selection)
228
- if defined?(cookies)
229
- cookies[metric_variant_sym] = { :value => metric_variant.id } #, :domain => WILD_DOMAIN
340
+ if !mg_storage.nil?
341
+ mg_storage[metric_variant_sym] = metric_variant.id #, :domain => WILD_DOMAIN
230
342
  end
231
343
 
232
344
  return metric_variant
233
345
  end
234
346
 
235
- def get_metric_convert(metric_type, convert_type, is_switch = false)
347
+ def get_metric(metric_type, is_switch = false)
236
348
 
237
- metric = Metric.first(:conditions => { :metric_type => metric_type.to_s } )
349
+ metric = Mg::Metric.first(:conditions => { :metric_type => metric_type.to_s } )
238
350
 
239
- conv = Convert.find_by_convert_type( convert_type.to_s )
240
- if conv.nil?
241
- logger.warn "Missing convert type #{convert_type.to_s} -- creating"
242
- conv = Convert.create( :convert_type => convert_type.to_s, :name => convert_type.to_s ) if conv.nil?
243
- end
244
-
245
351
  if metric.nil? #we don't have a metric of this type
246
352
  logger.warn "Missing metric type #{metric_type.to_s} -- creating"
247
- metric = Metric.create( :metric_type => metric_type.to_s, :title => metric_type.to_s, :convert_id => conv.id, :is_switch => is_switch )
353
+ metric = Mg::Metric.create( :metric_type => metric_type.to_s, :title => metric_type.to_s, :is_switch => is_switch )
248
354
  end
249
355
 
250
- return metric, conv
356
+ return metric
251
357
  end
252
358
  end
253
359
 
@@ -263,6 +369,18 @@ module MetricTracking
263
369
  def sv(*args, &block)
264
370
  @controller.send(:sv, *args, &block)
265
371
  end
372
+
373
+ def bd(*args, &block)
374
+ @controller.send(:bd, *args, &block)
375
+ end
376
+
377
+ def bdd(*args, &block)
378
+ @controller.send(:bdd, *args, &block)
379
+ end
380
+
381
+ def bds(*args, &block)
382
+ @controller.send(:bds, *args, &block)
383
+ end
266
384
  end
267
385
  end
268
386