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 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 `--sync` option
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('-d', '--dashboard', 'show dashboard') { action = :dashboard }
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.email 'dev@localhost'
5
- c.db_adapter 'sqlite3'
6
- c.db_host 'localhost'
7
- c.db_username 'root'
8
- c.db_password 'secret'
9
- c.db_database 'db.sqlite3'
10
- c.sites ['localhost', 'example.org']
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']
@@ -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.1'
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 self.get_by_cookie(site, cookie)
123
- visitor = Visitor.get(cookie.to_s.split('-').first)
124
- visitor && visitor.site == site && visitor.cookie == cookie ?
125
- visitor :
126
- nil
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
- get '/' do
163
- site = Site.first(:domain => request.host)
164
- halt(404) if site.nil?
165
-
166
- if request.cookies['charted']
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
- if visitor.nil?
171
- visitor = Visitor.create(
172
- :site => site,
173
- :resolution => params[:resolution],
174
- :user_agent => request.user_agent,
175
- :ip_address => request.ip)
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
- :value => visitor.cookie,
179
- :expires => (Date.today + 365*2).to_time)
271
+ value: @visitor.cookie,
272
+ expires: (Date.today + 365*2).to_time)
180
273
  end
181
274
 
182
- visit = Visit.create(
183
- :visitor => visitor,
184
- :path => params[:path],
185
- :title => params[:title],
186
- :referrer => params[:referrer])
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
- :to => Charted.config.email,
193
- :from => "charted@#{Charted.config.email.split('@')[1..-1].join}",
194
- :subject => 'Charted Error',
195
- :body => request.env['sinatra.error'].to_s
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
- tables = []
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.max_width(max_width)
211
- chart2.max_width(max_width)
212
- chart.title "Total Visits".colorize(:light_green)
213
- chart2.title "Unique Visits".colorize(:light_green)
214
- table = Dashes::Table.new
215
- table.spacing :min, :min, :max
216
- table.row('Total'.colorize(:light_blue),
217
- 'Unique'.colorize(:light_blue),
218
- 'Visits'.colorize(:light_green))
219
- table.separator
220
- table.max_width(max_width)
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
- table.align :right, :right, :left
231
- chart.row date.strftime('%b %Y'), visits
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
- tables << table
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
- table.max_width(max_width)
246
- table.spacing :min, :min, :max
247
- table.row('Total'.colorize(:light_blue),
248
- '%'.colorize(:light_blue),
249
- column.colorize(:light_green))
250
- table.separator
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
- table.align :right, :right, :left
261
- tables << table
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
- grid = Dashes::Grid.new
265
- grid.width(`tput cols`.to_i)
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
@@ -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(:domain => 'localhost')
66
+ site = Charted::Site.create(domain: 'localhost')
64
67
  visitor = Charted::Visitor.create(
65
- :site => site,
66
- :ip_address => '67.188.42.140',
67
- :user_agent =>
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
- :visitor => visitor,
71
- :path => '/',
72
- :title => 'Prime',
73
- :referrer => 'http://www.google.com?q=Charted+Test')
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, Charted::Visitor.get_by_cookie(site, visitor.cookie))
84
- assert_nil(Charted::Visitor.get_by_cookie(site, "#{visitor.id}-zzzzz"))
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('localhost', visit.referrer)
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))
@@ -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
- 1000.times do
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.1
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-03 00:00:00.000000000 Z
12
+ date: 2013-03-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: sinatra