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,43 @@
1
+ require 'active_support'
2
+
3
+ module MetaEvents
4
+ # This module defines methods that we add to ActionController::Base if we're being used with Rails.
5
+ module ControllerMethods
6
+ # We use this for the #included block, below.
7
+ extend ActiveSupport::Concern
8
+
9
+ # Declares a new "frontend event". A frontend event is, at its core, a binding from a name to (an event name,
10
+ # a set of properties to fire with that event); by default, the name used is just the event name, without its
11
+ # normal prefix (_i.e._, +foo_bar+, not +ab1_foo_bar+).
12
+ #
13
+ # You declare the frontend event here, on the server side; the server renders into the page this very binding,
14
+ # and the frontend JavaScript (+meta_events.js.erb+) can pick up that data and expose it by very easy-to-use
15
+ # JavaScript functions.
16
+ #
17
+ # +category+ is the category for your event;
18
+ def meta_events_define_frontend_event(category, event, properties = { }, options = { })
19
+ options.assert_valid_keys(:name, :tracker)
20
+
21
+ name = (options[:name] || "#{category}_#{event}").to_s
22
+ tracker = options[:tracker] || meta_events_tracker
23
+
24
+ @_meta_events_registered_clientside_events ||= { }
25
+ @_meta_events_registered_clientside_events[name] = tracker.effective_properties(category, event, properties)
26
+ end
27
+
28
+ # Returns the set of defined frontend events.
29
+ def meta_events_defined_frontend_events
30
+ @_meta_events_registered_clientside_events || { }
31
+ end
32
+
33
+ def meta_events_tracker
34
+ raise "You must implement the method #meta_events_tracker on your controllers for this method to work; it should return the MetaEvents::Tracker instance (ideally, cached, using just something like @tracker ||=) that you want to use."
35
+ end
36
+
37
+ # When we get included into a controller, declare these methods as helper methods, so they're available to views,
38
+ # too.
39
+ included do
40
+ helper_method :meta_events_define_frontend_event, :meta_events_defined_frontend_events, :meta_events_tracker
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,78 @@
1
+ require "meta_events"
2
+ require "meta_events/definition/event"
3
+ require "active_support/core_ext"
4
+
5
+ module MetaEvents
6
+ module Definition
7
+ # A Category is the middle level of hierarchy in the MetaEvents DSL. Child of a Version and parent of an Event, it
8
+ # groups together a set of Events that logically belong together. Programmatically, it serves no purpose other than
9
+ # to group together related events, so that the namespace of events doesn't get enormous.
10
+ #
11
+ class Category
12
+ class << self
13
+ # Normalizes the name of a category, so that we don't run into crazy Symbol-vs.-String bugs.
14
+ def normalize_name(name)
15
+ raise ArgumentError, "Must supply a name for a category, not: #{name.inspect}" if name.blank?
16
+ name.to_s.strip.downcase.to_sym
17
+ end
18
+ end
19
+
20
+ attr_reader :version, :name
21
+
22
+ # Creates a new instance. +version+ must be the ::MetaEvents::Definition::Version to which this Category should belong;
23
+ # +name+ is the name of the category. +options+ can contain:
24
+ #
25
+ # [:retired_at] If passed, this must be a String that can be parsed by Time.parse; it indicates the time at which
26
+ # this category was retired, meaning that it no longer can be used to fire events. (The code does
27
+ # not actually care about the value of this Time; that's used only for record-keeping purposes --
28
+ # rather, it's used simply as a flag indicating that the category has been retired, and events in
29
+ # it should no longer be allowed to be fired.)
30
+ #
31
+ # The block passed to this constructor is evaluated in the context of this object; this is how we build our
32
+ # DSL.
33
+ def initialize(version, name, options = { }, &block)
34
+ raise ArgumentError, "You must pass a Version, not: #{version.inspect}" unless version.kind_of?(::MetaEvents::Definition::Version)
35
+
36
+ @version = version
37
+ @name = self.class.normalize_name(name)
38
+ @events = { }
39
+
40
+ options.assert_valid_keys(:retired_at)
41
+
42
+ @retired_at = Time.parse(options[:retired_at]) if options[:retired_at]
43
+
44
+ instance_eval(&block) if block
45
+ end
46
+
47
+ # Declares a new event. +name+ is the name of the event; all additional arguments are passed to the constructor
48
+ # of ::MetaEvents::Definition::Event. It is an error to try to define two events with the same name.
49
+ def event(name, *args, &block)
50
+ event = ::MetaEvents::Definition::Event.new(self, name, *args, &block)
51
+ raise ArgumentError, "Category #{self.name.inspect} already has an event named #{event.name.inspect}" if @events[event.name]
52
+ @events[event.name] = event
53
+ end
54
+
55
+ # Returns the full prefix that events of this Category should use.
56
+ def prefix
57
+ "#{version.prefix}#{name}_"
58
+ end
59
+
60
+ # Retrieves an event with the given name; raises +ArgumentError+ if there is no such event.
61
+ def event_named(name)
62
+ name = ::MetaEvents::Definition::Event.normalize_name(name)
63
+ @events[name] || raise(ArgumentError, "#{self} has no event named #{name.inspect}; it has: #{@events.keys.sort_by(&:to_s).inspect}")
64
+ end
65
+
66
+ # Returns the effective time at which this category was retired, or +nil+ if it is not retired. This is the
67
+ # earliest of the time at which this category was retired and the time at which the version was retired.
68
+ def retired_at
69
+ [ @retired_at, version.retired_at ].compact.min
70
+ end
71
+
72
+ # Override #to_s, for a cleaner view.
73
+ def to_s
74
+ "<Category #{name.inspect} of #{version}>"
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,122 @@
1
+ require "meta_events"
2
+ require "meta_events/definition/version"
3
+
4
+ module MetaEvents
5
+ module Definition
6
+ # A DefinitionSet is the root of the MetaEvents DSL. Generally speaking, any application will have exactly one
7
+ # DefinitionSet, which contains the definition of every event it currently or has ever fired.
8
+ #
9
+ # The only reason a DefinitionSet is not a singleton object (or just class methods) is that it is extremely
10
+ # useful for testing to be able to use a separate DefinitionSet.
11
+ #
12
+ # A single DefinitionSet has a +global_events_prefix+, which is prepended to every event fired. This can be used
13
+ # to easily distinguish events that come through this system from events before it was introduced, or versus events
14
+ # fired by some other system entirely.
15
+ class DefinitionSet
16
+ class BaseError < StandardError; end
17
+ class RetiredEventError < BaseError; end
18
+
19
+ class << self
20
+ # Creates an MetaEvents::Definition::DefinitionSet. +source+ can be one of:
21
+ #
22
+ # * An MetaEvents::Definition::DefinitionSet; we simply return it. This can seem a little redundant (and it is), but
23
+ # it helps us write much cleaner code in other classes (like MetaEvents::Tracker).
24
+ # * An IO (or StringIO, which doesn't actually inherit from IO but effectively is one); or
25
+ # * A path to a File.
26
+ #
27
+ # In both of the last two cases, we interpret the contents of the file as Ruby code in the context of the new
28
+ # DefinitionSet -- in other words, it should look something like:
29
+ #
30
+ # global_events_prefix :mp
31
+ #
32
+ # version 1, '2014-01-01' do
33
+ # category :foo do
34
+ # event :bar, '2014-01-16', 'this is great'
35
+ # end
36
+ # end
37
+ def from(source)
38
+ source = new(:definition_text => source) unless source.kind_of?(self)
39
+ source
40
+ end
41
+ end
42
+
43
+ # Creates a new instance. +global_events_prefix+ must be a String or Symbol; it will be prepended to the name
44
+ # of every event fired. You can pass the empty string if you want.
45
+ #
46
+ # The block passed to this constructor is evaluated in the context of this object; this is how we build our
47
+ # DSL.
48
+ def initialize(options = { }, &block)
49
+ @global_events_prefix = nil
50
+ @versions = { }
51
+
52
+ options.assert_valid_keys(:global_events_prefix, :definition_text)
53
+
54
+ global_events_prefix options[:global_events_prefix] if options[:global_events_prefix]
55
+
56
+ @source_description = "passed-in data/block"
57
+
58
+ if (source = options[:definition_text])
59
+ if source.kind_of?(String)
60
+ File.open(File.expand_path(source)) { |f| read_from(f) }
61
+ else
62
+ read_from(source)
63
+ end
64
+ end
65
+
66
+ instance_eval(&block) if block
67
+
68
+ if global_events_prefix.blank?
69
+ raise ArgumentError, "When reading events from #{@source_description}: you must declare a global_events_prefix, or else pass one to the constructor"
70
+ end
71
+ end
72
+
73
+ # Sets the +global_events_prefix+ -- the string that will be prepended (with the version) to every single
74
+ # event fired through this DefinitionSet.
75
+ def global_events_prefix(prefix = nil)
76
+ if prefix
77
+ @global_events_prefix = prefix.to_s
78
+ else
79
+ @global_events_prefix
80
+ end
81
+ end
82
+
83
+ # Declares a new version. The +number+ is required and must be unique. For +introduced+ and +options+, see the
84
+ # constructor of ::MetaEvents::Definition::Version.
85
+ #
86
+ # The block passed is evaluated in the context of the new Version; this is how we build our DSL.
87
+ def version(number, introduced, options = { }, &block)
88
+ version = ::MetaEvents::Definition::Version.new(self, number, introduced, options, &block)
89
+ raise "There is already a version #{version.number.inspect}" if @versions[version.number]
90
+ @versions[version.number] = version
91
+ end
92
+
93
+ # Returns the Version object for the given number, or raises an exception if there is none.
94
+ def fetch_version(number)
95
+ @versions[number] || raise(ArgumentError, "No such version #{number.inspect}; I have: #{@versions.keys.sort_by(&:to_s).inspect}")
96
+ end
97
+
98
+ # Fetches an ::MetaEvents::Definition::Event object directly, by version number, category, and event.
99
+ def fetch_event(version_num, category_name, event_name)
100
+ fetch_version(version_num).fetch_event(category_name, event_name)
101
+ end
102
+
103
+ private
104
+ def read_from(source)
105
+ # StringIO is, really annoyingly, *not* an actual subclass of IO.
106
+ raise ArgumentError, "Invalid source: #{source.inspect}" unless source.kind_of?(IO) || source.kind_of?(StringIO)
107
+ args = [ source.read ]
108
+
109
+ if source.respond_to?(:path) && source.respond_to?(:lineno) && source.path && source.lineno
110
+ args += [ source.path, source.lineno ]
111
+ @source_description = "#{source.path}:#{source.lineno}"
112
+ end
113
+
114
+ begin
115
+ instance_eval(*args)
116
+ rescue Exception => e
117
+ raise "When reading event definitions from #{@source_description}, we got an exception: (#{e.class.name}) #{e.message}\n #{e.backtrace.join("\n ")}"
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,163 @@
1
+ require "meta_events"
2
+
3
+ # An ::MetaEvents::Definition::Event is the lowest level of the MetaEvents DSL. It belongs to a Category (which, in turn,
4
+ # belongs to a Version), and should represent a single, consistent "thing that happened" that you want to track.
5
+ # The name of an Event must be unique within its Category and Version.
6
+ #
7
+ # The definition of "single, consistent" depends very much on your context and the scope of what you're tracking,
8
+ # and will require significant judgement calls. For example, if your event is +:signup+ (in category +:user+), and
9
+ # you change from a lengthy, complex, demanding signup process to Facebook signup -- with an optional form of all the
10
+ # lengthy information later -- is that the same event? It depends greatly on how you think of it; you could keep it
11
+ # the same event, or introduce a new +:simple_signup+ event -- it depends on how you want to track it.
12
+ module MetaEvents
13
+ module Definition
14
+ class Event
15
+ attr_reader :category, :name
16
+
17
+ class << self
18
+ # Normalizes the name of an Event, so that we don't run into crazy Symbol-vs.-String bugs.
19
+ def normalize_name(name)
20
+ raise ArgumentError, "Must supply a name for an event, not: #{name.inspect}" if name.blank?
21
+ name.to_s.strip.downcase.to_sym
22
+ end
23
+ end
24
+
25
+ # Creates a new instance. +category+ must be the ::MetaEvents::Definition::Category that this event is part of; +name+
26
+ # must be the name of the event.
27
+ #
28
+ # In order to create a new instance, you must also supply a description for the instance and indicate when it was
29
+ # introduced; this is part of the required record-keeping that makes the MetaEvents DSL useful. You can do this in one
30
+ # of several ways (expressed as it would look in the DSL):
31
+ #
32
+ # category :user do
33
+ # event :signup, "2014-01-01", "a new user we've never heard of before signs up"
34
+ # end
35
+ #
36
+ # or:
37
+ #
38
+ # category :user do
39
+ # event :signup, :introduced => "2014-01-01", :desc => "a new user we've never heard of before signs up"
40
+ # end
41
+ #
42
+ # or:
43
+ #
44
+ # category :user do
45
+ # event :signup do
46
+ # introduced "2014-01-01"
47
+ # desc "a new user we've never heard of before signs up"
48
+ # end
49
+ # end
50
+ #
51
+ # You can also combine these in any way you want; what's important is just that they get set, or else you'll get an
52
+ # exception at definition time.
53
+ def initialize(category, name, *args, &block)
54
+ raise ArgumentError, "Must supply a Category, not #{category.inspect}" unless category.kind_of?(::MetaEvents::Definition::Category)
55
+
56
+ @category = category
57
+ @name = self.class.normalize_name(name)
58
+ @notes = [ ]
59
+
60
+ apply_options!(args.extract_options!)
61
+ args = apply_args!(args)
62
+
63
+ raise ArgumentError, "Too many arguments: don't know what to do with #{args.inspect}" if args.present?
64
+
65
+ instance_eval(&block) if block
66
+
67
+ ensure_complete!
68
+ end
69
+
70
+ # Given a set of properties, validates this event -- that is, either returns without doing anything if everything is
71
+ # OK, or raises an exception if the event should not be allowed to be fired. Currently, all we do is fail if the
72
+ # event has been retired (directly, or via its Category or Version); however, this could easily be extended to
73
+ # provide for required properties, property validation, or anything else.
74
+ def validate!(properties)
75
+ if retired_at
76
+ raise ::MetaEvents::Definition::DefinitionSet::RetiredEventError, "Event #{full_name} was retired at #{retired_at.inspect} (or its category or version was); you can't use it any longer."
77
+ end
78
+ end
79
+
80
+ # Returns, or sets, the description for an event.
81
+ def desc(text = nil)
82
+ @description = text if text
83
+ @description
84
+ end
85
+
86
+ # Returns, or sets, the introduced-at time for an event.
87
+ def introduced(time = nil)
88
+ @introduced = Time.parse(time) if time
89
+ @introduced
90
+ end
91
+
92
+ # Returns the name of the category for an event.
93
+ def category_name
94
+ category.name
95
+ end
96
+
97
+ # Returns the full name of an event, including all prefixes.
98
+ def full_name
99
+ "#{category.prefix}#{name}"
100
+ end
101
+
102
+ # Returns the time at which this event has been retired, if any -- this is the earliest time from its category
103
+ # (which, in turn, is the earliest of the category and the version), and this event. If an event has been retired,
104
+ # then #validate! will fail.
105
+ def retired_at(value = nil)
106
+ @retired_at = Time.parse(value) if value
107
+ [ @retired_at, category.retired_at ].compact.min
108
+ end
109
+
110
+ # Adds a note to this event. Notes are simply metadata right now -- useful for indicating what the history of an
111
+ # event is, significant changes in its meaning, and so on.
112
+ def note(when_left, who, text)
113
+ raise ArgumentError, "You must specify when this note was left" if when_left.blank?
114
+ when_left = Time.parse(when_left)
115
+ raise ArgumentError, "You must specify who left this note" if who.blank?
116
+ raise ArgumentError, "You must specify an actual note" if text.blank?
117
+
118
+ @notes << { :when_left => when_left, :who => who, :text => text }
119
+ end
120
+
121
+ # Returns all notes associated with this event, as an array of Hashes.
122
+ def notes
123
+ @notes
124
+ end
125
+
126
+ # Override for clearer data.
127
+ def to_s
128
+ "<Event #{name.inspect} of #{category}>"
129
+ end
130
+
131
+ private
132
+ # Called at the very end of the constructor, to ensure that you have declared all required properties for this
133
+ # event.
134
+ def ensure_complete!
135
+ raise ArgumentError, "You must specify a description for event #{full_name}, either as an argument, in the options, or using 'desc'" if @description.blank?
136
+ raise ArgumentError, "You must record when you introduced event #{full_name}, either as an argument, in the options, or using 'introduced'" if (! @introduced)
137
+ end
138
+
139
+ # Called with the set of options (which can be empty) supplied in the constructor; responsible for applying those
140
+ # to the object properly.
141
+ def apply_options!(options)
142
+ options.assert_valid_keys(:introduced, :desc, :description, :retired_at)
143
+
144
+ introduced options[:introduced] if options[:introduced]
145
+ desc options[:desc] if options[:desc]
146
+ desc options[:description] if options[:description]
147
+
148
+
149
+ @retired_at = Time.parse(options[:retired_at]) if options[:retired_at]
150
+ end
151
+
152
+ # Called with the arguments (past the category and event name) supplied to the constructor; responsible for
153
+ # applying those to the object properly.
154
+ def apply_args!(args)
155
+ intro = args.shift
156
+ d = args.shift
157
+ introduced intro if intro
158
+ desc d if d
159
+ args
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,89 @@
1
+ require "meta_events"
2
+ require "meta_events/definition/category"
3
+
4
+ module MetaEvents
5
+ module Definition
6
+ # A Version is the common top level of hierarchy in the MetaEvents DSL. A Version represents a version of your
7
+ # application's _entire_ event hierarchy -- that is, you should create a new Version if (and only if) you decide
8
+ # to rearrange major parts of, or all of, your event hierarchy. (This is something that, in my experience, happens
9
+ # more often than you'd imagine. ;)
10
+ #
11
+ # A Version belongs to a DefinitionSet; it has a +number+, which is just an integer (that you'll probably be
12
+ # happier with if you make sequential, but no such requirement is imposed), the date (and possibly time) that it
13
+ # was introduced (for record-keeping purposes). Additionally, you can mark a version as _retired_, which means that
14
+ # it is still accessible for record-keeping purposes but will not allow any of its events to actually be fired.
15
+ class Version
16
+ attr_reader :definition_set, :number, :introduced
17
+
18
+ # Creates a new instance. +definition_set+ is the MetaEvents::Definition::DefinitionSet to which this Version belongs;
19
+ # +number+ is an integer telling you, well, which version this is -- it must be unique within the DefinitionSet.
20
+ # +introduced+ is a String that must be parseable using Time.parse; this should be the date (and time, if you
21
+ # really want to be precise) that the Version was first used, for record-keeping purposes.
22
+ #
23
+ # Currently, +options+ can contain:
24
+ #
25
+ # [:retired_at] If present, must be a String representing a date and/or time (as parseable by Time.parse); its
26
+ # presence indicates that this version is _retired_, meaning that you will not be allowed to fire
27
+ # events from this version. (The date and time itself is present for record-keeping purposes.)
28
+ # Set this if (and only if) this version should no longer be in use, presumably because it has
29
+ # been superseded by another version.
30
+ #
31
+ # Note that neither the introduction time nor retired-at time are actually compared with +Time.now+ in any way;
32
+ # the introduction time is not used in the event mechanism at all, and the +retired_at+ time is treated as a
33
+ # simple boolean flag (if present, you can't fire events from this version).
34
+ #
35
+ # The block passed to this constructor is evaluated in the context of this object; this is how we build our
36
+ # DSL.
37
+ def initialize(definition_set, number, introduced, options = { }, &block)
38
+ raise ArgumentError, "You must pass a DefinitionSet, not #{definition_set.inspect}" unless definition_set.kind_of?(::MetaEvents::Definition::DefinitionSet)
39
+ raise ArgumentError, "You must pass a version, not #{number.inspect}" unless number.kind_of?(Integer)
40
+
41
+ @definition_set = definition_set
42
+ @number = number
43
+ @introduced = Time.parse(introduced)
44
+ @categories = { }
45
+
46
+ options.assert_valid_keys(:retired_at)
47
+
48
+ @retired_at = Time.parse(options[:retired_at]) if options[:retired_at]
49
+
50
+ instance_eval(&block) if block
51
+ end
52
+
53
+ # Returns the prefix that all events in this version should have -- something like "st1", for example.
54
+ def prefix
55
+ "#{definition_set.global_events_prefix}#{number}_"
56
+ end
57
+
58
+ # Declares a category within this version; this is part of our DSL. See the constructor of
59
+ # ::MetaEvents::Definition::Category for more information about the arguments.
60
+ def category(name, options = { }, &block)
61
+ category = ::MetaEvents::Definition::Category.new(self, name, options, &block)
62
+ raise ArgumentError, "There is already a category named #{name.inspect}" if @categories[category.name]
63
+ @categories[category.name] = category
64
+ end
65
+
66
+ # Returns the Category with the given name, or raises ArgumentError if there is no such category.
67
+ def category_named(name)
68
+ name = ::MetaEvents::Definition::Category.normalize_name(name)
69
+ @categories[name] || raise(ArgumentError, "#{self} has no category #{name.inspect}; it has: #{@categories.keys.sort_by(&:to_s).inspect}")
70
+ end
71
+
72
+ # Returns the ::MetaEvents::Definition::Event object for the given category and event name, or raises
73
+ # ArgumentError if no such category or event exists.
74
+ def fetch_event(category_name, event_name)
75
+ category_named(category_name).event_named(event_name)
76
+ end
77
+
78
+ # Returns the Time at which this version was retired, or +nil+ if it is still active.
79
+ def retired_at
80
+ @retired_at
81
+ end
82
+
83
+ # Override #to_s, for a cleaner view.
84
+ def to_s
85
+ "<Version #{number}>"
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,6 @@
1
+ require 'rails'
2
+
3
+ module MetaEvents
4
+ class Engine < ::Rails::Engine
5
+ end
6
+ end
@@ -0,0 +1,114 @@
1
+ require 'json'
2
+
3
+ module MetaEvents
4
+ # This module gets included as a Rails helper module, if Rails is available. It defines methods that are usable
5
+ # by views to do tracking from the front-end -- both auto-tracking and frontend events.
6
+ module Helpers
7
+ class << self
8
+ # Defines (or returns) the prefix we use for our class and data attributes; this just needs to be unique enough
9
+ # that we are highly unlikely to collide with any other attributes.
10
+ def meta_events_javascript_tracking_prefix(prefix = nil)
11
+ if prefix == nil
12
+ @_meta_events_javascript_tracking_prefix
13
+ else
14
+ if (prefix.kind_of?(String) || prefix.kind_of?(Symbol)) && prefix.to_s.strip.length > 0
15
+ @_meta_events_javascript_tracking_prefix = prefix.to_s.strip
16
+ @_meta_events_javascript_tracking_prefix = $1 if @_meta_events_javascript_tracking_prefix =~ /^([^_]+)_+$/i
17
+ else
18
+ raise ArgumentError, "Must supply a String or Symbol, not: #{prefix.inspect}"
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ # The default prefix we use.
25
+ meta_events_javascript_tracking_prefix "mejtp"
26
+
27
+ # PRIVATE (though there's no point to declaring something private in a helper; everything's being called from the
28
+ # same object anyway). Simply prepends our prefix, plus an underscore, to whatever's passed in.
29
+ def meta_events_prefix_attribute(name)
30
+ "#{MetaEvents::Helpers.meta_events_javascript_tracking_prefix}_#{name}"
31
+ end
32
+
33
+ # Given a Hash of attributes for an element -- and, optionally, a MetaEvents::Tracker instance to use; we default
34
+ # to using the one exposed by the +meta_events_tracker+ method -- extracts a +:meta_event+ property, if present,
35
+ # and turns it into exactly the attributes that +meta_events.js.erb+ can use to detect that this is an element
36
+ # we want to track (and thus correctly return it from its +forAllTrackableElements+ method). If no +:meta_event+
37
+ # key is present on the incoming set of attributes, simply returns exactly its input.
38
+ #
39
+ # The +:meta_event+ property must be a Hash, containing:
40
+ #
41
+ # [:category] The name of the category of the event;
42
+ # [:event] The name of the event within the category;
43
+ # [:properties] Any additional properties to fire with the event; this is optional.
44
+ def meta_events_tracking_attributes_for(input_attributes, event_tracker = meta_events_tracker)
45
+ # See if we've even got an event...
46
+ return input_attributes unless input_attributes && (input_attributes[:meta_event] || input_attributes['meta_event'])
47
+
48
+ # If so, let's start populating our set of output attributes.
49
+ # #with_indifferent_access dups the Hash even if it already has indifferent access, which is important here
50
+ output_attributes = input_attributes.with_indifferent_access
51
+ event_data = output_attributes.delete(:meta_event)
52
+
53
+ # A little error-checking...
54
+ unless event_data.kind_of?(Hash)
55
+ raise ArgumentError, ":meta_event must be a Hash, not: #{event_data.inspect}"
56
+ end
57
+
58
+ event_data.assert_valid_keys(%w{category event properties})
59
+
60
+ # Grab our event data...
61
+ category = event_data[:category]
62
+ event = event_data[:event]
63
+ properties = event_data[:properties] || { }
64
+
65
+ unless category && event
66
+ raise ArgumentError, "You must supply :category and :event in your :meta_event attributes, not: #{event_data.inspect}"
67
+ end
68
+
69
+ # Ask the Tracker to compute the set of properties we should be firing with this event...
70
+ props_data = event_tracker.effective_properties(category, event, properties)
71
+
72
+ # Add our class to the +:class+ attribute -- Rails supports declaring +:class+ as an Array, and so we'll use
73
+ # that here. It works fine even if +:class+ is a string of space-separated class names.
74
+ classes = Array(output_attributes.delete(:class) || [ ])
75
+ classes << meta_events_prefix_attribute("trk")
76
+ output_attributes[:class] = classes
77
+
78
+ # Set the data attributes we'll be looking for...
79
+ output_attributes["data-#{meta_events_prefix_attribute('evt')}"] = props_data[:event_name]
80
+ output_attributes["data-#{meta_events_prefix_attribute('prp')}"] = props_data[:properties].to_json
81
+
82
+ # And we're done!
83
+ output_attributes
84
+ end
85
+
86
+ # This works exactly like Rails' built-in +link_to+ method, except that it takes a +:meta_event+ property in
87
+ # +html_options+ and turns the link into a tracked link, using #meta_events_tracking_attributes_for, above.
88
+ #
89
+ # The +:meta_event+ property is actually required; this is because, presumably, you're calling this method exactly
90
+ # because you want to track something, and if you didn't pass +:meta_event+, you probably misspelled or forgot
91
+ # about it.
92
+ #
93
+ # Obviously, feel free to create a shorter alias for this method in your application; we give it a long, unique
94
+ # name here so that we don't accidentally collide with another helper method in your project.
95
+ def meta_events_tracked_link_to(name = nil, options = nil, html_options = nil, &block)
96
+ unless html_options && html_options[:meta_event]
97
+ raise ArgumentError, "You asked for a tracked link, but you didn't provide a :meta_event: #{html_options.inspect}"
98
+ end
99
+
100
+ link_to(name, options, meta_events_tracking_attributes_for(html_options, meta_events_tracker), &block)
101
+ end
102
+
103
+
104
+ # Returns a JavaScript string that, when placed on a page into which +meta_events.js.erb+ has been included, sets
105
+ # up all defined front-end events so that they can be fired by that JavaScript.
106
+ def meta_events_frontend_events_javascript
107
+ out = ""
108
+ (meta_events_defined_frontend_events || { }).each do |name, properties|
109
+ out << "MetaEvents.registerFrontendEvent(#{name.to_json}, #{properties.to_json});\n"
110
+ end
111
+ out.html_safe
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,29 @@
1
+ module MetaEvents
2
+ class Railtie < Rails::Railtie
3
+ def say(x)
4
+ ::Rails.logger.info "MetaEvents: #{x}"
5
+ end
6
+
7
+ initializer "meta_events.configure_rails_initialization" do
8
+ ::ActiveSupport.on_load(:action_view) do
9
+ include ::MetaEvents::Helpers
10
+ end
11
+
12
+ ::ActiveSupport.on_load(:action_controller) do
13
+ include ::MetaEvents::ControllerMethods
14
+ end
15
+
16
+ return if ::MetaEvents::Tracker.default_definitions
17
+
18
+ config_meta_events = File.expand_path(File.join(::Rails.root, 'config', 'meta_events.rb'))
19
+ if File.exist?(config_meta_events)
20
+ ::MetaEvents::Tracker.default_definitions = config_meta_events
21
+ say "Loaded event definitions from #{config_meta_events.inspect}"
22
+
23
+ if defined?(::Spring)
24
+ Spring.watch config_meta_events
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end