redis_analytics 0.1.0 → 0.6.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 (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