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.
- data/CHANGELOG.md +12 -0
- data/CONTRIBUTING.md +33 -0
- data/Guardfile +10 -0
- data/README.md +105 -23
- data/Rakefile +3 -11
- data/TODO.md +12 -0
- data/bin/redis_analytics_dashboard +1 -1
- data/config.ru +13 -0
- data/lib/redis_analytics.rb +5 -1
- data/lib/redis_analytics/analytics.rb +17 -203
- data/lib/redis_analytics/api.rb +60 -0
- data/lib/redis_analytics/configuration.rb +47 -16
- data/lib/redis_analytics/dashboard.rb +28 -89
- data/lib/redis_analytics/dashboard/public/{favicon.ico → img/favicon.ico} +0 -0
- data/lib/redis_analytics/dashboard/public/javascripts/{bootstrap.min.js → vendor/bootstrap/bootstrap.min.js} +0 -0
- data/lib/redis_analytics/dashboard/public/javascripts/{jquery-1.9.1.min.js → vendor/jquery-1.9.1.min.js} +0 -0
- data/lib/redis_analytics/dashboard/public/javascripts/{jquery-jvectormap-1.2.2.min.js → vendor/jquery-jvectormap-1.2.2.min.js} +0 -0
- data/lib/redis_analytics/dashboard/public/javascripts/{jquery-jvectormap-world-mill-en.js → vendor/jquery-jvectormap-world-mill-en.js} +0 -0
- data/lib/redis_analytics/dashboard/public/javascripts/{morris.min.js → vendor/morris.min.js} +0 -0
- data/lib/redis_analytics/dashboard/public/javascripts/{raphael-min.js → vendor/raphael-min.js} +0 -0
- data/lib/redis_analytics/dashboard/views/activity.erb +7 -0
- data/lib/redis_analytics/dashboard/views/dialogs/unique_visits.erb +41 -0
- data/lib/redis_analytics/dashboard/views/dialogs/visits.erb +40 -0
- data/lib/redis_analytics/dashboard/views/footer.erb +1 -3
- data/lib/redis_analytics/dashboard/views/header.erb +17 -42
- data/lib/redis_analytics/dashboard/views/layout.erb +5 -22
- data/lib/redis_analytics/dashboard/views/visits.erb +38 -143
- data/lib/redis_analytics/dashboard/views/visits_js.erb +110 -247
- data/lib/redis_analytics/dashboard/views/widgets/bounce_rate.erb +3 -0
- data/lib/redis_analytics/dashboard/views/widgets/browsers_donut.erb +8 -0
- data/lib/redis_analytics/dashboard/views/widgets/first_visits.erb +3 -0
- data/lib/redis_analytics/dashboard/views/widgets/page_depth.erb +3 -0
- data/lib/redis_analytics/dashboard/views/widgets/referers_donut.erb +8 -0
- data/lib/redis_analytics/dashboard/views/widgets/total_page_views.erb +3 -0
- data/lib/redis_analytics/dashboard/views/widgets/total_visits.erb +3 -0
- data/lib/redis_analytics/dashboard/views/widgets/unique_visits_line.erb +26 -0
- data/lib/redis_analytics/dashboard/views/widgets/visit_duration.erb +3 -0
- data/lib/redis_analytics/dashboard/views/widgets/visit_spark.erb +23 -0
- data/lib/redis_analytics/dashboard/views/widgets/visitor_recency_slices.erb +39 -0
- data/lib/redis_analytics/dashboard/views/widgets/visits_area.erb +30 -0
- data/lib/redis_analytics/dashboard/views/widgets/visits_donut.erb +8 -0
- data/lib/redis_analytics/dashboard/views/widgets/world_map.erb +50 -0
- data/lib/redis_analytics/filter.rb +33 -0
- data/lib/redis_analytics/helpers.rb +36 -30
- data/lib/redis_analytics/metrics.rb +96 -0
- data/lib/redis_analytics/time_ext.rb +72 -2
- data/lib/redis_analytics/tracker.rb +13 -5
- data/lib/redis_analytics/version.rb +1 -1
- data/lib/redis_analytics/visit.rb +122 -0
- data/redis_analytics.gemspec +19 -14
- data/spec/lib/redis_analytics/analytics_spec.rb +59 -0
- data/spec/lib/redis_analytics/configuration_spec.rb +158 -0
- data/spec/lib/redis_analytics/dashboard_spec.rb +32 -0
- data/spec/lib/redis_analytics/filter_spec.rb +34 -0
- data/spec/lib/redis_analytics/tracker_spec.rb +20 -0
- data/spec/spec_helper.rb +13 -6
- data/spec/support/fakeredis.rb +1 -0
- data/wsd.png +0 -0
- metadata +268 -126
- data/lib/redis_analytics/config.ru +0 -10
- data/spec/redis_analytics_spec.rb +0 -57
@@ -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,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,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_(
|
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
|
-
|
17
|
-
data(granularity,
|
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
|
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
|
-
|
27
|
+
|
27
28
|
to_date = (options[:to_date] || Time.now).send("end_of_#{x}")
|
28
|
-
i = from_date.send("beginning_of_#{x}")
|
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
|
-
|
35
|
-
|
36
|
-
|
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
|
40
|
+
if metric_type(metric_name) == 'String'
|
41
41
|
if aggregate
|
42
|
-
union_key = "#{
|
43
|
-
|
44
|
-
|
45
|
-
return
|
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|
|
47
|
+
return time.zip(union.map{|x| Hash[RedisAnalytics.redis_connection.zrange(x, 0, -1, :with_scores => true)]})
|
48
48
|
end
|
49
|
-
elsif
|
49
|
+
elsif metric_type(metric_name) == 'Fixnum'
|
50
50
|
if aggregate
|
51
|
-
|
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(
|
53
|
+
return time.zip(RedisAnalytics.redis_connection.mget(*union).map(&:to_i))
|
57
54
|
end
|
58
55
|
else
|
59
|
-
|
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 =
|
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
|