charted 0.0.1 → 0.0.2
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/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
|