charted 0.0.8 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +31 -26
- data/{charted → bin/charted} +1 -2
- data/config.ru +10 -8
- data/lib/charted.rb +12 -483
- data/lib/charted/app.rb +59 -0
- data/lib/charted/command.rb +210 -0
- data/lib/charted/config.rb +30 -0
- data/lib/charted/model.rb +161 -0
- data/lib/charted/version.rb +3 -0
- data/migrate/001_init_schema.rb +56 -0
- data/public/charted/script.js +96 -0
- data/test/app_test.rb +160 -0
- data/test/command_test.rb +70 -0
- data/test/config_test.rb +11 -0
- data/test/fixtures.rb +1 -1
- data/test/helper.rb +43 -0
- data/test/model_test.rb +101 -0
- metadata +86 -65
- data/test/charted_test.rb +0 -389
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: a42efb242a29b6a2634fce409c83332faec2bb9f
|
4
|
+
data.tar.gz: 9107198182f58f9f07c10fe72d27b44f5fefca70
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9fc09e83cc5c4bfb4cc34c9c13b461a40d6d8d07bdca7f582dd8a706452245deefb2ced18e8aebfaa38114333ae7c8df7f258169fd8fe93e396f069806636225
|
7
|
+
data.tar.gz: 8ad15e63db5b8576d3955088f292ea92b77f48b0e5f55771ab3c58bc92dde8f3aca49480bcf6cb6ac3f6eb877fd8ff06f2162896c074b133d8ac013407502563
|
data/README.md
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
-
Description
|
2
|
-
===========
|
1
|
+
# Description
|
3
2
|
|
4
3
|
Charted is a minimal web traffic analytics app.
|
5
4
|
|
6
|
-
Installation
|
7
|
-
============
|
5
|
+
# Installation
|
8
6
|
|
9
7
|
$ gem install charted
|
10
8
|
|
@@ -18,14 +16,19 @@ Setup a `config.ru` file and run it like any other Sinatra application.
|
|
18
16
|
require 'charted'
|
19
17
|
|
20
18
|
Charted.configure do |c|
|
21
|
-
c.
|
22
|
-
c.
|
23
|
-
c.db_adapter 'mysql'
|
24
|
-
c.db_host 'localhost'
|
25
|
-
c.db_username 'root'
|
26
|
-
c.db_password 'secret'
|
27
|
-
c.db_database 'charted'
|
19
|
+
c.delete_after 365 # only keep a years worth of data
|
20
|
+
c.error_email 'john@mailinator.com' # production exceptions are sent here
|
28
21
|
c.sites ['hughbien.com', 'example.com']
|
22
|
+
c.db_options( # database config
|
23
|
+
adapter: 'postgres',
|
24
|
+
host: 'localhost',
|
25
|
+
username: 'root',
|
26
|
+
password: 'secret',
|
27
|
+
database: 'charted')
|
28
|
+
c.email_options( # error email config (see Pony gem for options)
|
29
|
+
from: 'errors@mailinator.com',
|
30
|
+
via: :smtp,
|
31
|
+
via_options: {host: 'smtp.example.org'})
|
29
32
|
end
|
30
33
|
|
31
34
|
run Charted::App if !ENV['CHARTED_CMD']
|
@@ -45,8 +48,7 @@ you other JavaScript assets.
|
|
45
48
|
|
46
49
|
$ charted --js > /path/to/my/project/public/charted.js
|
47
50
|
|
48
|
-
Updating
|
49
|
-
========
|
51
|
+
# Updating
|
50
52
|
|
51
53
|
$ gem install charted
|
52
54
|
|
@@ -54,8 +56,7 @@ You may need to generate a `charted.js` file again:
|
|
54
56
|
|
55
57
|
$ charted --js > /path/to/my/project/public/charted.js
|
56
58
|
|
57
|
-
Usage
|
58
|
-
=====
|
59
|
+
# Usage
|
59
60
|
|
60
61
|
The web application is for end users, to get information about your traffic use
|
61
62
|
the included command line application.
|
@@ -110,8 +111,14 @@ events/conversions/experiments table:
|
|
110
111
|
|
111
112
|
charted --clean "Buy Button"
|
112
113
|
|
113
|
-
|
114
|
-
|
114
|
+
# Rack Extensions
|
115
|
+
|
116
|
+
I recommend using these extensions with Charted:
|
117
|
+
|
118
|
+
* [Rack Timeout](https://github.com/heroku/rack-timeout)
|
119
|
+
* [Rack Throttle](https://github.com/datagraph/rack-throttle)
|
120
|
+
|
121
|
+
# Development
|
115
122
|
|
116
123
|
Put this in your `zshrc` or `bashrc`:
|
117
124
|
|
@@ -119,7 +126,7 @@ Put this in your `zshrc` or `bashrc`:
|
|
119
126
|
|
120
127
|
Then run:
|
121
128
|
|
122
|
-
$
|
129
|
+
$ bin/charted --migrate
|
123
130
|
$ shotgun
|
124
131
|
|
125
132
|
Head on over to `http://localhost:9393/charted/prime.html`. This is where
|
@@ -127,16 +134,14 @@ recordings should occur.
|
|
127
134
|
|
128
135
|
Tests are setup to run via `ruby test/*_test.rb` or via `rake`.
|
129
136
|
|
130
|
-
TODO
|
131
|
-
====
|
137
|
+
# TODO
|
132
138
|
|
133
|
-
*
|
134
|
-
*
|
135
|
-
*
|
136
|
-
*
|
139
|
+
* handle case where visitor visits "/charted" or any other route directly (bad input)
|
140
|
+
* record/display uniques for pages, etc... perhaps --unique option
|
141
|
+
* add --limit NUMBER to limit number of rows per table outputted
|
142
|
+
* add date range options (--start "3 days ago" --end "1 day ago")
|
137
143
|
|
138
|
-
License
|
139
|
-
=======
|
144
|
+
# License
|
140
145
|
|
141
146
|
`geoip.dat` is provided by MaxMind at <http://dev.maxmind.com/geoip/geolite>.
|
142
147
|
|
data/{charted → bin/charted}
RENAMED
data/config.ru
CHANGED
@@ -1,15 +1,17 @@
|
|
1
1
|
require File.expand_path('lib/charted', File.dirname(__FILE__))
|
2
2
|
|
3
|
-
Charted.configure
|
3
|
+
Charted.configure do |c|
|
4
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
5
|
c.sites ['localhost', 'example.org']
|
12
|
-
|
6
|
+
c.error_email 'dev@localhost'
|
7
|
+
c.db_options(
|
8
|
+
adapter: 'sqlite',
|
9
|
+
host: 'localhost',
|
10
|
+
username: 'root',
|
11
|
+
password: 'secret',
|
12
|
+
database: 'db.sqlite3')
|
13
|
+
c.email_options(via: :sendmail)
|
14
|
+
end if ENV['RACK_ENV'] != 'test'
|
13
15
|
|
14
16
|
if !ENV['CHARTED_CMD']
|
15
17
|
map('/charted') { run Charted::App }
|
data/lib/charted.rb
CHANGED
@@ -1,43 +1,25 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require 'dm-core'
|
4
|
-
require 'dm-types'
|
5
|
-
require 'dm-timestamps'
|
6
|
-
require 'dm-validations'
|
7
|
-
require 'dm-aggregates'
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'dashes'
|
8
3
|
require 'date'
|
9
|
-
require 'digest/sha1'
|
10
|
-
require 'json'
|
11
|
-
require 'uri'
|
12
4
|
require 'geoip'
|
5
|
+
require 'json'
|
13
6
|
require 'pony'
|
14
|
-
require 'useragent'
|
15
7
|
require 'search_terms'
|
16
|
-
require '
|
8
|
+
require 'securerandom'
|
9
|
+
require 'sequel'
|
10
|
+
require 'sinatra/base'
|
11
|
+
require 'uri'
|
12
|
+
require 'useragent'
|
17
13
|
|
18
|
-
|
19
|
-
|
14
|
+
require_relative 'charted/app'
|
15
|
+
require_relative 'charted/command'
|
16
|
+
require_relative 'charted/config'
|
17
|
+
require_relative 'charted/version'
|
20
18
|
|
21
19
|
module Charted
|
22
|
-
VERSION = '0.0.8'
|
23
20
|
GEOIP = GeoIP.new("#{File.dirname(__FILE__)}/../geoip.dat")
|
24
21
|
JS_FILE = "#{File.dirname(__FILE__)}/../public/charted/script.js"
|
25
22
|
|
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
23
|
def self.prev_month(date, delta=1)
|
42
24
|
date = Date.new(date.year, date.month, 1)
|
43
25
|
delta.times { date = Date.new((date - 1).year, (date - 1).month, 1) }
|
@@ -49,457 +31,4 @@ module Charted
|
|
49
31
|
delta.times { date = Date.new((date + 32).year, (date + 32).month, 1) }
|
50
32
|
date
|
51
33
|
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
|
-
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
|
-
|
79
|
-
class Site
|
80
|
-
include DataMapper::Resource
|
81
|
-
|
82
|
-
property :id, Serial
|
83
|
-
property :domain, String, :required => true, :unique => true
|
84
|
-
property :created_at, DateTime
|
85
|
-
|
86
|
-
has n, :visitors
|
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
|
96
|
-
end
|
97
|
-
|
98
|
-
class Visitor
|
99
|
-
include DataMapper::Resource
|
100
|
-
|
101
|
-
property :id, Serial
|
102
|
-
property :secret, String, :required => true
|
103
|
-
property :resolution, String
|
104
|
-
property :created_at, DateTime
|
105
|
-
property :platform, String
|
106
|
-
property :browser, String
|
107
|
-
property :browser_version, String
|
108
|
-
property :country, String
|
109
|
-
property :bucket, Integer
|
110
|
-
|
111
|
-
belongs_to :site
|
112
|
-
has n, :visits
|
113
|
-
has n, :events
|
114
|
-
has n, :conversions
|
115
|
-
has n, :experiments
|
116
|
-
|
117
|
-
validates_presence_of :site
|
118
|
-
|
119
|
-
def initialize(*args)
|
120
|
-
super
|
121
|
-
self.secret = self.class.generate_secret
|
122
|
-
end
|
123
|
-
|
124
|
-
def cookie
|
125
|
-
"#{self.id}-#{self.bucket}-#{self.secret}"
|
126
|
-
end
|
127
|
-
|
128
|
-
def user_agent=(user_agent)
|
129
|
-
ua = UserAgent.parse(user_agent)
|
130
|
-
self.browser = ua.browser
|
131
|
-
self.browser_version = ua.version
|
132
|
-
self.platform = ua.platform == 'X11' ? 'Linux' : ua.platform
|
133
|
-
end
|
134
|
-
|
135
|
-
def ip_address=(ip)
|
136
|
-
return if ip.to_s =~ /^\s*$/ || ip == '127.0.0.1'
|
137
|
-
name = GEOIP.country(ip).country_name
|
138
|
-
|
139
|
-
return if name =~ /^\s*$/ || name == 'N/A'
|
140
|
-
self.country = name
|
141
|
-
rescue SocketError
|
142
|
-
# invalid IP address, skip setting country
|
143
|
-
end
|
144
|
-
|
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
|
177
|
-
end
|
178
|
-
|
179
|
-
def self.generate_secret
|
180
|
-
Digest::SHA1.hexdigest("#{Time.now}-#{rand(100)}")[0..4]
|
181
|
-
end
|
182
|
-
end
|
183
|
-
|
184
|
-
class Visit
|
185
|
-
include DataMapper::Resource
|
186
|
-
|
187
|
-
property :id, Serial
|
188
|
-
property :path, String, required: true
|
189
|
-
property :title, String, required: true
|
190
|
-
property :referrer, String, length: 2048
|
191
|
-
property :search_terms, String
|
192
|
-
property :created_at, DateTime
|
193
|
-
|
194
|
-
belongs_to :visitor
|
195
|
-
has 1, :site, :through => :visitor
|
196
|
-
|
197
|
-
validates_presence_of :visitor
|
198
|
-
|
199
|
-
before :save, :set_search_terms
|
200
|
-
|
201
|
-
def set_search_terms
|
202
|
-
return if self.referrer.to_s =~ /^\s*$/
|
203
|
-
self.search_terms = URI.parse(self.referrer).search_string
|
204
|
-
end
|
205
|
-
end
|
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
|
-
|
251
|
-
DataMapper.finalize
|
252
|
-
|
253
|
-
class App < Sinatra::Base
|
254
|
-
set :logging, true
|
255
|
-
|
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
|
261
|
-
|
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])
|
269
|
-
response.set_cookie(
|
270
|
-
'charted',
|
271
|
-
value: @visitor.cookie,
|
272
|
-
expires: (Date.today + 365*2).to_time)
|
273
|
-
end
|
274
|
-
|
275
|
-
begin
|
276
|
-
referrer = params[:referrer].to_s
|
277
|
-
referrer = nil if URI.parse(referrer).host == @site.domain || referrer =~ /^\s*$/
|
278
|
-
rescue URI::InvalidURIError
|
279
|
-
referrer = nil
|
280
|
-
end
|
281
|
-
@visitor.visits.create(
|
282
|
-
path: params[:path],
|
283
|
-
title: params[:title],
|
284
|
-
referrer: referrer)
|
285
|
-
@visitor.start_conversions(params[:conversions])
|
286
|
-
@visitor.start_experiments(params[:experiments])
|
287
|
-
'/**/'
|
288
|
-
end
|
289
|
-
|
290
|
-
get '/record' do
|
291
|
-
halt(404) if @visitor.nil?
|
292
|
-
@visitor.make_events(params[:events])
|
293
|
-
@visitor.end_goals(params[:goals])
|
294
|
-
'/**/'
|
295
|
-
end
|
296
|
-
|
297
|
-
error do
|
298
|
-
Pony.mail(
|
299
|
-
to: Charted.config.email,
|
300
|
-
from: "charted@#{Charted.config.email.split('@')[1..-1].join}",
|
301
|
-
subject: 'Charted Error',
|
302
|
-
body: request.env['sinatra.error'].to_s
|
303
|
-
) if Charted.config.email && self.class.environment == :production
|
304
|
-
end
|
305
|
-
end
|
306
|
-
|
307
|
-
class Command
|
308
|
-
attr_accessor :config_loaded, :output
|
309
|
-
attr_reader :site
|
310
|
-
|
311
|
-
def clean(label=nil)
|
312
|
-
load_config
|
313
|
-
sys_exit("Please set 'delete_after' config.") if Charted.config.delete_after.nil?
|
314
|
-
|
315
|
-
threshold = Date.today - Charted.config.delete_after
|
316
|
-
Visit.all(:created_at.lt => threshold).destroy
|
317
|
-
Event.all(:created_at.lt => threshold).destroy
|
318
|
-
Conversion.all(:created_at.lt => threshold).destroy
|
319
|
-
Experiment.all(:created_at.lt => threshold).destroy
|
320
|
-
Visitor.all(:created_at.lt => threshold).each do |visitor|
|
321
|
-
visitor.destroy if visitor.visits.count == 0 &&
|
322
|
-
visitor.events.count == 0 &&
|
323
|
-
visitor.conversions.count == 0 &&
|
324
|
-
visitor.experiments.count == 0
|
325
|
-
end
|
326
|
-
|
327
|
-
if label
|
328
|
-
Event.all(label: label).destroy
|
329
|
-
Conversion.all(label: label).destroy
|
330
|
-
Experiment.all(label: label).destroy
|
331
|
-
end
|
332
|
-
end
|
333
|
-
|
334
|
-
def dashboard
|
335
|
-
site_required
|
336
|
-
nodes = []
|
337
|
-
max_width = [`tput cols`.to_i / 2, 60].min
|
338
|
-
chart = Dashes::Chart.new.
|
339
|
-
max_width(max_width).
|
340
|
-
title("Total Visits")
|
341
|
-
chart2 = Dashes::Chart.new.
|
342
|
-
max_width(max_width).
|
343
|
-
title("Unique Visits")
|
344
|
-
table = Dashes::Table.new.
|
345
|
-
max_width(max_width).
|
346
|
-
spacing(:min, :min, :max).
|
347
|
-
align(:right, :right, :left).
|
348
|
-
row('Total', 'Unique', 'Visits').
|
349
|
-
separator
|
350
|
-
(0..11).each do |delta|
|
351
|
-
date = Charted.prev_month(Date.today, delta)
|
352
|
-
visits = @site.visits.count(
|
353
|
-
:created_at.gte => date,
|
354
|
-
:created_at.lt => Charted.next_month(date))
|
355
|
-
unique = @site.visitors.count(:visits => {
|
356
|
-
:created_at.gte => date,
|
357
|
-
:created_at.lt => Charted.next_month(date)})
|
358
|
-
table.row(format(visits), format(unique), date.strftime('%B %Y'))
|
359
|
-
chart.row(date.strftime('%b %Y'), visits)
|
360
|
-
chart2.row(date.strftime('%b %Y'), unique)
|
361
|
-
end
|
362
|
-
nodes += [table, chart, chart2]
|
363
|
-
[[:browser, 'Browsers', :visitors],
|
364
|
-
[:resolution, 'Resolutions', :visitors],
|
365
|
-
[:platform, 'Platforms', :visitors],
|
366
|
-
[:country, 'Countries', :visitors],
|
367
|
-
[:title, 'Pages', :visits],
|
368
|
-
[:referrer, 'Referrers', :visits],
|
369
|
-
[:search_terms, 'Searches', :visits]].each do |field, column, type|
|
370
|
-
table = Dashes::Table.new.
|
371
|
-
max_width(max_width).
|
372
|
-
spacing(:min, :min, :max).
|
373
|
-
align(:right, :right, :left).
|
374
|
-
row('Total', '%', column).separator
|
375
|
-
rows = []
|
376
|
-
total = @site.send(type).count(field.not => nil)
|
377
|
-
@site.send(type).aggregate(field, :all.count).each do |label, count|
|
378
|
-
label = label.to_s.strip
|
379
|
-
next if label == ""
|
380
|
-
label = "#{label[0..37]}..." if label.length > 40
|
381
|
-
rows << [format(count), "#{((count / total.to_f) * 100).round}%", label]
|
382
|
-
end
|
383
|
-
add_truncated(table, rows)
|
384
|
-
nodes << table
|
385
|
-
end
|
386
|
-
table = Dashes::Table.new.
|
387
|
-
max_width(max_width).
|
388
|
-
spacing(:min, :min, :max).
|
389
|
-
align(:right, :right, :left).
|
390
|
-
row('Total', 'Unique', 'Events').
|
391
|
-
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
|
-
add_truncated(table, rows)
|
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', 'End', 'Conversions').
|
405
|
-
separator
|
406
|
-
rows = []
|
407
|
-
@site.conversions.aggregate(:label, :all.count).each do |label, count|
|
408
|
-
ended = @site.conversions.count(label: label, :ended_at.not => nil)
|
409
|
-
rows << [format(count), format(ended), label]
|
410
|
-
end
|
411
|
-
add_truncated(table, rows)
|
412
|
-
nodes << table
|
413
|
-
|
414
|
-
table = Dashes::Table.new.
|
415
|
-
max_width(max_width).
|
416
|
-
spacing(:min, :min, :max).
|
417
|
-
align(:right, :right, :left).
|
418
|
-
row('Start', 'End', 'Experiments').
|
419
|
-
separator
|
420
|
-
rows = []
|
421
|
-
@site.experiments.aggregate(:label, :bucket, :all.count).each do |label, bucket, count|
|
422
|
-
ended = @site.experiments.count(label: label, bucket: bucket, :ended_at.not => nil)
|
423
|
-
rows << [format(count), format(ended), "#{label}: #{bucket}"]
|
424
|
-
end
|
425
|
-
add_truncated(table, rows)
|
426
|
-
nodes << table
|
427
|
-
|
428
|
-
nodes.reject! do |node|
|
429
|
-
minimum = node.is_a?(Dashes::Table) ? 1 : 0
|
430
|
-
node.instance_variable_get(:@rows).size == minimum # TODO: hacked
|
431
|
-
end
|
432
|
-
print(Dashes::Grid.new.width(`tput cols`.to_i).add(*nodes))
|
433
|
-
end
|
434
|
-
|
435
|
-
def js
|
436
|
-
print(File.read(JS_FILE))
|
437
|
-
end
|
438
|
-
|
439
|
-
def migrate
|
440
|
-
load_config
|
441
|
-
DataMapper.auto_upgrade!
|
442
|
-
Charted.config.sites.each do |domain|
|
443
|
-
if Site.first(:domain => domain).nil?
|
444
|
-
Site.create(:domain => domain)
|
445
|
-
end
|
446
|
-
end
|
447
|
-
end
|
448
|
-
|
449
|
-
def site=(domain)
|
450
|
-
load_config
|
451
|
-
sites = Site.all(:domain.like => "%#{domain}%")
|
452
|
-
|
453
|
-
if sites.length > 1
|
454
|
-
sys_exit("\"#{domain}\" ambiguous: #{sites.map(&:domain).join(', ')}")
|
455
|
-
elsif sites.length < 1
|
456
|
-
sys_exit("No sites matching \"#{domain}\"")
|
457
|
-
else
|
458
|
-
@site = sites.first
|
459
|
-
end
|
460
|
-
end
|
461
|
-
|
462
|
-
private
|
463
|
-
def load_config
|
464
|
-
return if @config_loaded
|
465
|
-
file = ENV['CHARTED_CONFIG']
|
466
|
-
load(file)
|
467
|
-
@config_loaded = true
|
468
|
-
rescue LoadError
|
469
|
-
sys_exit("CHARTED_CONFIG not set, please set to `config.ru` file.")
|
470
|
-
end
|
471
|
-
|
472
|
-
def sys_exit(reason)
|
473
|
-
print(reason)
|
474
|
-
ENV['RACK_ENV'] == 'test' ? raise(ExitError.new) : exit
|
475
|
-
end
|
476
|
-
|
477
|
-
def print(string)
|
478
|
-
ENV['RACK_ENV'] == 'test' ? (@output ||= []) << string : puts(string)
|
479
|
-
end
|
480
|
-
|
481
|
-
def site_required
|
482
|
-
load_config
|
483
|
-
if @site.nil? && Site.count == 1
|
484
|
-
@site = Site.first
|
485
|
-
elsif @site.nil?
|
486
|
-
sys_exit('Please specify website with --site')
|
487
|
-
end
|
488
|
-
end
|
489
|
-
|
490
|
-
def format(num)
|
491
|
-
num.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse
|
492
|
-
end
|
493
|
-
|
494
|
-
def add_truncated(table, rows)
|
495
|
-
rows = rows.sort_by { |r| r[0].gsub(/[^\d]/, '').to_i }.reverse
|
496
|
-
if rows.length > 12
|
497
|
-
rows = rows[0..11]
|
498
|
-
rows << ['...', '...', '...']
|
499
|
-
end
|
500
|
-
rows.each { |row| table.row(*row) }
|
501
|
-
end
|
502
|
-
end
|
503
|
-
|
504
|
-
class ExitError < RuntimeError; end
|
505
34
|
end
|