loganb-mixpanel 0.7.1
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/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
|
+
|