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