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.
@@ -0,0 +1,3 @@
1
+ module Charted
2
+ VERSION = '0.1.0'
3
+ end
@@ -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();
@@ -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
@@ -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
@@ -1,4 +1,4 @@
1
- require File.expand_path('../lib/charted', File.dirname(__FILE__))
1
+ require_relative '../lib/charted'
2
2
  require 'date'
3
3
 
4
4
  module Charted
@@ -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