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
@@ -0,0 +1,37 @@
|
|
1
|
+
module MetaEvents
|
2
|
+
# A MetaEvents::TestReceiver is a very simple object that conforms to the call signature required by the
|
3
|
+
# MetaEvents::Tracker for event receivers. It writes each event as human-readable text to a +target+, which can be:
|
4
|
+
#
|
5
|
+
# * A block (or any object that responds to #call), which will be passed a String;
|
6
|
+
# * A Logger (or any object that responds to #info), which will be passed a String;
|
7
|
+
# * An IO (like +STDOUT+ or +STDERR+, or any object that responds to #puts), which will be passed a String.
|
8
|
+
#
|
9
|
+
# This object is useful for watching and debugging events in development environments.
|
10
|
+
class TestReceiver
|
11
|
+
def initialize(target = nil, &block)
|
12
|
+
@target = target || block || ::Rails.logger
|
13
|
+
end
|
14
|
+
|
15
|
+
def track(distinct_id, event_name, properties)
|
16
|
+
string = "Tracked event: #{event_name.inspect} for user #{distinct_id.inspect}"
|
17
|
+
properties.keys.sort.each do |k|
|
18
|
+
value = properties[k]
|
19
|
+
unless value == nil
|
20
|
+
string << "\n %30s: %s" % [ k, properties[k] ]
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
say(string)
|
25
|
+
end
|
26
|
+
|
27
|
+
def say(string)
|
28
|
+
if @target.respond_to?(:call)
|
29
|
+
@target.call "#{string}\n"
|
30
|
+
elsif @target.respond_to?(:info)
|
31
|
+
@target.info "#{string}"
|
32
|
+
else
|
33
|
+
@target.puts string
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,493 @@
|
|
1
|
+
require "meta_events"
|
2
|
+
require "active_support"
|
3
|
+
require "ipaddr"
|
4
|
+
|
5
|
+
module MetaEvents
|
6
|
+
# The MetaEvents::Tracker is the primary (and only) class you ordinarily use from the MetaEvents system. By itself,
|
7
|
+
# it does not actually call any event-tracking services; it takes the events you give it, expands them into
|
8
|
+
# fully-qualified event names, expands nested properties, validates it all against the DSL, and then calls through to
|
9
|
+
# one or more event _receivers_, which are simply any object that responds to a very simple method signature.
|
10
|
+
#
|
11
|
+
# ## Instantiation and Lifecycle
|
12
|
+
#
|
13
|
+
# A MetaEvents::Tracker object is designed to be created once in each context where you're processing actions on
|
14
|
+
# behalf of a particular user -- for example, once in the request cycle in a Rails application, most likely as part
|
15
|
+
# of ApplicationController. This is because a Tracker accepts, in its constructor, a +distinct_id+, which is the
|
16
|
+
# way you identify a particular user to your events system. It is possible to override this on an event-by-event
|
17
|
+
# basis (by passing a +:distinct_id+ property explicitly to the event), but it is generally cleaner and easier to
|
18
|
+
# simply instantiate the MetaEvents::Tracker object once for each processing cycle.
|
19
|
+
#
|
20
|
+
# Further, a Tracker accepts _implicit properties_ on creation; this is a set of zero or more properties that get
|
21
|
+
# automatically added to every event processed by the tracker. Typically, these will be user-centric properties,
|
22
|
+
# like the user's location, age, plan, or anything else. By using the support for #to_event_properties (below), the
|
23
|
+
# canonical form of Tracker instantiation looks something like:
|
24
|
+
#
|
25
|
+
# event_tracker = MetaEvents::Tracker.new(current_user.id, request.remote_ip,
|
26
|
+
# :implicit_properties => { :user => current_user })
|
27
|
+
#
|
28
|
+
# ...which will automatically add all properties exposed by User#to_event_properties on every single event fired by
|
29
|
+
# that Tracker.
|
30
|
+
#
|
31
|
+
# See the discussion for #initialize, too -- there are certain things you want to do for logged-out users and at the
|
32
|
+
# point when a user signs up.
|
33
|
+
#
|
34
|
+
# If you concurrently are firing events from multiple versions in the MetaEvents DSL, you'll need to use multiple
|
35
|
+
# MetaEvents::Tracker instances -- any given Tracker only works with a single version at once. Since the point of DSL
|
36
|
+
# versions is to support wholesale overhauls of your entire events system, this is probably fine; the set of
|
37
|
+
# implicit properties you want to use will almost certainly have changed, too.
|
38
|
+
#
|
39
|
+
# Any way you choose to use an MetaEvents::Tracker is fine -- the overhead to creating one is pretty small.
|
40
|
+
#
|
41
|
+
# ## Event Receivers
|
42
|
+
#
|
43
|
+
# To make an MetaEvents::Tracker actually do something, it must have one or more _event receiver_s. An event receiver is
|
44
|
+
# any object that responds to the following method:
|
45
|
+
#
|
46
|
+
# track(distinct_id, event_name, event_properties)
|
47
|
+
#
|
48
|
+
# ...where +distinct_id+ is a String or Integer that uniquely identifies the user for which we're firing the event,
|
49
|
+
# +event_name+ is a String which is the full name of the event (more on that below), and +event_properties+
|
50
|
+
# is a map of String keys (the names of properties) to values that are numbers (any Numeric -- integer or
|
51
|
+
# floating-point -- will do), true, false, nil, a Time, or a String. This interface is designed to be extremely simple, and
|
52
|
+
# is modeled after the popular Mixpanel (https://www.mixpanel.com/) API.
|
53
|
+
#
|
54
|
+
# **IMPORTANT**: Event receivers are called sequentially, in a loop, directly inside the call to #event!. If they
|
55
|
+
# raise an exception, it will be propagated through and will be received by the caller of #event!; if they are slow
|
56
|
+
# or time out, this latency will be directly experienced by the caller to #event!. This is intentional, because only
|
57
|
+
# you can know whether you want to swallow these exceptions or propagate them, or whether you need to make event
|
58
|
+
# reporting asynchronous -- and, if so, _how_ -- or not. Think carefully, and add asychronicity or exception handling
|
59
|
+
# if needed.
|
60
|
+
#
|
61
|
+
# Provided with this library is MetaEvents::TestReceiver, which will accept an IO object (like STDOUT), a Logger, or
|
62
|
+
# a block, and will accept events and write them as human-readable strings to this destination. Also, the
|
63
|
+
# 'mixpanel-ruby' gem is plug-compatible with this library -- an instance of Mixpanel::Tracker is a valid event
|
64
|
+
# receiver.
|
65
|
+
#
|
66
|
+
# To specify the event receiver(s), you can (in order of popularity):
|
67
|
+
#
|
68
|
+
# * Configure the default receiver(s) for all MetaEvents::Tracker instances that are not otherwise specified by using
|
69
|
+
# <tt>MetaEvents::Tracker.default_event_receivers = [ receiver1, receiver2 ]
|
70
|
+
# * Specify receivers at the time you create a new MetaEvents::Tracker:
|
71
|
+
# <tt>tracker = MetaEvents::Tracker.new(current_user.id, request.remote_ip, :event_receivers => [ receiver1, receiver2 ])
|
72
|
+
# * Modify an existing MetaEvents::Tracker:
|
73
|
+
# <tt>my_tracker.event_receivers = [ receiver1, receiver2 ]
|
74
|
+
#
|
75
|
+
# ## Version Specification
|
76
|
+
#
|
77
|
+
# As mentioned above, any given MetaEvents::Tracker can only fire events from a single version within the MetaEvents DSL.
|
78
|
+
# Since the point of DSL versions is to support wholesale overhauls of your entire events system, this is probably
|
79
|
+
# fine; the set of implicit properties you want to use will almost certainly have changed, too.
|
80
|
+
#
|
81
|
+
# To specify the version within your MetaEvents DSL that a Tracker will work against, you can:
|
82
|
+
#
|
83
|
+
# * Set the default for all MetaEvents::Tracker instances using <tt>MetaEvents::Tracker.default_version = 1</tt>; or
|
84
|
+
# * Specify the version at the time you create a new MetaEvents::Tracker: <tt>tracker = MetaEvents::Tracker.new(current_user.id, request.remote_ip, :version => 1)</tt>;
|
85
|
+
#
|
86
|
+
# <tt>MetaEvents::Tracker.default_version</tt> is 1 by default, so, until you define your second version, you can safely
|
87
|
+
# ignore this.
|
88
|
+
#
|
89
|
+
# ## Setting Up Definitions
|
90
|
+
#
|
91
|
+
# Part of the whole point of the MetaEvents::Tracker is that it works against the MetaEvents DSL. If you're using this with
|
92
|
+
# Rails, you simply need to create <tt>config/events.rb</tt> with something like:
|
93
|
+
#
|
94
|
+
# global_events_prefix :pz
|
95
|
+
#
|
96
|
+
# version 1, '2014-01-30' do
|
97
|
+
# category :user do
|
98
|
+
# event :signup, '2014-02-01', 'a user first creates their account'
|
99
|
+
# event :login, '2014-02-01', 'a user enters their password'
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# ...and it will "just work".
|
104
|
+
#
|
105
|
+
# If you're not using Rails or you don't want to do this, it's still easy enough. You can specify a set of events
|
106
|
+
# in one of two ways:
|
107
|
+
#
|
108
|
+
# * As a separate file, using the MetaEvents DSL, just like the <tt>config/events.rb</tt> example above;
|
109
|
+
# * Directly as an instance of MetaEvents::Definition::DefinitionSet, using any mechanism you choose.
|
110
|
+
#
|
111
|
+
# Once you have either of the above, you can set up your MetaEvents::Tracker with it in any of these ways:
|
112
|
+
#
|
113
|
+
# * <tt>MetaEvents::Tracker.default_definitions = "path/to/myfile"</tt>;
|
114
|
+
# * <tt>MetaEvents::Tracker.default_definitions = my_definition_set</tt> -- both of these will set the definitions for
|
115
|
+
# any and all MetaEvents::Tracker instances that do not have definitions directly set on them;
|
116
|
+
# * <tt>my_tracker = MetaEvents::Tracker.new(current_user.id, request.remote_ip, :definitions => "path/to/myfile")</tt>;
|
117
|
+
# * <tt>my_tracker = MetaEvents::Tracker.new(current_user.id, request.remote_ip, :definitions => my_definition_set)</tt> -- setting it in the constructor.
|
118
|
+
#
|
119
|
+
# ## Implicit Properties
|
120
|
+
#
|
121
|
+
# When you create an MetaEvents::Tracker instance, you can add implicit properties to it simply by passing the
|
122
|
+
# +:implicit_properties+ option to the constructor. These properties will be automatically attached to all events
|
123
|
+
# fired by that object, unless they are explicitly overridden with a different value (+nil+ will work if needed)
|
124
|
+
# passed in the individual event call.
|
125
|
+
#
|
126
|
+
# ## Property Merging: Sub-Hashes
|
127
|
+
#
|
128
|
+
# Sometimes you have large numbers of properties that pertain to a particular entity in your system. For this reason,
|
129
|
+
# the MetaEvents::Tracker supports _sub-hashes_:
|
130
|
+
#
|
131
|
+
# my_tracker.event!(:user, :signed_up,
|
132
|
+
# :user => { :first_name => 'Jane', :last_name => 'Dunham', :city => 'Seattle' }, :color => 'green')
|
133
|
+
#
|
134
|
+
# This will result in a call to the event receivers that looks like this:
|
135
|
+
#
|
136
|
+
# receiver.track('some_distinct_id', 'ab1_user_signed_up', {
|
137
|
+
# 'user_first_name' => 'Jane',
|
138
|
+
# 'user_last_name' => 'Dunham',
|
139
|
+
# 'user_city' => 'Seattle',
|
140
|
+
# 'color' => 'green'
|
141
|
+
# })
|
142
|
+
#
|
143
|
+
# Using this mechanism, you can easily sling around entire sets of properties without needing to write lots of code
|
144
|
+
# using Hash, #merge, and so on. Even better, if you accidentally collide two properties with each other this way
|
145
|
+
# (such as if you specified a separate, top-level +:user_city+ key above), MetaEvents::Tracker will let you know about it.
|
146
|
+
#
|
147
|
+
# ## Property Merging: to_event_properties
|
148
|
+
#
|
149
|
+
# What you _really_ want, however, is to be able to pass entire objects into an event -- this is where the real power
|
150
|
+
# of the MetaEvents::Tracker comes in handy.
|
151
|
+
#
|
152
|
+
# If you pass into an event, or into the implicit-properties set, a key that's bound to a value that's an object that
|
153
|
+
# responds to #to_event_properties, then this method will be called, and its properties merged in. For example,
|
154
|
+
# assume you have the following:
|
155
|
+
#
|
156
|
+
# class User < ActiveRecord::Base
|
157
|
+
# ...
|
158
|
+
# def to_event_properties
|
159
|
+
# {
|
160
|
+
# :age => ((Time.now - date_of_birth) / 1.year).floor,
|
161
|
+
# :payment_level => payment_level,
|
162
|
+
# :city => home_city
|
163
|
+
# ...
|
164
|
+
# }
|
165
|
+
# end
|
166
|
+
# ...
|
167
|
+
# end
|
168
|
+
#
|
169
|
+
# ...and now you make a call like this:
|
170
|
+
#
|
171
|
+
# my_tracker.event!(:user, :logged_in, :user => current_user, :last_login => current_user.last_login)
|
172
|
+
#
|
173
|
+
# You'll end up with a set of properties like this:
|
174
|
+
#
|
175
|
+
# receiver.track('some_distinct_id', 'ab1_user_logged_in', {
|
176
|
+
# 'user_age' => 27,
|
177
|
+
# 'user_payment_level' => 'enterprise',
|
178
|
+
# 'user_city' => 'Seattle',
|
179
|
+
# 'last_login' => 2014-02-03 17:28:34 -0800
|
180
|
+
# })
|
181
|
+
#
|
182
|
+
# Using this mechanism, you can (and should!) define standard #to_event_properties methods on many of your models,
|
183
|
+
# and then pass in models frequently -- this allows you to easily build large sets of properties to pass with your
|
184
|
+
# events, which is one of the keys to making many event-tracking tools as powerful as possible.
|
185
|
+
#
|
186
|
+
# Because this mechanism works the way it does, you can also pass in multiple models of the same type:
|
187
|
+
#
|
188
|
+
# my_tracker.event!(:user, :sent_message, :from => from_user, :to => to_user)
|
189
|
+
#
|
190
|
+
# ...becomes:
|
191
|
+
#
|
192
|
+
# receiver.track('some_distinct_id', 'ab1_user_sent_message', {
|
193
|
+
# 'from_age' => 27,
|
194
|
+
# 'from_payment_level' => 'enterprise',
|
195
|
+
# 'from_city' => 'Seattle',
|
196
|
+
# 'to_age' => 35,
|
197
|
+
# 'to_payment_level' => 'personal',
|
198
|
+
# 'to_city' => 'San Francisco'
|
199
|
+
# })
|
200
|
+
#
|
201
|
+
# Note that if you need different #to_event_properties objects for different situations, as sometimes occurs, the
|
202
|
+
# fact that Hash merging works the same way means you can build it yourself, trivially:
|
203
|
+
#
|
204
|
+
# my_tracker.event!(:user, :logged_in,
|
205
|
+
# :user => current_user.login_event_properties, :last_login => current_user.last_login)
|
206
|
+
#
|
207
|
+
# ...or however you'd like it to work.
|
208
|
+
#
|
209
|
+
# ## The Global Events Prefix
|
210
|
+
#
|
211
|
+
# No matter how you configure the MetaEvents::Tracker, you _must_ specify a "global events prefix" -- either using the
|
212
|
+
# MetaEvents DSL (<tt>global_events_prefix :foo</tt>), or in the constructor
|
213
|
+
# (<tt>MetaEvents::Tracker.new(current_user.id, request.remote_ip, :global_events_prefix => :foo)</tt>).
|
214
|
+
#
|
215
|
+
# The point of the global events prefix is to help distinguish events generated by this system from any events you
|
216
|
+
# may have feeding into a target system that are generated elsewhere. You can set the global events prefix to anything
|
217
|
+
# you like; it, plus, the version number, will be prepended to all event names. For example, if you set it to +'pz'+,
|
218
|
+
# and you're using version 3, then an event +:foo+ in a category +:bar+ will have the full name +pz3_foo_bar+.
|
219
|
+
#
|
220
|
+
# We recommend that you keep the global events prefix short, simply because tools like Mixpanel often have a
|
221
|
+
# relatively small amount of screen real estate available for event names.
|
222
|
+
class Tracker
|
223
|
+
class EventError < StandardError; end
|
224
|
+
class PropertyCollisionError < EventError; end
|
225
|
+
|
226
|
+
# ## Class Attributes
|
227
|
+
|
228
|
+
# The set of event receivers that MetaEvents::Tracker instances will use if they aren't configured otherwise. This is
|
229
|
+
# useful if you want to set up event receivers "as configuration" -- for example, in config/environment.rb in
|
230
|
+
# Rails.
|
231
|
+
cattr_accessor :default_event_receivers
|
232
|
+
self.default_event_receivers = [ ]
|
233
|
+
|
234
|
+
# The set of event definitions from the MetaEvents DSL that MetaEvents::Tracker instances will use, by default (_i.e._, if not
|
235
|
+
# passed a separate definitions file using <tt>:definitions =></tt> in the constructor). You can set this to
|
236
|
+
# the pathname of a file containing event definitions, an +IO+ object containing the text of event definitions, or
|
237
|
+
# an ::MetaEvents::Definition::DefinitionSet object that you create any way you want.
|
238
|
+
#
|
239
|
+
# Reading +default_definitions+ always will return an instance of ::MetaEvents::Definition::DefinitionSet.
|
240
|
+
class << self
|
241
|
+
def default_definitions=(source)
|
242
|
+
@default_definitions = ::MetaEvents::Definition::DefinitionSet.from(source)
|
243
|
+
end
|
244
|
+
attr_reader :default_definitions
|
245
|
+
end
|
246
|
+
|
247
|
+
# The default version that new MetaEvents::Tracker instances will use to look up events in the MetaEvents DSL.
|
248
|
+
cattr_accessor :default_version
|
249
|
+
self.default_version = 1
|
250
|
+
|
251
|
+
# ## Instance Attributes
|
252
|
+
|
253
|
+
# The set of event receivers that this MetaEvents::Tracker instance will use. This should always be an Array (although
|
254
|
+
# it can be empty if you don't want to send events anywhere).
|
255
|
+
attr_accessor :event_receivers
|
256
|
+
|
257
|
+
# The ::MetaEvents::Definitions::DefinitionSet that this Tracker is using.
|
258
|
+
attr_reader :definitions
|
259
|
+
|
260
|
+
# The version of events that this Tracker is using.
|
261
|
+
attr_reader :version
|
262
|
+
|
263
|
+
# Creates a new instance.
|
264
|
+
#
|
265
|
+
# +distinct_id+ is the "distinct ID" of the user on behalf of whom events are going to be fired; this can be +nil+
|
266
|
+
# if there is no such user (for example, if you're firing events from a background job that has nothing to do with
|
267
|
+
# any particular user). This will be automatically added to all events fired from this MetaEvents::Tracker as a
|
268
|
+
# property named +"distinct_id"+. Typically, this will be the primary key of your +users+ table, although it can
|
269
|
+
# be any unique identifier you want.
|
270
|
+
#
|
271
|
+
# +ip+ is the IP address of the user. This is called out as an explicit parameter so that you don't forget it; you
|
272
|
+
# can pass +nil+ if you need to or if it isn't relevant, but you generally should pass it -- systems like Mixpanel
|
273
|
+
# use it to do geolocation for the client. If you need to override this on an event-by-event basis, simply pass
|
274
|
+
# a property named +ip+.
|
275
|
+
#
|
276
|
+
# (If a user has not logged in yet, you will probably want to assign them a unique ID anyway, via a cookie, and
|
277
|
+
# then pass this ID here. If the user logs in to an already-existing account, you probably just want to switch to
|
278
|
+
# using their logged-in user ID, since the stuff they did before they logged in isn't very interesting -- you
|
279
|
+
# already have them as a user. But if they sign up for a new account, you'll lose tracking across that boundary
|
280
|
+
# unless your events provider provides something like Mixpanel's +alias+ call; making that kind of call is beyond
|
281
|
+
# the scope of MetaEvents, and should be done separately.)
|
282
|
+
#
|
283
|
+
# +options+ can contain:
|
284
|
+
#
|
285
|
+
# [:definitions] If present, this can be anything accepted by ::MetaEvents::Definition::DefinitionSet#from, which will
|
286
|
+
# currently accept a pathname to a file, an +IO+ object that contains the text of definitions, or
|
287
|
+
# an ::MetaEvents::Definition::DefinitionSet that you create however you want. If you don't pass
|
288
|
+
# +:definitions+, then this will use whatever the class property +:default_event_receivers+ is
|
289
|
+
# set to. (If neither one of these is set, you will receive an ArgumentError.)
|
290
|
+
# [:version] If present, this should be an integer that specifies which version within the specified MetaEvents DSL this
|
291
|
+
# MetaEvents::Tracker should fire events from. A single Tracker can only fire events from one version; if you
|
292
|
+
# need to support multiple versions simultaneously (for example, if you want to have a period of overlap
|
293
|
+
# during the transition from one version of your events system to another), create multiple Trackers.
|
294
|
+
# [:event_receivers] If present, this should be a (possibly empty) Array that lists the set of event-receiver
|
295
|
+
# objects that you want fired events delivered to.
|
296
|
+
# [:implicit_properties] If present, this should be a Hash; this defines a set of properties that will get included
|
297
|
+
# with every event fired from this Tracker. This can use the hash-merge and object syntax
|
298
|
+
# (#to_event_properties) documented above. Any properties explicitly passed with an event
|
299
|
+
# that have the same name as these properties will override these properties for that event.
|
300
|
+
def initialize(distinct_id, ip, options = { })
|
301
|
+
options.assert_valid_keys(:definitions, :version, :implicit_properties, :event_receivers)
|
302
|
+
|
303
|
+
definitions = options[:definitions] || self.class.default_definitions
|
304
|
+
unless definitions
|
305
|
+
raise ArgumentError, "We have no event definitions to use. You must either set event definitions for " +
|
306
|
+
"all event trackers using #{self.class.name}.default_definitions = (DefinitionSet or file), " +
|
307
|
+
"or pass them to this constructor using :definitions." +
|
308
|
+
"If you're using Rails, you can also simply put your definitions in the file " +
|
309
|
+
"config/meta_events.rb, and they will be automatically loaded."
|
310
|
+
end
|
311
|
+
|
312
|
+
@definitions = ::MetaEvents::Definition::DefinitionSet.from(definitions)
|
313
|
+
@version = options[:version] || self.class.default_version || raise(ArgumentError, "Must specify a :version")
|
314
|
+
|
315
|
+
@implicit_properties = { }
|
316
|
+
self.class.merge_properties(@implicit_properties, { :ip => normalize_ip(ip).to_s }) if ip
|
317
|
+
self.class.merge_properties(@implicit_properties, options[:implicit_properties] || { })
|
318
|
+
self.distinct_id = distinct_id if distinct_id
|
319
|
+
|
320
|
+
self.event_receivers = Array(options[:event_receivers] || self.class.default_event_receivers.dup)
|
321
|
+
end
|
322
|
+
|
323
|
+
# In certain cases, you will only have access to the distinct ID later, and this allows for that use case. (For
|
324
|
+
# example, if you create the Tracker in your Rails application's ApplicationController, then, when you process the
|
325
|
+
# login action for your application, there will be no distinct ID when the Tracker is created -- because the user
|
326
|
+
# does not have the proper cookie set yet -- but you'll discover the distinct ID in the middle of the action.)
|
327
|
+
def distinct_id=(new_value)
|
328
|
+
new_distinct_id = self.class.normalize_scalar_property_value(new_value)
|
329
|
+
if new_distinct_id == :invalid_property_value
|
330
|
+
raise ArgumentError, "This is not an acceptable value for a distinct ID: #{new_distinct_id.inspect}"
|
331
|
+
end
|
332
|
+
@distinct_id = new_distinct_id
|
333
|
+
end
|
334
|
+
|
335
|
+
attr_reader :distinct_id
|
336
|
+
|
337
|
+
# Fires an event. +category_name+ must be the name of a category in the MetaEvents DSL (within the version that this
|
338
|
+
# Tracker is using -- which is 1 if you haven't changed it); +event_name+ must be the name
|
339
|
+
# of an event. +additional_properties+, if present, must be a Hash; the properties supplied will be combined with
|
340
|
+
# any implicit properties defined on this Tracker, and sent along with the event.
|
341
|
+
#
|
342
|
+
# +additional_properties+ can use the sub-hash and object syntax discussed, above, under the introduction to this
|
343
|
+
# class.
|
344
|
+
def event!(category_name, event_name, additional_properties = { })
|
345
|
+
event_data = effective_properties(category_name, event_name, additional_properties)
|
346
|
+
|
347
|
+
self.event_receivers.each do |receiver|
|
348
|
+
receiver.track(event_data[:distinct_id], event_data[:event_name], event_data[:properties])
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# Given a category, an event, and (optionally) additional properties, performs all of the expansion and validation
|
353
|
+
# of #event!, but does not actually fire the event -- rather, returns a Hash containing:
|
354
|
+
#
|
355
|
+
# [:distinct_id] The +distinct_id+ that should be passed with the event; this can be +nil+ if there is no distinct
|
356
|
+
# ID being passed.
|
357
|
+
# [:event_name] The fully-qualified event name, including +global_events_prefix+ and version number, exactly as
|
358
|
+
# it should be passed to an events backend.
|
359
|
+
# [:properties] The full set of properties, expanded (so values will only be scalars, never Hashes or objects),
|
360
|
+
# with String keys, exactly as they should be passed to an events system.
|
361
|
+
#
|
362
|
+
# This method can be used for many things, but its primary purpose is to support front-end (Javascript-fired)
|
363
|
+
# events: you can have it compute exactly the set of properties that should be attached to such events, embed
|
364
|
+
# them into the page (using HTML +data+ attributes, JavaScript literals, or any other storage mechanism you want),
|
365
|
+
# and then have the front-end fire them. This allows consistency between front-end and back-end events, and is
|
366
|
+
# another big advantage of MetaEvents.
|
367
|
+
def effective_properties(category_name, event_name, additional_properties = { })
|
368
|
+
event = version_object.fetch_event(category_name, event_name)
|
369
|
+
|
370
|
+
explicit = { }
|
371
|
+
self.class.merge_properties(explicit, additional_properties)
|
372
|
+
properties = @implicit_properties.merge(explicit)
|
373
|
+
|
374
|
+
event.validate!(properties)
|
375
|
+
# We need to do this instead of just using || so that you can override a present distinct_id with nil.
|
376
|
+
net_distinct_id = if properties.has_key?('distinct_id') then properties.delete('distinct_id') else self.distinct_id end
|
377
|
+
|
378
|
+
{
|
379
|
+
:distinct_id => net_distinct_id,
|
380
|
+
:event_name => event.full_name,
|
381
|
+
:properties => properties
|
382
|
+
}
|
383
|
+
end
|
384
|
+
|
385
|
+
private
|
386
|
+
# When we're expanding Hashes, we don't want to get into infinite recursion if you accidentally create a circular
|
387
|
+
# reference. Rather than adding code to actually detect true circular references, we simply refuse to expand
|
388
|
+
# Hashes beyond this many layers deep.
|
389
|
+
MAX_DEPTH = 10
|
390
|
+
|
391
|
+
# Returns the ::MetaEvents::Definition::Version object we should use for this Tracker.
|
392
|
+
def version_object
|
393
|
+
@definitions.fetch_version(version)
|
394
|
+
end
|
395
|
+
|
396
|
+
# Accepts an IP address (or nil) in String, Integer, or IPAddr formats, and returns an IPAddr (or nil).
|
397
|
+
def normalize_ip(ip)
|
398
|
+
case ip
|
399
|
+
when nil then nil
|
400
|
+
when String then IPAddr.new(ip)
|
401
|
+
when Integer then IPAddr.new(ip, Socket::AF_INET)
|
402
|
+
when IPAddr then ip
|
403
|
+
else raise ArgumentError, "IP must be a String, IPAddr, or Integer, not: #{ip.inspect}"
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
class << self
|
408
|
+
# Given a target Hash of properties in +target+, and a source Hash of properties in +source+, merges all properties
|
409
|
+
# in +source+ into +target+, obeying our hash-expansion rules (as specified in the introduction to this class).
|
410
|
+
# All new properties are added with their keys as Strings, and values must be:
|
411
|
+
#
|
412
|
+
# * A scalar of type Numeric (integer and floating-point numbers are both accepted), true, false, or nil;
|
413
|
+
# * A String or Symbol (and Symbols are converted to Strings before being used);
|
414
|
+
# * A Time;
|
415
|
+
# * A Hash, which will be recursively added using its key, plus an underscore, as the prefix
|
416
|
+
# (that is, <tt>{ :foo => { :bar => :baz }}</tt> will become <tt>{ 'foo_bar' => 'baz' }</tt>);
|
417
|
+
# * An object that responds to <tt>#to_event_properties</tt>, which must in turn return a Hash; #to_event_properties
|
418
|
+
# will be called, and it will then be treated exactly like a Hash, above.
|
419
|
+
#
|
420
|
+
# +prefix+ and +depth+ are only used for internal recursive calls:
|
421
|
+
#
|
422
|
+
# +prefix+ is a prefix that should be applied to all keys in the +source+ Hash before merging them into the +target+
|
423
|
+
# Hash. (Nothing is added to this prefix first, so, if you want an underscore separating it from the key, include
|
424
|
+
# the underscore in the +prefix+.)
|
425
|
+
#
|
426
|
+
# +depth+ should be an integer, indicating how many layers of recursive calls we've invoked; this is simply to
|
427
|
+
# prevent infinite recursion -- if this exceeds +MAX_DEPTH+, above, then an exception will be raised.
|
428
|
+
def merge_properties(target, source, prefix = nil, depth = 0)
|
429
|
+
if depth > MAX_DEPTH
|
430
|
+
raise "Nesting in EventTracker is too great; do you have a circular reference? " +
|
431
|
+
"We reached depth: #{depth.inspect}; expanding: #{source.inspect} with prefix #{prefix.inspect} into #{target.inspect}"
|
432
|
+
end
|
433
|
+
|
434
|
+
unless source.kind_of?(Hash)
|
435
|
+
raise ArgumentError, "You must supply a Hash for properties at #{prefix.inspect}; you supplied: #{source.inspect}"
|
436
|
+
end
|
437
|
+
|
438
|
+
source.each do |key, value|
|
439
|
+
prefixed_key = "#{prefix}#{key}"
|
440
|
+
|
441
|
+
if target.has_key?(prefixed_key)
|
442
|
+
raise PropertyCollisionError, %{Because of hash delegation, multiple properties with the key #{prefixed_key.inspect} are
|
443
|
+
present. This can happen, for example, if you do this:
|
444
|
+
|
445
|
+
event!(:foo_bar => 'baz', :foo => { :bar => 'quux' })
|
446
|
+
|
447
|
+
...since we will expand the second hash into a :foo_bar key, but there is already
|
448
|
+
one present.}
|
449
|
+
end
|
450
|
+
|
451
|
+
net_value = normalize_scalar_property_value(value)
|
452
|
+
if net_value == :invalid_property_value
|
453
|
+
if value.kind_of?(Hash)
|
454
|
+
merge_properties(target, value, "#{prefixed_key}_", depth + 1)
|
455
|
+
elsif value.respond_to?(:to_event_properties)
|
456
|
+
merge_properties(target, value.to_event_properties, "#{prefixed_key}_", depth + 1)
|
457
|
+
else
|
458
|
+
raise ArgumentError, "Event property #{prefixed_key.inspect} is not a valid scalar, Hash, or object that " +
|
459
|
+
"responds to #to_event_properties, but rather #{value.inspect} (#{value.class.name})."
|
460
|
+
end
|
461
|
+
else
|
462
|
+
target[prefixed_key] = net_value
|
463
|
+
end
|
464
|
+
end
|
465
|
+
end
|
466
|
+
|
467
|
+
FLOAT_INFINITY = (1.0 / 0.0)
|
468
|
+
|
469
|
+
# Given a potential scalar value for a property, either returns the value that should actually be set in the
|
470
|
+
# resulting set of properties (for example, converting Symbols to Strings) or returns +:invalid_property_value+
|
471
|
+
# if that isn't a valid scalar value for a property.
|
472
|
+
def normalize_scalar_property_value(value)
|
473
|
+
return "NaN" if value.kind_of?(Float) && value.nan?
|
474
|
+
|
475
|
+
case value
|
476
|
+
when true, false, nil then value
|
477
|
+
when ActiveSupport::Duration then value.to_i
|
478
|
+
when FLOAT_INFINITY then "+infinity"
|
479
|
+
when -FLOAT_INFINITY then "-infinity"
|
480
|
+
when Numeric then value
|
481
|
+
when String then value.strip
|
482
|
+
when Symbol then value.to_s.strip
|
483
|
+
when Time then value.utc.strftime("%Y-%m-%dT%H:%M:%S")
|
484
|
+
when Array then
|
485
|
+
out = value.map { |e| normalize_scalar_property_value(e) }
|
486
|
+
out = :invalid_property_value if out.detect { |e| e == :invalid_property_value }
|
487
|
+
out
|
488
|
+
else :invalid_property_value
|
489
|
+
end
|
490
|
+
end
|
491
|
+
end
|
492
|
+
end
|
493
|
+
end
|
data/lib/meta_events.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
require "meta_events/version"
|
2
|
+
require "meta_events/definition/definition_set"
|
3
|
+
require "meta_events/tracker"
|
4
|
+
require "meta_events/test_receiver"
|
5
|
+
require "meta_events/helpers"
|
6
|
+
require "meta_events/controller_methods"
|
7
|
+
|
8
|
+
# See if we can load Rails -- but don't fail if we can't; we'll just use this to decide whether we should
|
9
|
+
# load the Railtie or not.
|
10
|
+
begin
|
11
|
+
gem 'rails'
|
12
|
+
rescue Gem::LoadError => le
|
13
|
+
# ok
|
14
|
+
end
|
15
|
+
|
16
|
+
begin
|
17
|
+
require 'rails'
|
18
|
+
rescue LoadError => le
|
19
|
+
# ok
|
20
|
+
end
|
21
|
+
|
22
|
+
if defined?(::Rails)
|
23
|
+
require "meta_events/railtie"
|
24
|
+
require "meta_events/engine"
|
25
|
+
end
|
26
|
+
|
27
|
+
module MetaEvents
|
28
|
+
# Your code goes here...
|
29
|
+
end
|
data/meta_events.gemspec
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'meta_events/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "meta_events"
|
8
|
+
spec.version = MetaEvents::VERSION
|
9
|
+
spec.authors = ["Andrew Geweke"]
|
10
|
+
spec.email = ["ageweke@swiftype.com"]
|
11
|
+
spec.summary = %q{Structured, documented, powerful event emitting library for Mixpanel and other such systems.}
|
12
|
+
spec.homepage = "http://www.github.com/swiftype/meta_events"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_dependency "json", "~> 1.0"
|
21
|
+
|
22
|
+
if RUBY_VERSION =~ /^1\.8\./
|
23
|
+
spec.add_dependency "activesupport", ">= 3.0", "< 4.0"
|
24
|
+
else
|
25
|
+
spec.add_dependency "activesupport", ">= 3.0", "<= 4.99.99"
|
26
|
+
end
|
27
|
+
|
28
|
+
spec.add_development_dependency "bundler", "~> 1.5"
|
29
|
+
spec.add_development_dependency "rake"
|
30
|
+
spec.add_development_dependency "rspec", "~> 2.14"
|
31
|
+
end
|