mixpanel 0.8.1 → 0.9.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.
- data/README.rdoc +11 -5
- data/lib/mixpanel.rb +8 -2
- data/lib/mixpanel/tracker.rb +97 -0
- data/lib/mixpanel/tracker/middleware.rb +124 -0
- data/lib/mixpanel/tracker/subprocess.rb +29 -0
- data/mixpanel.gemspec +1 -1
- data/spec/mixpanel/mixpanel_spec.rb +4 -107
- data/spec/mixpanel/{mixpanel_middleware_spec.rb → tracker/middleware_spec.rb} +3 -3
- data/spec/mixpanel/tracker_spec.rb +113 -0
- data/spec/support/rack_apps.rb +2 -2
- metadata +10 -9
- data/lib/mixpanel/mixpanel.rb +0 -95
- data/lib/mixpanel/mixpanel_middleware.rb +0 -119
- data/lib/mixpanel/mixpanel_subprocess.rb +0 -30
data/README.rdoc
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
Mixpanel is a real-time analytics service that helps companies understand how users interact with web applications.
|
4
4
|
http://mixpanel.com
|
5
5
|
|
6
|
-
== What does this Gem
|
6
|
+
== What does this Gem does?
|
7
7
|
|
8
8
|
* Track events with properties directly from your backend.
|
9
9
|
* Track events with properties through javascript using a rack middleware.
|
@@ -20,20 +20,20 @@ In your environment config file add this.
|
|
20
20
|
|
21
21
|
Rails::Initializer.run do |config|
|
22
22
|
|
23
|
-
config.middleware.use "
|
23
|
+
config.middleware.use "Mixpanel::Tracker::Middleware", "YOUR_MIXPANEL_API_TOKEN"
|
24
24
|
|
25
25
|
If you want to use the asynchronous version of Mixpanel's javascript API
|
26
26
|
|
27
27
|
Rails::Initializer.run do |config|
|
28
28
|
|
29
|
-
config.middleware.use "
|
29
|
+
config.middleware.use "Mixpanel::Tracker::Middleware", "YOUR_MIXPANEL_API_TOKEN", :async => true
|
30
30
|
|
31
31
|
In your application_controller class add a method to instance mixpanel.
|
32
32
|
|
33
33
|
before_filter :initialize_mixpanel
|
34
34
|
|
35
35
|
def initialize_mixpanel
|
36
|
-
@mixpanel = Mixpanel.new("YOUR_MIXPANEL_API_TOKEN", request.env, true)
|
36
|
+
@mixpanel = Mixpanel::Tracker.new("YOUR_MIXPANEL_API_TOKEN", request.env, true)
|
37
37
|
end
|
38
38
|
|
39
39
|
Then in each request you want to track some event you can use:
|
@@ -54,10 +54,16 @@ To execute any javascript API call
|
|
54
54
|
|
55
55
|
== Notes
|
56
56
|
|
57
|
-
There are two forms of async operation:
|
57
|
+
There are two forms of async operation:
|
58
58
|
* Using MixpanelMiddleware, events are queued via Mixpanel#append_event and inserted into a JavaScript block within the HTML response.
|
59
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
60
|
|
61
|
+
== Deprecation Notes
|
62
|
+
|
63
|
+
For a short term this method will be accepted but it will be deprecated soon.
|
64
|
+
|
65
|
+
Mixpanel.new
|
66
|
+
|
61
67
|
== Collaborators and Maintainers
|
62
68
|
|
63
69
|
* {Alvaro Gil}[https://github.com/zevarito] (Author)
|
data/lib/mixpanel.rb
CHANGED
@@ -1,2 +1,8 @@
|
|
1
|
-
require 'mixpanel/
|
2
|
-
|
1
|
+
require 'mixpanel/tracker'
|
2
|
+
|
3
|
+
module Mixpanel
|
4
|
+
def self.new(token, env, async = false)
|
5
|
+
Kernel.warn("DEPRECATED: Use Mixpanel::Tracker.new instead")
|
6
|
+
Mixpanel::Tracker.new(token, env, async)
|
7
|
+
end
|
8
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require "open-uri"
|
2
|
+
require 'base64'
|
3
|
+
require 'json'
|
4
|
+
require 'thread'
|
5
|
+
require 'mixpanel/tracker/middleware'
|
6
|
+
|
7
|
+
module Mixpanel
|
8
|
+
class Tracker
|
9
|
+
def initialize(token, env, async = false)
|
10
|
+
@token = token
|
11
|
+
@env = env
|
12
|
+
@async = async
|
13
|
+
clear_queue
|
14
|
+
end
|
15
|
+
|
16
|
+
def append_event(event, properties = {})
|
17
|
+
append_api('track', event, properties)
|
18
|
+
end
|
19
|
+
|
20
|
+
def append_api(type, *args)
|
21
|
+
queue << [type, args.map {|arg| arg.to_json}]
|
22
|
+
end
|
23
|
+
|
24
|
+
def track_event(event, properties = {})
|
25
|
+
params = build_event(event, properties.merge(:token => @token, :time => Time.now.utc.to_i, :ip => ip))
|
26
|
+
parse_response request(params)
|
27
|
+
end
|
28
|
+
|
29
|
+
def ip
|
30
|
+
@env.has_key?("REMOTE_ADDR") ? @env["REMOTE_ADDR"] : ""
|
31
|
+
end
|
32
|
+
|
33
|
+
def queue
|
34
|
+
@env["mixpanel_events"]
|
35
|
+
end
|
36
|
+
|
37
|
+
def clear_queue
|
38
|
+
@env["mixpanel_events"] = []
|
39
|
+
end
|
40
|
+
|
41
|
+
class <<self
|
42
|
+
WORKER_MUTEX = Mutex.new
|
43
|
+
|
44
|
+
def worker
|
45
|
+
WORKER_MUTEX.synchronize do
|
46
|
+
@worker || (@worker = IO.popen(self.cmd, 'w'))
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def dispose_worker(w)
|
51
|
+
WORKER_MUTEX.synchronize do
|
52
|
+
if(@worker == w)
|
53
|
+
@worker = nil
|
54
|
+
w.close
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def cmd
|
60
|
+
@cmd || begin
|
61
|
+
require 'escape'
|
62
|
+
require 'rbconfig'
|
63
|
+
interpreter = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
|
64
|
+
subprocess = File.join(File.dirname(__FILE__), 'tracker/subprocess.rb')
|
65
|
+
@cmd = Escape.shell_command([interpreter, subprocess])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def parse_response(response)
|
73
|
+
response == "1" ? true : false
|
74
|
+
end
|
75
|
+
|
76
|
+
def request(params)
|
77
|
+
data = Base64.encode64(JSON.generate(params)).gsub(/\n/,'')
|
78
|
+
url = "http://api.mixpanel.com/track/?data=#{data}"
|
79
|
+
|
80
|
+
if(@async)
|
81
|
+
w = Tracker.worker
|
82
|
+
begin
|
83
|
+
url << "\n"
|
84
|
+
w.write(url)
|
85
|
+
rescue Errno::EPIPE => e
|
86
|
+
Tracker.dispose_worker(w)
|
87
|
+
end
|
88
|
+
else
|
89
|
+
open(url).read
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def build_event(event, properties)
|
94
|
+
{:event => event, :properties => properties}
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require 'rack'
|
2
|
+
|
3
|
+
module Mixpanel
|
4
|
+
class Tracker
|
5
|
+
class Middleware
|
6
|
+
def initialize(app, mixpanel_token, options={})
|
7
|
+
@app = app
|
8
|
+
@token = mixpanel_token
|
9
|
+
@options = {
|
10
|
+
:async => false
|
11
|
+
}.merge(options)
|
12
|
+
end
|
13
|
+
|
14
|
+
def call(env)
|
15
|
+
@env = env
|
16
|
+
|
17
|
+
@status, @headers, @response = @app.call(env)
|
18
|
+
|
19
|
+
update_response!
|
20
|
+
update_content_length!
|
21
|
+
delete_event_queue!
|
22
|
+
|
23
|
+
[@status, @headers, @response]
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def update_response!
|
29
|
+
@response.each do |part|
|
30
|
+
if is_regular_request? && is_html_response?
|
31
|
+
insert_at = part.index('</head')
|
32
|
+
unless insert_at.nil?
|
33
|
+
part.insert(insert_at, render_event_tracking_scripts) unless queue.empty?
|
34
|
+
part.insert(insert_at, render_mixpanel_scripts) #This will insert the mixpanel initialization code before the queue of tracking events.
|
35
|
+
end
|
36
|
+
elsif is_ajax_request? && is_html_response?
|
37
|
+
part.insert(0, render_event_tracking_scripts) unless queue.empty?
|
38
|
+
elsif is_ajax_request? && is_javascript_response?
|
39
|
+
part.insert(0, render_event_tracking_scripts(false)) unless queue.empty?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def update_content_length!
|
45
|
+
new_size = 0
|
46
|
+
@response.each{|part| new_size += part.bytesize}
|
47
|
+
@headers.merge!("Content-Length" => new_size.to_s)
|
48
|
+
end
|
49
|
+
|
50
|
+
def is_regular_request?
|
51
|
+
!is_ajax_request?
|
52
|
+
end
|
53
|
+
|
54
|
+
def is_ajax_request?
|
55
|
+
@env.has_key?("HTTP_X_REQUESTED_WITH") && @env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
|
56
|
+
end
|
57
|
+
|
58
|
+
def is_html_response?
|
59
|
+
@headers["Content-Type"].include?("text/html") if @headers.has_key?("Content-Type")
|
60
|
+
end
|
61
|
+
|
62
|
+
def is_javascript_response?
|
63
|
+
@headers["Content-Type"].include?("text/javascript") if @headers.has_key?("Content-Type")
|
64
|
+
end
|
65
|
+
|
66
|
+
def render_mixpanel_scripts
|
67
|
+
if @options[:async]
|
68
|
+
<<-EOT
|
69
|
+
<script type='text/javascript'>
|
70
|
+
var mpq = [];
|
71
|
+
mpq.push(["init", "#{@token}"]);
|
72
|
+
(function() {
|
73
|
+
var mp = document.createElement("script"); mp.type = "text/javascript"; mp.async = true;
|
74
|
+
mp.src = (document.location.protocol == 'https:' ? 'https:' : 'http:') + "//api.mixpanel.com/site_media/js/api/mixpanel.js";
|
75
|
+
var s = document.getElementsByTagName("script")[0]; s.parentNode.insertBefore(mp, s);
|
76
|
+
})();
|
77
|
+
</script>
|
78
|
+
EOT
|
79
|
+
else
|
80
|
+
<<-EOT
|
81
|
+
<script type='text/javascript'>
|
82
|
+
var mp_protocol = (('https:' == document.location.protocol) ? 'https://' : 'http://');
|
83
|
+
document.write(unescape('%3Cscript src="' + mp_protocol + 'api.mixpanel.com/site_media/js/api/mixpanel.js" type="text/javascript"%3E%3C/script%3E'));
|
84
|
+
</script>
|
85
|
+
<script type='text/javascript'>
|
86
|
+
try {
|
87
|
+
var mpmetrics = new MixpanelLib('#{@token}');
|
88
|
+
} catch(err) {
|
89
|
+
null_fn = function () {};
|
90
|
+
var mpmetrics = {
|
91
|
+
track: null_fn, track_funnel: null_fn, register: null_fn, register_once: null_fn, register_funnel: null_fn
|
92
|
+
};
|
93
|
+
}
|
94
|
+
</script>
|
95
|
+
EOT
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete_event_queue!
|
100
|
+
@env.delete('mixpanel_events')
|
101
|
+
end
|
102
|
+
|
103
|
+
def queue
|
104
|
+
return [] if !@env.has_key?('mixpanel_events') || @env['mixpanel_events'].empty?
|
105
|
+
@env['mixpanel_events']
|
106
|
+
end
|
107
|
+
|
108
|
+
def render_event_tracking_scripts(include_script_tag=true)
|
109
|
+
return "" if queue.empty?
|
110
|
+
|
111
|
+
if @options[:async]
|
112
|
+
output = queue.map {|type, arguments| %(mpq.push(["#{type}", #{arguments.join(', ')}]);) }.join("\n")
|
113
|
+
else
|
114
|
+
output = queue.map {|type, arguments| %(mpmetrics.#{type}(#{arguments.join(', ')});) }.join("\n")
|
115
|
+
end
|
116
|
+
|
117
|
+
output = "try {#{output}} catch(err) {}"
|
118
|
+
|
119
|
+
include_script_tag ? "<script type='text/javascript'>#{output}</script>" : output
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'open-uri'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
module Mixpanel
|
5
|
+
class Tracker
|
6
|
+
class Subprocess
|
7
|
+
Q = Queue.new
|
8
|
+
ENDMARKER = Object.new
|
9
|
+
|
10
|
+
Thread.abort_on_exception = true
|
11
|
+
producer = Thread.new do
|
12
|
+
STDIN.each_line() do |url|
|
13
|
+
STDERR.puts("Dropped: #{url}") && next if Q.length > 10000
|
14
|
+
Q << url
|
15
|
+
end
|
16
|
+
Q << ENDMARKER
|
17
|
+
end
|
18
|
+
|
19
|
+
loop do
|
20
|
+
url = Q.pop
|
21
|
+
break if(url == ENDMARKER)
|
22
|
+
url.chomp!
|
23
|
+
next if(url.empty?) #for testing
|
24
|
+
open(url).read
|
25
|
+
end
|
26
|
+
producer.join
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/mixpanel.gemspec
CHANGED
@@ -2,7 +2,7 @@ files = ['README.rdoc', 'LICENSE', 'Rakefile', 'mixpanel.gemspec', '{spec,lib}/*
|
|
2
2
|
|
3
3
|
spec = Gem::Specification.new do |s|
|
4
4
|
s.name = "mixpanel"
|
5
|
-
s.version = "0.
|
5
|
+
s.version = "0.9.0"
|
6
6
|
s.rubyforge_project = "mixpanel"
|
7
7
|
s.description = "Simple lib to track events in Mixpanel service. It can be used in any rack based framework."
|
8
8
|
s.author = "Alvaro Gil"
|
@@ -1,113 +1,10 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
3
|
describe Mixpanel do
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
4
|
+
context "Deprecated initialization mode" do
|
5
|
+
it "should instantiate the object as it was doing before but drop a deprecation warning" do
|
6
|
+
mixpanel = Mixpanel.new(MIX_PANEL_TOKEN, @env = {"REMOTE_ADDR" => "127.0.0.1"})
|
7
|
+
mixpanel.should be_kind_of(Mixpanel::Tracker)
|
111
8
|
end
|
112
9
|
end
|
113
10
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
describe
|
3
|
+
describe Mixpanel::Tracker::Middleware do
|
4
4
|
include Rack::Test::Methods
|
5
5
|
|
6
6
|
describe "Dummy apps, no text/html" do
|
@@ -136,7 +136,7 @@ describe MixpanelMiddleware do
|
|
136
136
|
|
137
137
|
describe "Tracking async appended events" do
|
138
138
|
before do
|
139
|
-
@mixpanel = Mixpanel.new(MIX_PANEL_TOKEN, {})
|
139
|
+
@mixpanel = Mixpanel::Tracker.new(MIX_PANEL_TOKEN, {})
|
140
140
|
@mixpanel.append_event("Visit", {:article => 1})
|
141
141
|
@mixpanel.append_event("Sign in")
|
142
142
|
end
|
@@ -209,7 +209,7 @@ describe MixpanelMiddleware do
|
|
209
209
|
|
210
210
|
describe "Tracking appended events" do
|
211
211
|
before do
|
212
|
-
@mixpanel = Mixpanel.new(MIX_PANEL_TOKEN, {})
|
212
|
+
@mixpanel = Mixpanel::Tracker.new(MIX_PANEL_TOKEN, {})
|
213
213
|
@mixpanel.append_event("Visit", {:article => 1})
|
214
214
|
@mixpanel.append_event("Sign in")
|
215
215
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Mixpanel::Tracker do
|
4
|
+
before(:each) do
|
5
|
+
@mixpanel = Mixpanel::Tracker.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::Tracker.worker
|
89
|
+
w.should == Mixpanel::Tracker.worker
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should be able to write lines to the worker" do
|
93
|
+
w = Mixpanel::Tracker.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::Tracker.worker
|
106
|
+
Mixpanel::Tracker.dispose_worker(w)
|
107
|
+
|
108
|
+
w.closed?.should == true
|
109
|
+
w2 = Mixpanel::Tracker.worker
|
110
|
+
w2.should_not == w
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/spec/support/rack_apps.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
def setup_rack_application(application, options = {}, mixpanel_options = {})
|
2
|
-
stub!(:app).and_return(
|
2
|
+
stub!(:app).and_return(Mixpanel::Tracker::Middleware.new(application.new(options), MIX_PANEL_TOKEN, mixpanel_options))
|
3
3
|
end
|
4
4
|
|
5
5
|
def html_document
|
@@ -200,4 +200,4 @@ def large_script
|
|
200
200
|
</form>
|
201
201
|
</div>
|
202
202
|
EOT
|
203
|
-
end
|
203
|
+
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mixpanel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 59
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 9
|
9
|
+
- 0
|
10
|
+
version: 0.9.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Alvaro Gil
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-06-20 00:00:00 -03:00
|
19
19
|
default_executable:
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -129,13 +129,14 @@ files:
|
|
129
129
|
- LICENSE
|
130
130
|
- Rakefile
|
131
131
|
- mixpanel.gemspec
|
132
|
-
- spec/mixpanel/mixpanel_middleware_spec.rb
|
133
132
|
- spec/mixpanel/mixpanel_spec.rb
|
133
|
+
- spec/mixpanel/tracker/middleware_spec.rb
|
134
|
+
- spec/mixpanel/tracker_spec.rb
|
134
135
|
- spec/spec_helper.rb
|
135
136
|
- spec/support/rack_apps.rb
|
136
|
-
- lib/mixpanel/
|
137
|
-
- lib/mixpanel/
|
138
|
-
- lib/mixpanel/
|
137
|
+
- lib/mixpanel/tracker/middleware.rb
|
138
|
+
- lib/mixpanel/tracker/subprocess.rb
|
139
|
+
- lib/mixpanel/tracker.rb
|
139
140
|
- lib/mixpanel.rb
|
140
141
|
has_rdoc: true
|
141
142
|
homepage: http://cuboxsa.com
|
data/lib/mixpanel/mixpanel.rb
DELETED
@@ -1,95 +0,0 @@
|
|
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.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
|
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
|
@@ -1,119 +0,0 @@
|
|
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
|
@@ -1,30 +0,0 @@
|
|
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
|