loganb-mixpanel 0.7.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ (The MIT License)
2
+
3
+ Copyright (c) 2010 Alvaro Gil, cuboxsa.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,66 @@
1
+ == What is Mixpanel (the service) ?
2
+
3
+ Mixpanel is a real-time analytics service that helps companies understand how users interact with web applications.
4
+ http://mixpanel.com
5
+
6
+ == What does this Gem do?
7
+
8
+ * Track events with properties directly from your backend.
9
+ * Track events with properties through javascript using a rack middleware.
10
+
11
+
12
+ == How to install?
13
+
14
+ gem install mixpanel
15
+
16
+
17
+ == How to use it with a Rails application?
18
+
19
+ In your environment config file add this.
20
+
21
+ Rails::Initializer.run do |config|
22
+
23
+ config.middleware.use "MixpanelMiddleware", "YOUR_MIXPANEL_API_TOKEN"
24
+
25
+ If you want to use the asynchronous version of Mixpanel's javascript API
26
+
27
+ Rails::Initializer.run do |config|
28
+
29
+ config.middleware.use "MixpanelMiddleware", "YOUR_MIXPANEL_API_TOKEN", :async => true
30
+
31
+ In your application_controller class add a method to instance mixpanel.
32
+
33
+ before_filter :initialize_mixpanel
34
+
35
+ def initialize_mixpanel
36
+ @mixpanel = Mixpanel.new("YOUR_MIXPANEL_API_TOKEN", request.env, true)
37
+ end
38
+
39
+ Then in each request you want to track some event you can use:
40
+
41
+ To track events directly from your backend...
42
+
43
+ @mixpanel.track_event("Sign in", {:some => "property"})
44
+
45
+ To track events after response with javascript...
46
+
47
+ @mixpanel.append_event("Sign in", {:some => "property"})
48
+
49
+ To execute any javascript API call
50
+
51
+ @mixpanel.append_api("register", {:some => "property"})
52
+ @mixpanel.append_api("identify", "Unique Identifier")
53
+
54
+
55
+ == Notes
56
+
57
+ There are two forms of async operation:
58
+ * Using MixpanelMiddleware, events are queued via Mixpanel#append_event and inserted into a JavaScript block within the HTML response.
59
+ * Using Mixpanel.new(…, …, true), events are sent to a subprocess via a pipe and the sub process which asynchronously send events to Mixpanel. This process uses a single thread to upload events, and may start dropping events if your application generates them at a very high rate.
60
+
61
+ == Collaborators and Maintainers
62
+
63
+ * {Alvaro Gil}[https://github.com/zevarito] (Author)
64
+ * {Nathan Baxter}[https://github.com/LogicWolfe]
65
+ * {Jake Mallory}[https://github.com/tinomen]
66
+ * {Logan Bowers}[https://github.com/loganb]
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'spec/rake/spectask'
2
+
3
+ task :default => :spec
4
+
5
+ desc "Run all examples"
6
+ Spec::Rake::SpecTask.new('spec') do |t|
7
+ t.spec_opts = ["-u -c -fs"]
8
+ t.spec_files = FileList['spec/**/*_spec.rb']
9
+ end
data/lib/mixpanel.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'mixpanel/mixpanel'
2
+ require 'mixpanel/mixpanel_middleware'
@@ -0,0 +1,95 @@
1
+ require "open-uri"
2
+ require 'base64'
3
+ require 'json'
4
+
5
+ require 'thread'
6
+
7
+ class Mixpanel
8
+ def initialize(token, env, async = false)
9
+ @token = token
10
+ @env = env
11
+ @async = async
12
+ clear_queue
13
+ end
14
+
15
+ def append_event(event, properties = {})
16
+ append_api('track', event, properties)
17
+ end
18
+
19
+ def append_api(type, *args)
20
+ queue << [type, args.map {|arg| arg.to_json}]
21
+ end
22
+
23
+ def track_event(event, properties = {})
24
+ params = build_event(event, properties.merge(:token => @token, :time => Time.now.utc.to_i, :ip => ip))
25
+ parse_response request(params)
26
+ end
27
+
28
+ def ip
29
+ @env.has_key?("REMOTE_ADDR") ? @env["REMOTE_ADDR"] : ""
30
+ end
31
+
32
+ def queue
33
+ @env["mixpanel_events"]
34
+ end
35
+
36
+ def clear_queue
37
+ @env["mixpanel_events"] = []
38
+ end
39
+
40
+ class <<self
41
+ WORKER_MUTEX = Mutex.new
42
+
43
+ def worker
44
+ WORKER_MUTEX.synchronize do
45
+ @worker || (@worker = IO.popen(self.cmd, 'w'))
46
+ end
47
+ end
48
+
49
+ def dispose_worker(w)
50
+ WORKER_MUTEX.synchronize do
51
+ if(@worker == w)
52
+ @worker = nil
53
+ w.close
54
+ end
55
+ end
56
+ end
57
+
58
+ def cmd
59
+ @cmd || begin
60
+ require 'escape'
61
+ require 'rbconfig'
62
+ interpreter = File.join(RbConfig::CONFIG['bindir'], RbConfig::CONFIG['RUBY_SO_NAME'])
63
+ subprocess = File.join(File.dirname(__FILE__), 'mixpanel_subprocess.rb')
64
+ @cmd = Escape.shell_command([interpreter, subprocess])
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def parse_response(response)
72
+ response == "1" ? true : false
73
+ end
74
+
75
+ def request(params)
76
+ data = Base64.encode64(JSON.generate(params)).gsub(/\n/,'')
77
+ url = "http://api.mixpanel.com/track/?data=#{data}"
78
+
79
+ if(@async)
80
+ w = Mixpanel.worker
81
+ begin
82
+ url << "\n"
83
+ w.write(url)
84
+ rescue Errno::EPIPE => e
85
+ Mixpanel.dispose_worker(w)
86
+ end
87
+ else
88
+ open(url).read
89
+ end
90
+ end
91
+
92
+ def build_event(event, properties)
93
+ {:event => event, :properties => properties}
94
+ end
95
+ end
@@ -0,0 +1,119 @@
1
+ require 'rack'
2
+
3
+ class MixpanelMiddleware
4
+ def initialize(app, mixpanel_token, options={})
5
+ @app = app
6
+ @token = mixpanel_token
7
+ @options = {
8
+ :async => false
9
+ }.merge(options)
10
+ end
11
+
12
+ def call(env)
13
+ @env = env
14
+
15
+ @status, @headers, @response = @app.call(env)
16
+
17
+ update_response!
18
+ update_content_length!
19
+ delete_event_queue!
20
+
21
+ [@status, @headers, @response]
22
+ end
23
+
24
+ private
25
+
26
+ def update_response!
27
+ @response.each do |part|
28
+ if is_regular_request? && is_html_response?
29
+ insert_at = part.index('</head')
30
+ unless insert_at.nil?
31
+ part.insert(insert_at, render_event_tracking_scripts) unless queue.empty?
32
+ part.insert(insert_at, render_mixpanel_scripts) #This will insert the mixpanel initialization code before the queue of tracking events.
33
+ end
34
+ elsif is_ajax_request? && is_html_response?
35
+ part.insert(0, render_event_tracking_scripts) unless queue.empty?
36
+ elsif is_ajax_request? && is_javascript_response?
37
+ part.insert(0, render_event_tracking_scripts(false)) unless queue.empty?
38
+ end
39
+ end
40
+ end
41
+
42
+ def update_content_length!
43
+ new_size = 0
44
+ @response.each{|part| new_size += part.bytesize}
45
+ @headers.merge!("Content-Length" => new_size.to_s)
46
+ end
47
+
48
+ def is_regular_request?
49
+ !is_ajax_request?
50
+ end
51
+
52
+ def is_ajax_request?
53
+ @env.has_key?("HTTP_X_REQUESTED_WITH") && @env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
54
+ end
55
+
56
+ def is_html_response?
57
+ @headers["Content-Type"].include?("text/html") if @headers.has_key?("Content-Type")
58
+ end
59
+
60
+ def is_javascript_response?
61
+ @headers["Content-Type"].include?("text/javascript") if @headers.has_key?("Content-Type")
62
+ end
63
+
64
+ def render_mixpanel_scripts
65
+ if @options[:async]
66
+ <<-EOT
67
+ <script type='text/javascript'>
68
+ var mpq = [];
69
+ mpq.push(["init", "#{@token}"]);
70
+ (function() {
71
+ var mp = document.createElement("script"); mp.type = "text/javascript"; mp.async = true;
72
+ mp.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + "//api.mixpanel.com/site_media/js/api/mixpanel.js";
73
+ var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(mp, s);
74
+ })();
75
+ </script>
76
+ EOT
77
+ else
78
+ <<-EOT
79
+ <script type='text/javascript'>
80
+ var mp_protocol = (('https:' == document.location.protocol) ? 'https://' : 'http://');
81
+ document.write(unescape('%3Cscript src="' + mp_protocol + 'api.mixpanel.com/site_media/js/api/mixpanel.js" type="text/javascript"%3E%3C/script%3E'));
82
+ </script>
83
+ <script type='text/javascript'>
84
+ try {
85
+ var mpmetrics = new MixpanelLib('#{@token}');
86
+ } catch(err) {
87
+ null_fn = function () {};
88
+ var mpmetrics = {
89
+ track: null_fn, track_funnel: null_fn, register: null_fn, register_once: null_fn, register_funnel: null_fn
90
+ };
91
+ }
92
+ </script>
93
+ EOT
94
+ end
95
+ end
96
+
97
+ def delete_event_queue!
98
+ @env.delete('mixpanel_events')
99
+ end
100
+
101
+ def queue
102
+ return [] if !@env.has_key?('mixpanel_events') || @env['mixpanel_events'].empty?
103
+ @env['mixpanel_events']
104
+ end
105
+
106
+ def render_event_tracking_scripts(include_script_tag=true)
107
+ return "" if queue.empty?
108
+
109
+ if @options[:async]
110
+ output = queue.map {|type, arguments| %(mpq.push(["#{type}", #{arguments.join(', ')}]);) }.join("\n")
111
+ else
112
+ output = queue.map {|type, arguments| %(mpmetrics.#{type}(#{arguments.join(', ')});) }.join("\n")
113
+ end
114
+
115
+ output = "try {#{output}} catch(err) {}"
116
+
117
+ include_script_tag ? "<script type='text/javascript'>#{output}</script>" : output
118
+ end
119
+ end
@@ -0,0 +1,30 @@
1
+
2
+ require 'rubygems'
3
+ require 'mixpanel'
4
+ require 'open-uri'
5
+
6
+ require 'thread'
7
+
8
+ class Mixpanel::Subprocess
9
+ Q = Queue.new
10
+ ENDMARKER = Object.new
11
+
12
+ Thread.abort_on_exception = true
13
+ producer = Thread.new do
14
+ STDIN.each_line() do |url|
15
+ STDERR.puts("Dropped: #{url}") && next if Q.length > 10000
16
+ Q << url
17
+ end
18
+ Q << ENDMARKER
19
+ end
20
+
21
+ loop do
22
+ url = Q.pop
23
+ break if(url == ENDMARKER)
24
+ url.chomp!
25
+ next if(url.empty?) #for testing
26
+
27
+ open(url).read
28
+ end
29
+ producer.join
30
+ end
data/mixpanel.gemspec ADDED
@@ -0,0 +1,22 @@
1
+ files = ['README.rdoc', 'LICENSE', 'Rakefile', 'mixpanel.gemspec', '{spec,lib}/**/*'].map {|f| Dir[f]}.flatten
2
+
3
+ spec = Gem::Specification.new do |s|
4
+ s.name = "loganb-mixpanel"
5
+ s.version = "0.7.1"
6
+ s.description = "Simple lib to track events in Mixpanel service. It can be used in any rack based framework."
7
+ s.author = "Alvaro Gil"
8
+ s.email = "logan@datacurrent.com"
9
+ s.homepage = "http://github.com/loganb/mixpanel"
10
+ s.platform = Gem::Platform::RUBY
11
+ s.summary = "Supports direct request api and javascript requests through a middleware."
12
+ s.files = files
13
+ s.require_path = "lib"
14
+ s.has_rdoc = false
15
+ s.extra_rdoc_files = ["README.rdoc"]
16
+ s.add_dependency 'json'
17
+ s.add_dependency 'rack'
18
+ s.add_development_dependency 'rspec'
19
+ s.add_development_dependency 'rack-test'
20
+ s.add_development_dependency 'fakeweb'
21
+ s.add_development_dependency 'nokogiri'
22
+ end
@@ -0,0 +1,282 @@
1
+ require 'spec_helper'
2
+
3
+ describe MixpanelMiddleware do
4
+ include Rack::Test::Methods
5
+
6
+ describe "Dummy apps, no text/html" do
7
+ before do
8
+ setup_rack_application(DummyApp, :body => html_document, :headers => {})
9
+ get "/"
10
+ end
11
+
12
+ it "should pass through if the document is not text/html content type" do
13
+ last_response.body.should == html_document
14
+ end
15
+ end
16
+
17
+ describe "Appending async mixpanel scripts" do
18
+ describe "With ajax requests" do
19
+ before do
20
+ setup_rack_application(DummyApp, {:body => html_document, :headers => {"Content-Type" => "text/html"}}, {:async => true})
21
+ get "/", {}, {"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
22
+ end
23
+
24
+ it "should not append mixpanel scripts to head element" do
25
+ Nokogiri::HTML(last_response.body).search('script').should be_empty
26
+ end
27
+
28
+ it "should not update Content-Length in headers" do
29
+ last_response.headers["Content-Length"].should == html_document.length.to_s
30
+ end
31
+ end
32
+
33
+ describe "With large ajax response" do
34
+ before do
35
+ setup_rack_application(DummyApp, {:body => large_script, :headers => {"Content-Type" => "text/html"}}, {:async => true})
36
+ get "/", {}, {"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
37
+ end
38
+
39
+ it "should not append mixpanel scripts to head element" do
40
+ last_response.body.index('var mp_protocol').should be_nil
41
+ end
42
+
43
+ it "should pass through if the document is not text/html content type" do
44
+ last_response.body.should == large_script
45
+ end
46
+ end
47
+
48
+ describe "With regular requests" do
49
+ before do
50
+ setup_rack_application(DummyApp, {:body => html_document, :headers => {"Content-Type" => "text/html"}}, {:async => true})
51
+ get "/"
52
+ end
53
+
54
+ it "should append mixpanel scripts to head element" do
55
+ Nokogiri::HTML(last_response.body).search('head script').should_not be_empty
56
+ Nokogiri::HTML(last_response.body).search('body script').should be_empty
57
+ end
58
+
59
+ it "should have 1 included script" do
60
+ Nokogiri::HTML(last_response.body).search('script').size.should == 1
61
+ end
62
+
63
+ it "should use the specified token instantiating mixpanel lib" do
64
+ last_response.should =~ /mpq.push\(\["init", "#{MIX_PANEL_TOKEN}"\]\)/
65
+ end
66
+
67
+ it "should define Content-Length if not exist" do
68
+ last_response.headers.has_key?("Content-Length").should == true
69
+ end
70
+
71
+ it "should update Content-Length in headers" do
72
+ last_response.headers["Content-Length"].should_not == html_document.length.to_s
73
+ end
74
+ end
75
+ end
76
+
77
+ describe "Appending mixpanel scripts" do
78
+ describe "With ajax requests" do
79
+ before do
80
+ setup_rack_application(DummyApp, :body => html_document, :headers => {"Content-Type" => "text/html"})
81
+ get "/", {}, {"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
82
+ end
83
+
84
+ it "should not append mixpanel scripts to head element" do
85
+ Nokogiri::HTML(last_response.body).search('script').should be_empty
86
+ end
87
+
88
+ it "should not update Content-Length in headers" do
89
+ last_response.headers["Content-Length"].should == html_document.length.to_s
90
+ end
91
+ end
92
+
93
+ describe "With large ajax response" do
94
+ before do
95
+ setup_rack_application(DummyApp, :body => large_script, :headers => {"Content-Type" => "text/html"})
96
+ get "/", {}, {"HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
97
+ end
98
+
99
+ it "should not append mixpanel scripts to head element" do
100
+ last_response.body.index('var mp_protocol').should be_nil
101
+ end
102
+
103
+ it "should pass through if the document is not text/html content type" do
104
+ last_response.body.should == large_script
105
+ end
106
+ end
107
+
108
+ describe "With regular requests" do
109
+ before do
110
+ setup_rack_application(DummyApp, :body => html_document, :headers => {"Content-Type" => "text/html"})
111
+ get "/"
112
+ end
113
+
114
+ it "should append mixpanel scripts to head element" do
115
+ Nokogiri::HTML(last_response.body).search('head script').should_not be_empty
116
+ Nokogiri::HTML(last_response.body).search('body script').should be_empty
117
+ end
118
+
119
+ it "should have 2 included scripts" do
120
+ Nokogiri::HTML(last_response.body).search('script').size.should == 2
121
+ end
122
+
123
+ it "should use the specified token instantiating mixpanel lib" do
124
+ last_response.should =~ /new MixpanelLib\('#{MIX_PANEL_TOKEN}'\)/
125
+ end
126
+
127
+ it "should define Content-Length if not exist" do
128
+ last_response.headers.has_key?("Content-Length").should == true
129
+ end
130
+
131
+ it "should update Content-Length in headers" do
132
+ last_response.headers["Content-Length"].should_not == html_document.length.to_s
133
+ end
134
+ end
135
+ end
136
+
137
+ describe "Tracking async appended events" do
138
+ before do
139
+ @mixpanel = Mixpanel.new(MIX_PANEL_TOKEN, {})
140
+ @mixpanel.append_event("Visit", {:article => 1})
141
+ @mixpanel.append_event("Sign in")
142
+ end
143
+
144
+ describe "With ajax requests and text/html response" do
145
+ before do
146
+ setup_rack_application(DummyApp, {:body => "<p>response</p>", :headers => {"Content-Type" => "text/html"}}, {:async => true})
147
+
148
+ get "/", {}, {"mixpanel_events" => @mixpanel.queue, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
149
+ end
150
+
151
+ it "should render only one script tag" do
152
+ Nokogiri::HTML(last_response.body).search('script').size.should == 1
153
+ end
154
+
155
+ it "should be tracking the correct events inside a script tag" do
156
+ script = Nokogiri::HTML(last_response.body).search('script')
157
+ script.inner_html.should =~ /try\s?\{(.*)\}\s?catch/m
158
+ script.inner_html.should =~ /mpq\.push\(\["track",\s?"Visit",\s?\{"article":1\}\]\)/
159
+ script.inner_html.should =~ /mpq\.push\(\["track",\s?"Sign in",\s?\{\}\]\)/
160
+ end
161
+
162
+ it "should delete events queue after use it" do
163
+ last_request.env.has_key?("mixpanel_events").should == false
164
+ end
165
+ end
166
+
167
+ describe "With ajax requests and text/javascript response" do
168
+ before do
169
+ setup_rack_application(DummyApp, {:body => "alert('response')", :headers => {"Content-Type" => "text/javascript"}}, {:async => true})
170
+ get "/", {}, {"mixpanel_events" => @mixpanel.queue, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
171
+ end
172
+
173
+ it "should not render a script tag" do
174
+ Nokogiri::HTML(last_response.body).search('script').size.should == 0
175
+ end
176
+
177
+ it "should be tracking the correct events inside a try/catch" do
178
+ script = last_response.body.match(/try\s?\{(.*)\}\s?catch/m)[1]
179
+ script.should =~ /mpq\.push\(\["track",\s?"Visit",\s?\{"article":1\}\]\)/
180
+ script.should =~ /mpq\.push\(\["track",\s?"Sign in",\s?\{\}\]\)/
181
+ end
182
+
183
+ it "should delete events queue after use it" do
184
+ last_request.env.has_key?("mixpanel_events").should == false
185
+ end
186
+ end
187
+
188
+ describe "With regular requests" do
189
+ before do
190
+ setup_rack_application(DummyApp, {:body => html_document, :headers => {"Content-Type" => "text/html"}}, {:async => true})
191
+
192
+ get "/", {}, {"mixpanel_events" => @mixpanel.queue}
193
+ end
194
+
195
+ it "should render 2 script tags" do
196
+ Nokogiri::HTML(last_response.body).search('script').size.should == 2
197
+ end
198
+
199
+ it "should be tracking the correct events" do
200
+ last_response.body.should =~ /mpq\.push\(\["track",\s?"Visit",\s?\{"article":1\}\]\)/
201
+ last_response.body.should =~ /mpq\.push\(\["track",\s?"Sign in",\s?\{\}\]\)/
202
+ end
203
+
204
+ it "should delete events queue after use it" do
205
+ last_request.env.has_key?("mixpanel_events").should == false
206
+ end
207
+ end
208
+ end
209
+
210
+ describe "Tracking appended events" do
211
+ before do
212
+ @mixpanel = Mixpanel.new(MIX_PANEL_TOKEN, {})
213
+ @mixpanel.append_event("Visit", {:article => 1})
214
+ @mixpanel.append_event("Sign in")
215
+ end
216
+
217
+ describe "With ajax requests and text/html response" do
218
+ before do
219
+ setup_rack_application(DummyApp, :body => "<p>response</p>", :headers => {"Content-Type" => "text/html"})
220
+
221
+ get "/", {}, {"mixpanel_events" => @mixpanel.queue, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
222
+ end
223
+
224
+ it "should render only one script tag" do
225
+ Nokogiri::HTML(last_response.body).search('script').size.should == 1
226
+ end
227
+
228
+ it "should be tracking the correct events inside a script tag" do
229
+ script = Nokogiri::HTML(last_response.body).search('script')
230
+ script.inner_html.should =~ /try\s?\{(.*)\}\s?catch/m
231
+ script.inner_html.should =~ /mpmetrics\.track\("Visit",\s?\{"article":1\}\)/
232
+ script.inner_html.should =~ /mpmetrics\.track\("Sign in",\s?\{\}\)/
233
+ end
234
+
235
+ it "should delete events queue after use it" do
236
+ last_request.env.has_key?("mixpanel_events").should == false
237
+ end
238
+ end
239
+
240
+ describe "With ajax requests and text/javascript response" do
241
+ before do
242
+ setup_rack_application(DummyApp, :body => "alert('response')", :headers => {"Content-Type" => "text/javascript"})
243
+ get "/", {}, {"mixpanel_events" => @mixpanel.queue, "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest"}
244
+ end
245
+
246
+ it "should not render a script tag" do
247
+ Nokogiri::HTML(last_response.body).search('script').size.should == 0
248
+ end
249
+
250
+ it "should be tracking the correct events inside a try/catch" do
251
+ script = last_response.body.match(/try\s?\{(.*)\}\s?catch/m)[1]
252
+ script.should =~ /mpmetrics\.track\("Visit",\s?\{"article":1\}\)/
253
+ script.should =~ /mpmetrics\.track\("Sign in",\s?\{\}\)/
254
+ end
255
+
256
+ it "should delete events queue after use it" do
257
+ last_request.env.has_key?("mixpanel_events").should == false
258
+ end
259
+ end
260
+
261
+ describe "With regular requests" do
262
+ before do
263
+ setup_rack_application(DummyApp, :body => html_document, :headers => {"Content-Type" => "text/html"})
264
+
265
+ get "/", {}, {"mixpanel_events" => @mixpanel.queue}
266
+ end
267
+
268
+ it "should render 3 script tags" do
269
+ Nokogiri::HTML(last_response.body).search('script').size.should == 3
270
+ end
271
+
272
+ it "should be tracking the correct events" do
273
+ last_response.body.should =~ /mpmetrics\.track\("Visit",\s?\{"article":1\}\)/
274
+ last_response.body.should =~ /mpmetrics\.track\("Sign in",\s?\{\}\)/
275
+ end
276
+
277
+ it "should delete events queue after use it" do
278
+ last_request.env.has_key?("mixpanel_events").should == false
279
+ end
280
+ end
281
+ end
282
+ end
@@ -0,0 +1,113 @@
1
+ require 'spec_helper'
2
+
3
+ describe Mixpanel do
4
+ before(:each) do
5
+ @mixpanel = Mixpanel.new(MIX_PANEL_TOKEN, @env = {"REMOTE_ADDR" => "127.0.0.1"})
6
+ end
7
+
8
+ context "Initializing object" do
9
+ it "should have an instance variable for token and events" do
10
+ @mixpanel.instance_variables.should include("@token", "@env")
11
+ end
12
+ end
13
+
14
+ context "Cleaning appended events" do
15
+ it "should clear the queue" do
16
+ @mixpanel.append_event("Sign up")
17
+ @mixpanel.queue.size.should == 1
18
+ @mixpanel.clear_queue
19
+ @mixpanel.queue.size.should == 0
20
+ end
21
+ end
22
+
23
+ context "Accessing Mixpanel through direct request" do
24
+ context "Tracking events" do
25
+ it "should track simple events" do
26
+ @mixpanel.track_event("Sign up").should == true
27
+ end
28
+
29
+ it "should call request method with token and time value" do
30
+ params = {:event => "Sign up", :properties => {:token => MIX_PANEL_TOKEN, :time => Time.now.utc.to_i, :ip => "127.0.0.1"}}
31
+
32
+ @mixpanel.should_receive(:request).with(params).and_return("1")
33
+ @mixpanel.track_event("Sign up").should == true
34
+ end
35
+ end
36
+ end
37
+
38
+ context "Accessing Mixpanel through javascript API" do
39
+ context "Appending events" do
40
+ it "should store the event under the appropriate key" do
41
+ @mixpanel.append_event("Sign up")
42
+ @env.has_key?("mixpanel_events").should == true
43
+ end
44
+
45
+ it "should be the same the queue than env['mixpanel_events']" do
46
+ @env['mixpanel_events'].object_id.should == @mixpanel.queue.object_id
47
+ end
48
+
49
+ it "should append simple events" do
50
+ @mixpanel.append_event("Sign up")
51
+ mixpanel_queue_should_include(@mixpanel, "track", "Sign up", {})
52
+ end
53
+
54
+ it "should append events with properties" do
55
+ @mixpanel.append_event("Sign up", {:referer => 'http://example.com'})
56
+ mixpanel_queue_should_include(@mixpanel, "track", "Sign up", {:referer => 'http://example.com'})
57
+ end
58
+
59
+ it "should give direct access to queue" do
60
+ @mixpanel.append_event("Sign up", {:referer => 'http://example.com'})
61
+ @mixpanel.queue.size.should == 1
62
+ end
63
+
64
+ it "should provide direct access to the JS api" do
65
+ @mixpanel.append_api('track', "Sign up", {:referer => 'http://example.com'})
66
+ mixpanel_queue_should_include(@mixpanel, "track", "Sign up", {:referer => 'http://example.com'})
67
+ end
68
+
69
+ it "should allow identify to be called through the JS api" do
70
+ @mixpanel.append_api('identify', "some@one.com")
71
+ mixpanel_queue_should_include(@mixpanel, "identify", "some@one.com")
72
+ end
73
+
74
+ it "should allow identify to be called through the JS api" do
75
+ @mixpanel.append_api('identify', "some@one.com")
76
+ mixpanel_queue_should_include(@mixpanel, "identify", "some@one.com")
77
+ end
78
+
79
+ it "should allow the tracking of super properties in JS" do
80
+ @mixpanel.append_api('register', {:user_id => 12345, :email => "some@one.com"})
81
+ mixpanel_queue_should_include(@mixpanel, 'register', {:user_id => 12345, :email => "some@one.com"})
82
+ end
83
+ end
84
+ end
85
+
86
+ context "Accessing Mixpanel asynchronously" do
87
+ it "should open a subprocess successfully" do
88
+ w = Mixpanel.worker
89
+ w.should == Mixpanel.worker
90
+ end
91
+
92
+ it "should be able to write lines to the worker" do
93
+ w = Mixpanel.worker
94
+
95
+ #On most systems this will exceed the pipe buffer size
96
+ 8.times do
97
+ 9000.times do
98
+ w.write("\n")
99
+ end
100
+ sleep 0.1
101
+ end
102
+ end
103
+
104
+ it "should dispose of a worker" do
105
+ w = Mixpanel.worker
106
+ Mixpanel.dispose_worker(w)
107
+
108
+ w.closed?.should == true
109
+ w2 = Mixpanel.worker
110
+ w2.should_not == w
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,20 @@
1
+ require 'ruby-debug'
2
+ require File.join(File.dirname(__FILE__), "../lib", "mixpanel")
3
+ require 'rack/test'
4
+ require 'fakeweb'
5
+ require 'nokogiri'
6
+ Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}
7
+
8
+ MIX_PANEL_TOKEN = "e2d8b0bea559147844ffab3d607d26a6"
9
+
10
+
11
+ def mixpanel_queue_should_include(mixpanel, type, *arguments)
12
+ mixpanel.queue.each do |event_type, event_arguments|
13
+ event_arguments.should == arguments.map{|arg| arg.to_json}
14
+ event_type.should == type
15
+ end
16
+ end
17
+
18
+ # Fakeweb
19
+ FakeWeb.allow_net_connect = false
20
+ FakeWeb.register_uri(:any, /http:\/\/api\.mixpanel\.com.*/, :body => "1")
@@ -0,0 +1,203 @@
1
+ def setup_rack_application(application, options = {}, mixpanel_options = {})
2
+ stub!(:app).and_return(MixpanelMiddleware.new(application.new(options), MIX_PANEL_TOKEN, mixpanel_options))
3
+ end
4
+
5
+ def html_document
6
+ <<-EOT
7
+ <html>
8
+ <head>
9
+ </head>
10
+ <body>
11
+ </body>
12
+ </html>
13
+ EOT
14
+ end
15
+
16
+ class DummyApp
17
+ def initialize(options)
18
+ @response_with = {}
19
+ @response_with[:status] = options[:status] || "200"
20
+ @response_with[:headers] = options[:headers] || {}
21
+ @response_with[:body] = options[:body] || ""
22
+ end
23
+
24
+ def call(env)
25
+ [@response_with[:status], @response_with[:headers], [@response_with[:body]]]
26
+ end
27
+ end
28
+
29
+ def large_script
30
+ <<-EOT
31
+ <script type='text/javascript'>
32
+ //<![CDATA[
33
+ function update_milestone_divs(obj){
34
+ $('#milestones').show();
35
+ $('#milestones').children('div').hide();
36
+
37
+ divid = obj.options[obj.selectedIndex].value;
38
+
39
+ divid = '#milestone_' + divid
40
+ $(divid).show()
41
+ }
42
+
43
+ $(document).ready(function() {
44
+ /*
45
+ * First step is to create title attributes for the rows in the table
46
+ * This isn't needed if the required 'title' attribute is already set in the HTML in the
47
+ * DOM
48
+ */
49
+ $('#milestone_table tbody tr').each( function() {
50
+ var sTitle;
51
+ var nTds = $('td', this);
52
+ var sPic = $(nTds[3]).text();
53
+ var sName = $(nTds[1]).text();
54
+
55
+ sTitle = '<img src='+sPic+' height=60 width=60/><br>'+sName;
56
+ this.setAttribute( 'title', sTitle );
57
+
58
+ } );
59
+
60
+ /* Apply the tooltips */
61
+ $('#milestone_table tbody tr[title]').tooltip( {
62
+ "delay": 0,
63
+ "track": true,
64
+ "fade": 250
65
+ } );
66
+
67
+ /* Init DataTables */
68
+ $('#milestone_table').dataTable({
69
+ "iDisplayLength": 10,
70
+ "aaSorting": [[ 2, "desc" ]],
71
+ });
72
+ });
73
+ //]]>
74
+ </script>
75
+ <style type='text/css'>
76
+ /*<![CDATA[*/
77
+ #milestone_table {
78
+ width:550px;
79
+
80
+ }
81
+
82
+ td { vertical-align:middle;}
83
+ td.select { width: 50px; padding-left:10px;}
84
+ td.title { text-align:left; font-size:120%; }
85
+ td.picture { width: 60px; display:none;}
86
+ td.when { text-align: center; width: 100px; }
87
+ /*]]>*/
88
+ </style>
89
+ <div class='item_box' id='milestone_attachment'>
90
+ <form action="http://big.application.com/inbox_items/28" enctype="multipart/form-data" method="post"><div style="margin:0;padding:0;display:inline"><input name="_method" type="hidden" value="put" /><input name="authenticity_token" type="hidden" value="QF1y2YhiuVJv7qS3u5jShr6mvqjx0NWAD1FPPJTwY/w=" /></div>
91
+ <input id="transform_to" name="transform_to" type="hidden" value="milestone_attachment" />
92
+ <div>
93
+ <div class='addBox roundo' style='float:left; display:block; margin:5px 0; padding:0 5px 5px 5px;'>
94
+ <p>
95
+ Attach to memory for
96
+ <select name='person_id' onchange='update_milestone_divs(this)'>
97
+ <option></option>
98
+ <option value='40'> Bill</option>
99
+ <option disabled='disabled'> Sally (no memories)</option>
100
+ <option value='169'> Tim</option>
101
+ <option disabled='disabled'> Betty (no memories)</option>
102
+ <option value='173'> Ted</option>
103
+ </select>
104
+ </p>
105
+ </div>
106
+ </div>
107
+ <div id='milestones'>
108
+ <div id='milestone_40' style='display:none;'>
109
+ <h4>Memories for Bill</h4>
110
+ <table id='milestone_table'>
111
+ <thead>
112
+ <tr>
113
+ <th>Select</th>
114
+ <th>Title</th>
115
+ <th>When?</th>
116
+ <th style='display:none;'>Picture</th>
117
+ </tr>
118
+ </thead>
119
+ <tr>
120
+ <td class='select'><input id="milestone_40_179" name="milestone_40" type="radio" value="179" /></td>
121
+ <td class='title'>Ran a race</td>
122
+ <td class='when'>10/07/2010</td>
123
+ <td class='picture'>
124
+ /system/photos/first_and_milestones/179/thumb/ed0e4428.jpeg
125
+ </td>
126
+ </tr>
127
+ </table>
128
+ </div>
129
+ <div id='milestone_169' style='display:none;'>
130
+ <h4>Memories for Tim</h4>
131
+ <table id='milestone_table'>
132
+ <thead>
133
+ <tr>
134
+ <th>Select</th>
135
+ <th>Title</th>
136
+ <th>When?</th>
137
+ <th style='display:none;'>Picture</th>
138
+ </tr>
139
+ </thead>
140
+ <tr>
141
+ <td class='select'><input id="milestone_169_204" name="milestone_169" type="radio" value="204" /></td>
142
+ <td class='title'>Kicked ball first time</td>
143
+ <td class='when'>03/12/1978</td>
144
+ <td class='picture'>
145
+ </td>
146
+ </tr>
147
+ </table>
148
+ </div>
149
+ <div id='milestone_173' style='display:none;'>
150
+ <h4>Memories for Ted</h4>
151
+ <table id='milestone_table'>
152
+ <thead>
153
+ <tr>
154
+ <th>Select</th>
155
+ <th>Title</th>
156
+ <th>When?</th>
157
+ <th style='display:none;'>Picture</th>
158
+ </tr>
159
+ </thead>
160
+ <tr>
161
+ <td class='select'><input id="milestone_173_195" name="milestone_173" type="radio" value="195" /></td>
162
+ <td class='title'>Testing the log</td>
163
+ <td class='when'>11/03/2010</td>
164
+ <td class='picture'>
165
+ </td>
166
+ </tr>
167
+ <tr>
168
+ <td class='select'><input id="milestone_173_196" name="milestone_173" type="radio" value="196" /></td>
169
+ <td class='title'>another test</td>
170
+ <td class='when'>11/03/2010</td>
171
+ <td class='picture'>
172
+ </td>
173
+ </tr>
174
+ <tr>
175
+ <td class='select'><input id="milestone_173_197" name="milestone_173" type="radio" value="197" /></td>
176
+ <td class='title'>one more</td>
177
+ <td class='when'>11/01/2010</td>
178
+ <td class='picture'>
179
+ </td>
180
+ </tr>
181
+ <tr>
182
+ <td class='select'><input id="milestone_173_198" name="milestone_173" type="radio" value="198" /></td>
183
+ <td class='title'>great time</td>
184
+ <td class='when'>11/03/2010</td>
185
+ <td class='picture'>
186
+ </td>
187
+ </tr>
188
+ <tr>
189
+ <td class='select'><input id="milestone_173_199" name="milestone_173" type="radio" value="199" /></td>
190
+ <td class='title'>please</td>
191
+ <td class='when'>11/03/2010</td>
192
+ <td class='picture'>
193
+ </td>
194
+ </tr>
195
+ </table>
196
+ </div>
197
+ <div class='clear'></div>
198
+ </div>
199
+ <div class="clear"></div><div id="buttonArea" style="width: 550px; margin-left: -20px"><input class="btn topper botter lbump20 blue " disabled="disabled" name="commit" style="display:none;" type="submit" value=".. saving .." /><input class="btn topper botter lbump20 blue " id="submit_btn" name="commit" onclick="$(this).hide(); $(this).prev().show();" type="submit" value="Save" /><a href="/inbox_items" class="btnSm topper botter lbump20">Cancel</a><a href="/inbox_items/28" class="btnSm red topper botter lbump20" id="delete_btn" onclick="if (confirm('Are you sure you want to delete this?')) { var f = document.createElement('form'); f.style.display = 'none'; this.parentNode.appendChild(f); f.method = 'POST'; f.action = this.href;var m = document.createElement('input'); m.setAttribute('type', 'hidden'); m.setAttribute('name', '_method'); m.setAttribute('value', 'delete'); f.appendChild(m);var s = document.createElement('input'); s.setAttribute('type', 'hidden'); s.setAttribute('name', 'authenticity_token'); s.setAttribute('value', 'QF1y2YhiuVJv7qS3u5jShr6mvqjx0NWAD1FPPJTwY/w='); f.appendChild(s);f.submit(); };return false;">Delete</a></div><div class="clear"></div>
200
+ </form>
201
+ </div>
202
+ EOT
203
+ end
metadata ADDED
@@ -0,0 +1,161 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: loganb-mixpanel
3
+ version: !ruby/object:Gem::Version
4
+ hash: 1
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 7
9
+ - 1
10
+ version: 0.7.1
11
+ platform: ruby
12
+ authors:
13
+ - Alvaro Gil
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-12-24 00:00:00 -08:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: json
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 3
30
+ segments:
31
+ - 0
32
+ version: "0"
33
+ type: :runtime
34
+ version_requirements: *id001
35
+ - !ruby/object:Gem::Dependency
36
+ name: rack
37
+ prerelease: false
38
+ requirement: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id002
49
+ - !ruby/object:Gem::Dependency
50
+ name: rspec
51
+ prerelease: false
52
+ requirement: &id003 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ hash: 3
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ type: :development
62
+ version_requirements: *id003
63
+ - !ruby/object:Gem::Dependency
64
+ name: rack-test
65
+ prerelease: false
66
+ requirement: &id004 !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ hash: 3
72
+ segments:
73
+ - 0
74
+ version: "0"
75
+ type: :development
76
+ version_requirements: *id004
77
+ - !ruby/object:Gem::Dependency
78
+ name: fakeweb
79
+ prerelease: false
80
+ requirement: &id005 !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ type: :development
90
+ version_requirements: *id005
91
+ - !ruby/object:Gem::Dependency
92
+ name: nokogiri
93
+ prerelease: false
94
+ requirement: &id006 !ruby/object:Gem::Requirement
95
+ none: false
96
+ requirements:
97
+ - - ">="
98
+ - !ruby/object:Gem::Version
99
+ hash: 3
100
+ segments:
101
+ - 0
102
+ version: "0"
103
+ type: :development
104
+ version_requirements: *id006
105
+ description: Simple lib to track events in Mixpanel service. It can be used in any rack based framework.
106
+ email: logan@datacurrent.com
107
+ executables: []
108
+
109
+ extensions: []
110
+
111
+ extra_rdoc_files:
112
+ - README.rdoc
113
+ files:
114
+ - README.rdoc
115
+ - LICENSE
116
+ - Rakefile
117
+ - mixpanel.gemspec
118
+ - spec/mixpanel/mixpanel_middleware_spec.rb
119
+ - spec/mixpanel/mixpanel_spec.rb
120
+ - spec/spec_helper.rb
121
+ - spec/support/rack_apps.rb
122
+ - lib/mixpanel/mixpanel.rb
123
+ - lib/mixpanel/mixpanel_middleware.rb
124
+ - lib/mixpanel/mixpanel_subprocess.rb
125
+ - lib/mixpanel.rb
126
+ has_rdoc: true
127
+ homepage: http://github.com/loganb/mixpanel
128
+ licenses: []
129
+
130
+ post_install_message:
131
+ rdoc_options: []
132
+
133
+ require_paths:
134
+ - lib
135
+ required_ruby_version: !ruby/object:Gem::Requirement
136
+ none: false
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ hash: 3
141
+ segments:
142
+ - 0
143
+ version: "0"
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ none: false
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ hash: 3
150
+ segments:
151
+ - 0
152
+ version: "0"
153
+ requirements: []
154
+
155
+ rubyforge_project:
156
+ rubygems_version: 1.3.7
157
+ signing_key:
158
+ specification_version: 3
159
+ summary: Supports direct request api and javascript requests through a middleware.
160
+ test_files: []
161
+