loganb-mixpanel 0.7.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +22 -0
- data/README.rdoc +66 -0
- data/Rakefile +9 -0
- data/lib/mixpanel.rb +2 -0
- data/lib/mixpanel/mixpanel.rb +95 -0
- data/lib/mixpanel/mixpanel_middleware.rb +119 -0
- data/lib/mixpanel/mixpanel_subprocess.rb +30 -0
- data/mixpanel.gemspec +22 -0
- data/spec/mixpanel/mixpanel_middleware_spec.rb +282 -0
- data/spec/mixpanel/mixpanel_spec.rb +113 -0
- data/spec/spec_helper.rb +20 -0
- data/spec/support/rack_apps.rb +203 -0
- metadata +161 -0
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
data/lib/mixpanel.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|