redis_analytics 0.1.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. data/CHANGELOG.md +12 -0
  2. data/CONTRIBUTING.md +33 -0
  3. data/Guardfile +10 -0
  4. data/README.md +105 -23
  5. data/Rakefile +3 -11
  6. data/TODO.md +12 -0
  7. data/bin/redis_analytics_dashboard +1 -1
  8. data/config.ru +13 -0
  9. data/lib/redis_analytics.rb +5 -1
  10. data/lib/redis_analytics/analytics.rb +17 -203
  11. data/lib/redis_analytics/api.rb +60 -0
  12. data/lib/redis_analytics/configuration.rb +47 -16
  13. data/lib/redis_analytics/dashboard.rb +28 -89
  14. data/lib/redis_analytics/dashboard/public/{favicon.ico → img/favicon.ico} +0 -0
  15. data/lib/redis_analytics/dashboard/public/javascripts/{bootstrap.min.js → vendor/bootstrap/bootstrap.min.js} +0 -0
  16. data/lib/redis_analytics/dashboard/public/javascripts/{jquery-1.9.1.min.js → vendor/jquery-1.9.1.min.js} +0 -0
  17. data/lib/redis_analytics/dashboard/public/javascripts/{jquery-jvectormap-1.2.2.min.js → vendor/jquery-jvectormap-1.2.2.min.js} +0 -0
  18. data/lib/redis_analytics/dashboard/public/javascripts/{jquery-jvectormap-world-mill-en.js → vendor/jquery-jvectormap-world-mill-en.js} +0 -0
  19. data/lib/redis_analytics/dashboard/public/javascripts/{morris.min.js → vendor/morris.min.js} +0 -0
  20. data/lib/redis_analytics/dashboard/public/javascripts/{raphael-min.js → vendor/raphael-min.js} +0 -0
  21. data/lib/redis_analytics/dashboard/views/activity.erb +7 -0
  22. data/lib/redis_analytics/dashboard/views/dialogs/unique_visits.erb +41 -0
  23. data/lib/redis_analytics/dashboard/views/dialogs/visits.erb +40 -0
  24. data/lib/redis_analytics/dashboard/views/footer.erb +1 -3
  25. data/lib/redis_analytics/dashboard/views/header.erb +17 -42
  26. data/lib/redis_analytics/dashboard/views/layout.erb +5 -22
  27. data/lib/redis_analytics/dashboard/views/visits.erb +38 -143
  28. data/lib/redis_analytics/dashboard/views/visits_js.erb +110 -247
  29. data/lib/redis_analytics/dashboard/views/widgets/bounce_rate.erb +3 -0
  30. data/lib/redis_analytics/dashboard/views/widgets/browsers_donut.erb +8 -0
  31. data/lib/redis_analytics/dashboard/views/widgets/first_visits.erb +3 -0
  32. data/lib/redis_analytics/dashboard/views/widgets/page_depth.erb +3 -0
  33. data/lib/redis_analytics/dashboard/views/widgets/referers_donut.erb +8 -0
  34. data/lib/redis_analytics/dashboard/views/widgets/total_page_views.erb +3 -0
  35. data/lib/redis_analytics/dashboard/views/widgets/total_visits.erb +3 -0
  36. data/lib/redis_analytics/dashboard/views/widgets/unique_visits_line.erb +26 -0
  37. data/lib/redis_analytics/dashboard/views/widgets/visit_duration.erb +3 -0
  38. data/lib/redis_analytics/dashboard/views/widgets/visit_spark.erb +23 -0
  39. data/lib/redis_analytics/dashboard/views/widgets/visitor_recency_slices.erb +39 -0
  40. data/lib/redis_analytics/dashboard/views/widgets/visits_area.erb +30 -0
  41. data/lib/redis_analytics/dashboard/views/widgets/visits_donut.erb +8 -0
  42. data/lib/redis_analytics/dashboard/views/widgets/world_map.erb +50 -0
  43. data/lib/redis_analytics/filter.rb +33 -0
  44. data/lib/redis_analytics/helpers.rb +36 -30
  45. data/lib/redis_analytics/metrics.rb +96 -0
  46. data/lib/redis_analytics/time_ext.rb +72 -2
  47. data/lib/redis_analytics/tracker.rb +13 -5
  48. data/lib/redis_analytics/version.rb +1 -1
  49. data/lib/redis_analytics/visit.rb +122 -0
  50. data/redis_analytics.gemspec +19 -14
  51. data/spec/lib/redis_analytics/analytics_spec.rb +59 -0
  52. data/spec/lib/redis_analytics/configuration_spec.rb +158 -0
  53. data/spec/lib/redis_analytics/dashboard_spec.rb +32 -0
  54. data/spec/lib/redis_analytics/filter_spec.rb +34 -0
  55. data/spec/lib/redis_analytics/tracker_spec.rb +20 -0
  56. data/spec/spec_helper.rb +13 -6
  57. data/spec/support/fakeredis.rb +1 -0
  58. data/wsd.png +0 -0
  59. metadata +268 -126
  60. data/lib/redis_analytics/config.ru +0 -10
  61. data/spec/redis_analytics_spec.rb +0 -57
@@ -0,0 +1,3 @@
1
+ <h4 id="bounce_rate">
2
+ <small class="muted">bounce rate</small>
3
+ </h4>
@@ -0,0 +1,8 @@
1
+ <table class="table table-bordered table-hover">
2
+ <tr>
3
+ <td>
4
+ <p class="text-center"><b>Browser Info</b></p>
5
+ <div id="browsers_donut" style="width:100%;height:160px;"></div>
6
+ </td>
7
+ </tr>
8
+ </table>
@@ -0,0 +1,3 @@
1
+ <h4 id="first_visits">
2
+ <small class="muted">first visits</small>
3
+ </h4>
@@ -0,0 +1,3 @@
1
+ <h4 id="page_depth">
2
+ <small class="muted">pages per visit</small>
3
+ </h4>
@@ -0,0 +1,8 @@
1
+ <table class="table table-bordered table-hover">
2
+ <tr>
3
+ <td>
4
+ <p class="text-center"><b>Traffic Sources</b></p>
5
+ <div id="referrers_donut" style="width:100%;height:160px;"></div>
6
+ </td>
7
+ </tr>
8
+ </table>
@@ -0,0 +1,3 @@
1
+ <h4 id="total_page_views">
2
+ <small class="muted">page views</small>
3
+ </h4>
@@ -0,0 +1,3 @@
1
+ <h4 id="total_visits">
2
+ <small class="muted">visits</small>
3
+ </h4>
@@ -0,0 +1,26 @@
1
+ <table class="table table-bordered table-hover">
2
+ <tr>
3
+ <td>
4
+ <p class="text-center">
5
+ <b>Unique Visits</b></p>
6
+ <div id="unique_visits_line" style="width:100%;height:160px;"></div>
7
+ </td>
8
+ </tr>
9
+ </table>
10
+
11
+ <script type="text/javascript">
12
+ unique_visits_line = Morris.Line({
13
+ element: 'unique_visits_line',
14
+ xkey: 'raw',
15
+ ykeys: ['unique_visits'],
16
+ labels: ['Unique Visits'],
17
+ ymax: 'auto',
18
+ hideHover: 'auto',
19
+ parseTime: false,
20
+ lineWidth: 1,
21
+ pointSize: 0,
22
+ grid: true,
23
+ pointFillColors: ['#fff'],
24
+ pointStrokeColors: ['#000'],
25
+ });
26
+ </script>
@@ -0,0 +1,3 @@
1
+ <h4 id="visit_duration">
2
+ <small class="muted">avg time spent</small>
3
+ </h4>
@@ -0,0 +1,23 @@
1
+ <div id="visits_spark" style="width:100%;height:90px;"></div>
2
+ <script type="text/javascript">
3
+ visits_spark = Morris.Line({
4
+ element: 'visits_spark',
5
+ xkey: 'date',
6
+ data: <%=@data[@range][:visits_page_views_plot].to_json%>,
7
+ ykeys: ['visits', 'page_views'],
8
+ labels: ['Visits', 'Page Views'],
9
+ xLabelFormat: function(x){return '';},
10
+ yLabelFormat: function(x){return '';},
11
+ hoverCallback: function(i,x){return x.data[i].date + '<br/>' + 'Page Views: ' + x.data[i].page_views + '<br/>Visits: ' + x.data[i].visits;},
12
+ ymax: 'auto',
13
+ ymin: 0,
14
+ smooth: true,
15
+ hideHover: 'auto',
16
+ parseTime: false,
17
+ lineWidth: 2,
18
+ pointSize: 0,
19
+ grid: false,
20
+ pointFillColors: ['#fff'],
21
+ pointStrokeColors: ['#000']
22
+ });
23
+ </script>
@@ -0,0 +1,39 @@
1
+ <table class="table table-bordered table-hover">
2
+ <tr>
3
+ <td>
4
+ <p class="text-center"><b>Visit Recency</b></p>
5
+ <div style="height:160px;">
6
+ <div style="margin:5px;">
7
+ <div id="recency_d" title="0 visits">
8
+ <small class="muted">Less than 1 day (<span id="recency_d_cent">0</span>)</small>
9
+ <div class="progress" style="height: 8px; margin-bottom:10px;">
10
+ <div id="recency_d_bar" class="bar" style="width: 0%;"></div>
11
+ </div>
12
+ </div>
13
+
14
+ <div id="recency_w" title="0 visits">
15
+ <small class="muted">1 to 7 days (<span id="recency_w_cent">0</span>)</small>
16
+ <div class="progress" style="height: 8px; margin-bottom:10px;">
17
+ <div id="recency_w_bar" class="bar" style="width: 0%;"></div>
18
+ </div>
19
+ </div>
20
+
21
+ <div id="recency_m" title="0 visits">
22
+ <small class="muted">7 to 30 days (<span id="recency_m_cent">0</span>)</small>
23
+ <div class="progress" style="height: 8px; margin-bottom:10px;">
24
+ <div id="recency_m_bar" class="bar" style="width: 0%;"></div>
25
+ </div>
26
+ </div>
27
+
28
+ <div id="recency_o" title="0 visits">
29
+ <small class="muted">More than 30 days (<span id="recency_o_cent">0</span>)</small>
30
+ <div class="progress" style="height: 8px; margin-bottom:10px;">
31
+ <div id="recency_o_bar" class="bar" style="width: 0%;"></div>
32
+ </div>
33
+ </div>
34
+
35
+ </div>
36
+ </div>
37
+ </td>
38
+ </tr>
39
+ </table>
@@ -0,0 +1,30 @@
1
+ <table class="table table-bordered table-hover">
2
+ <tr>
3
+ <td>
4
+ <p class="text-center"><b>Visits</b></p>
5
+ <div id="visits_area" style="width:100%;height:160px;"></div>
6
+ </td>
7
+ </tr>
8
+ </table>
9
+
10
+ <script type="text/javascript">
11
+ visits_area = Morris.Area({
12
+ element: 'visits_area',
13
+ xkey: 'raw',
14
+ //data: data,
15
+ ykeys: ['first_visits', 'repeat_visits'],
16
+ labels: ['First Visits', 'Repeat Visits'],
17
+ ymax: 'auto',
18
+ grid: true,
19
+ eventStrokeWidth: 2,
20
+ eventLineColors: ['#ccc', '#eee'],
21
+ smooth: true,
22
+ hideHover: 'auto',
23
+ parseTime: false,
24
+ lineWidth: 1,
25
+ pointSize: 0,
26
+ xLabelFormat: function(x) {return x.getFullYear() + '-' + x.getMonth() + '-' + x.getDate();},
27
+ pointFillColors: ['#fff'],
28
+ pointStrokeColors: ['#000']
29
+ });
30
+ </script>
@@ -0,0 +1,8 @@
1
+ <table class="table table-bordered table-hover">
2
+ <tr>
3
+ <td>
4
+ <p class="text-center"><b>Total Visits</b></p>
5
+ <div id="visits_donut" style="width:100%;height:160px;"></div>
6
+ </td>
7
+ </tr>
8
+ </table>
@@ -0,0 +1,50 @@
1
+ <table class="table table-bordered">
2
+ <tr>
3
+ <td>
4
+ <p class="text-center"><b>Visitor Map</b></p>
5
+ <div id="world-map" class="info" style="width: 100%px; height: 160px"></div>
6
+ </td>
7
+ </tr>
8
+ </table>
9
+
10
+ <script type="text/javascript">
11
+ // Map Overlay
12
+ var geoLocationData = {};
13
+ $(function(){
14
+ $('#world-map').vectorMap({
15
+ map: 'world_mill_en',
16
+ min: 0,
17
+ backgroundColor: '#fff',
18
+ regionStyle: {
19
+ initial: {
20
+ fill: '#ddd',
21
+ "fill-opacity": 1,
22
+ stroke: 'none',
23
+ "stroke-width": 0,
24
+ "stroke-opacity": 1
25
+ },
26
+ hover: {
27
+ fill: '#bbb',
28
+ "fill-opacity": 0.8
29
+ },
30
+ selected: {
31
+ fill: 'yellow'
32
+ },
33
+ selectedHover: {
34
+ }
35
+ },
36
+ series: {
37
+ regions: [{
38
+ scale: ['#C8EEFF', '#0071A4'],
39
+ values: geoLocationData,
40
+ normalizeFunction: 'polynomial'
41
+ }]
42
+ },
43
+ onRegionLabelShow: function(e, el, code){
44
+ c = geoLocationData[code];
45
+ if(c != undefined)
46
+ el.html(el.html()+' ('+c+')');
47
+ }
48
+ });
49
+ });
50
+ </script>
@@ -0,0 +1,33 @@
1
+ module Rack
2
+ module RedisAnalytics
3
+ class Filter
4
+ attr_reader :filter_proc
5
+
6
+ def initialize(filter_proc)
7
+ @filter_proc = filter_proc
8
+ end
9
+
10
+ def matches?(request, response)
11
+ filter_proc.call(request, response)
12
+ end
13
+
14
+ end
15
+
16
+ class PathFilter
17
+ attr_reader :filter_path
18
+
19
+ def initialize(filter_path)
20
+ @filter_path = filter_path
21
+ end
22
+
23
+ def matches?(request_path)
24
+ if filter_path.is_a?(String)
25
+ request_path == filter_path
26
+ elsif filter_path.is_a?(Regexp)
27
+ request_path =~ filter_path
28
+ end
29
+ end
30
+ end
31
+
32
+ end
33
+ end
@@ -1,69 +1,75 @@
1
- require 'active_support/core_ext'
2
-
3
1
  module Rack
4
2
  module RedisAnalytics
5
3
  module Helpers
6
4
 
7
5
  FORMAT_SPECIFIER = [['%Y', 365], ['%m', 30], ['%d', 24], ['%H', 60], ['%M', 60]]
6
+
8
7
  GRANULARITY = ['yearly', 'monthly', 'dayly', 'hourly', 'minutely']
9
8
 
10
- # all methods are private unless explicitly declared public
11
9
  private
12
-
13
10
  def method_missing(meth, *args, &block)
14
- if meth.to_s =~ /^(minute|hour|dai|day|month|year)ly_(new_visits|visits|page_views|second_page_views|unique_visits|visit_time|ratio_recency|ratio_browsers|ratio_platforms|ratio_devices|ratio_country|ratio_referrers)$/
11
+ if meth.to_s =~ /^(minute|hour|dai|day|month|year)ly_([a-z_0-9]+)$/
15
12
  granularity = ($1 == 'dai' ? 'day' : $1) + 'ly'
16
- type = $2
17
- data(granularity, type, *args)
13
+ metric_name = $2
14
+ data(granularity, metric_name, *args)
18
15
  else
19
16
  super
20
17
  end
21
18
  end
22
19
 
23
- def data(granularity, type, from_date, options = {})
20
+ def metric_type(metric_name)
21
+ RedisAnalytics.redis_connection.hget("#{RedisAnalytics.redis_namespace}:#METRICS", metric_name)
22
+ end
23
+
24
+ def data(granularity, metric_name, from_date, options = {})
24
25
  aggregate = options[:aggregate] || false
25
26
  x = granularity[0..-3]
26
- # puts "DATA: #{x} (#{type})"
27
+
27
28
  to_date = (options[:to_date] || Time.now).send("end_of_#{x}")
28
- i = from_date.send("beginning_of_#{x}") #unless x == 'minute'
29
+ i = from_date.send("beginning_of_#{x}")
29
30
 
30
- # puts "FROM: #{i} to #{to_date}"
31
31
  union = []
32
32
  time = []
33
33
  begin
34
- union << "#{Rack::RedisAnalytics.redis_namespace}:#{type}:#{i.strftime(FORMAT_SPECIFIER[0..GRANULARITY.index(granularity)].map{|x| x[0]}.join('_'))}"
35
- time << i
36
- i += 1.send(x) # FORMAT_SPECIFIER[GRANULARITY.index(granularity)..-1].map{|x| x[1]}.inject{|p,x| p*=x; p}
34
+ slice_key = i.strftime(FORMAT_SPECIFIER[0..GRANULARITY.index(granularity)].map{|x| x[0]}.join('_'))
35
+ union << "#{RedisAnalytics.redis_namespace}:#{metric_name}:#{slice_key}"
36
+ time << slice_key.split('_')
37
+ i += 1.send(x)
37
38
  end while i <= to_date
38
- # puts "UNION #{union.inspect}"
39
39
  seq = get_next_seq
40
- if type =~ /unique/
40
+ if metric_type(metric_name) == 'String'
41
41
  if aggregate
42
- union_key = "#{Rack::RedisAnalytics.redis_namespace}:#{seq}"
43
- Rack::RedisAnalytics.redis_connection.sunionstore(union_key, union)
44
- Rack::RedisAnalytics.redis_connection.expire(union_key, 100)
45
- return Rack::RedisAnalytics.redis_connection.scard(union_key)
42
+ union_key = "#{RedisAnalytics.redis_namespace}:#{seq}"
43
+ RedisAnalytics.redis_connection.zunionstore(union_key, union)
44
+ RedisAnalytics.redis_connection.expire(union_key, 100)
45
+ return Hash[RedisAnalytics.redis_connection.zrange(union_key, 0, -1, :with_scores => true)]
46
46
  else
47
- return time.zip(union.map{|x| Rack::RedisAnalytics.redis_connection.scard(x)})
47
+ return time.zip(union.map{|x| Hash[RedisAnalytics.redis_connection.zrange(x, 0, -1, :with_scores => true)]})
48
48
  end
49
- elsif type =~ /ratio/
49
+ elsif metric_type(metric_name) == 'Fixnum'
50
50
  if aggregate
51
- union_key = "#{Rack::RedisAnalytics.redis_namespace}:#{seq}"
52
- Rack::RedisAnalytics.redis_connection.zunionstore(union_key, union)
53
- Rack::RedisAnalytics.redis_connection.expire(union_key, 100)
54
- return Rack::RedisAnalytics.redis_connection.zrange(union_key, 0, -1, :with_scores => true)
51
+ return RedisAnalytics.redis_connection.mget(*union).map(&:to_i).inject(:+)
55
52
  else
56
- return time.zip(union.map{|x| Rack::RedisAnalytics.redis_connection.zrange(x,0,-1, :with_scores => true)})
53
+ return time.zip(RedisAnalytics.redis_connection.mget(*union).map(&:to_i))
57
54
  end
58
55
  else
59
- time.zip(Rack::RedisAnalytics.redis_connection.mget(*union).map(&:to_i))
56
+ if Metrics.public_instance_methods.any?{|m| m.to_s =~ /^#{metric_name}_ratio_per_(hit|visit)$/}
57
+ aggregate ? {} : time.zip([{}] * time.length)
58
+ elsif Metrics.public_instance_methods.any?{|m| m.to_s =~ /^#{metric_name}_count_per_(hit|visit)$/}
59
+ aggregate ? 0 : time.zip([0] * time.length)
60
+ else
61
+ aggregate ? 0 : time.zip([0] * time.length)
62
+ end
60
63
  end
61
64
  end
62
-
65
+
63
66
  def get_next_seq
64
- seq = Rack::RedisAnalytics.redis_connection.incr("#{Rack::RedisAnalytics.redis_namespace}:#SEQUENCER")
67
+ seq = RedisAnalytics.redis_connection.incr("#{RedisAnalytics.redis_namespace}:#SEQUENCER")
65
68
  end
66
69
 
70
+ def time_range
71
+ (request.cookies["_rarng"] || RedisAnalytics.default_range).to_sym
72
+ end
67
73
  end
68
74
  end
69
75
  end
@@ -0,0 +1,96 @@
1
+ module Rack
2
+ module RedisAnalytics
3
+ module Metrics
4
+
5
+ attr_reader :visit_time_count_per_visit
6
+ attr_reader :visits_count_per_visit, :first_visits_count_per_visit, :repeat_visits_count_per_visit
7
+ attr_reader :unique_visits_ratio_per_visit
8
+ attr_reader :page_views_count_per_hit, :second_page_views_count_per_hit
9
+
10
+ # Developers can override or define new public methods here
11
+ # Methods should start with track and end with count or types
12
+ # Return types should be Fixnum or String resp.
13
+ # If you return nil or an error nothing will be tracked
14
+
15
+ def browser_ratio_per_visit
16
+ user_agent.name.to_s
17
+ end
18
+
19
+ def platform_ratio_per_visit
20
+ user_agent.platform.to_s
21
+ end
22
+
23
+ def country_ratio_per_visit
24
+ if defined?(GeoIP)
25
+ begin
26
+ g = GeoIP.new(RedisAnalytics.geo_ip_data_path)
27
+ geo_country_code = g.country(@rack_request.ip).to_hash[:country_code2]
28
+ if geo_country_code and geo_country_code =~ /^[A-Z]{2}$/
29
+ return geo_country_code
30
+ end
31
+ rescue Exception => e
32
+ warn "Unable to fetch country info #{e}"
33
+ end
34
+ end
35
+ end
36
+
37
+ def recency_ratio_per_visit
38
+ # tracking for visitor recency
39
+ if @last_visit_time # from first_visit_cookie
40
+ days_since_last_visit = ((@t.to_i - @last_visit_time.to_i)/(24*3600)).round
41
+ if days_since_last_visit <= 1
42
+ return 'd'
43
+ elsif days_since_last_visit <= 7
44
+ return 'w'
45
+ elsif days_since_last_visit <= 30
46
+ return 'm'
47
+ else
48
+ return 'o'
49
+ end
50
+ end
51
+ end
52
+
53
+ def device_ratio_per_visit
54
+ return ((user_agent.mobile? or user_agent.tablet?) ? 'mobile' : 'desktop')
55
+ end
56
+
57
+ def referrer_ratio_per_visit
58
+ if @rack_request.referrer
59
+ ['google', 'bing', 'yahoo', 'cleartrip', 'github'].each do |referrer|
60
+ # this will track x.google.mysite.com as google so its buggy, fix the regex
61
+ if m = @rack_request.referrer.match(/^(https?:\/\/)?([a-zA-Z0-9\.\-]+\.)?(#{referrer})\.([a-zA-Z\.]+)(:[0-9]+)?(\/.*)?$/)
62
+ "REFERRER => #{m.to_a[3]}"
63
+ referrer = m.to_a[3]
64
+ else
65
+ referrer = 'other'
66
+ end
67
+ end
68
+ else
69
+ referrer = 'organic'
70
+ end
71
+ return referrer
72
+ end
73
+
74
+ # track the ratio of URL's visits
75
+ def url_ratio_per_hit
76
+ return @rack_request.path
77
+ end
78
+
79
+ # track the landing pages ratio
80
+ def landing_page_ratio_per_hit
81
+ return @rack_request.path if @page_view_seq_no.to_i == 0
82
+ end
83
+
84
+ # track the landing pages ratio
85
+ def http_response_ratio_per_hit
86
+ return @rack_response.status.to_s
87
+ end
88
+
89
+ private
90
+ def user_agent
91
+ Browser.new(:ua => @rack_request.user_agent, :accept_language => 'en-us')
92
+ end
93
+
94
+ end
95
+ end
96
+ end