meta_events 1.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 +17 -0
- data/.travis.yml +6 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +591 -0
- data/Rakefile +6 -0
- data/lib/meta_events/controller_methods.rb +43 -0
- data/lib/meta_events/definition/category.rb +78 -0
- data/lib/meta_events/definition/definition_set.rb +122 -0
- data/lib/meta_events/definition/event.rb +163 -0
- data/lib/meta_events/definition/version.rb +89 -0
- data/lib/meta_events/engine.rb +6 -0
- data/lib/meta_events/helpers.rb +114 -0
- data/lib/meta_events/railtie.rb +29 -0
- data/lib/meta_events/test_receiver.rb +37 -0
- data/lib/meta_events/tracker.rb +493 -0
- data/lib/meta_events/version.rb +3 -0
- data/lib/meta_events.rb +29 -0
- data/meta_events.gemspec +31 -0
- data/spec/meta_events/controller_methods_and_helpers_spec.rb +253 -0
- data/spec/meta_events/definition/category_spec.rb +102 -0
- data/spec/meta_events/definition/definition_set_spec.rb +142 -0
- data/spec/meta_events/definition/event_spec.rb +146 -0
- data/spec/meta_events/definition/version_spec.rb +93 -0
- data/spec/meta_events/tracker_spec.rb +466 -0
- data/vendor/assets/javascripts/meta_events.js.erb +154 -0
- metadata +154 -0
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
data/.travis.yml
ADDED
data/Gemfile
ADDED
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 — `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 — 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 — 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="{"ip":"127.0.0.1","color":"green","implicit_prop_1":"someValue"}">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 — 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 — 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 — 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 — 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
|
+
— 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 — 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 — tah-dah! — 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) — 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 — 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_ — 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 — _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 — 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 — `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 — 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
|