charted 0.0.1

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.
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2012, Hugh Bien
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification,
5
+ are permitted provided that the following conditions are met:
6
+
7
+ Redistributions of source code must retain the above copyright notice, this list
8
+ of conditions and the following disclaimer.
9
+
10
+ Redistributions in binary form must reproduce the above copyright notice, this
11
+ list of conditions and the following disclaimer in the documentation and/or
12
+ other materials provided with the distribution.
13
+
14
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
18
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
21
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,112 @@
1
+ Description
2
+ ===========
3
+
4
+ Charted is a minimal web traffic analytics app. Still under development!
5
+
6
+ Installation
7
+ ============
8
+
9
+ $ gem install charted
10
+
11
+ Setup a `config.ru` file and run it like any other Sinatra application.
12
+
13
+ require 'rubygems'
14
+ require 'charted'
15
+
16
+ Charted.configure do |c|
17
+ c.email 'john@mailinator.com' # production exceptions are sent here
18
+ c.delete_after 365 # only keep a years worth of data
19
+ c.db_adapter 'mysql'
20
+ c.db_host 'localhost'
21
+ c.db_username 'root'
22
+ c.db_password 'secret'
23
+ c.db_database 'charted'
24
+ c.sites ['hughbien.com', 'example.com']
25
+ end
26
+
27
+ run Charted::App if !ENV['CHARTED_CMD']
28
+
29
+ Stick this in your `bashrc` or `zshrc`:
30
+
31
+ CHARTED_CONFIG='/path/to/config.ru'
32
+
33
+ Then initialize the database:
34
+
35
+ $ charted --migrate
36
+
37
+ The app should be mounted to `/charted` path on your domain. Then in your app,
38
+ include the script right before the closing `</body>` tag:
39
+
40
+ <script src="/charted/script.js" async></script>
41
+
42
+ If you concatenate your JavaScript, you can generate the `script.js` file and
43
+ add it to your project. The downside being when you update the charted gem,
44
+ you'll also have to remember to update the JavaScript:
45
+
46
+ $ charted --js > /path/to/my/project/public/charted.js
47
+
48
+ Updating
49
+ ========
50
+
51
+ $ gem install charted
52
+
53
+ Usage
54
+ =====
55
+
56
+ The web application is for end users, to get information about your traffic use
57
+ the included command line application.
58
+
59
+ $ charted --help
60
+ $ charted --dashboard --site hugh # just needs the first few letters
61
+ +-------+--------+--------------------------------------+
62
+ | Total | Unique | Visits |
63
+ +-------+--------+--------------------------------------+
64
+ | 7,012 | 5,919 | February 2013 |
65
+ | 6,505 | 4,722 | January 2013 |
66
+ | 5,342 | 3,988 | December 2012 |
67
+ ...
68
+
69
+ Development
70
+ ===========
71
+
72
+ Put this in your `zshrc` or `bashrc`:
73
+
74
+ export CHARTED_CONFIG="/path/to/charted/config.ru"
75
+
76
+ Then run:
77
+
78
+ $ ./charted --migrate
79
+ $ shotgun
80
+
81
+ Head on over to `http://localhost:9393/charted/prime.html`. This is where
82
+ recordings should occur.
83
+
84
+ Tests are setup to run via `ruby test/*_test.rb` or via `rake`.
85
+
86
+ TODO
87
+ ====
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
+ * deploy task in Rakefile for development
104
+ * add `--sync` option
105
+
106
+ License
107
+ =======
108
+
109
+ `geoip.dat` is provided by MaxMind at <http://dev.maxmind.com/geoip/geolite>.
110
+
111
+ Copyright Hugh Bien - http://hughbien.com.
112
+ Released under BSD License, see LICENSE.md for more info.
data/charted ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require File.expand_path('lib/charted', File.dirname(__FILE__))
4
+ require 'dm-migrations'
5
+ require 'date'
6
+ require 'fileutils'
7
+
8
+ ENV['CHARTED_CMD'] = '1'
9
+
10
+ ARGV.options do |o|
11
+ cmd, action = Charted::Command.new, nil
12
+ o.set_summary_indent(' ')
13
+ o.banner = "Usage: #{File.basename($0)} [OPTION]"
14
+ o.on('-d', '--dashboard', 'show dashboard') { action = :dashboard }
15
+ o.on('-h', '--help', 'show this help message') { puts o; exit }
16
+ o.on('-m', '--migrate', 'migrates database') { cmd.migrate; exit }
17
+ o.on('-s', '--site domain', 'set site') { |site| cmd.site = site }
18
+ o.parse!
19
+ action.nil? ? puts(o) : cmd.send(action)
20
+ end
@@ -0,0 +1,15 @@
1
+ require File.expand_path('lib/charted', File.dirname(__FILE__))
2
+
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']
11
+ end
12
+
13
+ if !ENV['CHARTED_CMD']
14
+ map('/charted') { run Charted::App }
15
+ end
Binary file
@@ -0,0 +1,327 @@
1
+ require 'rubygems'
2
+ require 'sinatra/base'
3
+ require 'dm-core'
4
+ require 'dm-types'
5
+ require 'dm-timestamps'
6
+ require 'dm-validations'
7
+ require 'dm-aggregates'
8
+ require 'date'
9
+ require 'digest/sha1'
10
+ require 'json'
11
+ require 'uri'
12
+ require 'geoip'
13
+ require 'pony'
14
+ require 'useragent'
15
+ require 'search_terms'
16
+ require 'terminal-table'
17
+ require 'colorize'
18
+ require 'dashes'
19
+
20
+ DataMapper::Model.raise_on_save_failure = true
21
+
22
+ module Charted
23
+ VERSION = '0.0.1'
24
+ GEOIP = GeoIP.new("#{File.dirname(__FILE__)}/../geoip.dat")
25
+
26
+ def self.configure(setup_db=true)
27
+ yield self.config
28
+ DataMapper.setup(:default,
29
+ :adapter => config.db_adapter,
30
+ :host => config.db_host,
31
+ :username => config.db_username,
32
+ :password => config.db_password,
33
+ :database => config.db_database
34
+ ) if setup_db
35
+ end
36
+
37
+ def self.config
38
+ @config ||= Config.new
39
+ end
40
+
41
+ def self.prev_month(date, delta=1)
42
+ date = Date.new(date.year, date.month, 1)
43
+ delta.times { date = Date.new((date - 1).year, (date - 1).month, 1) }
44
+ date
45
+ end
46
+
47
+ def self.next_month(date, delta=1)
48
+ date = Date.new(date.year, date.month, 1)
49
+ delta.times { date = Date.new((date + 32).year, (date + 32).month, 1) }
50
+ date
51
+ end
52
+
53
+ class Config
54
+ def self.attr_option(*names)
55
+ names.each do |name|
56
+ define_method(name) do |*args|
57
+ value = args[0]
58
+ instance_variable_set("@#{name}".to_sym, value) if !value.nil?
59
+ instance_variable_get("@#{name}".to_sym)
60
+ end
61
+ end
62
+ end
63
+
64
+ attr_option :email, :delete_after, :sites,
65
+ :db_adapter, :db_host, :db_username, :db_password, :db_database
66
+ end
67
+
68
+ class Site
69
+ include DataMapper::Resource
70
+
71
+ property :id, Serial
72
+ property :domain, String, :required => true, :unique => true
73
+ property :created_at, DateTime
74
+
75
+ has n, :visitors
76
+ has n, :visits, :through => :visitors
77
+ end
78
+
79
+ class Visitor
80
+ include DataMapper::Resource
81
+
82
+ property :id, Serial
83
+ property :secret, String, :required => true
84
+ property :resolution, String
85
+ property :created_at, DateTime
86
+ property :platform, String
87
+ property :browser, String
88
+ property :browser_version, String
89
+ property :country, String
90
+
91
+ belongs_to :site
92
+ has n, :visits
93
+
94
+ validates_presence_of :site
95
+
96
+ def initialize(*args)
97
+ super
98
+ self.secret = self.class.generate_secret
99
+ end
100
+
101
+ def cookie
102
+ "#{self.id}-#{self.secret}"
103
+ end
104
+
105
+ def user_agent=(user_agent)
106
+ ua = UserAgent.parse(user_agent)
107
+ self.browser = ua.browser
108
+ self.browser_version = ua.version
109
+ self.platform = ua.platform == 'X11' ? 'Linux' : ua.platform
110
+ end
111
+
112
+ def ip_address=(ip)
113
+ return if ip.to_s =~ /^\s*$/ || ip == '127.0.0.1'
114
+ name = GEOIP.country(ip).country_name
115
+
116
+ return if name =~ /^\s*$/ || name == 'N/A'
117
+ self.country = name
118
+ rescue SocketError
119
+ # invalid IP address, skip setting country
120
+ end
121
+
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
127
+ end
128
+
129
+ def self.generate_secret
130
+ Digest::SHA1.hexdigest("#{Time.now}-#{rand(100)}")[0..4]
131
+ end
132
+ end
133
+
134
+ class Visit
135
+ include DataMapper::Resource
136
+
137
+ property :id, Serial
138
+ property :path, String, :required => true
139
+ property :title, String, :required => true
140
+ property :referrer, String
141
+ property :search_terms, String
142
+ property :created_at, DateTime
143
+
144
+ belongs_to :visitor
145
+ has 1, :site, :through => :visitor
146
+
147
+ validates_presence_of :visitor
148
+
149
+ before :save, :set_search_terms
150
+
151
+ def set_search_terms
152
+ return if self.referrer.to_s =~ /^\s*$/
153
+ self.search_terms = URI.parse(self.referrer).search_string
154
+ end
155
+ end
156
+
157
+ DataMapper.finalize
158
+
159
+ class App < Sinatra::Base
160
+ set :logging, true
161
+
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
169
+
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)
176
+ response.set_cookie(
177
+ 'charted',
178
+ :value => visitor.cookie,
179
+ :expires => (Date.today + 365*2).to_time)
180
+ end
181
+
182
+ visit = Visit.create(
183
+ :visitor => visitor,
184
+ :path => params[:path],
185
+ :title => params[:title],
186
+ :referrer => params[:referrer])
187
+ '/**/'
188
+ end
189
+
190
+ error do
191
+ 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
196
+ ) if Charted.config.email && self.class.environment == :production
197
+ end
198
+ end
199
+
200
+ class Command
201
+ attr_accessor :config_loaded, :output
202
+ attr_reader :site
203
+
204
+ def dashboard
205
+ site_required
206
+ tables = []
207
+ chart = Dashes::Chart.new
208
+ chart2 = Dashes::Chart.new
209
+ 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)
221
+ (0..11).each do |delta|
222
+ date = Charted.prev_month(Date.today, delta)
223
+ visits = @site.visits.count(
224
+ :created_at.gte => date,
225
+ :created_at.lt => Charted.next_month(date))
226
+ unique = @site.visitors.count(:visits => {
227
+ :created_at.gte => date,
228
+ :created_at.lt => Charted.next_month(date)})
229
+ 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
233
+ end
234
+ tables << table
235
+ tables << chart
236
+ tables << chart2
237
+ [[:browser, 'Browsers', :visitors],
238
+ [:resolution, 'Resolutions', :visitors],
239
+ [:platform, 'Platforms', :visitors],
240
+ [:country, 'Countries', :visitors],
241
+ [:title, 'Pages', :visits],
242
+ [:referrer, 'Referrers', :visits],
243
+ [: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
251
+ rows = []
252
+ total = @site.send(type).count(field.not => nil)
253
+ @site.send(type).aggregate(field, :all.count).each do |label, count|
254
+ label = label.to_s.strip
255
+ next if label == ""
256
+ label = "#{label[0..37]}..." if label.length > 40
257
+ rows << [format(count), "#{((count / total.to_f) * 100).round}%", label]
258
+ end
259
+ rows.sort_by { |r| r[1] }.reverse.each { |row| table.row(*row) }
260
+ table.align :right, :right, :left
261
+ tables << table
262
+ end
263
+
264
+ grid = Dashes::Grid.new
265
+ grid.width(`tput cols`.to_i)
266
+ tables.each { |t| grid.add(t) }
267
+ print(grid)
268
+ end
269
+
270
+ def migrate
271
+ load_config
272
+ DataMapper.auto_upgrade!
273
+ Charted.config.sites.each do |domain|
274
+ if Site.first(:domain => domain).nil?
275
+ Site.create(:domain => domain)
276
+ end
277
+ end
278
+ end
279
+
280
+ def site=(domain)
281
+ load_config
282
+ sites = Site.all(:domain.like => "%#{domain}%")
283
+
284
+ if sites.length > 1
285
+ sys_exit("\"#{domain}\" ambiguous: #{sites.map(&:domain).join(', ')}")
286
+ elsif sites.length < 1
287
+ sys_exit("No sites matching \"#{domain}\"")
288
+ else
289
+ @site = sites.first
290
+ end
291
+ end
292
+
293
+ private
294
+ def load_config
295
+ return if @config_loaded
296
+ file = ENV['CHARTED_CONFIG']
297
+ load(file)
298
+ @config_loaded = true
299
+ rescue LoadError
300
+ sys_exit("CHARTED_CONFIG not set, please set to `config.ru` file.")
301
+ end
302
+
303
+ def sys_exit(reason)
304
+ print(reason)
305
+ ENV['RACK_ENV'] == 'test' ? raise(ExitError.new) : exit
306
+ end
307
+
308
+ def print(string)
309
+ ENV['RACK_ENV'] == 'test' ? (@output ||= []) << string : puts(string)
310
+ end
311
+
312
+ def site_required
313
+ load_config
314
+ if @site.nil? && Site.count == 1
315
+ @site = Site.first
316
+ elsif @site.nil?
317
+ sys_exit('Please specify website with --site')
318
+ end
319
+ end
320
+
321
+ def format(num)
322
+ num.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse
323
+ end
324
+ end
325
+
326
+ class ExitError < RuntimeError; end
327
+ end
@@ -0,0 +1,253 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ require File.expand_path('../lib/charted', File.dirname(__FILE__))
4
+ require 'dm-migrations'
5
+ require 'minitest/autorun'
6
+ require 'rack'
7
+ require 'rack/test'
8
+ require 'rack/server'
9
+ require 'fileutils'
10
+
11
+ DataMapper.setup(:default, 'sqlite::memory:')
12
+ DataMapper.auto_migrate!
13
+
14
+ module Pony
15
+ def self.mail(fields)
16
+ @last_mail = fields
17
+ end
18
+
19
+ def self.last_mail
20
+ @last_mail
21
+ end
22
+ end
23
+
24
+ class ChartedTest < MiniTest::Unit::TestCase
25
+ def setup
26
+ Charted.configure(false) do |c|
27
+ c.email 'dev@localhost'
28
+ c.db_adapter 'sqlite3'
29
+ c.db_host 'localhost'
30
+ c.db_username 'root'
31
+ c.db_password 'secret'
32
+ c.db_database 'test.sqlite3'
33
+ c.sites ['localhost']
34
+ end
35
+ Pony.mail(nil)
36
+ end
37
+
38
+ def teardown
39
+ FileUtils.rm_rf(File.expand_path('../temp', File.dirname(__FILE__)))
40
+ end
41
+ end
42
+
43
+ class ConfigTest < ChartedTest
44
+ def test_db
45
+ assert_equal('dev@localhost', Charted.config.email)
46
+ assert_equal('sqlite3', Charted.config.db_adapter)
47
+ assert_equal('localhost', Charted.config.db_host)
48
+ assert_equal('root', Charted.config.db_username)
49
+ assert_equal('secret', Charted.config.db_password)
50
+ assert_equal('test.sqlite3', Charted.config.db_database)
51
+ assert_equal(['localhost'], Charted.config.sites)
52
+ end
53
+ end
54
+
55
+ class ModelTest < ChartedTest
56
+ def setup
57
+ Charted::Site.destroy
58
+ Charted::Visitor.destroy
59
+ Charted::Visit.destroy
60
+ end
61
+
62
+ def test_create
63
+ site = Charted::Site.create(:domain => 'localhost')
64
+ visitor = Charted::Visitor.create(
65
+ :site => site,
66
+ :ip_address => '67.188.42.140',
67
+ :user_agent =>
68
+ 'Mozilla/5.0 (X11; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1')
69
+ visit = Charted::Visit.create(
70
+ :visitor => visitor,
71
+ :path => '/',
72
+ :title => 'Prime',
73
+ :referrer => 'http://www.google.com?q=Charted+Test')
74
+ assert_equal(site, visit.site)
75
+ assert_equal([visit], site.visits)
76
+ assert_equal('Charted Test', visit.search_terms)
77
+ assert_match(/^\w{5}$/, visitor.secret)
78
+ assert_equal("#{visitor.id}-#{visitor.secret}", visitor.cookie)
79
+ assert_equal('Linux', visitor.platform)
80
+ assert_equal('Firefox', visitor.browser)
81
+ assert_equal('14.0.1', visitor.browser_version)
82
+
83
+ assert_equal(visitor, Charted::Visitor.get_by_cookie(site, visitor.cookie))
84
+ assert_nil(Charted::Visitor.get_by_cookie(site, "#{visitor.id}-zzzzz"))
85
+ end
86
+
87
+ def test_unique_identifier
88
+ assert_match(/^\w{5}$/, Charted::Visitor.generate_secret)
89
+ end
90
+
91
+ def test_user_agents
92
+ visitor = Charted::Visitor.new
93
+
94
+ visitor.user_agent = 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)'
95
+ assert_equal('Internet Explorer', visitor.browser)
96
+ assert_equal('7.0', visitor.browser_version)
97
+ assert_equal('Windows', visitor.platform)
98
+
99
+ visitor.user_agent = 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8'
100
+ assert_equal('Safari', visitor.browser)
101
+ assert_equal('1.2.2', visitor.browser_version)
102
+ assert_equal('Macintosh', visitor.platform)
103
+
104
+ visitor.user_agent = 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081216 Ubuntu/8.04 (hardy) Firefox/2.0.0.19'
105
+ assert_equal('Firefox', visitor.browser)
106
+ assert_equal('2.0.0.19', visitor.browser_version)
107
+ assert_equal('Linux', visitor.platform)
108
+ end
109
+
110
+ def test_blanks
111
+ site = Charted::Site.create(:domain => 'localhost')
112
+ visitor = Charted::Visitor.create(
113
+ :site => site,
114
+ :user_agent => '',
115
+ :ip_address => '')
116
+ visit = Charted::Visit.create(
117
+ :visitor => visitor,
118
+ :path => '/',
119
+ :title => 'Prime',
120
+ :referrer => '')
121
+ assert(site.id)
122
+ assert(visitor.id)
123
+ assert(visit.id)
124
+ end
125
+ end
126
+
127
+ class AppTest < ChartedTest
128
+ include Rack::Test::Methods
129
+
130
+ def setup
131
+ Charted::Site.destroy
132
+ Charted::Visitor.destroy
133
+ Charted::Visit.destroy
134
+ clear_cookies
135
+
136
+ @site = Charted::Site.create(:domain => 'example.org')
137
+ @params = {
138
+ :path => '/',
139
+ :title => 'Prime',
140
+ :referrer => 'localhost',
141
+ :resolution => '1280x800'
142
+ }
143
+ @env = {
144
+ 'HTTP_USER_AGENT' =>
145
+ 'Mozilla/5.0 (X11; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1',
146
+ 'REMOTE_ADDR' => '67.188.42.140'
147
+ }
148
+ end
149
+
150
+ def test_environment
151
+ assert_equal(:test, Charted::App.environment)
152
+ end
153
+
154
+ def test_bad_domain
155
+ get '/charted', @params, 'HTTP_HOST' => 'localhost'
156
+ assert_equal(404, last_response.status)
157
+ assert_equal(0, Charted::Visitor.count)
158
+ assert_equal(0, Charted::Visit.count)
159
+ end
160
+
161
+ def test_new_visitor
162
+ get '/charted', @params, @env
163
+ assert(last_response.ok?)
164
+ assert_equal(1, Charted::Visitor.count)
165
+ assert_equal(1, Charted::Visit.count)
166
+
167
+ visitor = Charted::Visitor.first
168
+ visit = Charted::Visit.first
169
+ assert_equal(@site, visitor.site)
170
+ assert_equal(@site, visit.site)
171
+ assert_equal('Prime', visit.title)
172
+ assert_equal('/', visit.path)
173
+ assert_equal('localhost', visit.referrer)
174
+ assert_equal('1280x800', visitor.resolution)
175
+ assert_equal('United States', visitor.country)
176
+ assert_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
177
+ end
178
+
179
+ def test_old_visitor
180
+ visitor = Charted::Visitor.create(:site => @site)
181
+ visit = Charted::Visit.create(
182
+ :visitor => visitor, :path => '/', :title => 'Prime')
183
+ set_cookie("charted=#{visitor.cookie}")
184
+
185
+ get '/charted', @params, @env
186
+ assert(last_response.ok?)
187
+ assert_equal(1, Charted::Visitor.count)
188
+ assert_equal(2, Charted::Visit.count)
189
+ assert_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
190
+ end
191
+
192
+ def test_visitor_bad_cookie
193
+ visitor = Charted::Visitor.create(:site => @site)
194
+ visit = Charted::Visit.create(
195
+ :visitor => visitor, :path => '/', :title => 'Prime')
196
+ set_cookie("charted=#{visitor.id}-zzzzz")
197
+
198
+ get '/charted', @params, @env
199
+ assert(last_response.ok?)
200
+ assert_equal(2, Charted::Visitor.count)
201
+ assert_equal(2, Charted::Visit.count)
202
+ refute_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
203
+ end
204
+
205
+ private
206
+ def app
207
+ @app ||= Rack::Server.new.app
208
+ end
209
+ end
210
+
211
+ class CommandTest < ChartedTest
212
+ def setup
213
+ @cmd = Charted::Command.new
214
+ @cmd.config_loaded = true
215
+ Charted::Site.destroy
216
+ Charted::Visitor.destroy
217
+ Charted::Visit.destroy
218
+ Charted::Site.create(:domain => 'localhost')
219
+ Charted::Site.create(:domain => 'example.org')
220
+ end
221
+
222
+ def test_site
223
+ assert_raises(Charted::ExitError) { @cmd.site = 'nomatch' }
224
+ assert_equal(['No sites matching "nomatch"'], @cmd.output)
225
+ assert_nil(@cmd.site)
226
+
227
+ @cmd.output = nil
228
+ assert_raises(Charted::ExitError) { @cmd.site = 'l' }
229
+ assert_equal(['"l" ambiguous: localhost, example.org'], @cmd.output)
230
+
231
+ @cmd.site = 'local'
232
+ assert_equal('localhost', @cmd.site.domain)
233
+
234
+ @cmd.site = 'ample'
235
+ assert_equal('example.org', @cmd.site.domain)
236
+ end
237
+
238
+ def test_dashboard
239
+ assert_raises(Charted::ExitError) { @cmd.dashboard }
240
+ assert_equal(['Please specify website with --site'], @cmd.output)
241
+
242
+ @cmd.output = nil
243
+ @cmd.site = 'localhost'
244
+ @cmd.dashboard
245
+ end
246
+
247
+ def test_format
248
+ assert_equal('-10,200', @cmd.send(:format, -10200))
249
+ assert_equal('-1', @cmd.send(:format, -1))
250
+ assert_equal('1', @cmd.send(:format, 1))
251
+ assert_equal('1,200,300', @cmd.send(:format, 1200300))
252
+ end
253
+ end
@@ -0,0 +1,59 @@
1
+ require File.expand_path('../lib/charted', File.dirname(__FILE__))
2
+ require 'date'
3
+
4
+ module Charted
5
+ class Fixtures
6
+ def self.load!
7
+ Charted::Site.destroy
8
+ Charted::Visitor.destroy
9
+ Charted::Visit.destroy
10
+ localhost = Charted::Site.create(:domain => 'localhost')
11
+ example = Charted::Site.create(:domain => 'example.org')
12
+
13
+ months = (0..11).map { |d| Charted.prev_month(Date.today, d) }
14
+ 1000.times do
15
+ visitor = Charted::Visitor.create(
16
+ :site => select_rand([localhost, example]),
17
+ :created_at => select_rand(months),
18
+ :resolution => select_rand(%w(1400x900 1280x800 1024x768)),
19
+ :platform => select_rand(['Linux', 'OS X', 'Windows']),
20
+ :browser => select_rand(%w(IE Firefox Safari Chrome)),
21
+ :browser_version => select_rand(%w(1 2 3 4 5)),
22
+ :country => select_rand(%w(USA CA FR CH)),
23
+ :ip_address => select_rand(%w(67.188.42.140 67.184.24.140)),
24
+ :user_agent => select_rand([
25
+ 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0)',
26
+ 'Mozilla/5.0 (X11; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1',
27
+ 'Mozilla/5.0 (Macintosh; U; PPC Mac OS X; en) AppleWebKit/125.2 (KHTML, like Gecko) Safari/125.8',
28
+ 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081216 Ubuntu/8.04 (hardy) Firefox/2.0.0.19']))
29
+ (rand(2) + 1).times do |index|
30
+ Charted::Visit.create(
31
+ :visitor => visitor,
32
+ :created_at => Charted.next_month(visitor.created_at, select_rand([0, index])),
33
+ :path => select_rand(%w(/ /page-one/ /page-two/ /page-three/)),
34
+ :title => select_rand(%w(Prime Optimus Alpha Beta Omega)),
35
+ :referrer => select_rand([
36
+ 'http://www.google.com?q=Charted+Test',
37
+ 'http://coverstrap.com',
38
+ 'http://news.ycombinator.com',
39
+ 'http://example.org']),
40
+ :search_terms => select_rand([
41
+ 'Charted Keywords',
42
+ 'Web Analytics',
43
+ 'Command Line Analytics']))
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+ def self.select_rand(items)
50
+ items[rand(items.length)]
51
+ end
52
+ end
53
+ end
54
+
55
+ if __FILE__ == $0
56
+ ENV['CHARTED_CMD'] = '1'
57
+ load(File.expand_path('../config.ru', File.dirname(__FILE__)))
58
+ Charted::Fixtures.load!
59
+ end
metadata ADDED
@@ -0,0 +1,199 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: charted
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Hugh Bien
9
+ autorequire:
10
+ bindir: .
11
+ cert_chain: []
12
+ date: 2013-03-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: sinatra
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: data_mapper
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: geoip
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pony
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :runtime
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: useragent
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :runtime
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: search_terms
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :runtime
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: colorize
112
+ requirement: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ! '>='
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ! '>='
124
+ - !ruby/object:Gem::Version
125
+ version: '0'
126
+ - !ruby/object:Gem::Dependency
127
+ name: dashes
128
+ requirement: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ! '>='
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :runtime
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ none: false
138
+ requirements:
139
+ - - ! '>='
140
+ - !ruby/object:Gem::Version
141
+ version: '0'
142
+ - !ruby/object:Gem::Dependency
143
+ name: minitest
144
+ requirement: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ! '>='
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ none: false
154
+ requirements:
155
+ - - ! '>='
156
+ - !ruby/object:Gem::Version
157
+ version: '0'
158
+ description: A Sinatra app for tracking web traffic on multiple sites.
159
+ email:
160
+ - hugh@hughbien.com
161
+ executables:
162
+ - charted
163
+ extensions: []
164
+ extra_rdoc_files: []
165
+ files:
166
+ - LICENSE.md
167
+ - README.md
168
+ - config.ru
169
+ - geoip.dat
170
+ - charted
171
+ - lib/charted.rb
172
+ - test/charted_test.rb
173
+ - test/fixtures.rb
174
+ - ./charted
175
+ homepage: https://github.com/hughbien/charted
176
+ licenses: []
177
+ post_install_message:
178
+ rdoc_options: []
179
+ require_paths:
180
+ - lib
181
+ required_ruby_version: !ruby/object:Gem::Requirement
182
+ none: false
183
+ requirements:
184
+ - - ! '>='
185
+ - !ruby/object:Gem::Version
186
+ version: '0'
187
+ required_rubygems_version: !ruby/object:Gem::Requirement
188
+ none: false
189
+ requirements:
190
+ - - ! '>='
191
+ - !ruby/object:Gem::Version
192
+ version: 1.3.6
193
+ requirements: []
194
+ rubyforge_project:
195
+ rubygems_version: 1.8.23
196
+ signing_key:
197
+ specification_version: 3
198
+ summary: Minimal web traffic analytics
199
+ test_files: []