mengpaneel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +6 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +421 -0
- data/Rakefile +2 -0
- data/app/assets/javascripts/mengpaneel.js +48 -0
- data/lib/mengpaneel/call_proxy.rb +51 -0
- data/lib/mengpaneel/controller.rb +28 -0
- data/lib/mengpaneel/delayer.rb +25 -0
- data/lib/mengpaneel/engine.rb +4 -0
- data/lib/mengpaneel/flusher.rb +39 -0
- data/lib/mengpaneel/manager.rb +92 -0
- data/lib/mengpaneel/replayer.rb +40 -0
- data/lib/mengpaneel/strategy/async_server_side.rb +35 -0
- data/lib/mengpaneel/strategy/base.rb +17 -0
- data/lib/mengpaneel/strategy/capable_client_side.rb +28 -0
- data/lib/mengpaneel/strategy/client_side.rb +92 -0
- data/lib/mengpaneel/strategy/delayed.rb +22 -0
- data/lib/mengpaneel/strategy/server_side.rb +41 -0
- data/lib/mengpaneel/tracker.rb +95 -0
- data/lib/mengpaneel/version.rb +3 -0
- data/lib/mengpaneel.rb +11 -0
- data/mengpaneel.gemspec +21 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 6d269ce3a419815bff9e35c98f2cd9c51d61ae77
|
4
|
+
data.tar.gz: e70e97bc700fd6d1bfc4990f0abd9925db00d8ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 6e53264302f521efee1722d00c47f0e68f72dff220f04b7480ad2b113c35ab49cf0679ea3be430cbb95e46fb2d4076b0a68dbabc339a34d1ae0ffea22bc1cfaf
|
7
|
+
data.tar.gz: e55c3e48ee8508ab28c50e474fddd780f65d373c4e20b073aeebf22ab5fe2c7d4fc198299209c99b0455afe9a6c25dc240f6f9b98285f5ed1ff779b58a5e33b0
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Douwe Maan
|
2
|
+
|
3
|
+
MIT License
|
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
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,421 @@
|
|
1
|
+
# Mengpaneel
|
2
|
+
|
3
|
+
TL;DR: Mengpaneel makes Mixpanel a breeze to use in Rails apps by giving you a single way to interact with Mixpanel from your controllers, with Mengpaneel taking it upon itself to make sure everything gets to Mixpanel. Fast-forward to "[So… How?!](#so-how)" to get started.
|
4
|
+
|
5
|
+
#### Hi
|
6
|
+
|
7
|
+
Good morning, and thank you for coming. From the look on your face, I sense that you're wondering why I invited you here today.
|
8
|
+
|
9
|
+
#### Sure am. Why _am_ I here?
|
10
|
+
|
11
|
+
You're here because I wanted to speak with you about a little thing I built, affectionately called "Mengpaneel."
|
12
|
+
|
13
|
+
#### All right, so what is "Mengpaneel?"
|
14
|
+
|
15
|
+
"Mengpaneel" is the Dutch word for "mixing console."
|
16
|
+
|
17
|
+
#### Sigh. What does Mengpaneel _do_?
|
18
|
+
|
19
|
+
Beside the above, "Mengpaneel" is the literal Dutch translation of "[Mixpanel](https://mixpanel.com)," which you'll know as "[t]he most advanced analytics platform ever for mobile and the web."
|
20
|
+
|
21
|
+
Mixpanel is great, but there are some problems you're likely to run into when trying to use it with a large server side web app.
|
22
|
+
|
23
|
+
Mengpaneel aims to address these problems for Ruby on Rails, but the problems and abstract solution apply to any framework.
|
24
|
+
|
25
|
+
#### You've got my attention. What "problems" are these?
|
26
|
+
|
27
|
+
Let me first take a step back and explain how Mixpanel works.
|
28
|
+
|
29
|
+
Put in "Explain Like I'm 5" terms, Mixpanel gathers events that happen in your app and does magic to them.
|
30
|
+
|
31
|
+
Because Mixpanel doesn't know your app like you do, you're responsible for deciding what events to track, tracking these events and finally getting them to Mixpanel. To help you do this, Mixpanel provides tracking libraries for a bunch of common languages.
|
32
|
+
|
33
|
+
For web apps, Mixpanel's preferred tracking library is their client side [JavaScript library](https://mixpanel.com/help/reference/javascript), going as far as actively discouraging use of libraries for [server](https://mixpanel.com/help/reference/ruby) [side](https://mixpanel.com/help/reference/python) [languages](https://mixpanel.com/help/reference/php).
|
34
|
+
|
35
|
+
There's good reason for this. As they [write](https://mixpanel.com/help/reference/ruby),
|
36
|
+
> [the JavaScript library] offers platform-specific features and conveniences that can make Mixpanel implementations much simpler, and can scale naturally to an unlimited number of clients.
|
37
|
+
|
38
|
+
#### Sounds good, so let's check out this JavaScript library!
|
39
|
+
|
40
|
+
All right, here's the documentation:
|
41
|
+
|
42
|
+
> **Sending events**
|
43
|
+
>
|
44
|
+
> Once you have the [setup] snippet in your page, you can track an event by calling `mixpanel.track` with the event name and properties.
|
45
|
+
>
|
46
|
+
> ```js
|
47
|
+
> mixpanel.track(
|
48
|
+
> "Clicked Ad",
|
49
|
+
> { "Banner Color": "Blue" }
|
50
|
+
> );
|
51
|
+
> ```
|
52
|
+
|
53
|
+
#### That doesn't look too hard; let's go and sprinkle `mixpanel.track` calls all over our code!
|
54
|
+
|
55
|
+
If only it were that easy.
|
56
|
+
|
57
|
+
The example events Mixpanel uses in their library documentation ("Clicked Ad," "Played Video," etc.) have been carefully chosen to be events that exclusively happen on the client side—in the browser, where favorite child JavaScript reigns.
|
58
|
+
|
59
|
+
#### Hmm. What then about server side events? Say "Place Order," clearly a database action?
|
60
|
+
|
61
|
+
Mixpanel suggests placing the `mixpanel.track` call on the page the user returns to after the event has happened, i.e. the "Thanks for your order" page. Problem solved!
|
62
|
+
|
63
|
+
#### "Thank you page," you say? It's 2014, my "Sign In" action redirects wherever the user initially tried to go—I don't do "thank you" pages.
|
64
|
+
|
65
|
+
And there you have our first problem.
|
66
|
+
|
67
|
+
If a "Thank you!" or "Success!" page is rendered or if the user is always redirected to the same dedicated page, placing the `mixpanel.track` call there is a fine option. But if you _don't_ know where the user will be redirected to, or if you're redirecting back to something like an "index" page, you don't want to place the track call there.
|
68
|
+
|
69
|
+
If you were clever, you could set a cookie `just_signed_in=true` just before redirecting, and place a check for that value in your app's layout view, but with dozens of different events, that's a slippery slope I don't want to go down.
|
70
|
+
|
71
|
+
Can you think of more examples of situations where Mixpanel's assumptions don't hold so well?
|
72
|
+
|
73
|
+
#### How about my "Create Blog Post" action? My `POST posts` endpoint is used by the website as well as the iPhone app—my action respects the "Accept" header, returning HTML and JSON, respectively.
|
74
|
+
|
75
|
+
If your action can be used as an API returning JSON, the JavaScript tracking library isn't going to be of much use and you're gonna be missing out on events from API users.
|
76
|
+
|
77
|
+
Mixpanel provides libraries for [iOS](https://mixpanel.com/help/reference/ios) and [Android](https://mixpanel.com/help/reference/android), but that doesn't help when the endpoint is used by 3rd party apps that you can hardly expect to send events to your Mixpanel account. Even if you don't allow 3rd party apps, integrating Mixpanel with your mobile app makes it hard to retroactively add events or event properties to your system, because updates to iOS and Android apps take a while to go out, which is gonna screw with your numbers until the app's been approved and every user has upgraded.
|
78
|
+
|
79
|
+
And that's two problems. Can you think of one more?
|
80
|
+
|
81
|
+
#### It's similar to the previous one, but what about my "Complete Payment" event? The endpoint in question is exclusively called by my payment provider to report on payment status, so it doesn't return HTML and the client isn't an app I control with its own Mixpanel library.
|
82
|
+
|
83
|
+
I'm loving this conversation, you seem to know exactly where I'm going without me needing to say a word—it's almost like I'm talking to myself.
|
84
|
+
|
85
|
+
Indeed, the third problem is with isolated endpoints that don't have access to any client side library to track events.
|
86
|
+
|
87
|
+
#### These all seem like very common situations, are you seriously saying Mixpanel is somehow oblivious to this?
|
88
|
+
|
89
|
+
I'm not. Mixpanel is definitely aware of this, which is where the aforementioned [server](https://mixpanel.com/help/reference/ruby) [side](https://mixpanel.com/help/reference/python) [libraries](https://mixpanel.com/help/reference/php) come in.
|
90
|
+
|
91
|
+
As they [write](https://mixpanel.com/help/reference/ruby),
|
92
|
+
> [t]he Mixpanel Ruby library is designed to be used for scripting, or in circumstances when a user isn't directly interacting with your application on the web or a mobile device.
|
93
|
+
|
94
|
+
Indeed, all of the problems you so pointedly pointed out can be solved by simply doing the event tracking from the server side. Instead of `mixpanel.track` calls in your views, you'll be having `mixpanel.track` calls in your controller actions.
|
95
|
+
|
96
|
+
#### So what's the big deal then? Why did I have to read almost a thousand words to reach this conclusion? _Why is Mengpaneel at all?_
|
97
|
+
|
98
|
+
Remember my saying the JavaScript library was Mixpanel's preferred tracking library? Remember my first quote from the Mixpanel documentation? Let me recite it again, because it's been a while:
|
99
|
+
> [the JavaScript library] offers platform-specific features and conveniences that can make Mixpanel implementations much simpler, and can scale naturally to an unlimited number of clients.
|
100
|
+
|
101
|
+
If you move away from the JavaScript library and use the Ruby library everywhere instead, you lose all of that.
|
102
|
+
|
103
|
+
One feature only readily available to the JavaScript library is the ability to link a user's previously anonymous behavior browsing your promotion website to their newly created account when they sign up. How valuable it is to know what your user did _before_ they became part of the priviliged group who decided to actually sign up cannot be overstated.
|
104
|
+
|
105
|
+
Second, information about the user's device, OS and browser is very interesting, but not available to the Ruby library unless you jump through some hoops with user agent parsing on every request.
|
106
|
+
|
107
|
+
Last, but definitely not least: the JavaScript library scales infinitely, while the Ruby library... doesn't. You don't want your server busy sending tens of thousands of events a day to Mixpanel, when it could be serving new (revenue generating!) requests instead.
|
108
|
+
|
109
|
+
#### That makes a lot of sense. I can't believe Mixpanel hasn't properly addressed this. So how does Mengpaneel solve these problems? I'm assuming you want to sell me your magic bullet?
|
110
|
+
|
111
|
+
You've got me :)
|
112
|
+
|
113
|
+
Mengpaneel addresses the problems mentioned by giving you a single way to interact with Mixpanel from your server side app, with Mengpaneel taking it upon itself to make sure everything gets to Mixpanel, using the best strategy available, whether it be client side, server side or something completely different.
|
114
|
+
|
115
|
+
You can call all the "mixpanel.whatever" methods you know and love from the JavaScript library, right from your your Rails controllers, without having to worry about lack of thank you pages, unpredictable redirects, AJAX requests, endpoints with multiple response content types and clients outside your control.
|
116
|
+
|
117
|
+
#### So… How?!
|
118
|
+
|
119
|
+
First, install Mengpaneel by adding it to your Gemfile:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
gem "mengpaneel"
|
123
|
+
# Don't forget to `bundle install`
|
124
|
+
```
|
125
|
+
|
126
|
+
Second, configure Mengpaneel with your Mixpanel token:
|
127
|
+
|
128
|
+
```ruby
|
129
|
+
# config/initializers/mengpaneel.rb
|
130
|
+
|
131
|
+
Mengpaneel.configure do |config|
|
132
|
+
config.token = "abc123" # or use ENV["MIXPANEL_TOKEN"] if you're into 12-factor
|
133
|
+
end
|
134
|
+
```
|
135
|
+
|
136
|
+
Third, include Mengpaneel in the controller(s) you plan to track Mixpanel events from. Include it in your `ApplicationController` if you want to use Mixpanel _everywhere_:
|
137
|
+
|
138
|
+
```ruby
|
139
|
+
class ApplicationController < ActionController::Base
|
140
|
+
include Mengpaneel::Controller
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
Fourth, always identify the currently signed in user with Mixpanel:
|
145
|
+
|
146
|
+
```ruby
|
147
|
+
class ApplicationController < ActionController::Base
|
148
|
+
# ...
|
149
|
+
|
150
|
+
before_action :setup_mixpanel
|
151
|
+
|
152
|
+
private
|
153
|
+
def setup_mixpanel
|
154
|
+
return unless user_signed_in?
|
155
|
+
|
156
|
+
# For technical reasons, you need to do setup from a `mengpaneel.setup` block.
|
157
|
+
# I'll go into those reasons later.
|
158
|
+
mengpaneel.setup do
|
159
|
+
mixpanel.identify(current_user.id)
|
160
|
+
|
161
|
+
mixpanel.people.set(
|
162
|
+
"ID" => current_user.id,
|
163
|
+
"$email" => current_user.email,
|
164
|
+
"$first_name" => current_user.first_name,
|
165
|
+
"$last_name" => current_user.last_name,
|
166
|
+
"$created" => current_user.created_at,
|
167
|
+
"$last_login" => current_user.current_sign_in_at
|
168
|
+
)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
```
|
173
|
+
|
174
|
+
Fifth, let Mixpanel know when an anonymous user got an identity (i.e. signed up):
|
175
|
+
|
176
|
+
```ruby
|
177
|
+
class RegistrationsController < Devise::RegistrationsController
|
178
|
+
# Devise::RegistrationsController automatically extends ApplicationController.
|
179
|
+
|
180
|
+
def create
|
181
|
+
# The Devise::RegistrationsController#create action yields to its caller
|
182
|
+
# so you can easily extend it with custom behaviour, like we do here!
|
183
|
+
super do
|
184
|
+
# We need to make sure signing up actually succeeded.
|
185
|
+
if resource.errors.blank?
|
186
|
+
# Technical reasons again, will get into those later.
|
187
|
+
mengpaneel.before_setup do
|
188
|
+
mixpanel.alias(resource.id)
|
189
|
+
end
|
190
|
+
|
191
|
+
mixpanel.track("Sign Up", "ID" => current_user.id,
|
192
|
+
"Email" => current_user.email,
|
193
|
+
"First name" => current_user.first_name,
|
194
|
+
"Last name" => current_user.last_name)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
```
|
200
|
+
|
201
|
+
Fourth, track Mixpanel events:
|
202
|
+
|
203
|
+
```ruby
|
204
|
+
class SessionsController < Devise::SessionsController
|
205
|
+
def create
|
206
|
+
super do
|
207
|
+
mixpanel.track("Sign In")
|
208
|
+
end
|
209
|
+
end
|
210
|
+
|
211
|
+
def destroy
|
212
|
+
super do
|
213
|
+
mixpanel.track("Sign Out")
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
```
|
218
|
+
|
219
|
+
```ruby
|
220
|
+
class PostsController < ApplicationController
|
221
|
+
respond_to :html, :json
|
222
|
+
|
223
|
+
def create
|
224
|
+
@post = Post.new(post_params)
|
225
|
+
|
226
|
+
respond_with(@post) do |format|
|
227
|
+
if @post.save
|
228
|
+
mixpanel.track("Create Blog Post", "Title" => @post.title)
|
229
|
+
|
230
|
+
format.html do
|
231
|
+
flash[:notice] = "Successfully created blog post!"
|
232
|
+
|
233
|
+
redirect_to post_path(@post)
|
234
|
+
end
|
235
|
+
|
236
|
+
format.json do
|
237
|
+
render json: @post
|
238
|
+
end
|
239
|
+
end
|
240
|
+
end
|
241
|
+
end
|
242
|
+
|
243
|
+
private
|
244
|
+
def post_params
|
245
|
+
params.require(:post).permit(:title, :body)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
class PaymentNotificationsController < ApplicationController
|
252
|
+
before_action :authenticate!
|
253
|
+
|
254
|
+
def notify
|
255
|
+
if params[:status] == "payment_complete"
|
256
|
+
@payment = Payment.find(params[:payment_id])
|
257
|
+
@payment.status = :paid
|
258
|
+
@payment.save!
|
259
|
+
|
260
|
+
mixpanel.track("Complete Payment", "Payment ID" => @payment.id,
|
261
|
+
"Amount" => @payment.amount)
|
262
|
+
end
|
263
|
+
|
264
|
+
response.content_type = "text/plain"
|
265
|
+
render text: "[accepted]"
|
266
|
+
end
|
267
|
+
|
268
|
+
private
|
269
|
+
def authenticate!
|
270
|
+
authenticate_or_request_with_http_basic do |username, password|
|
271
|
+
username == "payments" && password == "ftw"
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
```
|
276
|
+
|
277
|
+
Finally, if you want to track events from a script or background worker instead of a controller, you can use `Mengpaneel::Manager` directly, like this:
|
278
|
+
|
279
|
+
```ruby
|
280
|
+
class SubscriptionRenewalWorker
|
281
|
+
include Sidekiq::Worker
|
282
|
+
|
283
|
+
def perform(subscription_id)
|
284
|
+
subscription = Subscription.find(subscription_id)
|
285
|
+
|
286
|
+
subscription.renew!
|
287
|
+
|
288
|
+
Mengpaneel::Manager.new do |mengpaneel|
|
289
|
+
mengpaneel.setup do |mixpanel|
|
290
|
+
mixpanel.identify(subscription.user.id)
|
291
|
+
end
|
292
|
+
|
293
|
+
# Because the `mixpanel` method exposed in your controllers isn't
|
294
|
+
# available here, you need to get it explicitly from Mengpaneel.
|
295
|
+
mengpaneel.tracking do |mixpanel|
|
296
|
+
mixpanel.track("Renew Subscription", "Subscription ID" => subscription.id)
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
```
|
302
|
+
|
303
|
+
#### No, no, I mean, how does Mengpaneel do all this?
|
304
|
+
|
305
|
+
This is where it gets fun.
|
306
|
+
|
307
|
+
Basically, Mengpaneel works in three stages. In describing them, it's easiest to go from back to front, so let's start with the third and final Mengpaneel stage:
|
308
|
+
|
309
|
+
###### Flush
|
310
|
+
|
311
|
+
In the Flush stage, Mengpaneel makes sure events actually get to Mixpanel.
|
312
|
+
|
313
|
+
As said, Mengpaneel is smart enough to decide by itself how to flush events using the best strategy available, whether it be client side, server side or something completely different.
|
314
|
+
|
315
|
+
Since the problems discussed above all have to do with properties of the incoming request or outgoing response, Mengpaneel waits until you've finished building the response and then chooses a strategy by looking at the request and response.
|
316
|
+
|
317
|
+
In order, these are the strategies considered:
|
318
|
+
|
319
|
+
- `Delayed`: If the response is going to be a redirect, we can't use the JavaScript library to flush events. We could immediately give up and use the Ruby library, but most of the time redirects are inbound so we'll get to a non-redirect page of our app eventually.
|
320
|
+
|
321
|
+
Thus, we delay flushing events for now, saving them in a session to be considered in the next request.
|
322
|
+
|
323
|
+
- `ClientSide`: If the response isn't a redirect, using the JavaScript library is our best option for reasons mentioned earlier. We just need to verify that we're actually in an environment where JavaScript will be executed. That is, the response content type is HTML, we're not being requested using AJAX, we're not being downloaded as an attachment and we're not streaming data.
|
324
|
+
|
325
|
+
If all of these requirements are met, we flush all events by injecting calls to the JavaScript library into the response body.
|
326
|
+
|
327
|
+
- `CapableClientSide`: If injecting the JavaScript calls isn't going to work, our only option is to use the server side Ruby library, right? Well, not quite. Even if we can't get our code to be executed on the client side directly, we _can_ work something out with the client that's calling us, if they're willing and capable.
|
328
|
+
|
329
|
+
In this case, "capable" means that the client calling us is itself in a position to flush events to Mixpanel, and that if the server (that's us) were to give them a list of events they'd like to end up at Mixpanel, they would simply pass them along. "Willing" means they're actually advertising that capability, to be picked up on by the server.
|
330
|
+
|
331
|
+
To advertise this capability, the client adds the `X-Mengpaneel-Flush-Capable` header with value `true` to their request headers. Mengpaneel running on the server will pick up on this, and flush all tracking calls by putting them in a JSON-serialized array in the `X-Mengpaneel-Calls` response header.
|
332
|
+
|
333
|
+
When the client receives this response, it's their responsiblility to actually flush those calls to Mixpanel, by deserializing the header's contents, iterating over the events and calling the appropriate methods on the client side Mixpanel library.
|
334
|
+
|
335
|
+
This is a very useful feature in web apps with a very AJAX-heavy front end or in mobile apps, where most events would otherwise have to be flushed using the server side library but can now be flushed on the client side.
|
336
|
+
|
337
|
+
Mengpaneel comes with a small JavaScript library that does exactly what's described above for jQuery-based web apps. Install it by adding the following code to your app's main JavaScript file, after jQuery:
|
338
|
+
|
339
|
+
```js
|
340
|
+
//= require jquery
|
341
|
+
//= require mengpaneel
|
342
|
+
```
|
343
|
+
|
344
|
+
A library accomplishing the same thing should be trivial to write for iOS or Android.
|
345
|
+
|
346
|
+
- `AsyncServerSide`: And now we've arrived at our final option: using the server side Ruby library. Flush the events to Mixpanel from the same thread where the request is being handled would cause a small slowdown, so we've got one last trick up our sleeve.
|
347
|
+
|
348
|
+
If you have [Sidekiq](http://sidekiq.org/) installed, we'll queue a worker that will flush the events, to be handled by Sidekiq at a later time.
|
349
|
+
|
350
|
+
This asynchronous worker simply delegates to the last available strategy, which is also the one that will be used directly if Sidekiq isn't available, namely:
|
351
|
+
|
352
|
+
- `ServerSide`: And now we're at the _actual_ final option. If none of the other strategies where available, we use the official [`mixpanel-ruby`](https://github.com/mixpanel/mixpanel-ruby) gem to flush the events to Mixpanel right from our server side process.
|
353
|
+
|
354
|
+
At this point, some translation takes place from the JavaScript library API to the Ruby library API, to ensure you can write your controller calls as you would using the JavaScript library, while still doing The Right Thing<sup>TM</sup>.
|
355
|
+
|
356
|
+
And that's end of Flush, by far the most exciting stage in Mengpaneel. More important in the grand scheme of things however, is:
|
357
|
+
|
358
|
+
###### Tracking
|
359
|
+
|
360
|
+
In the Tracking stage, Mengpaneel doesn't actually do all that much. This stage is filled in by your own controller; it's where you call Mixpanel methods like `alias`, `identify`, `people.set` and `track`.
|
361
|
+
|
362
|
+
Mengpaneel's main responsibility is keeping track of all of the Mixpanel calls you make. Since we don't send them to Mixpanel immediately, but wait to do so until the Flush stage, we use a so-called `CallProxy` to pick up all calls so we can store them and handle them later.
|
363
|
+
|
364
|
+
For technical reasons, Mengpaneel does interfere a little in this stage; you've already seen the `mengpaneel.setup` and `mengpaneel.before_setup` calls.
|
365
|
+
|
366
|
+
Because Mengpaneel can delay calls until the next request, we need to make sure calls like `mixpanel.identify` and `mixpanel.people.set` aren't repeated when the next request's `mixpanel_setup` before-action is fired, because this would cause unnecessary requests to be sent to Mixpanel and would cause a flood of these when we're dealing with a chain of multiple redirects, each adding calls onto the one before it.
|
367
|
+
|
368
|
+
We also need to make sure `mixpanel.alias` calls are always flushed _before_ `mixpanel.identify` calls, since they need access to the original anonymous distinct user ID.
|
369
|
+
|
370
|
+
For this reason, Mengpaneel knows three modes, aptly named `before_setup`, `setup` and `tracking`—the default. To temporarily switch to a mode, simply wrap your event-tracking calls in a `mengpaneel.before_setup` or `mengpaneel.setup` block, as shown in the examples I gave before.
|
371
|
+
|
372
|
+
In Flush, calls from these three modes are always called in this order, so `mixpanel.alias` comes before `mixpanel.identify` comes before `mixpanel.track`.
|
373
|
+
|
374
|
+
Additionally, `mengpaneel.setup` overwrites `mengpaneel.setup` calls made earlier, thus preventing the flood of `mixpanel.identify` and `mixpanel.people.set` calls that would happen with delayed calls after a redirect.
|
375
|
+
|
376
|
+
Lastly, explicitly identifying calls as "setup" or "tracking" allows us to optimize the `[Async]ServerSide` strategy by doing nothing if no actual tracking calls were made. If we're not sending events, there's no need to do setup at all.
|
377
|
+
|
378
|
+
With tracking finished, we've arrived at the first stage to be executed and the last stage to be discussed, called:
|
379
|
+
|
380
|
+
###### Replay
|
381
|
+
|
382
|
+
In the Replay stage, Mengpaneel replays previously delayed calls.
|
383
|
+
|
384
|
+
As mentioned under Flush, the `Delayed` strategy delays flushing calls until the next request if the current one is a redirect by saving them in a session.
|
385
|
+
|
386
|
+
Before your controller action is called, Replayer does nothing more than reading this session, iterating over the calls saved therein and calling them in their respective tracking modes, just like you did from inside your controller action in the previous request, thus making sure no delayed calls get lost.
|
387
|
+
|
388
|
+
And there you have it!
|
389
|
+
|
390
|
+
#### Dude. Who are you, I mean, who should I thank for this?
|
391
|
+
|
392
|
+
My name is [Douwe Maan](http://www.douwemaan.com) and I'm a co-founder-slash-developer at [Stinngo](http://www.stinngo.com).
|
393
|
+
|
394
|
+
Besides that, you should thank [Mixpanel](https://mixpanel.com) since without them this project would've been very pointless indeed, as well as gems [event_tracker](https://github.com/doorkeeper/event_tracker) and [analytical](https://github.com/jkrall/analytical) from which I've taken inspiration.
|
395
|
+
|
396
|
+
#### Cool. And I can just, like, use this in my apps?
|
397
|
+
|
398
|
+
Sure, as long as you adhere to the following license:
|
399
|
+
|
400
|
+
> Copyright (c) 2014 Douwe Maan
|
401
|
+
>
|
402
|
+
> MIT License
|
403
|
+
>
|
404
|
+
> Permission is hereby granted, free of charge, to any person obtaining
|
405
|
+
> a copy of this software and associated documentation files (the
|
406
|
+
> "Software"), to deal in the Software without restriction, including
|
407
|
+
> without limitation the rights to use, copy, modify, merge, publish,
|
408
|
+
> distribute, sublicense, and/or sell copies of the Software, and to
|
409
|
+
> permit persons to whom the Software is furnished to do so, subject to
|
410
|
+
> the following conditions:
|
411
|
+
>
|
412
|
+
> The above copyright notice and this permission notice shall be
|
413
|
+
> included in all copies or substantial portions of the Software.
|
414
|
+
>
|
415
|
+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
416
|
+
> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
417
|
+
> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
418
|
+
> NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
419
|
+
> LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
420
|
+
> OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
421
|
+
> WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
(function($, undefined) {
|
2
|
+
var REQUEST_HEADER = "X-Mengpaneel-Flush-Capable";
|
3
|
+
var RESPONSE_HEADER = "X-Mengpaneel-Calls";
|
4
|
+
|
5
|
+
if (!window.mixpanel) return;
|
6
|
+
|
7
|
+
$(document).on("ajaxSend", function(event, xhr, options) {
|
8
|
+
if (options.crossDomain) return;
|
9
|
+
|
10
|
+
xhr.setRequestHeader(REQUEST_HEADER, "true");
|
11
|
+
});
|
12
|
+
|
13
|
+
$(document).on("ajaxComplete", function(event, xhr, options) {
|
14
|
+
if (options.crossDomain) return;
|
15
|
+
|
16
|
+
var rawCalls = xhr.getResponseHeader(RESPONSE_HEADER);
|
17
|
+
if (!rawCalls) return;
|
18
|
+
|
19
|
+
var calls;
|
20
|
+
try {
|
21
|
+
calls = $.parseJSON(rawCalls);
|
22
|
+
}
|
23
|
+
catch (e) {
|
24
|
+
return;
|
25
|
+
}
|
26
|
+
|
27
|
+
for(var i = 0, length = calls.length; i < length; i++) {
|
28
|
+
var call = calls[i];
|
29
|
+
var methodNames = call[0];
|
30
|
+
var args = call[1];
|
31
|
+
|
32
|
+
var object = window.mixpanel;
|
33
|
+
if (!object) return;
|
34
|
+
|
35
|
+
var methodName = methodNames.pop();
|
36
|
+
|
37
|
+
for(var j = 0, length2 = methodNames.length; j < length2; j++) {
|
38
|
+
var name = methodNames[j];
|
39
|
+
|
40
|
+
object = object[name];
|
41
|
+
}
|
42
|
+
|
43
|
+
var method = object[methodName];
|
44
|
+
|
45
|
+
method.apply(object, args);
|
46
|
+
}
|
47
|
+
});
|
48
|
+
})(jQuery);
|
@@ -0,0 +1,51 @@
|
|
1
|
+
module Mengpaneel
|
2
|
+
class CallProxy
|
3
|
+
attr_reader :method_name
|
4
|
+
attr_reader :args
|
5
|
+
attr_reader :calls
|
6
|
+
|
7
|
+
def initialize(method_name = nil, args = [])
|
8
|
+
@method_name = method_name
|
9
|
+
@args = args
|
10
|
+
|
11
|
+
@calls = []
|
12
|
+
end
|
13
|
+
|
14
|
+
def method_missing(method_name, *args)
|
15
|
+
save_call(method_name, *args)
|
16
|
+
end
|
17
|
+
|
18
|
+
def respond_to_missing?(method_name, include_private = false)
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
def full_method_name(prefixes)
|
23
|
+
[*prefixes, *self.method_name]
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_call(prefixes = [])
|
27
|
+
[full_method_name(prefixes), args] if self.method_name
|
28
|
+
end
|
29
|
+
|
30
|
+
def child_calls(prefixes = [])
|
31
|
+
method_name = full_method_name(prefixes)
|
32
|
+
@calls.flat_map { |proxy| proxy.all_calls(method_name) }
|
33
|
+
end
|
34
|
+
|
35
|
+
def all_calls(prefixes = [])
|
36
|
+
calls = []
|
37
|
+
calls << to_call(prefixes) if self.method_name && (@calls.empty? || !@args.empty?)
|
38
|
+
calls += child_calls(prefixes)
|
39
|
+
calls
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
def save_call(method_name, *args)
|
44
|
+
proxy = self.class.new(method_name, args)
|
45
|
+
|
46
|
+
@calls << proxy
|
47
|
+
|
48
|
+
proxy
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "active_support/concern"
|
2
|
+
|
3
|
+
require "mengpaneel/manager"
|
4
|
+
|
5
|
+
module Mengpaneel
|
6
|
+
module Controller
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
prepend_around_action :wrap_in_mengpaneel
|
11
|
+
|
12
|
+
delegate :mixpanel, to: :mengpaneel
|
13
|
+
|
14
|
+
helper_method :mengpaneel, :mixpanel
|
15
|
+
end
|
16
|
+
|
17
|
+
def mengpaneel
|
18
|
+
@mengpaneel ||= Manager.new(self)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
def wrap_in_mengpaneel(&block)
|
23
|
+
mengpaneel.wrap do
|
24
|
+
yield
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Mengpaneel
|
2
|
+
class Delayer
|
3
|
+
SESSION_KEY = "mengpaneel_delayed_calls".freeze
|
4
|
+
|
5
|
+
attr_reader :controller
|
6
|
+
|
7
|
+
def initialize(controller = nil)
|
8
|
+
@controller = controller
|
9
|
+
end
|
10
|
+
|
11
|
+
def load
|
12
|
+
(controller.session[SESSION_KEY] || {}).with_indifferent_access
|
13
|
+
end
|
14
|
+
|
15
|
+
def load!
|
16
|
+
calls = load
|
17
|
+
controller.session.delete(SESSION_KEY)
|
18
|
+
calls
|
19
|
+
end
|
20
|
+
|
21
|
+
def save(all_calls)
|
22
|
+
controller.session[SESSION_KEY] = all_calls
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require "mengpaneel/strategy/delayed"
|
2
|
+
require "mengpaneel/strategy/client_side"
|
3
|
+
require "mengpaneel/strategy/capable_client_side"
|
4
|
+
require "mengpaneel/strategy/async_server_side"
|
5
|
+
require "mengpaneel/strategy/server_side"
|
6
|
+
|
7
|
+
module Mengpaneel
|
8
|
+
class Flusher
|
9
|
+
STRATEGIES = [
|
10
|
+
Strategy::Delayed,
|
11
|
+
Strategy::ClientSide,
|
12
|
+
Strategy::CapableClientSide,
|
13
|
+
Strategy::AsyncServerSide,
|
14
|
+
Strategy::ServerSide
|
15
|
+
]
|
16
|
+
|
17
|
+
attr_reader :manager
|
18
|
+
|
19
|
+
def initialize(manager)
|
20
|
+
@manager = manager
|
21
|
+
end
|
22
|
+
|
23
|
+
def run
|
24
|
+
return unless Mengpaneel.token
|
25
|
+
|
26
|
+
if manager.flushing_strategy
|
27
|
+
strategy = Strategy.const_get(manager.flushing_strategy.to_s.classify)
|
28
|
+
flush_using(strategy)
|
29
|
+
else
|
30
|
+
STRATEGIES.find { |strategy| flush_using(strategy) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
def flush_using(strategy)
|
36
|
+
strategy.new(manager.all_calls, manager.controller).run
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "mengpaneel/call_proxy"
|
2
|
+
require "mengpaneel/replayer"
|
3
|
+
require "mengpaneel/flusher"
|
4
|
+
|
5
|
+
module Mengpaneel
|
6
|
+
class Manager
|
7
|
+
MODES = %i(before_setup setup tracking).freeze
|
8
|
+
|
9
|
+
attr_reader :controller
|
10
|
+
attr_reader :mode
|
11
|
+
|
12
|
+
attr_accessor :flushing_strategy
|
13
|
+
|
14
|
+
def initialize(controller = nil, &block)
|
15
|
+
@controller = controller
|
16
|
+
|
17
|
+
@mode = :tracking
|
18
|
+
|
19
|
+
wrap(&block) if block
|
20
|
+
end
|
21
|
+
|
22
|
+
def wrap(&block)
|
23
|
+
replay_delayed_calls
|
24
|
+
|
25
|
+
if block.arity == 1
|
26
|
+
yield(self)
|
27
|
+
else
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
ensure
|
31
|
+
flush_calls
|
32
|
+
end
|
33
|
+
|
34
|
+
def call_proxy
|
35
|
+
call_proxies[@mode]
|
36
|
+
end
|
37
|
+
alias_method :mixpanel, :call_proxy
|
38
|
+
|
39
|
+
def clear_calls(mode = @mode)
|
40
|
+
call_proxies.delete(mode)
|
41
|
+
end
|
42
|
+
|
43
|
+
def all_calls
|
44
|
+
call_proxies.map { |mode, call_proxy| [mode, call_proxy.all_calls] }.to_h
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_mode(mode, &block)
|
48
|
+
original_mode = @mode
|
49
|
+
@mode = mode
|
50
|
+
|
51
|
+
begin
|
52
|
+
if block.arity == 1
|
53
|
+
yield(mixpanel)
|
54
|
+
else
|
55
|
+
yield
|
56
|
+
end
|
57
|
+
ensure
|
58
|
+
@mode = original_mode
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def before_setup(&block)
|
63
|
+
with_mode(:before_setup, &block)
|
64
|
+
end
|
65
|
+
|
66
|
+
def setup(&block)
|
67
|
+
clear_calls(:setup)
|
68
|
+
with_mode(:setup, &block)
|
69
|
+
end
|
70
|
+
|
71
|
+
def tracking(&block)
|
72
|
+
with_mode(:tracking, &block)
|
73
|
+
end
|
74
|
+
|
75
|
+
def setup?
|
76
|
+
call_proxies[:setup].all_calls.length > 0
|
77
|
+
end
|
78
|
+
|
79
|
+
def replay_delayed_calls
|
80
|
+
Replayer.new(self).run
|
81
|
+
end
|
82
|
+
|
83
|
+
def flush_calls
|
84
|
+
Flusher.new(self).run
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
def call_proxies
|
89
|
+
@call_proxies ||= Hash.new { |proxies, mode| proxies[mode] = CallProxy.new }
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require "mengpaneel/delayer"
|
2
|
+
|
3
|
+
module Mengpaneel
|
4
|
+
class Replayer
|
5
|
+
attr_reader :manager
|
6
|
+
|
7
|
+
def initialize(manager)
|
8
|
+
@manager = manager
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
return unless manager.controller
|
13
|
+
|
14
|
+
delayed_calls = Delayer.new(manager.controller).load!
|
15
|
+
|
16
|
+
Manager::MODES.each do |mode|
|
17
|
+
next unless delayed_calls.has_key?(mode)
|
18
|
+
|
19
|
+
calls = delayed_calls[mode] || []
|
20
|
+
|
21
|
+
manager.send(mode) do
|
22
|
+
replay_calls(calls)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
def replay_calls(calls)
|
29
|
+
proxy = manager.call_proxy
|
30
|
+
|
31
|
+
calls.each do |method_names, args|
|
32
|
+
method_name = method_names.pop
|
33
|
+
|
34
|
+
object = method_names.inject(proxy) { |object, method_name| object.public_send(method_name) }
|
35
|
+
|
36
|
+
object.public_send(method_name, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "mengpaneel/strategy/base"
|
2
|
+
require "mengpaneel/strategy/server_side"
|
3
|
+
|
4
|
+
module Mengpaneel
|
5
|
+
module Strategy
|
6
|
+
class AsyncServerSide < Base
|
7
|
+
def run
|
8
|
+
return false unless self.class.async?
|
9
|
+
|
10
|
+
return true if all_calls[:tracking].blank?
|
11
|
+
|
12
|
+
Worker.perform_async(all_calls, controller.try(:request).try(:remote_ip))
|
13
|
+
|
14
|
+
true
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
def self.async?
|
19
|
+
defined?(::Sidekiq)
|
20
|
+
end
|
21
|
+
|
22
|
+
if async?
|
23
|
+
class Worker
|
24
|
+
include Sidekiq::Worker
|
25
|
+
|
26
|
+
def perform(all_calls, remote_ip = nil)
|
27
|
+
all_calls = all_calls.with_indifferent_access
|
28
|
+
|
29
|
+
Strategy::ServerSide.new(all_calls, nil, remote_ip).run
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Mengpaneel
|
2
|
+
module Strategy
|
3
|
+
class Base
|
4
|
+
attr_reader :all_calls
|
5
|
+
attr_reader :controller
|
6
|
+
|
7
|
+
def initialize(all_calls, controller = nil)
|
8
|
+
@all_calls = all_calls
|
9
|
+
@controller = controller
|
10
|
+
end
|
11
|
+
|
12
|
+
def run
|
13
|
+
raise NotImplementedError
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
require "mengpaneel/strategy/base"
|
2
|
+
|
3
|
+
module Mengpaneel
|
4
|
+
module Strategy
|
5
|
+
class CapableClientSide < Base
|
6
|
+
REQUEST_HEADER = "X-Mengpaneel-Flush-Capable".freeze
|
7
|
+
RESPONSE_HEADER = "X-Mengpaneel-Calls".freeze
|
8
|
+
|
9
|
+
delegate :request, :response, to: :controller, allow_nil: true
|
10
|
+
|
11
|
+
def run
|
12
|
+
return false unless controller
|
13
|
+
return false unless capable?
|
14
|
+
|
15
|
+
return true if all_calls[:tracking].blank?
|
16
|
+
|
17
|
+
response.headers[RESPONSE_HEADER] = JSON.dump(all_calls[:tracking])
|
18
|
+
|
19
|
+
true
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def capable?
|
24
|
+
%w(true 1).include?(request.headers[REQUEST_HEADER])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "mengpaneel/strategy/base"
|
2
|
+
|
3
|
+
module Mengpaneel
|
4
|
+
module Strategy
|
5
|
+
class ClientSide < Base
|
6
|
+
SETUP_CODE = <<-CODE.strip_heredoc
|
7
|
+
(function(f,b){if(!b.__SV){var a,e,i,g;window.mixpanel=b;b._i=[];b.init=function(a,e,d){function f(b,h){var a=h.split(".");2==a.length&&(b=b[a[0]],h=a[1]);b[h]=function(){b.push([h].concat(Array.prototype.slice.call(arguments,0)))}}var c=b;"undefined"!==typeof d?c=b[d]=[]:d="mixpanel";c.people=c.people||[];c.toString=function(b){var a="mixpanel";"mixpanel"!==d&&(a+="."+d);b||(a+=" (stub)");return a};c.people.toString=function(){return c.toString(1)+".people (stub)"};i="disable track track_pageview track_links track_forms register register_once alias unregister identify name_tag set_config people.set people.set_once people.increment people.append people.track_charge people.clear_charges people.delete_user".split(" ");
|
8
|
+
for(g=0;g<i.length;g++)f(c,i[g]);b._i.push([a,e,d])};b.__SV=1.2;a=f.createElement("script");a.type="text/javascript";a.async=!0;a.src="//cdn.mxpnl.com/libs/mixpanel-2.2.min.js";e=f.getElementsByTagName("script")[0];e.parentNode.insertBefore(a,e)}})(document,window.mixpanel||[]);
|
9
|
+
CODE
|
10
|
+
|
11
|
+
delegate :request, :response, :env, to: :controller, allow_nil: true
|
12
|
+
|
13
|
+
def run
|
14
|
+
return false unless controller
|
15
|
+
return false unless client_side?
|
16
|
+
|
17
|
+
response_body = response.body
|
18
|
+
|
19
|
+
head_end = response_body.index("</head")
|
20
|
+
return false unless head_end
|
21
|
+
|
22
|
+
if source = source_for_head
|
23
|
+
response_body.insert(head_end, source)
|
24
|
+
end
|
25
|
+
|
26
|
+
body_end = response_body.index("</body")
|
27
|
+
return false unless body_end
|
28
|
+
|
29
|
+
if source = source_for_body
|
30
|
+
response_body.insert(body_end, source)
|
31
|
+
end
|
32
|
+
|
33
|
+
response.body = response_body
|
34
|
+
|
35
|
+
true
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
def source_for_head
|
40
|
+
[
|
41
|
+
%{<script type="text/javascript">},
|
42
|
+
SETUP_CODE,
|
43
|
+
|
44
|
+
%{mixpanel.init(#{Mengpaneel.token.to_json});},
|
45
|
+
|
46
|
+
*javascript_calls(:before_setup),
|
47
|
+
*javascript_calls(:setup),
|
48
|
+
%{</script>}
|
49
|
+
].join("\n")
|
50
|
+
end
|
51
|
+
|
52
|
+
def source_for_body
|
53
|
+
return if all_calls[:tracking].blank?
|
54
|
+
|
55
|
+
[
|
56
|
+
%{<script type="text/javascript">},
|
57
|
+
*javascript_calls(:tracking),
|
58
|
+
%{</script>}
|
59
|
+
].join("\n")
|
60
|
+
end
|
61
|
+
|
62
|
+
def javascript_calls(mode)
|
63
|
+
calls = all_calls[mode] || []
|
64
|
+
|
65
|
+
calls.map do |method_names, args|
|
66
|
+
method_name = method_names.join(".")
|
67
|
+
arguments = args.map(&:to_json).join(", ")
|
68
|
+
|
69
|
+
%{mixpanel.#{method_name}(#{arguments});}
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def client_side?
|
74
|
+
html? && !request.xhr? && !attachment? && !streaming?
|
75
|
+
end
|
76
|
+
|
77
|
+
def html?
|
78
|
+
response.content_type && response.content_type.include?("text/html")
|
79
|
+
end
|
80
|
+
|
81
|
+
def attachment?
|
82
|
+
response.headers["Content-Disposition"].to_s.include?("attachment")
|
83
|
+
end
|
84
|
+
|
85
|
+
def streaming?
|
86
|
+
return false unless defined?(ActionController::Live)
|
87
|
+
|
88
|
+
env["action_controller.instance"].class.included_modules.include?(ActionController::Live)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "mengpaneel/strategy/base"
|
2
|
+
require "mengpaneel/delayer"
|
3
|
+
|
4
|
+
module Mengpaneel
|
5
|
+
module Strategy
|
6
|
+
class Delayed < Base
|
7
|
+
def run
|
8
|
+
return false unless controller
|
9
|
+
return false unless redirect?
|
10
|
+
|
11
|
+
Delayer.new(controller).save(all_calls)
|
12
|
+
|
13
|
+
true
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
def redirect?
|
18
|
+
(300...400).include?(controller.response.status)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require "mengpaneel/strategy/base"
|
2
|
+
require "mengpaneel/tracker"
|
3
|
+
|
4
|
+
module Mengpaneel
|
5
|
+
module Strategy
|
6
|
+
class ServerSide < Base
|
7
|
+
def initialize(all_calls, controller = nil, remote_ip = nil)
|
8
|
+
super(all_calls, controller)
|
9
|
+
|
10
|
+
@remote_ip = remote_ip || controller.try(:request).try(:remote_ip)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
return true if all_calls[:tracking].blank?
|
15
|
+
|
16
|
+
perform_calls(:before_setup)
|
17
|
+
perform_calls(:setup)
|
18
|
+
perform_calls(:tracking)
|
19
|
+
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
def tracker
|
25
|
+
@tracker ||= Tracker.new(Mengpaneel.token, @remote_ip)
|
26
|
+
end
|
27
|
+
|
28
|
+
def perform_calls(mode)
|
29
|
+
calls = all_calls[mode] || []
|
30
|
+
|
31
|
+
calls.each do |method_names, args|
|
32
|
+
method_name = method_names.pop
|
33
|
+
|
34
|
+
object = method_names.inject(tracker) { |object, method_name| object.public_send(method_name) }
|
35
|
+
|
36
|
+
object.public_send(method_name, *args)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
require "active_support/hash_with_indifferent_access"
|
2
|
+
require "mixpanel-ruby"
|
3
|
+
|
4
|
+
module Mengpaneel
|
5
|
+
class Tracker < Mixpanel::Tracker
|
6
|
+
attr_reader :token
|
7
|
+
attr_reader :remote_ip
|
8
|
+
attr_reader :distinct_id
|
9
|
+
|
10
|
+
def initialize(token, remote_ip = nil)
|
11
|
+
super(token)
|
12
|
+
@people = People.new(self)
|
13
|
+
|
14
|
+
@remote_ip = remote_ip
|
15
|
+
|
16
|
+
@disable_all_events = false
|
17
|
+
@disabled_events = []
|
18
|
+
|
19
|
+
@properties = HashWithIndifferentAccess.new
|
20
|
+
@properties["ip"] = @remote_ip if @remote_ip
|
21
|
+
end
|
22
|
+
|
23
|
+
def push(item)
|
24
|
+
method_name, args = item
|
25
|
+
send(method_name, *args)
|
26
|
+
end
|
27
|
+
|
28
|
+
def disable(events = nil)
|
29
|
+
if events
|
30
|
+
@disabled_events += events
|
31
|
+
else
|
32
|
+
@disable_all_events = true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def identify(distinct_id)
|
37
|
+
@distinct_id = distinct_id
|
38
|
+
end
|
39
|
+
|
40
|
+
def register(properties)
|
41
|
+
@properties.merge!(properties)
|
42
|
+
end
|
43
|
+
|
44
|
+
def register_once(properties, default_value = "None")
|
45
|
+
@properties.merge!(properties) do |key, oldval, newval|
|
46
|
+
oldval.nil? || oldval == default_value ? newval : oldval
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def unregister(property)
|
51
|
+
@properties.delete(property)
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_property(property)
|
55
|
+
@properties[property]
|
56
|
+
end
|
57
|
+
|
58
|
+
def track(event, properties = {})
|
59
|
+
return if @disable_all_events || @disabled_events.include?(event)
|
60
|
+
|
61
|
+
properties = @properties.merge(properties)
|
62
|
+
|
63
|
+
super(@distinct_id, event, properties)
|
64
|
+
end
|
65
|
+
|
66
|
+
%i(track_links track_forms alias set_config get_config).each do |name|
|
67
|
+
define_method(name) do |*args|
|
68
|
+
# Not supported on server side
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
class People < Mixpanel::People
|
73
|
+
attr_reader :tracker
|
74
|
+
|
75
|
+
def initialize(tracker)
|
76
|
+
@tracker = tracker
|
77
|
+
|
78
|
+
super(tracker.token)
|
79
|
+
end
|
80
|
+
|
81
|
+
%i(set set_once increment append track_charge clear_charges delete_user).each do |method_name|
|
82
|
+
define_method(method_name) do |*args|
|
83
|
+
args.unshift(tracker.distinct_id) unless args.first == tracker.distinct_id
|
84
|
+
super(*args)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def update(message)
|
89
|
+
message["$ip"] = tracker.remote_ip
|
90
|
+
|
91
|
+
super(message)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
data/lib/mengpaneel.rb
ADDED
data/mengpaneel.gemspec
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
$:.unshift File.expand_path("../lib", __FILE__)
|
2
|
+
require "mengpaneel/version"
|
3
|
+
|
4
|
+
Gem::Specification.new do |spec|
|
5
|
+
spec.name = "mengpaneel"
|
6
|
+
spec.version = Mengpaneel::VERSION
|
7
|
+
spec.author = "Douwe Maan"
|
8
|
+
spec.email = "douwe@selenight.nl"
|
9
|
+
spec.summary = "Mengpaneel makes Mixpanel a breeze to use in Rails apps."
|
10
|
+
spec.description = "Mengpaneel gives you a single way to interact with Mixpanel from your Rails controllers, with Mengpaneel taking it upon itself to make sure everything gets to Mixpanel."
|
11
|
+
spec.homepage = "https://github.com/DouweM/mengpaneel"
|
12
|
+
spec.license = "MIT"
|
13
|
+
|
14
|
+
spec.files = `git ls-files -z`.split("\x0")
|
15
|
+
spec.require_paths = ["lib"]
|
16
|
+
|
17
|
+
spec.add_dependency "activesupport"
|
18
|
+
spec.add_dependency "mixpanel-ruby"
|
19
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
20
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
21
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mengpaneel
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Douwe Maan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-10-30 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: activesupport
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: mixpanel-ruby
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.7'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.7'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
description: Mengpaneel gives you a single way to interact with Mixpanel from your
|
70
|
+
Rails controllers, with Mengpaneel taking it upon itself to make sure everything
|
71
|
+
gets to Mixpanel.
|
72
|
+
email: douwe@selenight.nl
|
73
|
+
executables: []
|
74
|
+
extensions: []
|
75
|
+
extra_rdoc_files: []
|
76
|
+
files:
|
77
|
+
- ".gitignore"
|
78
|
+
- Gemfile
|
79
|
+
- LICENSE.txt
|
80
|
+
- README.md
|
81
|
+
- Rakefile
|
82
|
+
- app/assets/javascripts/mengpaneel.js
|
83
|
+
- lib/mengpaneel.rb
|
84
|
+
- lib/mengpaneel/call_proxy.rb
|
85
|
+
- lib/mengpaneel/controller.rb
|
86
|
+
- lib/mengpaneel/delayer.rb
|
87
|
+
- lib/mengpaneel/engine.rb
|
88
|
+
- lib/mengpaneel/flusher.rb
|
89
|
+
- lib/mengpaneel/manager.rb
|
90
|
+
- lib/mengpaneel/replayer.rb
|
91
|
+
- lib/mengpaneel/strategy/async_server_side.rb
|
92
|
+
- lib/mengpaneel/strategy/base.rb
|
93
|
+
- lib/mengpaneel/strategy/capable_client_side.rb
|
94
|
+
- lib/mengpaneel/strategy/client_side.rb
|
95
|
+
- lib/mengpaneel/strategy/delayed.rb
|
96
|
+
- lib/mengpaneel/strategy/server_side.rb
|
97
|
+
- lib/mengpaneel/tracker.rb
|
98
|
+
- lib/mengpaneel/version.rb
|
99
|
+
- mengpaneel.gemspec
|
100
|
+
homepage: https://github.com/DouweM/mengpaneel
|
101
|
+
licenses:
|
102
|
+
- MIT
|
103
|
+
metadata: {}
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: '0'
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubyforge_project:
|
120
|
+
rubygems_version: 2.2.2
|
121
|
+
signing_key:
|
122
|
+
specification_version: 4
|
123
|
+
summary: Mengpaneel makes Mixpanel a breeze to use in Rails apps.
|
124
|
+
test_files: []
|