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
@@ -0,0 +1,56 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
change do
|
3
|
+
create_table(:sites) do
|
4
|
+
primary_key :id
|
5
|
+
column :domain, String, null: false, unique: true
|
6
|
+
column :created_at, DateTime, null: false
|
7
|
+
end
|
8
|
+
|
9
|
+
create_table(:visitors) do
|
10
|
+
primary_key :id
|
11
|
+
foreign_key :site_id, :sites
|
12
|
+
column :secret, String, null: false
|
13
|
+
column :resolution, String
|
14
|
+
column :created_at, DateTime
|
15
|
+
column :platform, String
|
16
|
+
column :browser, String
|
17
|
+
column :browser_version, String
|
18
|
+
column :country, String
|
19
|
+
column :bucket, Integer, null: false
|
20
|
+
end
|
21
|
+
|
22
|
+
create_table(:visits) do
|
23
|
+
primary_key :id
|
24
|
+
foreign_key :visitor_id, :visitors
|
25
|
+
column :path, String, null: false
|
26
|
+
column :title, String, null: false
|
27
|
+
column :referrer, String, size: 2048
|
28
|
+
column :search_terms, String
|
29
|
+
column :created_at, DateTime, null: false
|
30
|
+
end
|
31
|
+
|
32
|
+
create_table(:events) do
|
33
|
+
primary_key :id
|
34
|
+
foreign_key :visitor_id, :visitors
|
35
|
+
column :label, String, null: false
|
36
|
+
column :created_at, DateTime, null: false
|
37
|
+
end
|
38
|
+
|
39
|
+
create_table(:conversions) do
|
40
|
+
primary_key :id
|
41
|
+
foreign_key :visitor_id, :visitors
|
42
|
+
column :label, String, null: false
|
43
|
+
column :created_at, DateTime, null: false
|
44
|
+
column :ended_at, DateTime
|
45
|
+
end
|
46
|
+
|
47
|
+
create_table(:experiments) do
|
48
|
+
primary_key :id
|
49
|
+
foreign_key :visitor_id, :visitors
|
50
|
+
column :label, String, null: false
|
51
|
+
column :bucket, String, null: false
|
52
|
+
column :created_at, DateTime, null: false
|
53
|
+
column :ended_at, DateTime
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
var Charted = {
|
2
|
+
URL: "/charted/",
|
3
|
+
RECORD_URL: URL + "record",
|
4
|
+
send: function(url, queryString) {
|
5
|
+
if (this.cookie("chartedignore")) { return; }
|
6
|
+
if (typeof jQuery === 'undefined') {
|
7
|
+
var script = document.createElement("script");
|
8
|
+
script.type = "text/javascript";
|
9
|
+
script.async = true;
|
10
|
+
script.src = url + "?" + queryString;
|
11
|
+
document.getElementsByTagName("head")[0].appendChild(script);
|
12
|
+
} else if (url.match(/^https?:\/\//)) {
|
13
|
+
jQuery.getScript(url + "?" + queryString);
|
14
|
+
} else {
|
15
|
+
jQuery.get(url, queryString);
|
16
|
+
}
|
17
|
+
},
|
18
|
+
cookie: function(name) {
|
19
|
+
var obj = {};
|
20
|
+
var val = document.cookie.match(new RegExp("(?:^|;) *"+name+"=([^;]*)"));
|
21
|
+
if (val) {
|
22
|
+
val = this.decode(val[1]).split("&");
|
23
|
+
return val[0];
|
24
|
+
}
|
25
|
+
return null;
|
26
|
+
},
|
27
|
+
ignore: function() {
|
28
|
+
var date = new Date();
|
29
|
+
date.setTime(date.getTime()+(2*365*24*60*60*1000));
|
30
|
+
var expires = "; expires="+date.toGMTString();
|
31
|
+
document.cookie = "chartedignore=1"+expires+"; path=/";
|
32
|
+
},
|
33
|
+
clear: function() {
|
34
|
+
document.cookie = 'charted=; expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
35
|
+
},
|
36
|
+
strip: function(str) {
|
37
|
+
return String(str).replace(/^\s+|\s+$/g, '');
|
38
|
+
},
|
39
|
+
decode: function(encoded) {
|
40
|
+
return decodeURIComponent(encoded.replace("+", "%20"));
|
41
|
+
},
|
42
|
+
normalize: function(str) {
|
43
|
+
return str.toLowerCase().replace(' ', '-').replace(/\W/g, '');
|
44
|
+
},
|
45
|
+
events: function() {
|
46
|
+
var str = [];
|
47
|
+
for (var i = 0; i < arguments.length; i++) {
|
48
|
+
str.push(arguments[i]);
|
49
|
+
}
|
50
|
+
this.send(this.RECORD_URL, "events=" + encodeURIComponent(str.join(";")));
|
51
|
+
},
|
52
|
+
goals: function() {
|
53
|
+
var str = [];
|
54
|
+
for (var i = 0; i < arguments.length; i++) {
|
55
|
+
str.push(arguments[i]);
|
56
|
+
}
|
57
|
+
this.send(this.RECORD_URL, "goals=" + encodeURIComponent(str.join(";")));
|
58
|
+
},
|
59
|
+
init: function() {
|
60
|
+
var cookie = this.cookie("charted");
|
61
|
+
var bucketNum = cookie ?
|
62
|
+
parseInt(cookie.split("-")[1]) :
|
63
|
+
Math.floor(Math.random() * 10);
|
64
|
+
var convQuery = "";
|
65
|
+
var conversions = document.body.getAttribute("data-conversions");
|
66
|
+
if (conversions) {
|
67
|
+
convQuery = "&conversions=" + encodeURIComponent(conversions);
|
68
|
+
}
|
69
|
+
var expQuery = "";
|
70
|
+
var experiments = document.body.getAttribute("data-experiments");
|
71
|
+
if (experiments) {
|
72
|
+
expQuery = "&experiments=";
|
73
|
+
experiments = experiments.split(";");
|
74
|
+
for (var i = 0; i < experiments.length; i++) {
|
75
|
+
var experiment = experiments[i].split(":");
|
76
|
+
var label = this.strip(experiment[0]);
|
77
|
+
var buckets = experiment[1].split(",");
|
78
|
+
var bucket = buckets[bucketNum % buckets.length];
|
79
|
+
bucket = this.strip(bucket);
|
80
|
+
|
81
|
+
document.body.className +=
|
82
|
+
(this.normalize(label) + "-" + this.normalize(bucket) + " ");
|
83
|
+
expQuery += encodeURIComponent(label + ":" + bucket + ";");
|
84
|
+
}
|
85
|
+
}
|
86
|
+
Charted.send(
|
87
|
+
Charted.URL,
|
88
|
+
"path=" + encodeURIComponent(window.location.pathname) +
|
89
|
+
"&title=" + encodeURIComponent(document.title) +
|
90
|
+
"&referrer=" + encodeURIComponent(document.referrer) +
|
91
|
+
"&resolution=" + encodeURIComponent(screen.width+"x"+screen.height) +
|
92
|
+
"&bucket=" + bucketNum +
|
93
|
+
convQuery + expQuery);
|
94
|
+
}
|
95
|
+
};
|
96
|
+
Charted.init();
|
data/test/app_test.rb
ADDED
@@ -0,0 +1,160 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class AppTest < ChartedTest
|
4
|
+
include Rack::Test::Methods
|
5
|
+
|
6
|
+
def setup
|
7
|
+
super
|
8
|
+
clear_cookies
|
9
|
+
@site = Charted::Site.create(:domain => 'example.org')
|
10
|
+
@params = {
|
11
|
+
:bucket => 1,
|
12
|
+
:path => '/',
|
13
|
+
:title => 'Prime',
|
14
|
+
:referrer => 'http://localhost/?k=v',
|
15
|
+
:resolution => '1280x800'
|
16
|
+
}
|
17
|
+
@env = {
|
18
|
+
'HTTP_USER_AGENT' =>
|
19
|
+
'Mozilla/5.0 (X11; Linux i686; rv:14.0) Gecko/20100101 Firefox/14.0.1',
|
20
|
+
'REMOTE_ADDR' => '67.188.42.140'
|
21
|
+
}
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_environment
|
25
|
+
assert_equal(:test, Charted::App.environment)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_bad_domain
|
29
|
+
get '/charted', @params, 'HTTP_HOST' => 'localhost'
|
30
|
+
assert_equal(404, last_response.status)
|
31
|
+
assert_equal(0, Charted::Visitor.count)
|
32
|
+
assert_equal(0, Charted::Visit.count)
|
33
|
+
end
|
34
|
+
|
35
|
+
def test_error
|
36
|
+
raises_error = lambda { |*args| raise('Stubbed Error') }
|
37
|
+
Charted::Site.stub(:first, raises_error) do
|
38
|
+
assert_raises(RuntimeError) do
|
39
|
+
get '/charted', @params, @env
|
40
|
+
end
|
41
|
+
end
|
42
|
+
mail = Pony.last_mail
|
43
|
+
assert_equal('dev@localhost', mail[:to])
|
44
|
+
assert_equal('[Charted Error] Stubbed Error', mail[:subject])
|
45
|
+
assert(mail[:body])
|
46
|
+
end
|
47
|
+
|
48
|
+
def test_new_visitor
|
49
|
+
get '/charted', @params, @env
|
50
|
+
assert(last_response.ok?)
|
51
|
+
assert_equal(1, Charted::Visitor.count)
|
52
|
+
assert_equal(1, Charted::Visit.count)
|
53
|
+
|
54
|
+
visitor = Charted::Visitor.first
|
55
|
+
visit = Charted::Visit.first
|
56
|
+
assert_equal(@site, visitor.site)
|
57
|
+
assert_equal(@site, visit.site)
|
58
|
+
assert_equal('Prime', visit.title)
|
59
|
+
assert_equal('/', visit.path)
|
60
|
+
assert_equal('http://localhost/?k=v', visit.referrer)
|
61
|
+
assert_equal('1280x800', visitor.resolution)
|
62
|
+
assert_equal('United States', visitor.country)
|
63
|
+
assert_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
|
64
|
+
end
|
65
|
+
|
66
|
+
def test_old_visitor
|
67
|
+
visitor = Charted::Visitor.create(:site => @site)
|
68
|
+
visit = Charted::Visit.create(
|
69
|
+
:visitor => visitor, :path => '/', :title => 'Prime')
|
70
|
+
set_cookie("charted=#{visitor.cookie}")
|
71
|
+
|
72
|
+
get '/charted', @params, @env
|
73
|
+
assert(last_response.ok?)
|
74
|
+
assert_equal(1, Charted::Visitor.count)
|
75
|
+
assert_equal(2, Charted::Visit.count)
|
76
|
+
assert_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
|
77
|
+
end
|
78
|
+
|
79
|
+
def test_visitor_bad_cookie
|
80
|
+
visitor = Charted::Visitor.create(:site => @site)
|
81
|
+
visit = Charted::Visit.create(
|
82
|
+
:visitor => visitor, :path => '/', :title => 'Prime')
|
83
|
+
set_cookie("charted=#{visitor.id}-zzzzz")
|
84
|
+
|
85
|
+
get '/charted', @params, @env
|
86
|
+
assert(last_response.ok?)
|
87
|
+
assert_equal(2, Charted::Visitor.count)
|
88
|
+
assert_equal(2, Charted::Visit.count)
|
89
|
+
refute_equal(visitor.cookie, rack_mock_session.cookie_jar['charted'])
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_events # TODO: use correct HTTP methods?
|
93
|
+
get '/charted/record', events: 'Event Label;Event Label 2'
|
94
|
+
assert_equal(404, last_response.status)
|
95
|
+
|
96
|
+
visitor = @site.add_visitor({})
|
97
|
+
set_cookie("charted=#{visitor.cookie}")
|
98
|
+
get '/charted/record', events: 'Event Label;Event Label 2'
|
99
|
+
assert(last_response.ok?)
|
100
|
+
assert_equal(2, Charted::Event.count)
|
101
|
+
|
102
|
+
event = Charted::Event.first(label: 'Event Label')
|
103
|
+
assert_equal(@site, event.site)
|
104
|
+
assert_equal(visitor, event.visitor)
|
105
|
+
assert_equal('Event Label', event.label)
|
106
|
+
|
107
|
+
event2 = Charted::Event.first(label: 'Event Label 2')
|
108
|
+
assert(event2)
|
109
|
+
assert_equal('Event Label 2', event2.label)
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_conversions
|
113
|
+
visitor = @site.add_visitor({})
|
114
|
+
set_cookie("charted=#{visitor.cookie}")
|
115
|
+
get '/charted', @params.merge(conversions: 'Logo Clicked;Button Clicked'), @env
|
116
|
+
assert(last_response.ok?)
|
117
|
+
assert_equal(2, Charted::Conversion.count)
|
118
|
+
|
119
|
+
logo = visitor.conversions_dataset.first(label: 'Logo Clicked')
|
120
|
+
button = visitor.conversions_dataset.first(label: 'Button Clicked')
|
121
|
+
refute(logo.ended?)
|
122
|
+
refute(button.ended?)
|
123
|
+
|
124
|
+
get '/charted/record', goals: 'Logo Clicked;Button Clicked'
|
125
|
+
assert(last_response.ok?)
|
126
|
+
logo.reload
|
127
|
+
button.reload
|
128
|
+
assert(logo.ended?)
|
129
|
+
assert(button.ended?)
|
130
|
+
end
|
131
|
+
|
132
|
+
def test_experiments
|
133
|
+
visitor = @site.add_visitor({})
|
134
|
+
set_cookie("charted=#{visitor.cookie}")
|
135
|
+
get '/charted', @params.merge(experiments: 'Logo:A;Button:B'), @env
|
136
|
+
assert(last_response.ok?)
|
137
|
+
assert_equal(2, Charted::Experiment.count)
|
138
|
+
|
139
|
+
logo = visitor.experiments_dataset.first(label: 'Logo')
|
140
|
+
button = visitor.experiments_dataset.first(label: 'Button')
|
141
|
+
assert_equal('Logo', logo.label)
|
142
|
+
assert_equal('A', logo.bucket)
|
143
|
+
refute(logo.ended?)
|
144
|
+
assert_equal('Button', button.label)
|
145
|
+
assert_equal('B', button.bucket)
|
146
|
+
refute(button.ended?)
|
147
|
+
|
148
|
+
get '/charted/record', goals: 'Logo;Button'
|
149
|
+
assert(last_response.ok?)
|
150
|
+
logo.reload
|
151
|
+
button.reload
|
152
|
+
assert(logo.ended?)
|
153
|
+
assert(button.ended?)
|
154
|
+
end
|
155
|
+
|
156
|
+
private
|
157
|
+
def app
|
158
|
+
@app ||= Rack::Server.new.app
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class CommandTest < ChartedTest
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
@cmd = Charted::Command.new
|
7
|
+
@cmd.config_loaded = true
|
8
|
+
Charted::Site.create(:domain => 'localhost')
|
9
|
+
Charted::Site.create(:domain => 'example.org')
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_site
|
13
|
+
assert_raises(Charted::ExitError) { @cmd.site = 'nomatch' }
|
14
|
+
assert_equal(['No sites matching "nomatch"'], @cmd.output)
|
15
|
+
assert_nil(@cmd.site)
|
16
|
+
|
17
|
+
@cmd.output = nil
|
18
|
+
assert_raises(Charted::ExitError) { @cmd.site = 'l' }
|
19
|
+
assert_equal(['"l" ambiguous: localhost, example.org'], @cmd.output)
|
20
|
+
|
21
|
+
@cmd.site = 'local'
|
22
|
+
assert_equal('localhost', @cmd.site.domain)
|
23
|
+
|
24
|
+
@cmd.site = 'ample'
|
25
|
+
assert_equal('example.org', @cmd.site.domain)
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_clean
|
29
|
+
site = Charted::Site.first(domain: 'localhost')
|
30
|
+
visitor = site.add_visitor({})
|
31
|
+
visitor.add_event(label: 'Label')
|
32
|
+
visitor.add_conversion(label: 'Label')
|
33
|
+
visitor.add_experiment(label: 'Label', bucket: 'A')
|
34
|
+
@cmd.output = nil
|
35
|
+
@cmd.clean
|
36
|
+
visitor.reload
|
37
|
+
assert_equal(1, visitor.events.size)
|
38
|
+
assert_equal(1, visitor.conversions.size)
|
39
|
+
assert_equal(1, visitor.experiments.size)
|
40
|
+
|
41
|
+
@cmd.output = nil
|
42
|
+
@cmd.clean('Label')
|
43
|
+
visitor.reload
|
44
|
+
assert_equal(0, visitor.events.size)
|
45
|
+
assert_equal(0, visitor.conversions.size)
|
46
|
+
assert_equal(0, visitor.experiments.size)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_dashboard
|
50
|
+
assert_raises(Charted::ExitError) { @cmd.dashboard }
|
51
|
+
assert_equal(['Please specify website with --site'], @cmd.output)
|
52
|
+
|
53
|
+
@cmd.output = nil
|
54
|
+
@cmd.site = 'localhost'
|
55
|
+
@cmd.dashboard
|
56
|
+
end
|
57
|
+
|
58
|
+
def test_js
|
59
|
+
@cmd.output = nil
|
60
|
+
@cmd.js
|
61
|
+
assert_match("var Charted", @cmd.output[0])
|
62
|
+
end
|
63
|
+
|
64
|
+
def test_format
|
65
|
+
assert_equal('-10,200', @cmd.send(:format, -10200))
|
66
|
+
assert_equal('-1', @cmd.send(:format, -1))
|
67
|
+
assert_equal('1', @cmd.send(:format, 1))
|
68
|
+
assert_equal('1,200,300', @cmd.send(:format, 1200300))
|
69
|
+
end
|
70
|
+
end
|
data/test/config_test.rb
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
class ConfigTest < ChartedTest
|
4
|
+
def test_config
|
5
|
+
assert_equal(365, Charted.config.delete_after)
|
6
|
+
assert_equal('dev@localhost', Charted.config.error_email)
|
7
|
+
assert_equal(['localhost'], Charted.config.sites)
|
8
|
+
assert_equal('sqlite::memory', Charted.config.db_options)
|
9
|
+
assert_equal({via: :sendmail}, Charted.config.email_options)
|
10
|
+
end
|
11
|
+
end
|
data/test/fixtures.rb
CHANGED
data/test/helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
ENV['RACK_ENV'] = 'test'
|
2
|
+
|
3
|
+
require_relative '../lib/charted'
|
4
|
+
require 'minitest/autorun'
|
5
|
+
require 'rack'
|
6
|
+
require 'rack/test'
|
7
|
+
require 'rack/server'
|
8
|
+
require 'fileutils'
|
9
|
+
|
10
|
+
Charted.configure do |c|
|
11
|
+
c.delete_after 365
|
12
|
+
c.error_email 'dev@localhost'
|
13
|
+
c.sites ['localhost']
|
14
|
+
c.db_options 'sqlite::memory'
|
15
|
+
c.email_options(via: :sendmail)
|
16
|
+
end
|
17
|
+
Charted::Migrate.run
|
18
|
+
|
19
|
+
module Pony
|
20
|
+
def self.mail(fields)
|
21
|
+
Charted::Visit.select_all.delete
|
22
|
+
Charted::Event.select_all.delete
|
23
|
+
Charted::Conversion.select_all.delete
|
24
|
+
Charted::Experiment.select_all.delete
|
25
|
+
Charted::Visitor.select_all.delete
|
26
|
+
Charted::Site.select_all.delete
|
27
|
+
@last_mail = fields
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.last_mail
|
31
|
+
@last_mail
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
class ChartedTest < Minitest::Test
|
36
|
+
def setup
|
37
|
+
Pony.mail(nil)
|
38
|
+
end
|
39
|
+
|
40
|
+
def teardown
|
41
|
+
FileUtils.rm_rf(File.expand_path('../temp', File.dirname(__FILE__)))
|
42
|
+
end
|
43
|
+
end
|