mengpaneel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,6 @@
1
+ .DS_Store
2
+ *.gem
3
+ .bundle
4
+ Gemfile.lock
5
+ pkg
6
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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,4 @@
1
+ module Mengpaneel
2
+ class Engine < Rails::Engine
3
+ end
4
+ 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
@@ -0,0 +1,3 @@
1
+ module Mengpaneel
2
+ VERSION = "0.0.1"
3
+ end
data/lib/mengpaneel.rb ADDED
@@ -0,0 +1,11 @@
1
+ require "mengpaneel/version"
2
+ require "mengpaneel/engine"
3
+ require "mengpaneel/controller"
4
+
5
+ module Mengpaneel
6
+ mattr_accessor :token
7
+
8
+ def self.configure(&block)
9
+ yield(self)
10
+ end
11
+ end
@@ -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: []