gricer 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. data/MIT-LICENSE +20 -0
  2. data/README.rdoc +84 -0
  3. data/Rakefile +49 -0
  4. data/app/assets/images/gricer/fluid/breadcrumb.png +0 -0
  5. data/app/assets/javascripts/gricer.js.coffee +85 -0
  6. data/app/assets/javascripts/gricer_backend_jquery.js.coffee +352 -0
  7. data/app/assets/javascripts/jquery.flot.js +2599 -0
  8. data/app/assets/javascripts/jquery.flot.pie.js +750 -0
  9. data/app/assets/javascripts/jquery.flot.resize.js +60 -0
  10. data/app/assets/javascripts/jquery.flot.symbol.js +70 -0
  11. data/app/assets/javascripts/worldmap.js +146 -0
  12. data/app/assets/stylesheets/gricer/fluid-jquery-ui.css.scss +1298 -0
  13. data/app/assets/stylesheets/gricer/fluid.css.scss +240 -0
  14. data/app/assets/stylesheets/gricer/helpers/css3.css.scss +21 -0
  15. data/app/controllers/gricer/base_controller.rb +141 -0
  16. data/app/controllers/gricer/capture_controller.rb +42 -0
  17. data/app/controllers/gricer/dashboard_controller.rb +18 -0
  18. data/app/controllers/gricer/requests_controller.rb +42 -0
  19. data/app/controllers/gricer/sessions_controller.rb +24 -0
  20. data/app/helpers/gricer/base_helper.rb +22 -0
  21. data/app/models/gricer/agent.rb +789 -0
  22. data/app/models/gricer/request.rb +239 -0
  23. data/app/models/gricer/session.rb +433 -0
  24. data/app/views/gricer/capture/index.html.erb +1 -0
  25. data/app/views/gricer/dashboard/_menu.html.erb +10 -0
  26. data/app/views/gricer/dashboard/_overview.html.erb +33 -0
  27. data/app/views/gricer/dashboard/index.html.erb +19 -0
  28. data/config/routes.rb +51 -0
  29. data/lib/gricer.rb +36 -0
  30. data/lib/gricer/action_controller/base.rb +28 -0
  31. data/lib/gricer/action_controller/track.rb +132 -0
  32. data/lib/gricer/active_model/statistics.rb +167 -0
  33. data/lib/gricer/config.rb +125 -0
  34. data/lib/gricer/engine.rb +9 -0
  35. data/lib/gricer/localization.rb +3 -0
  36. data/lib/tasks/gricer_tasks.rake +92 -0
  37. data/spec/controllers/gricer/base_controller_spec.rb +207 -0
  38. data/spec/controllers/gricer/capture_controller_spec.rb +44 -0
  39. data/spec/controllers/gricer/dashboard_controller_spec.rb +44 -0
  40. data/spec/controllers/gricer/requests_controller_spec.rb +36 -0
  41. data/spec/controllers/gricer/sessions_controller_spec.rb +37 -0
  42. data/spec/dummy/Rakefile +7 -0
  43. data/spec/dummy/app/assets/javascripts/application.js +9 -0
  44. data/spec/dummy/app/assets/stylesheets/application.css +13 -0
  45. data/spec/dummy/app/assets/stylesheets/dashboard.css +4 -0
  46. data/spec/dummy/app/assets/stylesheets/scaffold.css +56 -0
  47. data/spec/dummy/app/assets/stylesheets/sessions.css +4 -0
  48. data/spec/dummy/app/controllers/application_controller.rb +23 -0
  49. data/spec/dummy/app/controllers/dashboard_controller.rb +19 -0
  50. data/spec/dummy/app/helpers/application_helper.rb +2 -0
  51. data/spec/dummy/app/helpers/dashboard_helper.rb +2 -0
  52. data/spec/dummy/app/views/dashboard/index.html.erb +236 -0
  53. data/spec/dummy/app/views/layouts/application.html.erb +16 -0
  54. data/spec/dummy/config.ru +4 -0
  55. data/spec/dummy/config/application.rb +48 -0
  56. data/spec/dummy/config/boot.rb +10 -0
  57. data/spec/dummy/config/cucumber.yml +9 -0
  58. data/spec/dummy/config/database.yml +25 -0
  59. data/spec/dummy/config/environment.rb +5 -0
  60. data/spec/dummy/config/environments/development.rb +27 -0
  61. data/spec/dummy/config/environments/production.rb +51 -0
  62. data/spec/dummy/config/environments/test.rb +39 -0
  63. data/spec/dummy/config/initializers/backtrace_silencers.rb +7 -0
  64. data/spec/dummy/config/initializers/inflections.rb +10 -0
  65. data/spec/dummy/config/initializers/mime_types.rb +5 -0
  66. data/spec/dummy/config/initializers/secret_token.rb +7 -0
  67. data/spec/dummy/config/initializers/session_store.rb +8 -0
  68. data/spec/dummy/config/initializers/wrap_parameters.rb +12 -0
  69. data/spec/dummy/config/locales/en.yml +5 -0
  70. data/spec/dummy/config/routes.rb +11 -0
  71. data/spec/dummy/db/schema.rb +241 -0
  72. data/spec/dummy/log/development.log +0 -0
  73. data/spec/dummy/public/404.html +26 -0
  74. data/spec/dummy/public/422.html +26 -0
  75. data/spec/dummy/public/500.html +26 -0
  76. data/spec/dummy/public/favicon.ico +0 -0
  77. data/spec/dummy/script/rails +6 -0
  78. data/spec/helpers/gricer/base_helper_spec.rb +28 -0
  79. data/spec/lib/gricer/action_controller/track_spec.rb +63 -0
  80. data/spec/models/gricer/agent_spec.rb +829 -0
  81. data/spec/models/gricer/request_spec.rb +145 -0
  82. data/spec/models/gricer/session_spec.rb +209 -0
  83. data/spec/routing/capture_routes_spec.rb +6 -0
  84. data/spec/routing/dashboard_routes_spec.rb +9 -0
  85. data/spec/routing/requests_routes_spec.rb +90 -0
  86. data/spec/routing/sessions_routes_spec.rb +115 -0
  87. data/spec/spec_helper.rb +23 -0
  88. metadata +185 -0
@@ -0,0 +1 @@
1
+ <%= params.inspect %>
@@ -0,0 +1,10 @@
1
+ <% items.each do |item| -%>
2
+ <% if item[1].to_sym == :menu -%>
3
+ <div>
4
+ <span><%= item[0] %></span>
5
+ <%= render 'menu', items: item[2] %>
6
+ </div>
7
+ <% else -%>
8
+ <%= link_to item[0], item[2], "data-gricer-#{item[1]}" => true %>
9
+ <% end -%>
10
+ <% end -%>
@@ -0,0 +1,33 @@
1
+ <div class='overview'>
2
+ <h2>
3
+ Overview for <%=l @stat_from%>
4
+ <% unless @stat_from == @stat_thru -%>
5
+ to <%=l @stat_thru%>
6
+ <% end -%>
7
+ </h2>
8
+
9
+ <p>
10
+ <% sessions_count = sessions.count.try(:to_f) -%>
11
+ <%=t '.sessions', count: sessions_count.to_i %>
12
+ </p>
13
+ <p>
14
+ <%=t '.new_sessions', count: sessions.new_visits.count %>
15
+ </p>
16
+ <p>
17
+ <%=t '.new_visitors', percentage: format_percentage(sessions.new_visitors) %>
18
+ </p>
19
+ <p>
20
+ <% request_count = requests.count.try(:to_f) -%>
21
+ <%=t '.requests', count: request_count.to_i %>
22
+ </p>
23
+ <p>
24
+ <%=t '.requests_per_session', count: '%3.2f' % sessions.requests_per_session %>
25
+ </p>
26
+
27
+ <p>
28
+ <%=t '.bounce_rate', percentage: format_percentage(sessions.bounce_rate) %>
29
+ </p>
30
+ <p>
31
+ <%=t '.avg_duration', time: format_seconds(sessions.avg_duration) %>
32
+ </p>
33
+ </div>
@@ -0,0 +1,19 @@
1
+ <div class="gricer-stat">
2
+ <div id="gricer-header">
3
+ <span class="path"></span>
4
+ <span class="dates">
5
+ <%= form_tag do -%>
6
+ <%= text_field_tag :from, @stat_from, id: 'gricer-from-field' -%>
7
+ -
8
+ <%= text_field_tag :thru, @stat_thru, id: 'gricer-thru-field' %>
9
+ <% end -%>
10
+ </span>
11
+ </div>
12
+ <div id="gricer-menu">
13
+ <%= render 'menu', items: ::Gricer.config.admin_menu %>
14
+ </div>
15
+
16
+ <div id="gricer-container"></div>
17
+
18
+ <%= javascript_include_tag :gricer_backend_jquery %>
19
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,51 @@
1
+ Rails.application.routes.draw do
2
+ post 'gricer_capture/:id' => 'gricer/capture#index', as: :gricer_capture
3
+
4
+ scope ::Gricer.config.admin_prefix do
5
+ root to: "gricer/dashboard#index", as: :gricer_root
6
+ post 'overview' => "gricer/dashboard#overview", as: :gricer_overview
7
+
8
+ scope 'sessions' do
9
+ [
10
+ 'agent.name', 'agent.os', 'agent.full_version', 'agent.major_version', 'agent.engine_name', 'agent.engine_version',
11
+ 'javascript', 'java', 'silverlight_version', 'silverlight_major_version', 'flash_version', 'flash_major_version',
12
+ 'country', 'city', 'domain', 'screen_size', 'screen_width', 'screen_height', 'screen_depth',
13
+ 'requested_locale_major', 'requested_locale_minor', 'requested_locale',
14
+ ].each do |field|
15
+ name = field.gsub('.', '_')
16
+ get "#{name}_process" => "gricer/sessions#process_stats", field: field, as: "gricer_sessions_#{name}_process"
17
+ get "#{name}_spread" => "gricer/sessions#spread_stats", field: field, as: "gricer_sessions_#{name}_spread"
18
+ end
19
+ end
20
+
21
+ scope 'requests' do
22
+ [
23
+ 'agent.name', 'agent.os', 'agent.full_version', 'agent.major_version', 'agent.engine_name', 'agent.engine_version',
24
+ 'host', 'path', 'method', 'protocol', 'entry_path',
25
+ 'referer_protocol', 'referer_host', 'referer_path', 'referer_params',
26
+ 'search_engine', 'search_query',
27
+ ].each do |field|
28
+ name = field.gsub('.', '_')
29
+ get "#{name}_process" => "gricer/requests#process_stats", field: field, as: "gricer_requests_#{name}_process"
30
+ get "#{name}_spread" => "gricer/requests#spread_stats", field: field, as: "gricer_requests_#{name}_spread"
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ # Gricer::Engine.routes.draw do
37
+ # root to: "dashboard#index"
38
+ #
39
+ # scope 'sessions' do
40
+ # [
41
+ # 'agent.name', 'agent.os', 'agent.full_version', 'agent.major_version', 'agent.engine_name', 'agent.engine_version',
42
+ # 'javascript', 'java', 'silverlight_version', 'silverlight_major_version', 'flash_version', 'flash_major_version',
43
+ # 'country', 'city', 'domain', 'screen_size', 'screen_width', 'screen_height', 'screen_depth',
44
+ # 'requested_major_locale', 'requested_minor_locale', 'requested_locale',
45
+ # ].each do |field|
46
+ # name = field.gsub('.', '_')
47
+ # get "#{name}_process" => "sessions#process_stats", field: field, as: "gricer_session_#{name}_process"
48
+ # get "#{name}_spread" => "sessions#spread_stats", field: field, as: "gricer_session_#{name}_spread"
49
+ # end
50
+ # end
51
+ # end
data/lib/gricer.rb ADDED
@@ -0,0 +1,36 @@
1
+ require "gricer/config"
2
+ require "gricer/engine"
3
+ require "gricer/localization"
4
+
5
+ require 'gricer/action_controller/base'
6
+ require 'gricer/action_controller/track'
7
+
8
+ require 'gricer/active_model/statistics'
9
+
10
+ # Gricer is a web analytics gem for Rails 3.1 and beyond
11
+ module Gricer
12
+ class << self
13
+ # To access the actual configuration of your Gricer, you can call this function.
14
+ #
15
+ # An example would be <tt>Gricer.config.table_name_prefix = 'stats_'</tt>
16
+ #
17
+ # See Gricer::Config for configuration options.
18
+ #
19
+ # @return [Gricer::Config] The actual configuration instance of Gricer
20
+ # @see Gricer::Config
21
+ def config
22
+ @config ||= Config.new
23
+ end
24
+
25
+ # To initialize Gricer it is handy to give it a block of options.
26
+ #
27
+ # See Gricer::Config for configuration options.
28
+ #
29
+ # @yield (config) The actual configuration instance of Gricer
30
+ # @return [Gricer::Config] The actual configuration instance of Gricer
31
+ # @see Gricer::Config
32
+ def configure(&block)
33
+ config.configure(&block)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ module Gricer
2
+ # Gricer's ActionController enhancements
3
+ module ActionController
4
+ # Gricer's ActionController base enhancements
5
+ module Base
6
+
7
+ # Include the helper functions into controllers.
8
+ def self.included(base)
9
+ base.module_eval do
10
+ helper_attr :gricer_user_id
11
+ end
12
+ end
13
+
14
+ # This function is a stub for the mechanism to include the current
15
+ # users id into the statistics of gricer.
16
+ # @example The most common case would be to pass the current_user's id. So this is the code you should add to your appliction_controller
17
+ # def gricer_user_id
18
+ # current_user.try(:id)
19
+ # end
20
+ # @return [Integer]
21
+ def gricer_user_id
22
+ nil
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ ActionController::Base.send :include, Gricer::ActionController::Base
@@ -0,0 +1,132 @@
1
+ module Gricer
2
+ # Around-Filter for tracking requests in Gricer
3
+ class TrackRequestFilter
4
+ # Around-Filter for tracking requests in Gricer
5
+ # @param controller The controller from which this Filter was included.
6
+ # @yield The controller's code block
7
+ def self.filter(controller, &block)
8
+ if controller.controller_path =~ /^gricer\// or controller.request.path =~ ::Gricer.config.exclude_paths
9
+ Rails.logger.debug "Gricer Track Request: Do not track '#{controller.controller_path}##{controller.action_name}' by config"
10
+ block.call
11
+ return
12
+ end
13
+
14
+ status = nil
15
+
16
+ options = {
17
+ request: controller.request,
18
+ controller: controller.controller_path,
19
+ action: controller.action_name,
20
+ params: controller.params,
21
+ session_id: controller.session[:gricer_session],
22
+ locale: I18n.locale
23
+ }
24
+
25
+ if controller.gricer_user_id
26
+ options[:user_id] = controller.gricer_user_id
27
+ end
28
+
29
+ options.keys.each do |key|
30
+ Rails.logger.debug key
31
+ end
32
+
33
+ gricer_request = ::Gricer::Request.create options
34
+ controller.gricer_request = gricer_request
35
+ controller.session[:gricer_session] = gricer_request.session_id
36
+
37
+ begin
38
+ benchmark = Benchmark.measure(&block)
39
+
40
+ gricer_request.update_attributes(
41
+ status_code: controller.response.status,
42
+ content_type: controller.response.content_type,
43
+ body_size: controller.response.body.size,
44
+ system_time: (benchmark.cstime*1000).to_i,
45
+ user_time: (benchmark.cutime*1000).to_i,
46
+ total_time: (benchmark.total*1000).to_i,
47
+ real_time: (benchmark.real*1000).to_i
48
+ )
49
+ rescue
50
+ gricer_request.update_attributes(
51
+ status_code: 500,
52
+ content_type: controller.response.content_type,
53
+ body_size: controller.response.body.size
54
+ )
55
+ raise
56
+ end
57
+ end
58
+ end
59
+
60
+ # Helper Method to include Javascript code to capture extra values like screen size
61
+ module TrackHelper
62
+ # Include Gricer's Javascript code to track values like screen size.
63
+ #
64
+ # You should include this at the end of your layout file. For pages matching the
65
+ # Gricer::Config.exclude_paths expression, nothing will be added to your page.
66
+ #
67
+ # @example For html.erb add at the end of your layout file:
68
+ # <html>
69
+ # <head>[...]</head>
70
+ # <body>
71
+ # [...]
72
+ # <%= gricer_track_tag %>
73
+ # </body>
74
+ # </html>
75
+ #
76
+ # @example For html.haml add at the end of your layout file:
77
+ # %html
78
+ # %head
79
+ # [...]
80
+ # %body
81
+ # [...]
82
+ # = gricer_track_tag
83
+
84
+ def gricer_track_tag
85
+ if gricer_request and defined?(gricer_capture_path)
86
+ content_tag :script, "jQuery(function($) {$.post('#{gricer_capture_path(gricer_request.id)}', Gricer.prepareValues());});", type: 'text/javascript'
87
+ end
88
+ end
89
+ end
90
+
91
+ module ActionController
92
+ # Gricer's Tracker module for ActionController
93
+ #
94
+ # To include the Tracker module into ActionController add
95
+ # gricer_track_requests to your ApplicationController or
96
+ # to any Controller you want to track with Gricer.
97
+ #
98
+ # @example
99
+ # class ApplicationController < ActionController::Base
100
+ # protect_from_forgery
101
+ # gricer_track_requests
102
+ # [...]
103
+ # end
104
+
105
+ module Tracker
106
+ # Include the helper functions and around_filter into controllers.
107
+ def self.included(base)
108
+ base.append_around_filter TrackRequestFilter
109
+ base.helper TrackHelper
110
+ base.helper_method :gricer_request
111
+ end
112
+
113
+ # Set the actual gricer request instance.
114
+ # @param gricer_request [Gricer::Request] The gricer request to be set as actual request instance.
115
+ def gricer_request=(gricer_request)
116
+ @gricer_request = gricer_request
117
+ end
118
+
119
+ # Get the actual gricer request instance.
120
+ # @return [Gricer::Request]
121
+ def gricer_request
122
+ @gricer_request
123
+ end
124
+ end
125
+ end
126
+ end
127
+
128
+ class ActionController::Base
129
+ def self.gricer_track_requests(options = {})
130
+ include Gricer::ActionController::Tracker
131
+ end
132
+ end
@@ -0,0 +1,167 @@
1
+ module Gricer
2
+ # Gricer's ActiveModel enhancements
3
+ module ActiveModel
4
+ # Gricer's statistics enhancements for ActiveModel
5
+ #
6
+ # To add statistics to an ActiveRecord just include the module:
7
+ # class SomeDataModel < ::ActiveRecord::Base
8
+ # include ActiveModel::Statistics
9
+ # [...]
10
+ # end
11
+ #
12
+ # This module provides two major enhancements:
13
+ #
14
+ # == Extendend attribute names
15
+ #
16
+ # You can use relation attributes by using a dot in your attribute name.
17
+ #
18
+ # *Example:*
19
+ #
20
+ # For getting the name of the attribute name of Agent associated to the Request:
21
+ # Gricer::Request.human_attributes('agent.name')
22
+ #
23
+ # == Statistics functions and filters
24
+ #
25
+ # Methods for filtering, grouping, and counting records
26
+ module Statistics
27
+ # Extend the ActiveModel with the statistics class methods.
28
+ # @see ClassMethods
29
+ def self.included(base)
30
+ base.extend ClassMethods
31
+ end
32
+
33
+ # Gricer's statistics enhancements for ActiveModel
34
+ #
35
+ # == Extended attribute names
36
+ # You can use relation attributes by using a dot in your attribute name.
37
+ #
38
+ # @example For getting the name of the attribute name of Agent associated to the Request:
39
+ # Gricer::Request.human_attributes('agent.name')
40
+ module ClassMethods
41
+ # Filter records for the time between from and thru
42
+ # @param from [Date, Time] Only get records on or after this point of time
43
+ # @param thru [Date, Time] Only get records before this point of time
44
+ # @return [ActiveRecord::Relation]
45
+ def between(from, thru)
46
+ #self.where(self.table_name => {:created_at.gte => from, :created_at.lt => thru})
47
+ self.where("\"#{table_name}\".\"created_at\" >= ? AND \"#{table_name}\".\"created_at\" < ?", from, thru)
48
+ end
49
+
50
+ # Filter records for the dates between from and thru
51
+ # @param from [Date] Only get records on or after this date
52
+ # @param thru [Date] Only get records on or before this date
53
+ # @return [ActiveRecord::Relation]
54
+ def between_dates(from, thru)
55
+ self.between(from.to_date.to_time, thru.to_date.to_time+1.day)
56
+ end
57
+
58
+ # Group records by attribute.
59
+ #
60
+ # @param attribute [String, Symbol] Attribute to group the records for. You can use gricers extended attribute names.
61
+ # @return [ActiveRecord::Relation]
62
+ def grouped_by(attribute)
63
+ parts = attribute.to_s.split('.')
64
+ if parts[1].blank?
65
+ self.group(attribute)
66
+ .select(attribute)
67
+ else
68
+ if association = self.reflect_on_association(parts[0].to_sym)
69
+ self.includes(association.name)
70
+ .group("\"#{association.table_name}\".\"#{parts[1]}\"")
71
+ .select("\"#{association.table_name}\".\"#{parts[1]}\"")
72
+ else
73
+ raise "Association '#{parts[0]}' not found on #{self.name}"
74
+ end
75
+ end
76
+ end
77
+
78
+ # Count records by attribute.
79
+ #
80
+ # @param attribute [String, Symbol] Attribute name to count for. You can use gricers extended attribute names.
81
+ # @return [ActiveRecord::Relation]
82
+ def count_by(attribute)
83
+ self.grouped_by(attribute).count(:id).sort{ |a,b| b[1] <=> a[1] }
84
+ end
85
+
86
+ # Filter records by attribute's value.
87
+ #
88
+ # @example
89
+ # some_relation.filter('agent.name', 'Internet Explorer').filter('agent.os', 'Windows')
90
+ # @param attribute [String, Symbol] Attribute name to filter. You can use gricers extended attribute names.
91
+ # @param value Attribute value to filter.
92
+ # @return [ActiveRecord::Relation]
93
+
94
+ def filter_by(attribute, value)
95
+ return self if attribute.blank?
96
+
97
+ Rails.logger.debug "Attr: #{attribute}, Value: #{value}"
98
+
99
+ parts = attribute.to_s.split('.')
100
+ if parts[1].blank?
101
+ self.where(attribute => value)
102
+ else
103
+ if association = self.reflect_on_association(parts[0].to_sym)
104
+ self.includes(association.name)
105
+ .where(association.table_name => {parts[1] => value})
106
+ else
107
+ raise "Association '#{parts[0]}' not found on #{self.name}"
108
+ end
109
+ end
110
+ end
111
+
112
+ # Extends ActiveModel::Translation#human_attribute_name to use Gricer's extended attribute names.
113
+ #
114
+ # @param attribute [String, Symbol] Attribute name to get human name for. You can use gricers extended attribute names.
115
+ # @param options [Hash] See ActiveModel::Translation for possible options.
116
+ # @return [String]
117
+ def human_attribute_name(attribute, options = {})
118
+ parts = attribute.to_s.split('.')
119
+ if parts[1].blank?
120
+ super attribute, options
121
+ else
122
+ if association = self.reflect_on_association(parts[0].to_sym)
123
+ association.active_record.human_attribute_name parts[1], options
124
+ else
125
+ parts[1].to_s.humanize
126
+ end
127
+ end
128
+
129
+ end
130
+
131
+ # Return hash for values of an attribute within the given time
132
+ #
133
+ #
134
+ # @example Get agent.name statistics for 2011-07-05 08:00 until 2011-07-06 08:00
135
+ # Gricer::Request.stat('agent.name', '2011-07-05 08:00', '2011-07-06 08:00')
136
+ # @example Get agent.engine_version statistics for agents with engine_name = 'Presto' and values between 2011-07-05 08:00 and last hour with values in 15 minutes steps
137
+ # Gricer::Request.where('gricer_agents.engine_name = ?', 'Presto').stat('agent.engine_version', '2011-07-05 08:00', Time.now - 1.hour, 15.minutes)
138
+ #
139
+ # @param attribute [String, Symbol] Attribute name to get data for. You can use gricers extended attribute names.
140
+ # @param from [Date, Time] Only get records on or after this point of time
141
+ # @param thru [Date, Time] Only get records before this point of time
142
+ # @param step [Integer] Time interval for grouping values
143
+ # @return [Hash]
144
+ def stat(attribute, from, thru, step = 1.hour)
145
+ query = self.grouped_by(attribute)
146
+
147
+ from = from.to_date.to_time
148
+ thru = thru.to_date.to_time+1.day
149
+
150
+ now = from
151
+
152
+ stats = {}
153
+
154
+ while now < thru do
155
+ self.between(now, now + step).count_by(attribute).each do |value|
156
+ stats[value[0]] ||= []
157
+ stats[value[0]] << [now.to_i*1000, value[1]]
158
+ end
159
+ now += step
160
+ end
161
+
162
+ return stats
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end