mixpanel 2.2.0 → 3.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +233 -105
- data/lib/mixpanel/async.rb +31 -0
- data/lib/mixpanel/event.rb +41 -0
- data/lib/mixpanel/middleware.rb +142 -0
- data/lib/mixpanel/person.rb +41 -0
- data/lib/mixpanel/subprocess.rb +27 -0
- data/lib/mixpanel/tracker.rb +54 -151
- data/lib/mixpanel.rb +1 -0
- data/mixpanel.gemspec +2 -1
- data/spec/mixpanel/{tracker/middleware_spec.rb → middleware_spec.rb} +10 -10
- data/spec/mixpanel/tracker_spec.rb +33 -90
- data/spec/spec_helper.rb +13 -3
- data/spec/support/rack_apps.rb +1 -1
- metadata +37 -24
- data/lib/mixpanel/tracker/middleware.rb +0 -144
- data/lib/mixpanel/tracker/subprocess.rb +0 -29
- data/spec/mixpanel/mixpanel_spec.rb +0 -11
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
[![Build Status](https://secure.travis-ci.org/zevarito/mixpanel.png)](http://travis-ci.org/zevarito/mixpanel)
|
1
|
+
[![Build Status](https://secure.travis-ci.org/zevarito/mixpanel.png?branch=master)](http://travis-ci.org/zevarito/mixpanel)
|
2
2
|
|
3
3
|
## Table of Contents
|
4
4
|
|
@@ -7,7 +7,11 @@
|
|
7
7
|
- [Install] (#install)
|
8
8
|
- [Rack Middleware] (#rack-middleware)
|
9
9
|
- [Usage] (#usage)
|
10
|
-
- [Initialize Mixpanel
|
10
|
+
- [Initialize Mixpanel] (#initialize-mixpanel)
|
11
|
+
- [Track Events Directly](#track-events-directly)
|
12
|
+
- [Import Events](#import-events)
|
13
|
+
- [Set Person Attributes Directly](#set-person-attributes-directly)
|
14
|
+
- [Increment Person Attributes Directly](#increment-person-attributes-directly)
|
11
15
|
- [Examples] (#examples)
|
12
16
|
- [How to use it from Rails controllers] (#how-to-use-it-from-rails-controllers)
|
13
17
|
- [How to track events using Resque and Rails] (#how-to-track-events-using-resque-and-rails)
|
@@ -22,8 +26,10 @@ http://mixpanel.com
|
|
22
26
|
|
23
27
|
## What does this Gem do?
|
24
28
|
|
25
|
-
- Track events with properties directly from your backend
|
26
|
-
- Track events with properties through
|
29
|
+
- Track events with properties directly from your backend
|
30
|
+
- Track events with properties through JavaScript using a Rack Middleware
|
31
|
+
- Set / increment user attributes directly from your backend
|
32
|
+
- Set / increment user attributes through JavaScript using a Rack Middleware
|
27
33
|
|
28
34
|
## Install
|
29
35
|
|
@@ -33,112 +39,232 @@ http://mixpanel.com
|
|
33
39
|
|
34
40
|
### Rack Middleware
|
35
41
|
|
36
|
-
*Only
|
42
|
+
*Only needed if you want to track events via Javascript.* This setup will allow your backend to have the client browser process the actual
|
43
|
+
requests over JavaScript rather than sending the request yourself.
|
37
44
|
|
38
45
|
If you are using Rails you can add this to your specific environment configuration file (located in config/environments/) or create a new
|
39
46
|
initializer for it:
|
40
47
|
|
41
48
|
```ruby
|
42
|
-
config.middleware.use "Mixpanel::
|
49
|
+
config.middleware.use "Mixpanel::Middleware", "YOUR_MIXPANEL_API_TOKEN", options
|
43
50
|
```
|
44
51
|
|
45
|
-
Where **options** is a
|
52
|
+
Where **options** is a hash that accepts the following keys:
|
46
53
|
|
47
|
-
* **insert_js_last** :
|
54
|
+
* **insert_js_last** : boolean
|
48
55
|
|
49
|
-
*Default: false
|
50
|
-
By default the scripts are inserted into the head of the html response. If you'd prefer the scripts to run after
|
51
|
-
all rendering has completed you can set the insert_js_last flag and they'll be added at the end of the body tag.
|
52
|
-
This will work whether or not you opt for the aynchronous version of the API. However, when inserting js into an
|
53
|
-
ajax response it will have no effect.
|
56
|
+
*Default: false*
|
54
57
|
|
55
|
-
|
58
|
+
By default the scripts are inserted into the head of the HTML response. If you'd prefer the scripts to run after
|
59
|
+
all rendering has completed, set the insert_js_last flag to true and they'll be added at the end of the body tag.
|
60
|
+
This will work whether or not you opt for the aynchronous version of the API. However, this will have no effect
|
61
|
+
when inserting JS into an AJAX response.
|
62
|
+
|
63
|
+
* **persist** : boolean
|
64
|
+
|
65
|
+
*Default: false*
|
56
66
|
|
57
|
-
*Default: false*.
|
58
67
|
If you would like, the Mixpanel gem may be configured to store its queue in a Rack session. This allows events
|
59
|
-
to be stored through redirects, helpful if you sign in and redirect but want to associate an event with that
|
60
|
-
action. The
|
61
|
-
|
68
|
+
to be stored through redirects, which can be helpful if you sign in and redirect but want to associate an event with that
|
69
|
+
action. The Mixpanel gem will also remove duplicate events from your queue for information that should only be
|
70
|
+
transmitted to the API once, such as `mixpanel.identify`, `mixpanel.name_tag`, `mixpanel.people.set`, and
|
62
71
|
`mixpanel.register`.
|
63
|
-
|
72
|
+
|
73
|
+
This allows you to use a before_filter to set these variables, redirect, and still have them only transmitted
|
64
74
|
once.
|
65
|
-
*To enable persistence*, you must set it in both places, Middleware and when you initialize Mixpanel class.
|
66
75
|
|
67
|
-
*
|
76
|
+
*To enable persistence*, you must set the flag twice: here when instantiating Middleware and again when you initialize
|
77
|
+
the Mixpanel class.
|
78
|
+
|
79
|
+
* **config** : hash
|
68
80
|
|
69
|
-
*Default: {}
|
81
|
+
*Default: {}*
|
70
82
|
|
71
|
-
You can also pass Mixpanel configuration details
|
72
|
-
(https://mixpanel.com/docs/integration-libraries/javascript-full-api#set_config)
|
83
|
+
You can also pass additional [Mixpanel configuration details](https://mixpanel.com/docs/integration-libraries/javascript-full-api#set_config).
|
73
84
|
|
74
85
|
## Usage
|
75
86
|
|
76
|
-
### Initialize Mixpanel
|
87
|
+
### Initialize Mixpanel
|
77
88
|
|
78
89
|
```ruby
|
79
|
-
@mixpanel = Mixpanel::Tracker.new
|
90
|
+
@mixpanel = Mixpanel::Tracker.new YOUR_MIXPANEL_API_TOKEN, options
|
80
91
|
```
|
81
|
-
Where **options** is a
|
92
|
+
Where **options** is a hash that accepts the following keys:
|
82
93
|
|
83
|
-
* **async** :
|
94
|
+
* **async** : boolean
|
84
95
|
|
85
|
-
*Default: false
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
96
|
+
*Default: false*
|
97
|
+
|
98
|
+
Built in async feature. Events are sent to a subprocess via a pipe and the sub process asynchronously send events to Mixpanel.
|
99
|
+
This value can be overwritten on subsequent method calls. I.e., this setting represents the default for your Mixpanel object,
|
100
|
+
but each call can overwrite this default setting.
|
101
|
+
|
102
|
+
This process uses a single thread to upload events, and may start dropping events if your application generates
|
103
|
+
them at a very high rate. While this is a simple way to have asynchronous interaction with Mixpanel, more robust solutions are
|
104
|
+
available. Specifically, see the [Resque example](#how-to-track-events-using-resque-and-rails) below.
|
105
|
+
|
106
|
+
* **persist** : boolean
|
107
|
+
|
108
|
+
*Default: false*
|
109
|
+
|
110
|
+
This is used in connection with the [Rack Middleware section](#rack-middleware) above. If you are not going to use Middleware
|
111
|
+
to send requests to Mixpanel through JavaScript, you don't need to worry about this option.
|
112
|
+
|
101
113
|
If you would like, the Mixpanel gem may be configured to store its queue in a Rack session. This allows events
|
102
|
-
to be stored through redirects, helpful if you sign in and redirect but want to associate an event with that
|
103
|
-
action. The
|
104
|
-
|
114
|
+
to be stored through redirects, which can be helpful if you sign in and redirect but want to associate an event with that
|
115
|
+
action. The Mixpanel gem will also remove duplicate events from your queue for information that should only be
|
116
|
+
transmitted to the API once, such as `mixpanel.identify`, `mixpanel.name_tag`, `mixpanel.people.set`, and
|
105
117
|
`mixpanel.register`.
|
106
|
-
|
118
|
+
|
119
|
+
This allows you to use a before_filter to set these variables, redirect, and still have them only transmitted
|
107
120
|
once.
|
108
|
-
*To enable persistence*, you must set it in both places, Middleware and when you initialize Mixpanel class.
|
109
121
|
|
110
|
-
*To enable
|
111
|
-
|
112
|
-
|
122
|
+
*To enable persistence*, you must set the flag twice: here when instantiating Middleware and again when you initialize
|
123
|
+
the Mixpanel class.
|
124
|
+
|
125
|
+
* **api_key** : string
|
126
|
+
|
127
|
+
*Default: nil*
|
128
|
+
|
129
|
+
When using the [import functionality](#import-events), you must set an API key to go along with your token. If not set when the
|
130
|
+
class is instantiated, you will be required to send the api key in the options hash of the import method.
|
131
|
+
|
132
|
+
* **env** : hash
|
133
|
+
|
134
|
+
*Default: {}*
|
135
|
+
|
136
|
+
This is used by the gem to append information from your request environment to your Mixpanel request. If you are calling this
|
137
|
+
directly from a controller, simply passing in `request.env` will be sufficient. However, as explained in the Resque example,
|
138
|
+
your environment might choke if it tries to convert that hash to JSON (not to mention how large that hash can be). You can just pass
|
139
|
+
in a subset of the full environment:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
env = {
|
143
|
+
'REMOTE_ADDR' => request.env['REMOTE_ADDR'],
|
144
|
+
'HTTP_X_FORWARDED_FOR' => request.env['HTTP_X_FORWARDED_FOR'],
|
145
|
+
'rack.session' => request.env['rack.session'],
|
146
|
+
'mixpanel_events' => request.env['mixpanel_events']
|
147
|
+
}
|
148
|
+
@mixpanel = Mixpanel::Tracker.new MIXPANEL_TOKEN, { :env => env }
|
149
|
+
```
|
150
|
+
|
151
|
+
Basically, this information is being used to: set the default IP address associated with the request, and grab any session variables
|
152
|
+
needed to run the Middleware stuff.
|
153
|
+
|
154
|
+
Additional information contained in your environment (e.g., http_referer) can simply be sent in as attributes where appropriate
|
155
|
+
for your use case.
|
156
|
+
|
157
|
+
### Track Events Directly
|
158
|
+
|
159
|
+
```ruby
|
160
|
+
@mixpanel.track event_name, properties, options
|
161
|
+
```
|
162
|
+
|
163
|
+
**event_name** is a string denoting how you want this event to appear in your Mixpanel dashboard.
|
164
|
+
|
165
|
+
**properties** is a hash of properties to be associated with the event. The keys in the properties can either be strings
|
166
|
+
or symbols. If you send in a key that matches a [special property](https://mixpanel.com/docs/properties-or-segments/special-or-reserved-properties),
|
167
|
+
it will automatically be converted to the correct form (e.g., `{ :os => 'Mac' }` will be converted to `{ :$os => 'Mac' }`).
|
168
|
+
|
169
|
+
**options** is a hash that accepts the following keys:
|
170
|
+
|
171
|
+
* **async** : boolean
|
172
|
+
|
173
|
+
*Default: the async value from when the class was instantiated*
|
174
|
+
|
175
|
+
* **api_key**: string
|
176
|
+
|
177
|
+
*Default: the api_key value from when the class was instantiated*
|
178
|
+
|
179
|
+
* **url**: string
|
180
|
+
|
181
|
+
*Default: `http://api.mixpanel.com/track/`*
|
182
|
+
|
183
|
+
This can be used to proxy Mixpanel API requests.
|
184
|
+
|
185
|
+
Example:
|
186
|
+
|
187
|
+
```ruby
|
188
|
+
@mixpanel.track 'Purchased credits', { :number => 5, 'First Time Buyer' => true }
|
189
|
+
```
|
190
|
+
|
191
|
+
### Import Events
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
@mixpanel.import event_name, properties, options
|
195
|
+
```
|
196
|
+
|
197
|
+
All of these options have the same meaning and same defaults as the [track method](#track-events-directly), except that the
|
198
|
+
default url is `http://api.mixpanel.com/import/`
|
199
|
+
|
200
|
+
Example:
|
201
|
+
|
202
|
+
```ruby
|
203
|
+
@mixpanel.import 'Purchased credits', { :number => 4, :time => 5.weeks.ago }, { :api_key => MY_API_KEY}
|
204
|
+
```
|
205
|
+
|
206
|
+
### Set Person Attributes Directly
|
207
|
+
|
208
|
+
```ruby
|
209
|
+
@mixpanel.set distinct_id, properties, options
|
210
|
+
```
|
211
|
+
|
212
|
+
**distinct_id** is whatever is used to identify the user to Mixpanel.
|
213
|
+
|
214
|
+
**properties** is a hash of properties to be set. The keys in the properties can either be strings
|
215
|
+
or symbols. If you send in a key that matches a [special property](https://mixpanel.com/docs/people-analytics/special-properties),
|
216
|
+
it will automatically be converted to the correct form (e.g., `{ :first_name => 'Chris' }` will be converted to `{ :$first_name => 'Chris' }`).
|
217
|
+
|
218
|
+
**options** is a hash that accepts the following keys:
|
219
|
+
|
220
|
+
* **async**: boolean
|
113
221
|
|
114
|
-
|
222
|
+
*Default: the async value from when the class was instantiated*
|
223
|
+
|
224
|
+
* **url**: string
|
225
|
+
|
226
|
+
*Default: `http://api.mixpanel.com/engage/`*
|
227
|
+
|
228
|
+
This can be used to proxy Mixpanel API requests
|
229
|
+
|
230
|
+
Example:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
@mixpanel.set 'john-doe', { :age => 31, :email => 'john@doe.com' }
|
234
|
+
```
|
235
|
+
|
236
|
+
### Increment Person Attributes Directly
|
115
237
|
|
116
238
|
```ruby
|
117
|
-
|
239
|
+
@mixpanel.increment distinct_id, properties, options
|
118
240
|
```
|
119
241
|
|
120
|
-
|
242
|
+
All of these options have the same meaning and same defaults as the [set method](#set-person-attributes-directly). Note that according to Mixpanel's
|
243
|
+
docs, you cannot combine set and increment requests, and that is why they are split here.
|
244
|
+
|
245
|
+
Example:
|
121
246
|
|
122
247
|
```ruby
|
123
|
-
|
124
|
-
@mixpanel.engage_add(@user.id, {:monkeys_punched => 12})
|
248
|
+
@mixpanel.increment 'john-doe', { :tokens => 5, :coins => -4 }
|
125
249
|
```
|
126
250
|
|
127
|
-
### Append
|
251
|
+
### Append Events To Be Tracked With Javascript
|
128
252
|
|
129
253
|
*Note*: You should setup the [Rack Middleware](#rack-middleware).
|
130
254
|
|
131
255
|
```ruby
|
132
|
-
@mixpanel.
|
256
|
+
@mixpanel.append_track event_name, properties
|
133
257
|
```
|
134
258
|
|
259
|
+
**event_name** and **properties** take the same form as [tracking the event directly](#track-events-directly).
|
260
|
+
|
135
261
|
### Execute Javascript API call
|
136
262
|
|
137
263
|
*Note*: You should setup the [Rack Middleware](#rack-middleware).
|
138
264
|
|
139
265
|
```ruby
|
140
|
-
@mixpanel.
|
141
|
-
@mixpanel.
|
266
|
+
@mixpanel.append("register", {:some => "property"})
|
267
|
+
@mixpanel.append("identify", "Unique Identifier")
|
142
268
|
```
|
143
269
|
|
144
270
|
### Prevent middleware from inserting code
|
@@ -159,49 +285,64 @@ $.ajax("/path/to/api/endpoint", {
|
|
159
285
|
|
160
286
|
### How to use it from Rails controllers?
|
161
287
|
|
162
|
-
In your ApplicationController class add a method to
|
288
|
+
In your ApplicationController class add a method to keep track of a Mixpanel instance.
|
163
289
|
|
164
290
|
```ruby
|
165
|
-
|
291
|
+
protected
|
292
|
+
def mixpanel
|
293
|
+
@mixpanel ||= Mixpanel::Tracker.new YOUR_MIXPANEL_API_TOKEN, { :env => request.env }
|
294
|
+
end
|
295
|
+
```
|
166
296
|
|
167
|
-
|
168
|
-
|
169
|
-
|
297
|
+
Then you can call against this method where it makes sense in your controller. For example, in the users#create method:
|
298
|
+
|
299
|
+
```ruby
|
300
|
+
def create
|
301
|
+
@user = User.create( :name => 'Jane Doe', :gender => 'female', :mixpanel_identifer => 'asdf' )
|
302
|
+
mixpanel.track 'User Created', { :gender => @user.gender, :distinct_id => @user.mixpanel_identifier, :time => @user.created_at } # Note that passing the time key overwrites the default value of Time.now
|
303
|
+
mixpanel.set @user.mixpanel_identifer, { :gender => @user.gender, :created => @user.created_at, :name => @user.name }
|
304
|
+
end
|
170
305
|
```
|
171
|
-
## How to track events using Resque and Rails
|
172
306
|
|
173
|
-
|
174
|
-
async calls using Resque.
|
307
|
+
## How to track events using Resque and Rails
|
175
308
|
|
176
|
-
|
309
|
+
While there is built-in async functionality, other options are more robust (e.g., using a dedicated queue manager). Below is an example of how this
|
310
|
+
might be done with [Resque](https://github.com/defunkt/resque), but the same concepts would apply no matter what queue manager you use.
|
177
311
|
|
178
312
|
```ruby
|
179
|
-
|
180
|
-
|
313
|
+
class MixpanelTrackEventJob
|
314
|
+
@queue = :slow
|
181
315
|
|
182
|
-
|
183
|
-
|
184
|
-
|
316
|
+
def self.mixpanel env
|
317
|
+
Mixpanel::Tracker.new MIXPANEL_TOKEN, { :env => env }
|
318
|
+
end
|
185
319
|
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
320
|
+
def self.perform name, properties, env
|
321
|
+
mixpanel(env).track name, params
|
322
|
+
end
|
323
|
+
end
|
190
324
|
```
|
191
325
|
|
192
326
|
```ruby
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
327
|
+
class UsersController < ApplicationController
|
328
|
+
def create
|
329
|
+
@user = User.new(params[:user])
|
330
|
+
|
331
|
+
if @user.save
|
332
|
+
env = {
|
333
|
+
'REMOTE_ADDR' => request.env['REMOTE_ADDR'],
|
334
|
+
'HTTP_X_FORWARDED_FOR' => request.env['HTTP_X_FORWARDED_FOR'],
|
335
|
+
'rack.session' => request.env['rack.session'],
|
336
|
+
'mixpanel_events' => request.env['mixpanel_events']
|
337
|
+
} # Trying to pass request.env to Resque is going to fail (it chokes when trying to conver it to JSON, but no worries...)
|
338
|
+
|
339
|
+
Resque.enqueue MixpanelTrackEventJob, 'Sign up', { :invited => params[:invited] }, env
|
340
|
+
redirect_to user_root_path
|
341
|
+
else
|
342
|
+
render :new
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
205
346
|
```
|
206
347
|
|
207
348
|
## Supported Ruby Platforms
|
@@ -211,20 +352,6 @@ async calls using Resque.
|
|
211
352
|
- 1.9.3
|
212
353
|
- JRuby 1.8 Mode
|
213
354
|
|
214
|
-
## Deprecation Notes
|
215
|
-
|
216
|
-
This way to initialize Mixpanel gem is not longer allowed.
|
217
|
-
|
218
|
-
```ruby
|
219
|
-
Mixpanel.new
|
220
|
-
```
|
221
|
-
|
222
|
-
Use this instead:
|
223
|
-
|
224
|
-
```ruby
|
225
|
-
Mixpanel::Tracker.new
|
226
|
-
```
|
227
|
-
|
228
355
|
## Collaborators and Maintainers
|
229
356
|
|
230
357
|
* [Alvaro Gil](https://github.com/zevarito) (Author)
|
@@ -240,3 +367,4 @@ Use this instead:
|
|
240
367
|
* [Travis Pew](https://github.com/travisp)
|
241
368
|
* [Sylvain Niles](https://github.com/sylvainsf)
|
242
369
|
* [GBH](https://github.com/GBH)
|
370
|
+
* [Goalee](https://github.com/Goalee)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module Mixpanel::Async
|
2
|
+
WORKER_MUTEX = Mutex.new
|
3
|
+
|
4
|
+
def worker
|
5
|
+
WORKER_MUTEX.synchronize do
|
6
|
+
@worker || (@worker = IO.popen(self.cmd, 'w'))
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
def dispose_worker(w)
|
11
|
+
WORKER_MUTEX.synchronize do
|
12
|
+
if(@worker == w)
|
13
|
+
@worker = nil
|
14
|
+
w.close
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def cmd
|
22
|
+
@cmd || begin
|
23
|
+
require 'escape'
|
24
|
+
require 'rbconfig'
|
25
|
+
interpreter = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
|
26
|
+
subprocess = File.join(File.dirname(__FILE__), 'subprocess.rb')
|
27
|
+
@cmd = Escape.shell_command([interpreter, subprocess])
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module Mixpanel::Event
|
2
|
+
EVENT_PROPERTIES = %w{initial_referrer initial_referring_domain search_engine os browser referrer referring_domain}
|
3
|
+
TRACK_URL = 'http://api.mixpanel.com/track/'
|
4
|
+
IMPORT_URL = 'http://api.mixpanel.com/import/'
|
5
|
+
|
6
|
+
def track(event, properties={}, options={})
|
7
|
+
track_event event, properties, options, TRACK_URL
|
8
|
+
end
|
9
|
+
|
10
|
+
def import(event, properties={}, options={})
|
11
|
+
track_event event, properties, options, IMPORT_URL
|
12
|
+
end
|
13
|
+
|
14
|
+
def append_track(event, properties={})
|
15
|
+
append 'track', event, track_properties(properties, false)
|
16
|
+
end
|
17
|
+
|
18
|
+
protected
|
19
|
+
|
20
|
+
def track_event(event, properties, options, default_url)
|
21
|
+
options.reverse_merge! :url => default_url, :async => @async, :api_key => @api_key
|
22
|
+
data = build_event event, track_properties(properties)
|
23
|
+
url = "#{options[:url]}?data=#{encoded_data(data)}"
|
24
|
+
url += "&api_key=#{options[:api_key]}" if options[:api_key].present?
|
25
|
+
parse_response request(url, options[:async])
|
26
|
+
end
|
27
|
+
|
28
|
+
def ip
|
29
|
+
(@env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_ADDR'] || '').split(',').last
|
30
|
+
end
|
31
|
+
|
32
|
+
def track_properties(properties, include_token=true)
|
33
|
+
properties.reverse_merge! :time => Time.now, :ip => ip
|
34
|
+
properties.reverse_merge! :token => @token if include_token
|
35
|
+
properties_hash properties, EVENT_PROPERTIES
|
36
|
+
end
|
37
|
+
|
38
|
+
def build_event(event, properties)
|
39
|
+
{ :event => event, :properties => properties_hash(properties, EVENT_PROPERTIES) }
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module Mixpanel
|
5
|
+
class Middleware
|
6
|
+
def initialize(app, mixpanel_token, options={})
|
7
|
+
@app = app
|
8
|
+
@token = mixpanel_token
|
9
|
+
@options = {
|
10
|
+
:insert_js_last => false,
|
11
|
+
:persist => false,
|
12
|
+
:config => {}
|
13
|
+
}.merge(options)
|
14
|
+
end
|
15
|
+
|
16
|
+
def call(env)
|
17
|
+
@env = env
|
18
|
+
|
19
|
+
@status, @headers, @response = @app.call(env)
|
20
|
+
|
21
|
+
if is_trackable_response?
|
22
|
+
merge_queue! if @options[:persist]
|
23
|
+
update_response!
|
24
|
+
update_content_length!
|
25
|
+
delete_event_queue!
|
26
|
+
end
|
27
|
+
|
28
|
+
[@status, @headers, @response]
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
def update_response!
|
34
|
+
@response.each do |part|
|
35
|
+
if is_regular_request? && is_html_response?
|
36
|
+
insert_at = part.index(@options[:insert_js_last] ? '</body' : '</head')
|
37
|
+
unless insert_at.nil?
|
38
|
+
part.insert(insert_at, render_event_tracking_scripts) unless queue.empty?
|
39
|
+
part.insert(insert_at, render_mixpanel_scripts) #This will insert the mixpanel initialization code before the queue of tracking events.
|
40
|
+
end
|
41
|
+
elsif is_ajax_request? && is_html_response?
|
42
|
+
part.insert(0, render_event_tracking_scripts) unless queue.empty?
|
43
|
+
elsif is_ajax_request? && is_javascript_response?
|
44
|
+
part.insert(0, render_event_tracking_scripts(false)) unless queue.empty?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def update_content_length!
|
50
|
+
new_size = 0
|
51
|
+
@response.each{|part| new_size += part.bytesize}
|
52
|
+
@headers.merge!("Content-Length" => new_size.to_s)
|
53
|
+
end
|
54
|
+
|
55
|
+
def is_regular_request?
|
56
|
+
!is_ajax_request?
|
57
|
+
end
|
58
|
+
|
59
|
+
def is_ajax_request?
|
60
|
+
@env.has_key?("HTTP_X_REQUESTED_WITH") && @env["HTTP_X_REQUESTED_WITH"] == "XMLHttpRequest"
|
61
|
+
end
|
62
|
+
|
63
|
+
def is_html_response?
|
64
|
+
@headers["Content-Type"].include?("text/html") if @headers.has_key?("Content-Type")
|
65
|
+
end
|
66
|
+
|
67
|
+
def is_javascript_response?
|
68
|
+
@headers["Content-Type"].include?("text/javascript") if @headers.has_key?("Content-Type")
|
69
|
+
end
|
70
|
+
|
71
|
+
def is_trackable_response?
|
72
|
+
return false if @status == 302
|
73
|
+
return false if @env.has_key?("HTTP_SKIP_MIXPANEL_MIDDLEWARE")
|
74
|
+
is_html_response? || is_javascript_response?
|
75
|
+
end
|
76
|
+
|
77
|
+
def render_mixpanel_scripts
|
78
|
+
<<-EOT
|
79
|
+
<!-- start Mixpanel -->
|
80
|
+
<script type="text/javascript">
|
81
|
+
(function(c,a){window.mixpanel=a;var b,d,h,e;b=c.createElement("script");
|
82
|
+
b.type="text/javascript";b.async=!0;b.src=("https:"===c.location.protocol?"https:":"http:")+
|
83
|
+
'//cdn.mxpnl.com/libs/mixpanel-2.1.min.js';d=c.getElementsByTagName("script")[0];
|
84
|
+
d.parentNode.insertBefore(b,d);a._i=[];a.init=function(b,c,f){function d(a,b){
|
85
|
+
var c=b.split(".");2==c.length&&(a=a[c[0]],b=c[1]);a[b]=function(){a.push([b].concat(
|
86
|
+
Array.prototype.slice.call(arguments,0)))}}var g=a;"undefined"!==typeof f?g=a[f]=[]:
|
87
|
+
f="mixpanel";g.people=g.people||[];h=['disable','track','track_pageview','track_links',
|
88
|
+
'track_forms','register','register_once','unregister','identify','name_tag',
|
89
|
+
'set_config','people.identify','people.set','people.increment'];for(e=0;e<h.length;e++)d(g,h[e]);
|
90
|
+
a._i.push([b,c,f])};a.__SV=1.1;})(document,window.mixpanel||[]);
|
91
|
+
|
92
|
+
mixpanel.init("#{@token}");
|
93
|
+
mixpanel.set_config(#{@options[:config].to_json});
|
94
|
+
</script>
|
95
|
+
<!-- end Mixpanel -->
|
96
|
+
EOT
|
97
|
+
end
|
98
|
+
|
99
|
+
def delete_event_queue!
|
100
|
+
if @options[:persist]
|
101
|
+
(@env['rack.session']).delete('mixpanel_events')
|
102
|
+
else
|
103
|
+
@env.delete('mixpanel_events')
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def queue
|
108
|
+
if @options[:persist]
|
109
|
+
return [] if !(@env['rack.session']).has_key?('mixpanel_events') || @env['rack.session']['mixpanel_events'].empty?
|
110
|
+
@env['rack.session']['mixpanel_events']
|
111
|
+
else
|
112
|
+
return [] if !@env.has_key?('mixpanel_events') || @env['mixpanel_events'].empty?
|
113
|
+
@env['mixpanel_events']
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def merge_queue!
|
118
|
+
present_hash = {}
|
119
|
+
special_events = ['identify', 'name_tag', 'people.set', 'register']
|
120
|
+
queue.uniq!
|
121
|
+
|
122
|
+
queue.reverse_each do |item|
|
123
|
+
is_special = special_events.include?(item[0])
|
124
|
+
if present_hash[item[0]] and is_special
|
125
|
+
queue.delete(item)
|
126
|
+
else
|
127
|
+
present_hash[item[0]] = true if is_special
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_event_tracking_scripts(include_script_tag=true)
|
133
|
+
return "" if queue.empty?
|
134
|
+
|
135
|
+
output = queue.map {|type, arguments| %(mixpanel.#{type}(#{arguments.join(', ')});) }.join("\n")
|
136
|
+
output = "try {#{output}} catch(err) {}"
|
137
|
+
|
138
|
+
include_script_tag ? "<script type='text/javascript'>#{output}</script>" : output
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|