redis_analytics 0.1.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 (41) hide show
  1. data/Gemfile +4 -0
  2. data/Gemfile.lock +57 -0
  3. data/LICENSE +19 -0
  4. data/README.md +98 -0
  5. data/Rakefile +19 -0
  6. data/bin/GeoIP.dat +0 -0
  7. data/bin/redis_analytics_dashboard +17 -0
  8. data/lib/redis_analytics.rb +22 -0
  9. data/lib/redis_analytics/analytics.rb +231 -0
  10. data/lib/redis_analytics/config.ru +10 -0
  11. data/lib/redis_analytics/configuration.rb +71 -0
  12. data/lib/redis_analytics/dashboard.rb +114 -0
  13. data/lib/redis_analytics/dashboard/public/css/bootstrap-responsive.min.css +9 -0
  14. data/lib/redis_analytics/dashboard/public/css/bootstrap.min.css +9 -0
  15. data/lib/redis_analytics/dashboard/public/css/jquery-jvectormap-1.2.2.css +37 -0
  16. data/lib/redis_analytics/dashboard/public/css/morris.css +2 -0
  17. data/lib/redis_analytics/dashboard/public/favicon.ico +0 -0
  18. data/lib/redis_analytics/dashboard/public/img/glyphicons-halflings-white.png +0 -0
  19. data/lib/redis_analytics/dashboard/public/img/glyphicons-halflings.png +0 -0
  20. data/lib/redis_analytics/dashboard/public/javascripts/bootstrap.min.js +6 -0
  21. data/lib/redis_analytics/dashboard/public/javascripts/jquery-1.9.1.min.js +5 -0
  22. data/lib/redis_analytics/dashboard/public/javascripts/jquery-jvectormap-1.2.2.min.js +8 -0
  23. data/lib/redis_analytics/dashboard/public/javascripts/jquery-jvectormap-world-mill-en.js +1 -0
  24. data/lib/redis_analytics/dashboard/public/javascripts/morris.min.js +1 -0
  25. data/lib/redis_analytics/dashboard/public/javascripts/raphael-min.js +10 -0
  26. data/lib/redis_analytics/dashboard/views/activity.erb +0 -0
  27. data/lib/redis_analytics/dashboard/views/footer.erb +5 -0
  28. data/lib/redis_analytics/dashboard/views/header.erb +44 -0
  29. data/lib/redis_analytics/dashboard/views/layout.erb +33 -0
  30. data/lib/redis_analytics/dashboard/views/visits.erb +176 -0
  31. data/lib/redis_analytics/dashboard/views/visits_js.erb +265 -0
  32. data/lib/redis_analytics/helpers.rb +69 -0
  33. data/lib/redis_analytics/railtie.rb +13 -0
  34. data/lib/redis_analytics/time_ext.rb +19 -0
  35. data/lib/redis_analytics/tracker.rb +26 -0
  36. data/lib/redis_analytics/version.rb +5 -0
  37. data/redis_analytics.gemspec +37 -0
  38. data/screenshot.png +0 -0
  39. data/spec/redis_analytics_spec.rb +57 -0
  40. data/spec/spec_helper.rb +47 -0
  41. metadata +216 -0
@@ -0,0 +1,265 @@
1
+ <script type="text/javascript" language="text/javascript">
2
+
3
+ visits_area = Morris.Area({
4
+ element: 'visits_area',
5
+ xkey: 'date',
6
+ data: <%=@data[@range][:visits_new_visits_plot].to_json%>,
7
+ ykeys: ['new_visits', 'returning_visits'],
8
+ labels: ['New Visits', 'Returning Visits'],
9
+ ymax: 'auto',
10
+ //events: ['2013-03-02', '2013-03-03'],
11
+ grid: true,
12
+ eventStrokeWidth: 2,
13
+ eventLineColors: ['#ccc', '#eee'],
14
+ smooth: true,
15
+ hideHover: 'auto',
16
+ parseTime: false,
17
+ lineWidth: 1,
18
+ pointSize: 0,
19
+ // xLabels: 'day',
20
+ xLabelFormat: function(x) {return '';},
21
+
22
+ // hoverCallback: function(index, series)
23
+ // {
24
+ // h = series.data[index];
25
+ // d = Date(h.x);
26
+ // return(d + '<br>' + h.new_visits);
27
+ // },
28
+ pointFillColors: ['#fff'],
29
+ pointStrokeColors: ['#000'],
30
+ });
31
+
32
+ visits_donut = Morris.Donut({
33
+ element: 'visits_donut',
34
+ data: <%=@data[@range][:visits_new_visits_donut].to_json%>
35
+ });
36
+
37
+ referrers_donut = Morris.Donut({
38
+ element: 'referrers_donut',
39
+ data: <%=@data[@range][:referrers_donut].to_json%>
40
+ });
41
+
42
+ browsers_donut = Morris.Donut({
43
+ element: 'browsers_donut',
44
+ data: <%=@data[@range][:browsers_donut].to_json%>
45
+ });
46
+
47
+ unique_visits_line = Morris.Line({
48
+ element: 'unique_visits_line',
49
+ xkey: 'unit',
50
+ data: <%=@data[@range][:unique_visits].to_json%>,
51
+ ykeys: ['unique_visits_this', 'unique_visits_last'],
52
+ labels: ['Current', 'Previous'],
53
+ ymax: 'auto',
54
+ hideHover: 'auto',
55
+ parseTime: false,
56
+ lineWidth: 1,
57
+ pointSize: 0,
58
+ grid: true,
59
+ pointFillColors: ['#fff'],
60
+ pointStrokeColors: ['#000'],
61
+ // hoverCallback: function(index, series)
62
+ // {
63
+ // h = series.data[index];
64
+ // this_week_color = series.lineColors[0];
65
+ // last_week_color = series.lineColors[1];
66
+ // html = '';
67
+ // html += '<font style="color:' + this_week_color + '">This ' + h.unit + ': ' + h.unique_visits_this + '</font>';
68
+ // html += '<br/>';
69
+ // html += '<font style="color:' + last_week_color + '">Last ' + h.unit + ': ' + h.unique_visits_last + '</font>';
70
+ // return html;
71
+ // },
72
+
73
+ });
74
+
75
+ // visits_spark = Morris.Line({
76
+ // element: 'visits_spark',
77
+ // xkey: 'date',
78
+ // data: <%=@data[@range][:visits_page_views_plot].to_json%>,
79
+ // ykeys: ['visits', 'page_views'],
80
+ // labels: ['Visits', 'Page Views'],
81
+ // xLabelFormat: function(x){return '';},
82
+ // yLabelFormat: function(x){return '';},
83
+ // hoverCallback: function(i,x){return x.data[i].date + '<br/>' + 'Page Views: ' + x.data[i].page_views + '<br/>Visits: ' + x.data[i].visits;},
84
+ // ymax: 'auto',
85
+ // ymin: 0,
86
+ // smooth: true,
87
+ // hideHover: 'auto',
88
+ // parseTime: false,
89
+ // lineWidth: 2,
90
+ // pointSize: 0,
91
+ // grid: false,
92
+ // pointFillColors: ['#fff'],
93
+ // pointStrokeColors: ['#000']
94
+ // });
95
+
96
+ function setSessionCookie(name,value) {
97
+ document.cookie = name+"="+value+"; path=/";
98
+ }
99
+
100
+ function changeTimeFrame(range)
101
+ {
102
+ var mapObject = $('#world-map').vectorMap('get', 'mapObject');
103
+ switch(range)
104
+ {
105
+ <% Rack::RedisAnalytics.time_range_formats.map{|x| x[0].to_s}.each do |range| %>
106
+ case '<%=range%>':
107
+ setSessionCookie("_rarng", "<%=range%>");
108
+ visits_area.setData(<%=@data[range.to_sym][:visits_new_visits_plot].to_json%>);
109
+ unique_visits_line.setData(<%=@data[range.to_sym][:unique_visits].to_json%>);
110
+
111
+ $("#total_visits")[0].innerHTML = '<%=@data[range.to_sym][:total_visits]%> <small class="muted">visits</small>';
112
+ $("#total_page_views")[0].innerHTML = '<%=@data[range.to_sym][:total_page_views]%> <small class="muted">page views</small>';
113
+ $("#page_depth")[0].innerHTML = '<%=parse_float((@data[range.to_sym][:total_page_views].to_f/@data[range.to_sym][:total_visits].to_f).round(2))%> <small class="muted">pages per visit</small>';
114
+ $("#bounce_rate")[0].innerHTML = '<%=parse_float((((@data[range.to_sym][:total_visits].to_f-@data[range.to_sym][:total_second_page_views].to_f)/@data[range.to_sym][:total_visits].to_f)*100).round(2))%>% <small class="muted">bounce rate</small>';
115
+ $("#visit_duration")[0].innerHTML = '<%=Time.at(@data[range.to_sym][:avg_visit_time].round(2)).gmtime.strftime("%R:%S")%> <small class="muted">avg time spent</small>';
116
+ $("#new_visits")[0].innerHTML = '<%=parse_float(((@data[range.to_sym][:total_new_visits].to_f/@data[range.to_sym][:total_visits].to_f)*100).round(2))%>% <small class="muted">new visits</small></h4>';
117
+
118
+ visits_donut = Morris.Donut({element: 'visits_donut', data: <%=@data[range.to_sym][:visits_new_visits_donut].to_json%>});
119
+ browsers_donut = Morris.Donut({element: 'browsers_donut', data: <%=@data[range.to_sym][:browsers_donut].to_json%>});
120
+ referrers_donut = Morris.Donut({element: 'referrers_donut', data: <%=@data[range.to_sym][:referrers_donut].to_json%>});
121
+ mapObject.series.regions[0].setValues(<%=@data[range.to_sym][:country_map].to_json%>);
122
+
123
+ $("#compare_unit")[0].innerHTML = '<%=range%>';
124
+
125
+ <% @data[range.to_sym][:visitor_recency_slices].each_with_index do |((k, v), i), c| %>
126
+ <% p = ((i.to_f/@data[range.to_sym][:visitor_recency_slices].map{|x| x[1]}.sum) * 100).round rescue '0' %>
127
+ <%= "$('#recency_#{c}')[0].style.width = '#{p}%';" %>
128
+ <%= "$('#recency_#{c}_num')[0].title = '#{i.to_i} visits';" %>
129
+ <%= "$('#recency_#{c}_cent')[0].innerHTML = '#{p}';" %>
130
+ <% end %>
131
+
132
+ break;
133
+ <% end %>
134
+ }
135
+ }
136
+
137
+ // Map Overlay
138
+
139
+ var geoLocationData = <%=@data[@range][:country_map].to_json%>;
140
+
141
+ $(function(){
142
+ $('#world-map').vectorMap({
143
+ map: 'world_mill_en',
144
+ min: 0,
145
+ backgroundColor: '#fff',
146
+ regionStyle: {
147
+ initial: {
148
+ fill: '#ddd',
149
+ "fill-opacity": 1,
150
+ stroke: 'none',
151
+ "stroke-width": 0,
152
+ "stroke-opacity": 1
153
+ },
154
+ hover: {
155
+ fill: '#bbb',
156
+ "fill-opacity": 0.8
157
+ },
158
+ selected: {
159
+ fill: 'yellow'
160
+ },
161
+ selectedHover: {
162
+ }
163
+ },
164
+ series: {
165
+ regions: [{
166
+ scale: ['#C8EEFF', '#0071A4'],
167
+ values: geoLocationData,
168
+ normalizeFunction: 'polynomial'
169
+ }]
170
+ },
171
+ onLabelShow: function(e, el, code){
172
+ el.html(el.html()+' (GDP - '+geoLocationData[code]+')');
173
+ }
174
+ });
175
+ });
176
+
177
+ function onDownload() {
178
+ document.location = 'data:Application/octet-stream,' +
179
+ encodeURIComponent(convertToCSV(<%=@data[@range][:visits_new_visits_plot].to_json%>));
180
+ }
181
+
182
+ function convertToCSV(objArray) {
183
+ var array = typeof objArray != 'object' ? JSON.parse(objArray) : objArray;
184
+ var str = '';
185
+
186
+ for (var i = 0; i < array.length; i++) {
187
+ var line = '';
188
+ for (var index in array[i]) {
189
+ if (line != '') line += ','
190
+
191
+ line += array[i][index];
192
+ }
193
+
194
+ str += line + '\r\n';
195
+ }
196
+
197
+ return str;
198
+ };
199
+
200
+ $('#visits_modal').on('show', function () {
201
+
202
+ colors = Highcharts.getOptions().colors;
203
+
204
+ // create the chart when all data is loaded
205
+ $(function(){
206
+
207
+ $('#visits_detail').highcharts('StockChart', {
208
+ chart: {
209
+ },
210
+
211
+ // tooltip: {
212
+ // pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
213
+ // valueDecimals: 2
214
+ // },
215
+ yAxis: {
216
+ allowDecimals: false
217
+ },
218
+ xAxis: {
219
+ allowDecimals: false
220
+ },
221
+
222
+ series: [{
223
+ name: 'Visits',
224
+ data: <%=@data[:all_visits].to_json%>
225
+ }]
226
+ });
227
+ });
228
+ });
229
+
230
+
231
+ $('#unique_visits_modal').on('show', function () {
232
+
233
+ colors = Highcharts.getOptions().colors;
234
+
235
+ // create the chart when all data is loaded
236
+ $(function(){
237
+
238
+ $('#unique_visits_detail').highcharts('StockChart', {
239
+ chart: {
240
+ },
241
+
242
+ // tooltip: {
243
+ // pointFormat: '<span style="color:{series.color}">{series.name}</span>: <b>{point.y}</b> ({point.change}%)<br/>',
244
+ // valueDecimals: 2
245
+ // },
246
+ yAxis: {
247
+ allowDecimals: false
248
+ },
249
+ xAxis: {
250
+ allowDecimals: false
251
+ },
252
+
253
+ series: [{
254
+ name: 'Unique Visits',
255
+ data: <%=@data[:all_unique_visits].to_json%>
256
+ }]
257
+ });
258
+ });
259
+ });
260
+
261
+ </script>
262
+
263
+ <style>
264
+ .modal {width:940px;left:36%;}
265
+ </style>
@@ -0,0 +1,69 @@
1
+ require 'active_support/core_ext'
2
+
3
+ module Rack
4
+ module RedisAnalytics
5
+ module Helpers
6
+
7
+ FORMAT_SPECIFIER = [['%Y', 365], ['%m', 30], ['%d', 24], ['%H', 60], ['%M', 60]]
8
+ GRANULARITY = ['yearly', 'monthly', 'dayly', 'hourly', 'minutely']
9
+
10
+ # all methods are private unless explicitly declared public
11
+ private
12
+
13
+ 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)$/
15
+ granularity = ($1 == 'dai' ? 'day' : $1) + 'ly'
16
+ type = $2
17
+ data(granularity, type, *args)
18
+ else
19
+ super
20
+ end
21
+ end
22
+
23
+ def data(granularity, type, from_date, options = {})
24
+ aggregate = options[:aggregate] || false
25
+ x = granularity[0..-3]
26
+ # puts "DATA: #{x} (#{type})"
27
+ to_date = (options[:to_date] || Time.now).send("end_of_#{x}")
28
+ i = from_date.send("beginning_of_#{x}") #unless x == 'minute'
29
+
30
+ # puts "FROM: #{i} to #{to_date}"
31
+ union = []
32
+ time = []
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}
37
+ end while i <= to_date
38
+ # puts "UNION #{union.inspect}"
39
+ seq = get_next_seq
40
+ if type =~ /unique/
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)
46
+ else
47
+ return time.zip(union.map{|x| Rack::RedisAnalytics.redis_connection.scard(x)})
48
+ end
49
+ elsif type =~ /ratio/
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)
55
+ else
56
+ return time.zip(union.map{|x| Rack::RedisAnalytics.redis_connection.zrange(x,0,-1, :with_scores => true)})
57
+ end
58
+ else
59
+ time.zip(Rack::RedisAnalytics.redis_connection.mget(*union).map(&:to_i))
60
+ end
61
+ end
62
+
63
+ def get_next_seq
64
+ seq = Rack::RedisAnalytics.redis_connection.incr("#{Rack::RedisAnalytics.redis_namespace}:#SEQUENCER")
65
+ end
66
+
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,13 @@
1
+ module Rack
2
+ module RedisAnalytics
3
+ class Railtie < Rails::Railtie
4
+ initializer "redis_analytics.middleware" do |app|
5
+ app.config.middleware.use "Rack::RedisAnalytics::Tracker"
6
+ end
7
+
8
+ initializer "redis_analytics.view_helpers" do
9
+ ActionController::Base.send :include, Rack::RedisAnalytics::Helpers
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,19 @@
1
+ module TimeExtensions
2
+ %w[ round floor ceil ].each do |_method|
3
+ define_method _method do |*args|
4
+ seconds = args.first || 60
5
+ Time.at((self.to_f / seconds).send(_method) * seconds)
6
+ end
7
+ end
8
+
9
+ def end_of_minute
10
+ change(:sec => 59, :usec => 999999.999)
11
+ end
12
+
13
+ def beginning_of_minute
14
+ change(:sec => 0, :usec => 0)
15
+ end
16
+ end
17
+
18
+ Time.send :include, TimeExtensions
19
+
@@ -0,0 +1,26 @@
1
+ module Rack
2
+ module RedisAnalytics
3
+ class Tracker
4
+
5
+ def initialize(app)
6
+ @app = Rack::Builder.new do
7
+ map '/' do
8
+ run Analytics.new(app)
9
+ end
10
+
11
+ if defined? Dashboard and RedisAnalytics.dashboard_endpoint
12
+ map RedisAnalytics.dashboard_endpoint do
13
+ run Dashboard.new
14
+ end
15
+ end
16
+ end
17
+ end
18
+
19
+ def call(env)
20
+ @app.call(env)
21
+ end
22
+
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,5 @@
1
+ module Rack
2
+ module RedisAnalytics
3
+ VERSION = '0.1.0'
4
+ end
5
+ end
@@ -0,0 +1,37 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require 'redis_analytics/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "redis_analytics"
7
+ spec.version = Rack::RedisAnalytics::VERSION
8
+ spec.date = '2013-02-15'
9
+ spec.authors = ["Schubert Cardozo"]
10
+ spec.email = ["cardozoschubert@gmail.com"]
11
+ spec.homepage = "https://github.com/saturnine/redis_analytics"
12
+ spec.summary = %q{A gem that provides a Redis based web analytics solution for your rack-compliant apps}
13
+ spec.description = %q{A gem that provides a Redis based web analytics solution for your rack-compliant apps. It gives you detailed analytics about visitors, unique visitors, browsers, OS, visitor recency, traffic sources and more}
14
+
15
+ spec.rubyforge_project = "redis_analytics"
16
+
17
+ spec.files = Dir.glob("**/*")
18
+
19
+ spec.executables = ['redis_analytics_dashboard']
20
+ spec.default_executable = 'redis_analytics_dashboard'
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_runtime_dependency('rack', '>= 1.4.0')
24
+ spec.add_runtime_dependency('redis', '>= 3.0.2')
25
+ spec.add_runtime_dependency('browser', '>= 0.1.6')
26
+ spec.add_runtime_dependency('sinatra', '>= 1.3.3')
27
+ spec.add_runtime_dependency('geoip', '>= 1.2.1')
28
+ spec.add_runtime_dependency('json', '>= 1.7.7')
29
+ spec.add_runtime_dependency('activesupport', '>= 3.2.0')
30
+
31
+ spec.add_development_dependency('rake', '>= 10.0.3')
32
+ spec.add_development_dependency('rspec', '>= 2.11.0')
33
+ spec.add_development_dependency('mocha', '>= 0.12.7')
34
+ spec.add_development_dependency('rack-test', '>= 0.6.2')
35
+
36
+ spec.required_ruby_version = '>= 1.9.2'
37
+ end
Binary file
@@ -0,0 +1,57 @@
1
+ require 'spec_helper'
2
+
3
+ describe Rack::RedisAnalytics::Analytics do
4
+ include Rack::Test::Methods
5
+
6
+ def app
7
+ Rack::Builder.app do
8
+ use Rack::RedisAnalytics::Analytics
9
+ run Proc.new { |env| [200, {'Content-Type' => 'text/html'}, "Hello!"] }
10
+ end
11
+ end
12
+
13
+ before(:each) do
14
+ @redis_connection = Rack::RedisAnalytics.redis_connection
15
+ clear_cookies
16
+ # @redis_connection.flushdb
17
+ end
18
+
19
+ # Spec for Cookies
20
+
21
+ context "when a user makes 2 visits and the visit cookie and returning user cookie are not expired" do
22
+ it "it should count as the same visit in the cookie" do
23
+ t1 = Time.now
24
+ Time.stubs(:now).returns(t1)
25
+ get '/'
26
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.visit_cookie_name}=1\.1\.#{t1.to_i}\.#{t1.to_i}")
27
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.returning_user_cookie_name}=1\.#{t1.to_i}\.#{t1.to_i}")
28
+ t2 = t1 + 5 # just adding 5 seconds
29
+ Time.stubs(:now).returns(t2)
30
+ get '/'
31
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.visit_cookie_name}=1\.1\.#{t1.to_i}\.#{t2.to_i}")
32
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.returning_user_cookie_name}=1\.#{t1.to_i}\.#{t2.to_i}")
33
+ end
34
+ end
35
+
36
+ context "when a user makes 2 visits, but visit cookie and returning user cookie are both non-existent" do
37
+ it "should count as a separate and new visit in the cookie" do
38
+ t1 = Time.now
39
+ Time.stubs(:now).returns(t1)
40
+ get '/'
41
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.visit_cookie_name}=1\.1\.#{t1.to_i}\.#{t1.to_i}")
42
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.returning_user_cookie_name}=1\.#{t1.to_i}.#{t1.to_i}")
43
+ clear_cookies
44
+
45
+ t2 = t1 + 5 # just adding 5 seconds
46
+ Time.stubs(:now).returns(t2)
47
+ get '/'
48
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.visit_cookie_name}=2\.2\.#{t2.to_i}\.#{t2.to_i}")
49
+ last_response.original_headers['Set-Cookie'].should =~ Regexp.new("#{Rack::RedisAnalytics.returning_user_cookie_name}=2\.#{t2.to_i}\.#{t2.to_i}")
50
+ end
51
+ end
52
+
53
+ context "when a user makes 2 visits, and visit cookie is expired but the returning user cookie exists" do
54
+ it "should count as a separate visit but not a new visit"
55
+ end
56
+
57
+ end