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,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,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
|