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.
- 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
|