meta_events 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,3 @@
1
+ module MetaEvents
2
+ VERSION = "1.0.1"
3
+ end
@@ -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
@@ -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