charted 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +1 -15
- data/charted +5 -3
- data/config.ru +8 -7
- data/lib/charted.rb +236 -65
- data/test/charted_test.rb +148 -12
- data/test/fixtures.rb +1 -1
- metadata +2 -2
data/README.md
CHANGED
@@ -86,22 +86,8 @@ Tests are setup to run via `ruby test/*_test.rb` or via `rake`.
|
|
86
86
|
TODO
|
87
87
|
====
|
88
88
|
|
89
|
-
* clean up `--dashboard` code
|
90
|
-
* track RSS subscribers
|
91
|
-
* add event tracking
|
92
|
-
* add funnel conversion tracking
|
93
|
-
* add AB testing
|
94
|
-
* add stats summary for AB tests
|
95
|
-
* add deletion via CLI
|
96
|
-
* add deletion every N days for fresh data
|
97
|
-
* add `--js` option
|
98
|
-
* add plugin system to CLI and recordings
|
99
|
-
* add config for dashboards (what to show/hide, how to order)
|
100
|
-
* ignore same domain referrers
|
101
|
-
* optimize with If-Not-Modified (or use ?timestamp parameter)
|
102
|
-
* ignore switch for developers
|
103
89
|
* deploy task in Rakefile for development
|
104
|
-
* add
|
90
|
+
* add date range option
|
105
91
|
|
106
92
|
License
|
107
93
|
=======
|
data/charted
CHANGED
@@ -8,13 +8,15 @@ require 'fileutils'
|
|
8
8
|
ENV['CHARTED_CMD'] = '1'
|
9
9
|
|
10
10
|
ARGV.options do |o|
|
11
|
-
cmd, action = Charted::Command.new, nil
|
11
|
+
cmd, action = Charted::Command.new, nil
|
12
12
|
o.set_summary_indent(' ')
|
13
13
|
o.banner = "Usage: #{File.basename($0)} [OPTION]"
|
14
|
-
o.on('-
|
14
|
+
o.on('-c', '--clean [label]', 'clean out old data') { |label| action = [:clean, label] }
|
15
|
+
o.on('-d', '--dashboard', 'show dashboard') { action = [:dashboard] }
|
15
16
|
o.on('-h', '--help', 'show this help message') { puts o; exit }
|
17
|
+
o.on('-j', '--js', 'output js code') { action = [:js] }
|
16
18
|
o.on('-m', '--migrate', 'migrates database') { cmd.migrate; exit }
|
17
19
|
o.on('-s', '--site domain', 'set site') { |site| cmd.site = site }
|
18
20
|
o.parse!
|
19
|
-
action.nil? ? puts(o) : cmd.send(action)
|
21
|
+
action.nil? ? puts(o) : cmd.send(*action.compact)
|
20
22
|
end
|
data/config.ru
CHANGED
@@ -1,13 +1,14 @@
|
|
1
1
|
require File.expand_path('lib/charted', File.dirname(__FILE__))
|
2
2
|
|
3
3
|
Charted.configure(ENV['RACK_ENV'] != 'test') do |c|
|
4
|
-
c.
|
5
|
-
c.
|
6
|
-
c.
|
7
|
-
c.
|
8
|
-
c.
|
9
|
-
c.
|
10
|
-
c.
|
4
|
+
c.delete_after 365
|
5
|
+
c.email 'dev@localhost'
|
6
|
+
c.db_adapter 'sqlite3'
|
7
|
+
c.db_host 'localhost'
|
8
|
+
c.db_username 'root'
|
9
|
+
c.db_password 'secret'
|
10
|
+
c.db_database 'db.sqlite3'
|
11
|
+
c.sites ['localhost', 'example.org']
|
11
12
|
end
|
12
13
|
|
13
14
|
if !ENV['CHARTED_CMD']
|
data/lib/charted.rb
CHANGED
@@ -13,15 +13,15 @@ require 'geoip'
|
|
13
13
|
require 'pony'
|
14
14
|
require 'useragent'
|
15
15
|
require 'search_terms'
|
16
|
-
require 'terminal-table'
|
17
16
|
require 'colorize'
|
18
17
|
require 'dashes'
|
19
18
|
|
20
19
|
DataMapper::Model.raise_on_save_failure = true
|
21
20
|
|
22
21
|
module Charted
|
23
|
-
VERSION = '0.0.
|
22
|
+
VERSION = '0.0.2'
|
24
23
|
GEOIP = GeoIP.new("#{File.dirname(__FILE__)}/../geoip.dat")
|
24
|
+
JS_FILE = "#{File.dirname(__FILE__)}/../public/charted/script.js"
|
25
25
|
|
26
26
|
def self.configure(setup_db=true)
|
27
27
|
yield self.config
|
@@ -65,6 +65,17 @@ module Charted
|
|
65
65
|
:db_adapter, :db_host, :db_username, :db_password, :db_database
|
66
66
|
end
|
67
67
|
|
68
|
+
module Endable
|
69
|
+
def ended?
|
70
|
+
!!ended_at
|
71
|
+
end
|
72
|
+
|
73
|
+
def end!
|
74
|
+
self.ended_at = DateTime.now
|
75
|
+
self.save
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
68
79
|
class Site
|
69
80
|
include DataMapper::Resource
|
70
81
|
|
@@ -74,6 +85,14 @@ module Charted
|
|
74
85
|
|
75
86
|
has n, :visitors
|
76
87
|
has n, :visits, :through => :visitors
|
88
|
+
has n, :events, :through => :visitors
|
89
|
+
has n, :conversions, :through => :visitors
|
90
|
+
has n, :experiments, :through => :visitors
|
91
|
+
|
92
|
+
def visitor_with_cookie(cookie)
|
93
|
+
visitor = self.visitors.get(cookie.to_s.split('-').first)
|
94
|
+
visitor && visitor.cookie == cookie ? visitor : nil
|
95
|
+
end
|
77
96
|
end
|
78
97
|
|
79
98
|
class Visitor
|
@@ -87,9 +106,13 @@ module Charted
|
|
87
106
|
property :browser, String
|
88
107
|
property :browser_version, String
|
89
108
|
property :country, String
|
109
|
+
property :bucket, Integer
|
90
110
|
|
91
111
|
belongs_to :site
|
92
112
|
has n, :visits
|
113
|
+
has n, :events
|
114
|
+
has n, :conversions
|
115
|
+
has n, :experiments
|
93
116
|
|
94
117
|
validates_presence_of :site
|
95
118
|
|
@@ -99,7 +122,7 @@ module Charted
|
|
99
122
|
end
|
100
123
|
|
101
124
|
def cookie
|
102
|
-
"#{self.id}-#{self.secret}"
|
125
|
+
"#{self.id}-#{self.bucket}-#{self.secret}"
|
103
126
|
end
|
104
127
|
|
105
128
|
def user_agent=(user_agent)
|
@@ -119,11 +142,38 @@ module Charted
|
|
119
142
|
# invalid IP address, skip setting country
|
120
143
|
end
|
121
144
|
|
122
|
-
def
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
145
|
+
def make_events(labels)
|
146
|
+
labels.to_s.split(';').map(&:strip).map do |label|
|
147
|
+
events.create(label: label)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def start_conversions(labels)
|
152
|
+
labels.to_s.split(';').map(&:strip).map do |label|
|
153
|
+
conversions.first(label: label) || self.conversions.create(label: label)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def start_experiments(labels) # label:bucket;...
|
158
|
+
labels.to_s.split(';').map do |str|
|
159
|
+
label, bucket = str.split(':', 2).map(&:strip)
|
160
|
+
exp = experiments.first(label: label)
|
161
|
+
if exp
|
162
|
+
exp.update(bucket: bucket) if exp.bucket != bucket
|
163
|
+
exp
|
164
|
+
else
|
165
|
+
self.experiments.create(label: label, bucket: bucket)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def end_goals(labels)
|
171
|
+
labels.to_s.split(';').map(&:strip).each do |label|
|
172
|
+
exp = experiments.first(label: label)
|
173
|
+
exp.end! if exp
|
174
|
+
conv = conversions.first(label: label)
|
175
|
+
conv.end! if conv
|
176
|
+
end
|
127
177
|
end
|
128
178
|
|
129
179
|
def self.generate_secret
|
@@ -154,45 +204,97 @@ module Charted
|
|
154
204
|
end
|
155
205
|
end
|
156
206
|
|
207
|
+
class Event
|
208
|
+
include DataMapper::Resource
|
209
|
+
|
210
|
+
property :id, Serial
|
211
|
+
property :label, String, :required => true
|
212
|
+
property :created_at, DateTime
|
213
|
+
|
214
|
+
belongs_to :visitor
|
215
|
+
has 1, :site, :through => :visitor
|
216
|
+
|
217
|
+
validates_presence_of :visitor
|
218
|
+
end
|
219
|
+
|
220
|
+
class Conversion
|
221
|
+
include DataMapper::Resource
|
222
|
+
include Endable
|
223
|
+
|
224
|
+
property :id, Serial
|
225
|
+
property :label, String, :required => true
|
226
|
+
property :created_at, DateTime
|
227
|
+
property :ended_at, DateTime
|
228
|
+
|
229
|
+
belongs_to :visitor
|
230
|
+
has 1, :site, :through => :visitor
|
231
|
+
|
232
|
+
validates_presence_of :visitor
|
233
|
+
end
|
234
|
+
|
235
|
+
class Experiment
|
236
|
+
include DataMapper::Resource
|
237
|
+
include Endable
|
238
|
+
|
239
|
+
property :id, Serial
|
240
|
+
property :label, String, :required => true
|
241
|
+
property :bucket, String, :required => true
|
242
|
+
property :created_at, DateTime
|
243
|
+
property :ended_at, DateTime
|
244
|
+
|
245
|
+
belongs_to :visitor
|
246
|
+
has 1, :site, :through => :visitor
|
247
|
+
|
248
|
+
validates_presence_of :visitor
|
249
|
+
end
|
250
|
+
|
157
251
|
DataMapper.finalize
|
158
252
|
|
159
253
|
class App < Sinatra::Base
|
160
254
|
set :logging, true
|
161
255
|
|
162
|
-
|
163
|
-
site = Site.first(:
|
164
|
-
halt(404) if site.nil?
|
165
|
-
|
166
|
-
|
167
|
-
visitor = Visitor.get_by_cookie(site, request.cookies['charted'])
|
168
|
-
end
|
256
|
+
before do
|
257
|
+
@site = Site.first(domain: request.host)
|
258
|
+
halt(404) if @site.nil?
|
259
|
+
@visitor = @site.visitor_with_cookie(request.cookies['charted'])
|
260
|
+
end
|
169
261
|
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
:
|
174
|
-
:
|
175
|
-
:
|
262
|
+
get '/' do
|
263
|
+
if @visitor.nil?
|
264
|
+
@visitor = @site.visitors.create(
|
265
|
+
resolution: params[:resolution],
|
266
|
+
user_agent: request.user_agent,
|
267
|
+
ip_address: request.ip,
|
268
|
+
bucket: params[:bucket])
|
176
269
|
response.set_cookie(
|
177
270
|
'charted',
|
178
|
-
:
|
179
|
-
:
|
271
|
+
value: @visitor.cookie,
|
272
|
+
expires: (Date.today + 365*2).to_time)
|
180
273
|
end
|
181
274
|
|
182
|
-
|
183
|
-
|
184
|
-
:
|
185
|
-
:
|
186
|
-
:
|
275
|
+
referrer = nil if URI.parse(params[:referrer].to_s).host == @site.domain
|
276
|
+
@visitor.visits.create(
|
277
|
+
path: params[:path],
|
278
|
+
title: params[:title],
|
279
|
+
referrer: referrer)
|
280
|
+
@visitor.start_conversions(params[:conversions])
|
281
|
+
@visitor.start_experiments(params[:experiments])
|
282
|
+
'/**/'
|
283
|
+
end
|
284
|
+
|
285
|
+
get '/record' do
|
286
|
+
halt(404) if @visitor.nil?
|
287
|
+
@visitor.make_events(params[:events])
|
288
|
+
@visitor.end_goals(params[:goals])
|
187
289
|
'/**/'
|
188
290
|
end
|
189
291
|
|
190
292
|
error do
|
191
293
|
Pony.mail(
|
192
|
-
:
|
193
|
-
:
|
194
|
-
:
|
195
|
-
:
|
294
|
+
to: Charted.config.email,
|
295
|
+
from: "charted@#{Charted.config.email.split('@')[1..-1].join}",
|
296
|
+
subject: 'Charted Error',
|
297
|
+
body: request.env['sinatra.error'].to_s
|
196
298
|
) if Charted.config.email && self.class.environment == :production
|
197
299
|
end
|
198
300
|
end
|
@@ -201,23 +303,47 @@ module Charted
|
|
201
303
|
attr_accessor :config_loaded, :output
|
202
304
|
attr_reader :site
|
203
305
|
|
306
|
+
def clean(label=nil)
|
307
|
+
load_config
|
308
|
+
sys_exit("Please set 'delete_after' config.") if Charted.config.delete_after.nil?
|
309
|
+
|
310
|
+
threshold = Date.today - Charted.config.delete_after
|
311
|
+
Visit.all(:created_at.lt => threshold).destroy
|
312
|
+
Event.all(:created_at.lt => threshold).destroy
|
313
|
+
Conversion.all(:created_at.lt => threshold).destroy
|
314
|
+
Experiment.all(:created_at.lt => threshold).destroy
|
315
|
+
Visitor.all(:created_at.lt => threshold).each do |visitor|
|
316
|
+
visitor.destroy if visitor.visits.count == 0 &&
|
317
|
+
visitor.events.count == 0 &&
|
318
|
+
visitor.conversions.count == 0 &&
|
319
|
+
visitor.experiments.count == 0
|
320
|
+
end
|
321
|
+
|
322
|
+
if label
|
323
|
+
Event.all(label: label).destroy
|
324
|
+
Conversion.all(label: label).destroy
|
325
|
+
Experiment.all(label: label).destroy
|
326
|
+
end
|
327
|
+
end
|
328
|
+
|
204
329
|
def dashboard
|
205
330
|
site_required
|
206
|
-
|
207
|
-
chart = Dashes::Chart.new
|
208
|
-
chart2 = Dashes::Chart.new
|
331
|
+
nodes = []
|
209
332
|
max_width = [`tput cols`.to_i / 2, 60].min
|
210
|
-
chart.
|
211
|
-
|
212
|
-
|
213
|
-
chart2
|
214
|
-
|
215
|
-
|
216
|
-
table.
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
333
|
+
chart = Dashes::Chart.new.
|
334
|
+
max_width(max_width).
|
335
|
+
title("Total Visits".colorize(:light_green))
|
336
|
+
chart2 = Dashes::Chart.new.
|
337
|
+
max_width(max_width).
|
338
|
+
title("Unique Visits".colorize(:light_green))
|
339
|
+
table = Dashes::Table.new.
|
340
|
+
max_width(max_width).
|
341
|
+
spacing(:min, :min, :max).
|
342
|
+
align(:right, :right, :left).
|
343
|
+
row('Total'.colorize(:light_blue),
|
344
|
+
'Unique'.colorize(:light_blue),
|
345
|
+
'Visits'.colorize(:light_green)).
|
346
|
+
separator
|
221
347
|
(0..11).each do |delta|
|
222
348
|
date = Charted.prev_month(Date.today, delta)
|
223
349
|
visits = @site.visits.count(
|
@@ -227,13 +353,10 @@ module Charted
|
|
227
353
|
:created_at.gte => date,
|
228
354
|
:created_at.lt => Charted.next_month(date)})
|
229
355
|
table.row(format(visits), format(unique), date.strftime('%B %Y'))
|
230
|
-
|
231
|
-
|
232
|
-
chart2.row date.strftime('%b %Y'), unique
|
356
|
+
chart.row(date.strftime('%b %Y'), visits)
|
357
|
+
chart2.row(date.strftime('%b %Y'), unique)
|
233
358
|
end
|
234
|
-
|
235
|
-
tables << chart
|
236
|
-
tables << chart2
|
359
|
+
nodes += [table, chart, chart2]
|
237
360
|
[[:browser, 'Browsers', :visitors],
|
238
361
|
[:resolution, 'Resolutions', :visitors],
|
239
362
|
[:platform, 'Platforms', :visitors],
|
@@ -241,13 +364,13 @@ module Charted
|
|
241
364
|
[:title, 'Pages', :visits],
|
242
365
|
[:referrer, 'Referrers', :visits],
|
243
366
|
[:search_terms, 'Searches', :visits]].each do |field, column, type|
|
244
|
-
table = Dashes::Table.new
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
'
|
249
|
-
|
250
|
-
|
367
|
+
table = Dashes::Table.new.
|
368
|
+
max_width(max_width).
|
369
|
+
spacing(:min, :min, :max).
|
370
|
+
align(:right, :right, :left).
|
371
|
+
row('Total'.colorize(:light_blue),
|
372
|
+
'%'.colorize(:light_blue),
|
373
|
+
column.colorize(:light_green)).separator
|
251
374
|
rows = []
|
252
375
|
total = @site.send(type).count(field.not => nil)
|
253
376
|
@site.send(type).aggregate(field, :all.count).each do |label, count|
|
@@ -257,14 +380,62 @@ module Charted
|
|
257
380
|
rows << [format(count), "#{((count / total.to_f) * 100).round}%", label]
|
258
381
|
end
|
259
382
|
rows.sort_by { |r| r[1] }.reverse.each { |row| table.row(*row) }
|
260
|
-
|
261
|
-
|
383
|
+
nodes << table
|
384
|
+
end
|
385
|
+
table = Dashes::Table.new.
|
386
|
+
max_width(max_width).
|
387
|
+
spacing(:min, :min, :max).
|
388
|
+
align(:right, :right, :left).
|
389
|
+
row('Total'.colorize(:light_blue),
|
390
|
+
'Unique'.colorize(:light_blue),
|
391
|
+
'Events'.colorize(:light_green)).separator
|
392
|
+
rows = []
|
393
|
+
@site.events.aggregate(:label, :all.count).each do |label, count|
|
394
|
+
unique = @site.visitors.count(:events => {label: label})
|
395
|
+
rows << [format(count), format(unique), label]
|
396
|
+
end
|
397
|
+
rows.sort_by { |r| r[1] }.reverse.each { |row| table.row(*row) }
|
398
|
+
nodes << table
|
399
|
+
|
400
|
+
table = Dashes::Table.new.
|
401
|
+
max_width(max_width).
|
402
|
+
spacing(:min, :min, :max).
|
403
|
+
align(:right, :right, :left).
|
404
|
+
row('Start'.colorize(:light_blue),
|
405
|
+
'End'.colorize(:light_blue),
|
406
|
+
'Conversions'.colorize(:light_green)).separator
|
407
|
+
rows = []
|
408
|
+
@site.conversions.aggregate(:label, :all.count).each do |label, count|
|
409
|
+
ended = @site.conversions.count(label: label, :ended_at.not => nil)
|
410
|
+
rows << [format(count), format(ended), label]
|
262
411
|
end
|
412
|
+
rows.sort_by { |r| r[1] }.reverse.each { |row| table.row(*row) }
|
413
|
+
nodes << table
|
414
|
+
|
415
|
+
table = Dashes::Table.new.
|
416
|
+
max_width(max_width).
|
417
|
+
spacing(:min, :min, :max).
|
418
|
+
align(:right, :right, :left).
|
419
|
+
row('Start'.colorize(:light_blue),
|
420
|
+
'End'.colorize(:light_blue),
|
421
|
+
'Experiments'.colorize(:light_green)).separator
|
422
|
+
rows = []
|
423
|
+
@site.experiments.aggregate(:label, :bucket, :all.count).each do |label, bucket, count|
|
424
|
+
ended = @site.experiments.count(label: label, bucket: bucket, :ended_at.not => nil)
|
425
|
+
rows << [format(count), format(ended), "#{label}: #{bucket}"]
|
426
|
+
end
|
427
|
+
rows.sort_by { |r| r[1] }.reverse.each { |row| table.row(*row) }
|
428
|
+
nodes << table
|
429
|
+
|
430
|
+
nodes.reject! do |node|
|
431
|
+
minimum = node.is_a?(Dashes::Table) ? 1 : 0
|
432
|
+
node.instance_variable_get(:@rows).size == minimum # TODO: hacked
|
433
|
+
end
|
434
|
+
print(Dashes::Grid.new.width(`tput cols`.to_i).add(*nodes))
|
435
|
+
end
|
263
436
|
|
264
|
-
|
265
|
-
|
266
|
-
tables.each { |t| grid.add(t) }
|
267
|
-
print(grid)
|
437
|
+
def js
|
438
|
+
print(File.read(JS_FILE))
|
268
439
|
end
|
269
440
|
|
270
441
|
def migrate
|
data/test/charted_test.rb
CHANGED
@@ -57,31 +57,69 @@ class ModelTest < ChartedTest
|
|
57
57
|
Charted::Site.destroy
|
58
58
|
Charted::Visitor.destroy
|
59
59
|
Charted::Visit.destroy
|
60
|
+
Charted::Event.destroy
|
61
|
+
Charted::Conversion.destroy
|
62
|
+
Charted::Experiment.destroy
|
60
63
|
end
|
61
64
|
|
62
65
|
def test_create
|
63
|
-
site = Charted::Site.create(:
|
66
|
+
site = Charted::Site.create(domain: 'localhost')
|
64
67
|
visitor = Charted::Visitor.create(
|
65
|
-
:
|
66
|
-
:
|
67
|
-
:
|
68
|
+
site: site,
|
69
|
+
bucket: 0,
|
70
|
+
ip_address: '67.188.42.140',
|
71
|
+
user_agent:
|
68
72
|
'Mozilla/5.0 (X11; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1')
|
69
73
|
visit = Charted::Visit.create(
|
70
|
-
:
|
71
|
-
:
|
72
|
-
:
|
73
|
-
:
|
74
|
+
visitor: visitor,
|
75
|
+
path: '/',
|
76
|
+
title: 'Prime',
|
77
|
+
referrer: 'http://www.google.com?q=Charted+Test')
|
78
|
+
|
74
79
|
assert_equal(site, visit.site)
|
75
80
|
assert_equal([visit], site.visits)
|
76
81
|
assert_equal('Charted Test', visit.search_terms)
|
77
82
|
assert_match(/^\w{5}$/, visitor.secret)
|
78
|
-
assert_equal("#{visitor.id}-#{visitor.secret}", visitor.cookie)
|
83
|
+
assert_equal("#{visitor.id}-#{visitor.bucket}-#{visitor.secret}", visitor.cookie)
|
79
84
|
assert_equal('Linux', visitor.platform)
|
80
85
|
assert_equal('Firefox', visitor.browser)
|
81
86
|
assert_equal('14.0.1', visitor.browser_version)
|
82
87
|
|
83
|
-
assert_equal(visitor,
|
84
|
-
assert_nil(
|
88
|
+
assert_equal(visitor, site.visitor_with_cookie(visitor.cookie))
|
89
|
+
assert_nil(site.visitor_with_cookie("#{visitor.id}-zzzzz"))
|
90
|
+
assert_nil(site.visitor_with_cookie("0-zzzzz"))
|
91
|
+
assert_nil(site.visitor_with_cookie(nil))
|
92
|
+
|
93
|
+
event = visitor.make_events('User Clicked').first
|
94
|
+
assert_equal(site, event.site)
|
95
|
+
assert_equal(visitor, event.visitor)
|
96
|
+
assert_equal('User Clicked', event.label)
|
97
|
+
|
98
|
+
conversion = visitor.start_conversions('User Purchased;User Abandon').first
|
99
|
+
visitor.start_conversions('User Purchased') # no effect
|
100
|
+
assert_equal(2, visitor.conversions.length)
|
101
|
+
assert_equal(site, conversion.site)
|
102
|
+
assert_equal(visitor, conversion.visitor)
|
103
|
+
assert_equal('User Purchased', conversion.label)
|
104
|
+
refute(conversion.ended?)
|
105
|
+
visitor.end_goals('User Purchased')
|
106
|
+
assert(conversion.ended?)
|
107
|
+
visitor.end_goals('Nonexistant') # no effect
|
108
|
+
assert_equal(2, visitor.conversions.length)
|
109
|
+
|
110
|
+
experiment = visitor.start_experiments('User Next:A').first
|
111
|
+
visitor.start_experiments('User Next:A') # no effect
|
112
|
+
visitor.start_experiments('User Next:B') # changes bucket
|
113
|
+
assert_equal(1, visitor.experiments.length)
|
114
|
+
assert_equal(site, experiment.site)
|
115
|
+
assert_equal(visitor, experiment.visitor)
|
116
|
+
assert_equal('User Next', experiment.label)
|
117
|
+
assert_equal('B', experiment.bucket)
|
118
|
+
refute(experiment.ended?)
|
119
|
+
visitor.end_goals('User Next')
|
120
|
+
assert(experiment.ended?)
|
121
|
+
visitor.end_goals('Nonexistant') # no effect
|
122
|
+
assert_equal(1, visitor.experiments.length)
|
85
123
|
end
|
86
124
|
|
87
125
|
def test_unique_identifier
|
@@ -131,10 +169,14 @@ class AppTest < ChartedTest
|
|
131
169
|
Charted::Site.destroy
|
132
170
|
Charted::Visitor.destroy
|
133
171
|
Charted::Visit.destroy
|
172
|
+
Charted::Event.destroy
|
173
|
+
Charted::Conversion.destroy
|
174
|
+
Charted::Experiment.destroy
|
134
175
|
clear_cookies
|
135
176
|
|
136
177
|
@site = Charted::Site.create(:domain => 'example.org')
|
137
178
|
@params = {
|
179
|
+
:bucket => 1,
|
138
180
|
:path => '/',
|
139
181
|
:title => 'Prime',
|
140
182
|
:referrer => 'localhost',
|
@@ -170,7 +212,7 @@ class AppTest < ChartedTest
|
|
170
212
|
assert_equal(@site, visit.site)
|
171
213
|
assert_equal('Prime', visit.title)
|
172
214
|
assert_equal('/', visit.path)
|
173
|
-
assert_equal(
|
215
|
+
assert_equal(nil, visit.referrer)
|
174
216
|
assert_equal('1280x800', visitor.resolution)
|
175
217
|
assert_equal('United States', visitor.country)
|
176
218
|
assert_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
|
@@ -202,6 +244,70 @@ class AppTest < ChartedTest
|
|
202
244
|
refute_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
|
203
245
|
end
|
204
246
|
|
247
|
+
def test_events # TODO: use correct HTTP methods?
|
248
|
+
get '/charted/record', events: 'Event Label;Event Label 2'
|
249
|
+
assert_equal(404, last_response.status)
|
250
|
+
|
251
|
+
visitor = @site.visitors.create
|
252
|
+
set_cookie("charted=#{visitor.cookie}")
|
253
|
+
get '/charted/record', events: 'Event Label;Event Label 2'
|
254
|
+
assert(last_response.ok?)
|
255
|
+
assert_equal(2, Charted::Event.count)
|
256
|
+
|
257
|
+
event = Charted::Event.first(label: 'Event Label')
|
258
|
+
assert_equal(@site, event.site)
|
259
|
+
assert_equal(visitor, event.visitor)
|
260
|
+
assert_equal('Event Label', event.label)
|
261
|
+
|
262
|
+
event2 = Charted::Event.first(label: 'Event Label 2')
|
263
|
+
assert(event2)
|
264
|
+
assert_equal('Event Label 2', event2.label)
|
265
|
+
end
|
266
|
+
|
267
|
+
def test_conversions
|
268
|
+
visitor = @site.visitors.create
|
269
|
+
set_cookie("charted=#{visitor.cookie}")
|
270
|
+
get '/charted', @params.merge(conversions: 'Logo Clicked;Button Clicked'), @env
|
271
|
+
assert(last_response.ok?)
|
272
|
+
assert_equal(2, Charted::Conversion.count)
|
273
|
+
|
274
|
+
logo = visitor.conversions.first(label: 'Logo Clicked')
|
275
|
+
button = visitor.conversions.first(label: 'Button Clicked')
|
276
|
+
refute(logo.ended?)
|
277
|
+
refute(button.ended?)
|
278
|
+
|
279
|
+
get '/charted/record', goals: 'Logo Clicked;Button Clicked'
|
280
|
+
assert(last_response.ok?)
|
281
|
+
logo.reload
|
282
|
+
button.reload
|
283
|
+
assert(logo.ended?)
|
284
|
+
assert(button.ended?)
|
285
|
+
end
|
286
|
+
|
287
|
+
def test_experiments
|
288
|
+
visitor = @site.visitors.create
|
289
|
+
set_cookie("charted=#{visitor.cookie}")
|
290
|
+
get '/charted', @params.merge(experiments: 'Logo:A;Button:B'), @env
|
291
|
+
assert(last_response.ok?)
|
292
|
+
assert_equal(2, Charted::Experiment.count)
|
293
|
+
|
294
|
+
logo = visitor.experiments.first(label: 'Logo')
|
295
|
+
button = visitor.experiments.first(label: 'Button')
|
296
|
+
assert_equal('Logo', logo.label)
|
297
|
+
assert_equal('A', logo.bucket)
|
298
|
+
refute(logo.ended?)
|
299
|
+
assert_equal('Button', button.label)
|
300
|
+
assert_equal('B', button.bucket)
|
301
|
+
refute(button.ended?)
|
302
|
+
|
303
|
+
get '/charted/record', goals: 'Logo;Button'
|
304
|
+
assert(last_response.ok?)
|
305
|
+
logo.reload
|
306
|
+
button.reload
|
307
|
+
assert(logo.ended?)
|
308
|
+
assert(button.ended?)
|
309
|
+
end
|
310
|
+
|
205
311
|
private
|
206
312
|
def app
|
207
313
|
@app ||= Rack::Server.new.app
|
@@ -215,6 +321,9 @@ class CommandTest < ChartedTest
|
|
215
321
|
Charted::Site.destroy
|
216
322
|
Charted::Visitor.destroy
|
217
323
|
Charted::Visit.destroy
|
324
|
+
Charted::Event.destroy
|
325
|
+
Charted::Conversion.destroy
|
326
|
+
Charted::Experiment.destroy
|
218
327
|
Charted::Site.create(:domain => 'localhost')
|
219
328
|
Charted::Site.create(:domain => 'example.org')
|
220
329
|
end
|
@@ -235,6 +344,27 @@ class CommandTest < ChartedTest
|
|
235
344
|
assert_equal('example.org', @cmd.site.domain)
|
236
345
|
end
|
237
346
|
|
347
|
+
def test_clean
|
348
|
+
site = Charted::Site.first(domain: 'localhost')
|
349
|
+
visitor = site.visitors.create
|
350
|
+
visitor.events.create(label: 'Label')
|
351
|
+
visitor.conversions.create(label: 'Label')
|
352
|
+
visitor.experiments.create(label: 'Label', bucket: 'A')
|
353
|
+
@cmd.output = nil
|
354
|
+
@cmd.clean
|
355
|
+
visitor.reload
|
356
|
+
assert_equal(1, visitor.events.size)
|
357
|
+
assert_equal(1, visitor.conversions.size)
|
358
|
+
assert_equal(1, visitor.experiments.size)
|
359
|
+
|
360
|
+
@cmd.output = nil
|
361
|
+
@cmd.clean('Label')
|
362
|
+
visitor.reload
|
363
|
+
assert_equal(0, visitor.events.size)
|
364
|
+
assert_equal(0, visitor.conversions.size)
|
365
|
+
assert_equal(0, visitor.experiments.size)
|
366
|
+
end
|
367
|
+
|
238
368
|
def test_dashboard
|
239
369
|
assert_raises(Charted::ExitError) { @cmd.dashboard }
|
240
370
|
assert_equal(['Please specify website with --site'], @cmd.output)
|
@@ -244,6 +374,12 @@ class CommandTest < ChartedTest
|
|
244
374
|
@cmd.dashboard
|
245
375
|
end
|
246
376
|
|
377
|
+
def test_js
|
378
|
+
@cmd.output = nil
|
379
|
+
@cmd.js
|
380
|
+
assert_match("var Charted", @cmd.output[0])
|
381
|
+
end
|
382
|
+
|
247
383
|
def test_format
|
248
384
|
assert_equal('-10,200', @cmd.send(:format, -10200))
|
249
385
|
assert_equal('-1', @cmd.send(:format, -1))
|
data/test/fixtures.rb
CHANGED
@@ -11,7 +11,7 @@ module Charted
|
|
11
11
|
example = Charted::Site.create(:domain => 'example.org')
|
12
12
|
|
13
13
|
months = (0..11).map { |d| Charted.prev_month(Date.today, d) }
|
14
|
-
|
14
|
+
200.times do
|
15
15
|
visitor = Charted::Visitor.create(
|
16
16
|
:site => select_rand([localhost, example]),
|
17
17
|
:created_at => select_rand(months),
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: charted
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.2
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: .
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-03-
|
12
|
+
date: 2013-03-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: sinatra
|