mixpanel 0.6.1 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
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 do?
7
7
 
8
8
  * Track events with properties directly from your backend.
9
9
  * Track events with properties through javascript using a rack middleware.
@@ -22,9 +22,15 @@ In your environment config file add this.
22
22
 
23
23
  config.middleware.use "MixpanelMiddleware", "YOUR_MIXPANEL_API_TOKEN"
24
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
+
25
31
  In your application_controller class add a method to instance mixpanel.
26
32
 
27
- before_filer :initialize_mixpanel
33
+ before_filter :initialize_mixpanel
28
34
 
29
35
  def initialize_mixpanel
30
36
  @mixpanel = Mixpanel.new("YOUR_MIXPANEL_API_TOKEN", request.env)
@@ -40,9 +46,20 @@ To track events after response with javascript...
40
46
 
41
47
  @mixpanel.append_event("Sign in", {:some => "property"})
42
48
 
49
+ To execute any javascript API call
50
+
51
+ @mixpanel.append_api("register", {:some => "property"})
52
+ @mixpanel.append_api("identify", "Unique Identifier")
53
+
43
54
 
44
55
  == Notes
45
56
 
46
57
  It is strongly recommended to call Mixpanel#track_event using an async lib
47
58
  like delayed job or similar, otherwise you will delay your server responses
48
59
  with mixpanel responses.
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]
@@ -10,9 +10,13 @@ class Mixpanel
10
10
  end
11
11
 
12
12
  def append_event(event, properties = {})
13
- queue << build_event(event, properties)
13
+ append_api('track', event, properties)
14
14
  end
15
-
15
+
16
+ def append_api(type, *args)
17
+ queue << [type, args.map {|arg| arg.to_json}]
18
+ end
19
+
16
20
  def track_event(event, properties = {})
17
21
  params = build_event(event, properties.merge(:token => @token, :time => Time.now.utc.to_i, :ip => ip))
18
22
  parse_response request(params)
@@ -1,20 +1,23 @@
1
1
  require 'rack'
2
2
 
3
3
  class MixpanelMiddleware
4
- def initialize(app, mixpanel_token)
4
+ def initialize(app, mixpanel_token, options={})
5
5
  @app = app
6
6
  @token = mixpanel_token
7
+ @options = {
8
+ :async => false
9
+ }.merge(options)
7
10
  end
8
11
 
9
12
  def call(env)
10
13
  @env = env
11
14
 
12
15
  @status, @headers, @response = @app.call(env)
13
-
16
+
14
17
  update_response!
15
18
  update_content_length!
16
19
  delete_event_queue!
17
-
20
+
18
21
  [@status, @headers, @response]
19
22
  end
20
23
 
@@ -23,12 +26,15 @@ class MixpanelMiddleware
23
26
  def update_response!
24
27
  @response.each do |part|
25
28
  if is_regular_request? && is_html_response?
26
- part.gsub!("</head>", "#{render_mixpanel_scripts}</head>")
27
- part.gsub!("</head>", "#{render_event_tracking_scripts}</head>")
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
28
34
  elsif is_ajax_request? && is_html_response?
29
- part.gsub!(part, render_event_tracking_scripts + part)
35
+ part.insert(0, render_event_tracking_scripts) unless queue.empty?
30
36
  elsif is_ajax_request? && is_javascript_response?
31
- part.gsub!(part, render_event_tracking_scripts(false) + part)
37
+ part.insert(0, render_event_tracking_scripts(false)) unless queue.empty?
32
38
  end
33
39
  end
34
40
  end
@@ -56,22 +62,36 @@ class MixpanelMiddleware
56
62
  end
57
63
 
58
64
  def render_mixpanel_scripts
59
- <<-EOT
60
- <script type='text/javascript'>
61
- var mp_protocol = (('https:' == document.location.protocol) ? 'https://' : 'http://');
62
- document.write(unescape('%3Cscript src="' + mp_protocol + 'api.mixpanel.com/site_media/js/api/mixpanel.js" type="text/javascript"%3E%3C/script%3E'));
63
- </script>
64
- <script type='text/javascript'>
65
- try {
66
- var mpmetrics = new MixpanelLib('#{@token}');
67
- } catch(err) {
68
- null_fn = function () {};
69
- var mpmetrics = {
70
- track: null_fn, track_funnel: null_fn, register: null_fn, register_once: null_fn, register_funnel: null_fn
71
- };
72
- }
73
- </script>
74
- EOT
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
75
95
  end
76
96
 
77
97
  def delete_event_queue!
@@ -86,7 +106,11 @@ class MixpanelMiddleware
86
106
  def render_event_tracking_scripts(include_script_tag=true)
87
107
  return "" if queue.empty?
88
108
 
89
- output = queue.map {|event| %(mpmetrics.track("#{event[:event]}", #{event[:properties].to_json});) }.join("\n")
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
90
114
 
91
115
  output = "try {#{output}} catch(err) {}"
92
116
 
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.6.1"
5
+ s.version = "0.7.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"
@@ -14,6 +14,66 @@ describe MixpanelMiddleware do
14
14
  end
15
15
  end
16
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
+
17
77
  describe "Appending mixpanel scripts" do
18
78
  describe "With ajax requests" do
19
79
  before do
@@ -29,7 +89,22 @@ describe MixpanelMiddleware do
29
89
  last_response.headers["Content-Length"].should == html_document.length.to_s
30
90
  end
31
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
32
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
+
33
108
  describe "With regular requests" do
34
109
  before do
35
110
  setup_rack_application(DummyApp, :body => html_document, :headers => {"Content-Type" => "text/html"})
@@ -59,6 +134,79 @@ describe MixpanelMiddleware do
59
134
  end
60
135
  end
61
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
+
62
210
  describe "Tracking appended events" do
63
211
  before do
64
212
  @mixpanel = Mixpanel.new(MIX_PANEL_TOKEN, {})
@@ -48,18 +48,38 @@ describe Mixpanel do
48
48
 
49
49
  it "should append simple events" do
50
50
  @mixpanel.append_event("Sign up")
51
- mixpanel_queue_should_include(@mixpanel, "Sign up", {})
51
+ mixpanel_queue_should_include(@mixpanel, "track", "Sign up", {})
52
52
  end
53
53
 
54
54
  it "should append events with properties" do
55
55
  @mixpanel.append_event("Sign up", {:referer => 'http://example.com'})
56
- mixpanel_queue_should_include(@mixpanel, "Sign up", {:referer => 'http://example.com'})
56
+ mixpanel_queue_should_include(@mixpanel, "track", "Sign up", {:referer => 'http://example.com'})
57
57
  end
58
58
 
59
59
  it "should give direct access to queue" do
60
60
  @mixpanel.append_event("Sign up", {:referer => 'http://example.com'})
61
61
  @mixpanel.queue.size.should == 1
62
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
63
83
  end
64
84
  end
65
85
  end
data/spec/spec_helper.rb CHANGED
@@ -7,10 +7,11 @@ Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].e
7
7
 
8
8
  MIX_PANEL_TOKEN = "e2d8b0bea559147844ffab3d607d26a6"
9
9
 
10
- def mixpanel_queue_should_include(mixpanel, event, properties)
11
- mixpanel.queue.each do |event_hash|
12
- event_hash[:event].should == event
13
- event_hash[:properties].should == properties if properties
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
14
15
  end
15
16
  end
16
17
 
@@ -1,5 +1,5 @@
1
- def setup_rack_application(application, options = {})
2
- stub!(:app).and_return(MixpanelMiddleware.new(application.new(options), MIX_PANEL_TOKEN))
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
3
  end
4
4
 
5
5
  def html_document
@@ -25,3 +25,179 @@ class DummyApp
25
25
  [@response_with[:status], @response_with[:headers], [@response_with[:body]]]
26
26
  end
27
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 CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mixpanel
3
3
  version: !ruby/object:Gem::Version
4
- hash: 5
4
+ hash: 3
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 6
9
- - 1
10
- version: 0.6.1
8
+ - 7
9
+ - 0
10
+ version: 0.7.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: 2010-07-16 00:00:00 -03:00
18
+ date: 2010-11-26 00:00:00 -02:00
19
19
  default_executable:
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency