mixpanel 2.2.0 → 3.0.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/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
|
-
[](http://travis-ci.org/zevarito/mixpanel)
|
1
|
+
[](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
|
+
|