meta_events 1.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: 17804195b940669ecc8da4161a1f15c0027e8a61
4
+ data.tar.gz: 1c8769fcc9213b3314429b982b3e039dd48a2e19
5
+ SHA512:
6
+ metadata.gz: 403256b58246ab75b6f935d2e3379a88b175730340c1dd9ba9fb26cae13ac0e6dd0ede0946675523ec154f8b912da898e7eb7280381a6f34c05df20845caed5b
7
+ data.tar.gz: 947da714267904239fb3bc510ded6ad3b8c528a4266b99c2d8db34eb5ef0a9d8fafd768f2ba090955427ffcc0b15383484d35e506a1bda0efc0d8a02c5bd4e11
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ rvm:
2
+ - "2.1.0"
3
+ - "2.0.0"
4
+ - "1.9.3"
5
+ - "1.8.7"
6
+ - "jruby-1.7.9"
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in meta_events.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Andrew Geweke
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,591 @@
1
+ `MetaEvents` is a Ruby gem that sits on top of a user-centric analytics system like
2
+ [Mixpanel](https://www.mixpanel.com/) and provides structure, documentation, and a historical record to events,
3
+ and a powerful properties system that makes it easy to pass large numbers of consistent properties with your events.
4
+
5
+ MetaEvents supports:
6
+
7
+ * Ruby 1.8.7, 1.9.3, 2.0.0, 2.1.0, or JRuby 1.7.9
8
+
9
+ These are, however, just the versions it's tested against; MetaEvents contains no code that should be at all
10
+ particularly dependent on exact Ruby versions, and should be compatible with a broad set of versions.
11
+
12
+ Current build status: ![Current Build Status](https://api.travis-ci.org/swiftype/meta_events.png?branch=master)
13
+
14
+ ### Background
15
+
16
+ Sending user-centric events to (_e.g._) Mixpanel is far from difficult; it's a single method call. However, in a
17
+ large project, adding calls to Mixpanel all over eventually starts causing issues:
18
+
19
+ * Understanding what, exactly, an event is tracking, including when it was introduced and when it was changed, is
20
+ _paramount_ to doing correct analysis. But once events have been around for a while, whoever put them there has
21
+ long-since forgotten (or they may even not be around any more), and trying to understand what `User Upgraded Account`
22
+ means, eighteen months later, involves an awful lot of spelunking. (Why did it suddenly triple, permanently, on
23
+ February 19th? Is that because we changed what the event means or because we improved the product?)
24
+ * Getting a holistic view of what events there are and how they interact becomes basically impossible; all you can do
25
+ is look at the output (_i.e._, Mixpanel) and hope you can put the pieces together from there.
26
+ * Critical to using Mixpanel well is to pass lots and lots of properties; engineers being the lazy folks that we are,
27
+ we often don't do this, and, when we do, they're named inconsistently and may mean different things in different
28
+ places.
29
+ * Often you want certain properties of the currently-logged-in user (for example) passed on every single event, and
30
+ there's not always a clean way to do this.
31
+
32
+ ### MetaEvents
33
+
34
+ `MetaEvents` helps solve this problem by adding a few critical features:
35
+
36
+ 1. The **MetaEvents DSL** requires developers to declare and document events as they add them (and if they don't, they
37
+ can't fire them); this is quick and easy, but enormously powerful as it gives you a holistic view of your events, a
38
+ historical record, and detailed documentation on each one.
39
+ 1. **Object properties support** means you can define the set of event properties an object in your system (like a
40
+ User) should expose, and then simply pass that object in your event — this makes it vastly easier to include
41
+ lots of properties, and be consistent about them.
42
+ 1. **Implicit properties support** means you can add contextual properties (like the currently-logged-in user) in a
43
+ single place, and then have every event include those properties.
44
+ 1. **Front-end integration** lets you very easily track events from DOM elements (like links) using JavaScript, and
45
+ use a powerful mechanism to fire front-end events in any way you want.
46
+
47
+ # Getting Started
48
+
49
+ Let's get started. We'll assume we're working in a Rails project, although MetaEvents has no dependencies on Rails or any other particular framework. We'll also assume you've installed the MetaEvents gem (ideally via your `Gemfile`).
50
+
51
+ ### Declaring Events
52
+
53
+ First, let's declare an event that we want to fire. Create `config/meta_events.rb` (MetaEvents automatically
54
+ configures this as your events file if you're using Rails; if not, use `MetaEvents::Tracker.default_definitions =` to
55
+ set the path to whatever file you like):
56
+
57
+ global_events_prefix :ab
58
+
59
+ version 1, "2014-02-04" do
60
+ category :user do
61
+ event :signed_up, "2014-02-04", "user creates a brand-new account"
62
+ end
63
+ end
64
+
65
+ Let's walk through this:
66
+
67
+ * `global_events_prefix` is a short string that gets added before every single event; this helps discriminate events
68
+ coming from MetaEvents from events coming from other systems. Choose this carefully, don't ever change it, and keep
69
+ it short — most tools, like Mixpanel, have limited screen real estate for displaying event names.
70
+ * `version 1` defines a version of _your entire events system_; this is useful in the case where you want to rework
71
+ the entire set of events you fire — which is not an uncommon thing. But, for a while, we'll only need a single
72
+ version, and we'll call it 1.
73
+ * `2014-02-04` is when this version first was used; this can be any date (and time, if you _really_ want to be precise)
74
+ that you want — it just has to be parseable by Ruby's `Time.parse` method. (MetaEvents never, ever compares
75
+ this date to `Time.now` or otherwise uses it; it's just for documentation.)
76
+ * `category :user` is just a grouping and namespacing of events; the category name is included in every event name
77
+ when fired.
78
+ * `event :signed_up` declares an event with a name; `2014-02-04` is required and is the date (and time) that this
79
+ event was introduced. (Again, this is just for documentation purposes.) `user creates a brand-new account` is also
80
+ just for documentation purposes (and also is required), and describes the exact purpose of this event.
81
+
82
+ ### Firing Events
83
+
84
+ To fire an event, we need an instance of `MetaEvents::Tracker`. For reasons to be explained shortly, we'll want an
85
+ instance of this class to be created at a level where we may have things in common (like the current user) — so,
86
+ in a Rails application, our `ApplicationController` is a good place. We need to pass it the _distinct ID_ of the user
87
+ that's signed in, which is almost always just the primary key from the `users` table — or `nil` if no user is
88
+ currently signed in. We also pass it the IP address of the user (which can safely be `nil`); Mixpanel, for example,
89
+ uses this for doing geolocation of users:
90
+
91
+ class ApplicationController < ActionController::Base
92
+ ...
93
+ def event_tracker
94
+ @event_tracker ||= MetaEvents::Tracker.new(current_user.try(:id), request.remote_ip)
95
+ end
96
+ ...
97
+ end
98
+
99
+ Now, from the controller, we can fire an event and pass a couple of properties:
100
+
101
+ class UsersController < ApplicationController
102
+ ...
103
+ def create
104
+ ...
105
+ event_tracker.event!(:user, :signed_up, { :user_gender => @new_user.gender, :user_age => @new_user.age })
106
+ ...
107
+ end
108
+ ...
109
+ end
110
+
111
+ We're just about all done; but, right now, the event isn't actually going anywhere, because we haven't configured any
112
+ _event receivers_.
113
+
114
+ ### Hooking Up Mixpanel and a Test Receiver
115
+
116
+ An _event receiver_ is any object that responds to a method `#track(distinct_id, event_name, event_properties)`, where
117
+ `distinct_id` is the distinct ID of the user, `event_name` is a `String` and `event_name` is a Hash mapping `String`
118
+ property names to simple scalar values &mdash; `true`, `false`, `nil`, numbers (all `Numeric`s, including both
119
+ integers and floating-point numbers, are supported), `String`s (and `Symbol`s will be converted to `String`s
120
+ transparently), and `Time` objects.
121
+
122
+ Fortunately, the [Mixpanel](https://github.com/mixpanel/mixpanel-ruby) Gem complies with this interface perfectly.
123
+ So, in `config/environments/production.rb` (or any other file that loads before your first event gets fired):
124
+
125
+ MetaEvents::Tracker.default_event_receivers << Mixpanel::Tracker.new("0123456789abcdef")
126
+
127
+ (where `0123456789abcdef` is actually your Mixpanel API token)
128
+
129
+ In our development environment, we may or may not want to include Mixpanel itself (so we can either add or not add the
130
+ Mixpanel event receiver, above); however, we might also want to print events to the console or some other file as
131
+ they are fired. So, in `config/environments/development.rb`:
132
+
133
+ MetaEvents::Tracker.default_event_receivers << MetaEvents::TestReceiver.new
134
+
135
+ This will print events as they are fired to your Rails log (_e.g._, `log/development.log`); you can pass an argument
136
+ to the constructor of `TestReceiver` that's a `Logger`, an `IO` (_e.g._, `STDOUT`, `STDERR`, an open `File` object),
137
+ or a block (or anything responding to `call`), if you want it to go elsewhere.
138
+
139
+ ### Testing It Out
140
+
141
+ Now, when you fire an event, you should get output like this in your Rails log:
142
+
143
+ Tracked event: user 483123, "ab1_user_signed_up"
144
+ user_age: 27
145
+ user_gender: female
146
+
147
+ ...and, if you have configured Mixpanel properly, it will have been sent to Mixpanel, too!
148
+
149
+ ### Firing Front-End Events
150
+
151
+ Generally speaking, firing events from the back end (your application server talking to Mixpanel or some other service
152
+ directly) is more reliable, while firing events from the front end (JavaScript in your users' browsers talking to
153
+ Mixpanel or some other service) is more scalable &mdash; so you may wish to fire events from the front end, too.
154
+ Further, there are certain events (scrolling, JavaScript manipulation in the browser, and so on) that simply don't
155
+ exist on the back end and can't be tracked from there &mdash; at least, not without adding calls back to your server
156
+ from the front-end JavaScript.
157
+
158
+ **IMPORTANT**: In case it isn't obvious, _any property you include in a front-end event is visible to your users_.
159
+ No matter what tricks you might include to obscure that data, it fundamentally will be present on your users' computers
160
+ and thus visible to them if they want to take a look. This is no different than the situation would be without
161
+ MetaEvents, but, because MetaEvents makes it so easy to add large amounts of properties (which is a good thing!),
162
+ you should take extra care with your `#to_event_properties` methods once you start firing front-end events.
163
+
164
+ You can fire front-end events with MetaEvents in two ways: _auto-tracking_ and _frontend events_. Both methods require
165
+ the use of Rails (because `MetaEvents::ControllerMethods` is intended for use with `ActionController`, and
166
+ `MetaEvents::Helpers` is intended for use with `ActionView`), although the techniques are generally applicable and
167
+ easy enough to use with any framework.
168
+
169
+ #### Auto-Tracking
170
+
171
+ Auto-tracking is the easiest way of triggering front-end events. MetaEvents provides a Rails helper method that adds
172
+ certain attributes to any DOM element you wish (like a link); it then provides a JavaScript function that automatically
173
+ picks up these attributes, decodes them, and calls any function you want with them.
174
+
175
+ As an example, in a view, you simply convert:
176
+
177
+ <%= link_to("go here", user_awesome_path, :class => "my_class") %>
178
+
179
+ ...to:
180
+
181
+ <%= meta_events_tracked_link_to("go here", user_awesome_path, :class => "my_class",
182
+ :meta_event => { :category => :user, :event => :awesome,
183
+ :properties => { :color => 'green' } }) %>
184
+
185
+ (Not immediately obvious: the `:meta_event` attribute is just part of the `html_options` `Hash` that
186
+ `link_to` accepts, not an additional parameter. `meta_events_tracked_link_to` accepts exactly the same parameters as
187
+ `link_to`.)
188
+
189
+ This automatically turns the generated HTML from:
190
+
191
+ <a href="/users/awesome" class="my_class">go here</a>
192
+
193
+ to something like this:
194
+
195
+ <a href="/users/awesome" class="my_class mejtp_trk" data-mejtp-event="ab1_user_awesome"
196
+ data-mejtp-prp="{&quot;ip&quot;:&quot;127.0.0.1&quot;,&quot;color&quot;:&quot;green&quot;,&quot;implicit_prop_1&quot;:&quot;someValue&quot;}">go here</a>
197
+
198
+ `mejtp` stands for "MetaEvents JavaScript Tracking Prefix", and is simply a likely-unique prefix for these values.
199
+ (You can change it with `MetaEvents::Helpers.meta_events_javascript_tracking_prefix 'foo'`.) `mejtp_trk` is the class
200
+ that allows us to easily detect which elements are set up for tracking; the two data attributes pass the full name
201
+ of the event, and a JSON-encoded string of all the properties (both implicit and explicit) to pass with the event.
202
+
203
+ Now, add this to a Javascript file in your application:
204
+
205
+ //= require meta_events
206
+
207
+ And, finally, call something like this:
208
+
209
+ $(document).ready(function() {
210
+ MetaEvents.forAllTrackableElements(document, function(id, element, eventName, properties) {
211
+ mixpanel.track_links("#" + id, eventName, properties);
212
+ })
213
+ });
214
+
215
+ `MetaEvents.forAllTrackableElements` accepts a root element to start searching at, and a callback function. It finds
216
+ all elements with class `mejtp_trk` on them underneath that element, extracts the event name and properties, and adds
217
+ a generated DOM ID to that element if it doesn't have one already. It then calls your callback function, passing that
218
+ (existing or generated) DOM ID, the element itself, the name of the event, and the full set of properties (decoded, as
219
+ a JavaScript Object here). You can then (as above) easily use this to do anything you want, like telling Mixpanel to
220
+ track that link properly.
221
+
222
+ `forAllTrackableElements` also sets a certain data attribute on each element as it processes it, and knows to skip
223
+ elements that already have that attribute set, so it's safe to call as often as you wish &mdash; for example, if
224
+ the DOM changes. It does _not_ know when the DOM changes, however, so, if you add content to your page, you will
225
+ need to re-call it.
226
+
227
+ #### Frontend Events
228
+
229
+ Use Frontend Events only if Auto-Tracking isn't flexible enough for your purposes; Auto-Tracking is simpler in
230
+ most ways.
231
+
232
+ Because MetaEvents leverages the events DSL to define events, and calls methods on your Ruby models (and other objects)
233
+ to create large numbers of properties, you cannot simply fire an event by name from the front-end without a _little_
234
+ extra work &mdash; otherwise, how would we get those properties? However, it's not much more work.
235
+
236
+ First off, make sure you get this into your layout in a `<script>` tag somewhere &mdash; at the bottom of the page is
237
+ perfectly fine:
238
+
239
+ <%= meta_events_frontend_events_javascript %>
240
+
241
+ This allows MetaEvents to pass event data properly from the backend to the frontend for any events you'll be firing.
242
+
243
+ Now, as an example, let's imagine we implement a JavaScript game on our site, and want to fire events when the user
244
+ wins, loses, or gets a new high score. First, let's define those in our DSL:
245
+
246
+ global_events_prefix :ab
247
+
248
+ version 1, "2014-02-11" do
249
+ category :jsgame do
250
+ event :won, "2014-02-11", "user won a game!"
251
+ event :lost, "2014-02-11", "user lost a game"
252
+ event :new_high_score, "2014-02-11", "user got a new high score"
253
+ end
254
+ end
255
+
256
+ Now, in whatever controller action renders the page that the game is on, we need to _register_ these events. This
257
+ tells the front-end integration that we might fire them from the resulting page; it therefore embeds JavaScript in the
258
+ page that defines the set of properties for those events, so that the front end has access to the data it needs:
259
+
260
+ class GameController < ApplicationController
261
+ def game
262
+ ...
263
+ meta_events_define_frontend_event(:jsgame, :won, { :winning_streak => current_winning_streak })
264
+ meta_events_define_frontend_event(:jsgame, :lost, { :losing_streak => current_losing_streak })
265
+ meta_events_define_frontend_event(:jsgame, :new_high_score, { :previous_high_score => current_high_score })
266
+ ...
267
+ end
268
+ end
269
+
270
+ This will allow us to make the following calls in the frontend, from our game code:
271
+
272
+ if (wonGame) {
273
+ MetaEvents.event('jsgame_won');
274
+ } else {
275
+ MetaEvents.event('jsgame_lost');
276
+ }
277
+
278
+ if (currentScore > highScore) {
279
+ MetaEvents.event('jsgame_new_high_score', { score: currentScore });
280
+ }
281
+
282
+ What's happened here is that `meta_events_define_frontend_event` took the set of properties you passed, merged them
283
+ with any implicit properties defined, and passed them to the frontend via the `meta_events_frontend_events_javascript`
284
+ output we added above. It binds each event to an _event alias_, which, by default, is just the category name and the
285
+ event name, joined with an underscore. So when you call `MetaEvents.event`, it simply takes the string you pass it,
286
+ looks up the event stored under that alias, merges any properties you supply with the ones passed from the backend,
287
+ and fires it off. (You can, in fact, supply as many additional JavaScript objects/hashes as you want after the
288
+ event alias; they will all be merged together, along with the properties supplied by the backend.)
289
+
290
+ ##### Aliasing Event Names
291
+
292
+ If you need to be able to fire the exact same event with _different_ sets of properties from different places in a
293
+ single page, you can alias the event using the `:name` property:
294
+
295
+ class GameController < ApplicationController
296
+ def game
297
+ ...
298
+ meta_events_define_frontend_event(:jsgame, :paused_game, { :while => :winning }, { :name => :paused_while_winning })
299
+ meta_events_define_frontend_event(:jsgame, :paused_game, { :while => :losing }, { :name => :paused_while_losing })
300
+ ...
301
+ end
302
+ end
303
+
304
+ ...
305
+ if (winning) {
306
+ MetaEvents.event('paused_while_winning');
307
+ } else {
308
+ MetaEvents.event('paused_while_losing');
309
+ }
310
+
311
+ Both calls from the JavaScript will fire the event `ab1_jsgame_paused_game`, but one of them will pass
312
+ `while: 'winning'`, and the other `while: 'losing'`.
313
+
314
+ ##### Definition Cycle
315
+
316
+ Calls to `meta_events_define_frontend_event` get aggregated on the current controller object, during the request
317
+ cycle. If you have events that can get fired on any page, then, for example, use a `before_filter` to always
318
+ define them, or a method you mix in and call, or any other mechanism you want.
319
+
320
+ ##### The Frontend Events Handler
321
+
322
+ `MetaEvents.event` calls the current _frontend event handler_ on the `MetaEvents` JavaScript object; by default this
323
+ just calls `mixpanel.track`. By calling `MetaEvents.setEventHandler(myFunction)`, you can set it to anything you want;
324
+ it gets passed the fully-qualified event name and set of all properties.
325
+
326
+ ### More About Distinct IDs
327
+
328
+ We glossed over the discussion of the distinct ID above. In short, it is a unique identifier (of no particular format;
329
+ both Strings and integers are acceptable) that is unique to the user in question, based on your application's
330
+ definition of 'user'. Using the primary key from your `users` table is typically a great way to do it.
331
+
332
+ There are a few situations where you need to take special care, however:
333
+
334
+ * **What about visitors who aren't signed in yet?** In this case, you will want to generate a unique ID and assign it
335
+ to the visitor anyway; generating a very large random number and putting it in a cookie in their browser is a good
336
+ way to do this, as well as using something like nginx's `ngx_http_userid_module`.
337
+ (Note that Mixpanel has facilities to do this automatically; however, it uses cookies set on their
338
+ domain, which means you can't read them, which limits it unacceptably &mdash; server-side code and even your own
339
+ Javascript will be unable to use this ID.)
340
+ * **What do I do when a user logs in?** Typically, you simply want to switch completely from using their old
341
+ (cookie-based) unique ID to using the primary key of your `users` table (or whatever you use for tracking logged-in
342
+ users). This may seem counterintuitive, but it makes sense, particularly in broad consumer applications: until
343
+ someone logs in, all you really know is which _browser_ is hitting your site, not which _user_. Activity that happens
344
+ in the signed-out state might be the user who eventually logs in...but it also might not be, in the case of shared
345
+ machines; further, activity that happens before the user logs in is unlikely to be particularly interesting to you
346
+ &mdash; you already have the user as a registered user, and so this isn't a conversion or sign-up funnel. Effectively
347
+ treating the activity that happens before they sign in as a completely separate user is actually exactly the right
348
+ thing to do. The correct code structure is simply to call `#distinct_id=` on your `MetaEvents::Tracker` at exactly
349
+ the point at which you log them in (using your session, or a cookie, or whatever), and be done with it.
350
+ * **What do I do when a user signs up?** This is the tricky case. You really want to correlate all the activity that
351
+ happened before the signup process with the activity afterwards, so that you can start seeing things like "users who
352
+ come in through funnel X convert to truly active/paid/whatever users at a higher rate than those through funnel Y".
353
+ This requires support from your back-end analytics provider; Mixpanel calls it _aliasing_, and it's accessed via
354
+ their `alias` call. It effectively says "the user with autogenerated ID X is the exact same user as the user with
355
+ primary-key ID Y". Making this call is beyond the scope of MetaEvents, but is quite easy to do assuming your
356
+ analytics provider supports it.
357
+
358
+ You may also wish to see Mixpanel's documentation about distinct ID, [here](https://mixpanel.com/docs/managing-users/what-the-unique-identifer-does-and-why-its-important), [here](https://mixpanel.com/docs/managing-users/assigning-your-own-unique-identifiers-to-users), and [here](https://mixpanel.com/docs/integration-libraries/using-mixpanel-alias).
359
+
360
+ # The Real Power of MetaEvents
361
+
362
+ Now that we've gotten the basics out of the way, we can start using the real power of MetaEvents.
363
+
364
+ ### Adding Implicit Properties
365
+
366
+ Very often, just by being in some particular part of code, you already know a fair amount of data that you want to
367
+ pass as events. For example, if you're inside a Rails controller action, and you have a current user, you're probably
368
+ going to want to pass properties about that user to any event that happens in the controller action.
369
+
370
+ You could add these to every single call to `#event!`, but MetaEvents has a better way. When you create the
371
+ `MetaEvents::Tracker` instance, you can define _implicit properties_. Let's add some now:
372
+
373
+ class ApplicationController < ActionController::Base
374
+ ...
375
+ def event_tracker
376
+ implicit_properties = { }
377
+ if current_user
378
+ implicit_properties.merge!(
379
+ :user_gender => current_user.gender,
380
+ :user_age => current_user.age
381
+ )
382
+ end
383
+ @event_tracker ||= MetaEvents::Tracker.new(current_user.try(:id), request.remote_ip,
384
+ :implicit_properties => implicit_properties)
385
+ end
386
+ ...
387
+ end
388
+
389
+ Now, these properties will get passed on every event fired by this Tracker. (This is, in fact, the biggest
390
+ consideration when deciding when and where you'll create new `MetaEvents::Tracker` instances: implicit properties are
391
+ extremely useful, so you'll want the lifecycle of a Tracker to match closely the lifecycle of something in your
392
+ application that has implicit properties.)
393
+
394
+ ### Multi-Object Events
395
+
396
+ We're also going to face another problem: many events involve multiple underlying objects, each of which has many
397
+ properties that are defined on it. For example, imagine we have an event triggered when a user sends a message to
398
+ another user. We have at least three entities: the 'from' user, the 'to' user, and the message itself. If we really
399
+ want to instrument this event properly, we're going to want something like this:
400
+
401
+ event_tracker.event!(:user, :sent_message, {
402
+ :from_user_country => from_user.country,
403
+ :from_user_state => from_user.state,
404
+ :from_user_postcode => from_user.postcode,
405
+ :from_user_city => from_user.city,
406
+ :from_user_language => from_user.language,
407
+ :from_user_referred_from => from_user.referred_from,
408
+ :from_user_gender => from_user.gender,
409
+ :from_user_age => from_user.age,
410
+
411
+ :to_user_country => to_user.country,
412
+ :to_user_state => to_user.state,
413
+ :to_user_postcode => to_user.postcode,
414
+ :to_user_city => to_user.city,
415
+ :to_user_language => to_user.language,
416
+ :to_user_referred_from => to_user.referred_from,
417
+ :to_user_gender => to_user.gender,
418
+ :to_user_age => to_user.age,
419
+
420
+ :message_sent_at => message.sent_at,
421
+ :message_type => message.type,
422
+ :message_length => message.length,
423
+ :message_language => message.language,
424
+ :message_attachments => message.attachments?
425
+ })
426
+
427
+ Needless to say, this kind of sucks. Either we're going to end up with a ton of duplicate, unmaintainable code, or
428
+ we'll just cut back and only pass a few properties &mdash; greatly reducing the possibilities of our analytics
429
+ system.
430
+
431
+ ### Using Hashes to Factor Out Naming
432
+
433
+ We can improve this situation by using a feature of MetaEvents: when properties are nested in sub-hashes, they get
434
+ automatically expanded and their names prefixed by the outer hash key. So let's define a couple of methods on models:
435
+
436
+ class User < ActiveRecord::Base
437
+ def to_event_properties
438
+ {
439
+ :country => country,
440
+ :state => state,
441
+ :postcode => postcode,
442
+ :city => city,
443
+ :language => language,
444
+ :referred_from => referred_from,
445
+ :gender => gender,
446
+ :age => age
447
+ }
448
+ end
449
+ end
450
+
451
+ class Message < ActiveRecord::Base
452
+ def to_event_properties
453
+ {
454
+ :sent_at => sent_at,
455
+ :type => type,
456
+ :length => length,
457
+ :language => language,
458
+ :attachments => attachments?
459
+ }
460
+ end
461
+ end
462
+
463
+ Now, we can pass the exact same set of properties as the above example, by simply doing:
464
+
465
+ event_tracker.event!(:user, :sent_message, {
466
+ :from_user => from_user.to_event_properties,
467
+ :to_user => to_user.to_event_properties,
468
+ :message => message.to_event_properties
469
+ })
470
+
471
+ **SO** much better.
472
+
473
+ ### Moving Hash Generation To Objects
474
+
475
+ And &mdash; tah-dah! &mdash; MetaEvents supports this syntax automatically. If you pass an object as a property, and
476
+ that object defines a method called `#to_event_properties`, then it will be called automatically, and replaced.
477
+ Our code now looks like:
478
+
479
+ event_tracker.event!(:user, :sent_message, { :from_user => from_user, :to_user => to_user, :message => message })
480
+
481
+ ### How to Take the Most Advantage
482
+
483
+ To make the most use of MetaEvents, define `#to_event_properties` very liberally on objects in your system, make them
484
+ return any properties you even think might be useful, and pass them to events. MetaEvents will expand them for you,
485
+ allowing large numbers of properties on events, which allows Mixpanel and other such systems to be of the most use
486
+ to you.
487
+
488
+ # Miscellaneous and Trivia
489
+
490
+ A few things before we're done:
491
+
492
+ ### Mixpanel, Aliasing, and People
493
+
494
+ MetaEvents is _not_ intended as a complete superset of a backend analytics library (like Mixpanel) &mdash; there are
495
+ features of those libraries that are not implemented via MetaEvents, and which should be used by direct calls to the
496
+ service in question.
497
+
498
+ For example, Mixpanel has an `alias` call that lets you tell it that a user with a particular distinct ID is actually
499
+ the same person as a user with a different distinct ID &mdash; this is typically used at signup, when you convert from
500
+ an "anonymous" distinct ID representing the unknown user who is poking around your site to the actual official user ID
501
+ (typically your `users` table primary key) of that user. MetaEvents does not, in any way, attempt to support this; it
502
+ allows you to pass whatever `distinct_id` you want in the `#event!` call, but, if you want to use `alias`, you should
503
+ make that Mixpanel call directly. (See also the discussion above about _distinct ID_.)
504
+
505
+ Similarly, Mixpanel's People functionality is not in any way directly supported by MetaEvents. You may well use the
506
+ Tracker's `#effective_properties` method to compute a set of properties that you pass to Mixpanel's People system,
507
+ but there are no calls directly in MetaEvents to do this for you.
508
+
509
+ ### Retiring an Event
510
+
511
+ Often you'll have events that you _retire_ &mdash; they were used in the past, but no longer. You could just delete
512
+ them from your MetaEvents DSL file, but this will mean the historical record is suddenly gone. (Well, there's source
513
+ control, but that's a pain.)
514
+
515
+ Rather than doing this, you can retire them:
516
+
517
+ global_events_prefix :ab
518
+
519
+ version 1, "2014-02-04" do
520
+ category :user do
521
+ event :logged_in_with_facebook, "2014-02-04", "user creates a brand-new account", :retired_at => "2014-06-01"
522
+ event :signed_up, "2014-02-04", "user creates a brand-new account"
523
+ end
524
+ end
525
+
526
+ Given the above, trying to call `event!(:user, :logged_in_with_facebook)` will fail with an exception, because the
527
+ event has been retired. (Note that, once again, the actual date passed to `:retired_at` is simply for record-keeping
528
+ purposes; the exception is generated if `:retired_at` is set to _anything_.)
529
+
530
+ You can retire events, categories, and entire versions; this system ensures the DSL continues to be a historical record
531
+ of what things were in the past, as well as what they are today.
532
+
533
+ ### Adding Notes to Events
534
+
535
+ You can also add notes to events. They must be tagged with the author and the time, and they can be very useful for
536
+ documenting changes:
537
+
538
+ global_events_prefix :ab
539
+
540
+ version 1, "2014-02-04" do
541
+ category :user do
542
+ event :signed_up, "2014-02-04", "user creates a brand-new account" do
543
+ note "2014-03-17", "jsmith", "Moved sign-up button to the home page -- should increase signups significantly"
544
+ end
545
+ end
546
+ end
547
+
548
+ This allows you to record changes to events, as well as the events themselves.
549
+
550
+ ### Documenting Events
551
+
552
+ Currently, the documentation for the MetaEvents DSL is the source to that DSL itself &mdash; _i.e._,
553
+ `config/meta_events.rb` or something similar. However, methods on the DSL objects created (accessible via
554
+ a `Tracker`'s `#definitions` method, or `MetaEvents::Tracker`'s `default_definitions` class method) allow for
555
+ introspection, and could easily be extended to, _e.g._, generate HTML fully documenting the events.
556
+
557
+ Patches are welcome. ;-)
558
+
559
+ ### Times
560
+
561
+ MetaEvents correctly converts any `Time` object you pass into the correct String format for Mixpanel (_e.g._,
562
+ `2014-02-03T15:49:17`), converting it to UTC first. This should make your times much cleaner.
563
+
564
+ ### Adding a New Version
565
+
566
+ What is this top-level `version` in the DSL? Well, every once in a while, you will want to completely redo your set of
567
+ events &mdash; perhaps you've learned a lot about using your analytics system, and realize you want them configured
568
+ in a different way.
569
+
570
+ When you want to do this, define a new top-level `version` in your DSL, and pass `:version => 2` (or whatever number
571
+ you gave the new version) when creating your `MetaEvents::Tracker`. The tracker will look under that version for
572
+ categories and events, and completely ignore other versions; your events will be called things like `ab2_user_signup`
573
+ instead of `ab1_user_signup`, and so on. The old version can still stay present in your DSL for documentation and
574
+ historical purposes.
575
+
576
+ When you're completely done with the old version, retire it &mdash; `version 1, :retired_at => '2014-06-01' do ...`.
577
+
578
+ Often, you'll want to run two versions simultaneously, because you want to have a transition period where you fire
579
+ _both_ sets of events &mdash; this is hugely helpful in figuring out how your old events map to new events and
580
+ when adjusting bases for the new events. (If you simply flash-cut from an old version to a new one on a single day,
581
+ it is difficult or impossible to know if true underlying usage, etc., _actually_ changed, or if it's just an artifact
582
+ of changing events.) You can simply create two `MetaEvents::Tracker` instances, one for each version, and use them
583
+ in parallel.
584
+
585
+ ## Contributing
586
+
587
+ 1. Fork it ( http://github.com/swiftype/meta_events/fork )
588
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
589
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
590
+ 4. Push to the branch (`git push origin my-new-feature`)
591
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec