charted 0.0.8 → 0.1.0
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.
- 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
|