Giter Site home page Giter Site logo

meta_events's Introduction

MetaEvents is a Ruby gem that sits on top of a user-centric analytics system like Mixpanel and provides structure, documentation, and a historical record to events, and a powerful properties system that makes it easy to pass large numbers of consistent properties with your events.

MetaEvents supports:

  • 1.9.3, 2.0.0, 2.1.2, or JRuby 1.7.12

These are, however, just the versions it's tested against; MetaEvents contains no code that should be at all particularly dependent on exact Ruby versions, and should be compatible with a broad set of versions.

Current build status: Current Build Status

Brought to you by the folks at Swiftype. First version written by Andrew Geweke. For additional contributors, see CONTRIBUTORS.

Installation

If you're in a project using Bundler — for example, any Rails project, most Gems, and probably most other Ruby software these days — and therefore have a Gemfile, simply add this to the end of your Gemfile:

gem 'meta_events'

Alternatively, if you aren't using Bundler:

gem install meta_events

Background

Sending user-centric events to (e.g.) Mixpanel is far from difficult; it's a single method call. However, in a large project, adding calls to Mixpanel all over eventually starts causing issues:

  • Understanding what, exactly, an event is tracking, including when it was introduced and when it was changed, is paramount to doing correct analysis. But once events have been around for a while, whoever put them there has long-since forgotten (or they may even not be around any more), and trying to understand what User Upgraded Account means, eighteen months later, involves an awful lot of spelunking. (Why did it suddenly triple, permanently, on February 19th? Is that because we changed what the event means or because we improved the product?)
  • Getting a holistic view of what events there are and how they interact becomes basically impossible; all you can do is look at the output (i.e., Mixpanel) and hope you can put the pieces together from there.
  • Critical to using Mixpanel well is to pass lots and lots of properties; engineers being the lazy folks that we are, we often don't do this, and, when we do, they're named inconsistently and may mean different things in different places.
  • Often you want certain properties of the currently-logged-in user (for example) passed on every single event, and there's not always a clean way to do this.

MetaEvents

MetaEvents helps solve this problem by adding a few critical features:

  1. The MetaEvents DSL requires developers to declare and document events as they add them (and if they don't, they can't fire them); this is quick and easy, but enormously powerful as it gives you a holistic view of your events, a historical record, and detailed documentation on each one.
  2. Object properties support means you can define the set of event properties an object in your system (like a User) should expose, and then simply pass that object in your event — this makes it vastly easier to include lots of properties, and be consistent about them.
  3. Implicit properties support means you can add contextual properties (like the currently-logged-in user) in a single place, and then have every event include those properties.
  4. Front-end integration lets you very easily track events from DOM elements (like links) using JavaScript, and use a powerful mechanism to fire front-end events in any way you want.

Getting Started

Let's get started. We'll assume we're working in a Rails project, although MetaEvents has no dependencies on Rails or any other particular framework. We'll also assume you've installed the MetaEvents gem (ideally via your Gemfile).

Note: You will also need to provide whatever API your analytics system uses; for example, in the case of Mixpanel, you must also add gem mixpanel-ruby to your Gemfile.

Declaring Events

First, let's declare an event that we want to fire. Create config/meta_events.rb (MetaEvents automatically configures this as your events file if you're using Rails; if not, use MetaEvents::Tracker.default_definitions = to set the path to whatever file you like):

global_events_prefix :ab

version 1, "2014-02-04" do
  category :user do
    event :signed_up, "2014-02-04", "user creates a brand-new account"
  end
end

Let's walk through this:

  • global_events_prefix is a short string that gets added before every single event; this helps discriminate events coming from MetaEvents from events coming from other systems. Choose this carefully, don't ever change it, and keep it short — most tools, like Mixpanel, have limited screen real estate for displaying event names.
  • version 1 defines a version of your entire events system; this is useful in the case where you want to rework the entire set of events you fire — which is not an uncommon thing. But, for a while, we'll only need a single version, and we'll call it 1.
  • 2014-02-04 is when this version first was used; this can be any date (and time, if you really want to be precise) that you want — it just has to be parseable by Ruby's Time.parse method. (MetaEvents never, ever compares this date to Time.now or otherwise uses it; it's just for documentation.)
  • category :user is just a grouping and namespacing of events; the category name is included in every event name when fired.
  • event :signed_up declares an event with a name; 2014-02-04 is required and is the date (and time) that this event was introduced. (Again, this is just for documentation purposes.) user creates a brand-new account is also just for documentation purposes (and also is required), and describes the exact purpose of this event.

Firing Events

To fire an event, we need an instance of MetaEvents::Tracker. For reasons to be explained shortly, we'll want an instance of this class to be created at a level where we may have things in common (like the current user) — so, in a Rails application, our ApplicationController is a good place. We need to pass it the distinct ID of the user that's signed in, which is almost always just the primary key from the users table — or nil if no user is currently signed in. We also pass it the IP address of the user (which can safely be nil); Mixpanel, for example, uses this for doing geolocation of users:

class ApplicationController < ActionController::Base
  ...
  def meta_events_tracker
    @meta_events_tracker ||= MetaEvents::Tracker.new(current_user.try(:id), request.remote_ip)
  end
  ...
end

Now, from the controller, we can fire an event and pass a couple of properties:

class UsersController < ApplicationController
  ...
  def create
    ...
    meta_events_tracker.event!(:user, :signed_up, { :user_gender => @new_user.gender, :user_age => @new_user.age })
    ...
  end
  ...
end

We're just about all done; but, right now, the event isn't actually going anywhere, because we haven't configured any event receivers.

Hooking Up Mixpanel and a Test Receiver

An event receiver is any object that responds to a method #track(distinct_id, event_name, event_properties), where distinct_id is the distinct ID of the user, event_name is a String and event_properties is a Hash mapping String property names to simple scalar values — true, false, nil, numbers (all Numerics, including both integers and floating-point numbers, are supported), Strings (and Symbols will be converted to Strings transparently), and Time objects.

Fortunately, the Mixpanel Gem complies with this interface perfectly. So, in config/environments/production.rb (or any other file that loads before your first event gets fired):

MetaEvents::Tracker.default_event_receivers << Mixpanel::Tracker.new("0123456789abcdef")

(where 0123456789abcdef is actually your Mixpanel API token)

In our development environment, we may or may not want to include Mixpanel itself (so we can either add or not add the Mixpanel event receiver, above); however, we might also want to print events to the console or some other file as they are fired. So, in config/environments/development.rb:

MetaEvents::Tracker.default_event_receivers << MetaEvents::TestReceiver.new

This will print events as they are fired to your Rails log (e.g., log/development.log); you can pass an argument to the constructor of TestReceiver that's a Logger, an IO (e.g., STDOUT, STDERR, an open File object), or a block (or anything responding to call), if you want it to go elsewhere.

Testing It Out

Now, when you fire an event, you should get output like this in your Rails log:

Tracked event: user 483123, "ab1_user_signed_up"
                          user_age: 27
                       user_gender: female

...and, if you have configured Mixpanel properly, it will have been sent to Mixpanel, too!

Firing Front-End Events

Generally speaking, firing events from the back end (your application server talking to Mixpanel or some other service directly) is more reliable, while firing events from the front end (JavaScript in your users' browsers talking to Mixpanel or some other service) is more scalable — so you may wish to fire events from the front end, too. Further, there are certain events (scrolling, JavaScript manipulation in the browser, and so on) that simply don't exist on the back end and can't be tracked from there — at least, not without adding calls back to your server from the front-end JavaScript.

IMPORTANT: In case it isn't obvious, any property you include in a front-end event is visible to your users. No matter what tricks you might include to obscure that data, it fundamentally will be present on your users' computers and thus visible to them if they want to take a look. This is no different than the situation would be without MetaEvents, but, because MetaEvents makes it so easy to add large amounts of properties (which is a good thing!), you should take extra care with your #to_event_properties methods once you start firing front-end events.

You can fire front-end events with MetaEvents in two ways: auto-tracking and frontend events. Both methods require the use of Rails (because MetaEvents::ControllerMethods is intended for use with ActionController, and MetaEvents::Helpers is intended for use with ActionView), although the techniques are generally applicable and easy enough to use with any framework.

Note: Again, because MetaEvents does not directly require any one user-centric analytics system, you must make sure the JavaScript API to whatever system you're using is loaded, too. So, for example, if you're using Mixpanel, make sure the JavaScript code Mixpanel provides you to use its API is loaded on your page as well as the MetaEvents JavaScript code.

Auto-Tracking

Auto-tracking is the easiest way of triggering front-end events. MetaEvents provides a Rails helper method that adds certain attributes to any DOM element you wish (like a link); it then provides a JavaScript function that automatically picks up these attributes, decodes them, and calls any function you want with them.

As an example, in a view, you simply convert:

<%= link_to("go here", user_awesome_path, :class => "my_class") %>

...to:

<%= meta_events_tracked_link_to("go here", user_awesome_path, :class => "my_class",
                                :meta_event => { :category => :user, :event => :awesome,
                                                 :properties => { :color => 'green' } }) %>

(Not immediately obvious: the :meta_event attribute is just part of the html_options Hash that link_to accepts, not an additional parameter. meta_events_tracked_link_to accepts exactly the same parameters as link_to.)

This automatically turns the generated HTML from:

<a href="/users/awesome" class="my_class">go here</a>

to something like this:

<a href="/users/awesome" class="my_class mejtp_trk" data-mejtp-event="ab1_user_awesome"
   data-mejtp-prp="{&quot;ip&quot;:&quot;127.0.0.1&quot;,&quot;color&quot;:&quot;green&quot;,&quot;implicit_prop_1&quot;:&quot;someValue&quot;}">go here</a>

mejtp stands for "MetaEvents JavaScript Tracking Prefix", and is simply a likely-unique prefix for these values. (You can change it with MetaEvents::Helpers.meta_events_javascript_tracking_prefix 'foo'.) mejtp_trk is the class that allows us to easily detect which elements are set up for tracking; the two data attributes pass the full name of the event, and a JSON-encoded string of all the properties (both implicit and explicit) to pass with the event.

Now, add this to a Javascript file in your application:

//= require meta_events

And, finally, call something like this:

$(document).ready(function() {
  MetaEvents.forAllTrackableElements(document, function(id, element, eventName, properties) {
    mixpanel.track_links("#" + id, eventName, properties);
  })
});

MetaEvents.forAllTrackableElements accepts a root element to start searching at, and a callback function. It finds all elements with class mejtp_trk on them underneath that element, extracts the event name and properties, and adds a generated DOM ID to that element if it doesn't have one already. It then calls your callback function, passing that (existing or generated) DOM ID, the element itself, the name of the event, and the full set of properties (decoded, as a JavaScript Object here). You can then (as above) easily use this to do anything you want, like telling Mixpanel to track that link properly.

forAllTrackableElements also sets a certain data attribute on each element as it processes it, and knows to skip elements that already have that attribute set, so it's safe to call as often as you wish — for example, if the DOM changes. It does not know when the DOM changes, however, so, if you add content to your page, you will need to re-call it.

Frontend Events

Use Frontend Events only if Auto-Tracking isn't flexible enough for your purposes; Auto-Tracking is simpler in most ways.

Because MetaEvents leverages the events DSL to define events, and calls methods on your Ruby models (and other objects) to create large numbers of properties, you cannot simply fire an event by name from the front-end without a little extra work — otherwise, how would we get those properties? However, it's not much more work.

First off, make sure you get this into your layout in a <script> tag somewhere — at the bottom of the page is perfectly fine:

<%= meta_events_frontend_events_javascript %>

This allows MetaEvents to pass event data properly from the backend to the frontend for any events you'll be firing.

Now, as an example, let's imagine we implement a JavaScript game on our site, and want to fire events when the user wins, loses, or gets a new high score. First, let's define those in our DSL:

global_events_prefix :ab

version 1, "2014-02-11" do
  category :jsgame do
    event :won, "2014-02-11", "user won a game!"
    event :lost, "2014-02-11", "user lost a game"
    event :new_high_score, "2014-02-11", "user got a new high score"
  end
end

Now, in whatever controller action renders the page that the game is on, we need to register these events. This tells the front-end integration that we might fire them from the resulting page; it therefore embeds JavaScript in the page that defines the set of properties for those events, so that the front end has access to the data it needs:

class GameController < ApplicationController
  def game
    ...
    meta_events_define_frontend_event(:jsgame, :won, { :winning_streak => current_winning_streak })
    meta_events_define_frontend_event(:jsgame, :lost, { :losing_streak => current_losing_streak })
    meta_events_define_frontend_event(:jsgame, :new_high_score, { :previous_high_score => current_high_score })
    ...
  end
end

This will allow us to make the following calls in the frontend, from our game code:

if (wonGame) {
  MetaEvents.event('jsgame_won');
} else {
  MetaEvents.event('jsgame_lost');
}

if (currentScore > highScore) {
  MetaEvents.event('jsgame_new_high_score', { score: currentScore });
}

What's happened here is that meta_events_define_frontend_event took the set of properties you passed, merged them with any implicit properties defined, and passed them to the frontend via the meta_events_frontend_events_javascript output we added above. It binds each event to an event alias, which, by default, is just the category name and the event name, joined with an underscore. So when you call MetaEvents.event, it simply takes the string you pass it, looks up the event stored under that alias, merges any properties you supply with the ones passed from the backend, and fires it off. (You can, in fact, supply as many additional JavaScript objects/hashes as you want after the event alias; they will all be merged together, along with the properties supplied by the backend.)

Aliasing Event Names

If you need to be able to fire the exact same event with different sets of properties from different places in a single page, you can alias the event using the :name property:

class GameController < ApplicationController
  def game
    ...
    meta_events_define_frontend_event(:jsgame, :paused_game, { :while => :winning }, { :name => :paused_while_winning })
    meta_events_define_frontend_event(:jsgame, :paused_game, { :while => :losing }, { :name => :paused_while_losing })
    ...
  end
end
...
if (winning) {
  MetaEvents.event('paused_while_winning');
} else {
  MetaEvents.event('paused_while_losing');
}

Both calls from the JavaScript will fire the event ab1_jsgame_paused_game, but one of them will pass while: 'winning', and the other while: 'losing'.

Definition Cycle

Calls to meta_events_define_frontend_event get aggregated on the current controller object, during the request cycle. If you have events that can get fired on any page, then, for example, use a before_filter to always define them, or a method you mix in and call, or any other mechanism you want.

The Frontend Events Handler

MetaEvents.event calls the current frontend event handler on the MetaEvents JavaScript object; by default this just calls mixpanel.track. By calling MetaEvents.setEventHandler(myFunction), you can set it to anything you want; it gets passed the fully-qualified event name and set of all properties.

More About Distinct IDs

We glossed over the discussion of the distinct ID above. In short, it is a unique identifier (of no particular format; both Strings and integers are acceptable) that is unique to the user in question, based on your application's definition of 'user'. Using the primary key from your users table is typically a great way to do it.

There are a few situations where you need to take special care, however:

  • What about visitors who aren't signed in yet? In this case, you will want to generate a unique ID and assign it to the visitor anyway; generating a very large random number and putting it in a cookie in their browser is a good way to do this, as well as using something like nginx's ngx_http_userid_module. (Note that Mixpanel has facilities to do this automatically; however, it uses cookies set on their domain, which means you can't read them, which limits it unacceptably — server-side code and even your own Javascript will be unable to use this ID.) If you do this, take a look at web_server_uid, a gem that makes manipulating the resulting IDs vastly easier in Ruby.
  • What do I do when a user logs in? Typically, you simply want to switch completely from using their old (cookie-based) unique ID to using the primary key of your users table (or whatever you use for tracking logged-in users). This may seem counterintuitive, but it makes sense, particularly in broad consumer applications: until someone logs in, all you really know is which browser is hitting your site, not which user. Activity that happens in the signed-out state might be the user who eventually logs in...but it also might not be, in the case of shared machines; further, activity that happens before the user logs in is unlikely to be particularly interesting to you — you already have the user as a registered user, and so this isn't a conversion or sign-up funnel. Effectively treating the activity that happens before they sign in as a completely separate user is actually exactly the right thing to do. The correct code structure is simply to call #distinct_id= on your MetaEvents::Tracker at exactly the point at which you log them in (using your session, or a cookie, or whatever), and be done with it.
  • What do I do when a user signs up? This is the tricky case. You really want to correlate all the activity that happened before the signup process with the activity afterwards, so that you can start seeing things like "users who come in through funnel X convert to truly active/paid/whatever users at a higher rate than those through funnel Y". This requires support from your back-end analytics provider; Mixpanel calls it aliasing, and it's accessed via their alias call. It effectively says "the user with autogenerated ID X is the exact same user as the user with primary-key ID Y". Making this call is beyond the scope of MetaEvents, but is quite easy to do assuming your analytics provider supports it.

You may also wish to see Mixpanel's documentation about distinct ID, here, here, and here.

The Real Power of MetaEvents

Now that we've gotten the basics out of the way, we can start using the real power of MetaEvents.

Adding Implicit Properties

Very often, just by being in some particular part of code, you already know a fair amount of data that you want to pass as events. For example, if you're inside a Rails controller action, and you have a current user, you're probably going to want to pass properties about that user to any event that happens in the controller action.

You could add these to every single call to #event!, but MetaEvents has a better way. When you create the MetaEvents::Tracker instance, you can define implicit properties. Let's add some now:

class ApplicationController < ActionController::Base
  ...
  def meta_events_tracker
    implicit_properties = { }
    if current_user
      implicit_properties.merge!(
        :user_gender => current_user.gender,
        :user_age => current_user.age
      )
    end
    @meta_events_tracker ||= MetaEvents::Tracker.new(current_user.try(:id), request.remote_ip,
                                                    :implicit_properties => implicit_properties)
  end
  ...
end

Now, these properties will get passed on every event fired by this Tracker. (This is, in fact, the biggest consideration when deciding when and where you'll create new MetaEvents::Tracker instances: implicit properties are extremely useful, so you'll want the lifecycle of a Tracker to match closely the lifecycle of something in your application that has implicit properties.)

Multi-Object Events

We're also going to face another problem: many events involve multiple underlying objects, each of which has many properties that are defined on it. For example, imagine we have an event triggered when a user sends a message to another user. We have at least three entities: the 'from' user, the 'to' user, and the message itself. If we really want to instrument this event properly, we're going to want something like this:

meta_events_tracker.event!(:user, :sent_message, {
  :from_user_country => from_user.country,
  :from_user_state => from_user.state,
  :from_user_postcode => from_user.postcode,
  :from_user_city => from_user.city,
  :from_user_language => from_user.language,
  :from_user_referred_from => from_user.referred_from,
  :from_user_gender => from_user.gender,
  :from_user_age => from_user.age,

  :to_user_country => to_user.country,
  :to_user_state => to_user.state,
  :to_user_postcode => to_user.postcode,
  :to_user_city => to_user.city,
  :to_user_language => to_user.language,
  :to_user_referred_from => to_user.referred_from,
  :to_user_gender => to_user.gender,
  :to_user_age => to_user.age,

  :message_sent_at => message.sent_at,
  :message_type => message.type,
  :message_length => message.length,
  :message_language => message.language,
  :message_attachments => message.attachments?
  })

Needless to say, this kind of sucks. Either we're going to end up with a ton of duplicate, unmaintainable code, or we'll just cut back and only pass a few properties — greatly reducing the possibilities of our analytics system.

Using Hashes to Factor Out Naming

We can improve this situation by using a feature of MetaEvents: when properties are nested in sub-hashes, they get automatically expanded and their names prefixed by the outer hash key. So let's define a couple of methods on models:

class User < ActiveRecord::Base
  def to_event_properties
    {
      :country => country,
      :state => state,
      :postcode => postcode,
      :city => city,
      :language => language,
      :referred_from => referred_from,
      :gender => gender,
      :age => age
    }
  end
end

class Message < ActiveRecord::Base
  def to_event_properties
    {
      :sent_at => sent_at,
      :type => type,
      :length => length,
      :language => language,
      :attachments => attachments?
    }
  end
end

Now, we can pass the exact same set of properties as the above example, by simply doing:

meta_events_tracker.event!(:user, :sent_message, {
  :from_user => from_user.to_event_properties,
  :to_user => to_user.to_event_properties,
  :message => message.to_event_properties
  })

SO much better.

Moving Hash Generation To Objects

And — tah-dah! — MetaEvents supports this syntax automatically. If you pass an object as a property, and that object defines a method called #to_event_properties, then it will be called automatically, and replaced. Our code now looks like:

meta_events_tracker.event!(:user, :sent_message, { :from_user => from_user, :to_user => to_user, :message => message })

How to Take the Most Advantage

To make the most use of MetaEvents, define #to_event_properties very liberally on objects in your system, make them return any properties you even think might be useful, and pass them to events. MetaEvents will expand them for you, allowing large numbers of properties on events, which allows Mixpanel and other such systems to be of the most use to you.

Miscellaneous and Trivia

A few things before we're done:

Performance and Asynchronousness

When deploying Mixpanel or other such systems in a live application, you very often want to dispatch event reporting in the background, so that any slowdown in Mixpanel's servers doesn't slow down your application (or, in the worst case, bring it down entirely). Typically, this is done using something like Resque, Sidekiq, or another queue-based system. There are literally dozens of these systems available for Ruby, and it's highly likely that your codebase either uses one already or will soon. Additionally, many users layer additional features like logging, durability, or other infrastructure services on top of the base functionality of these packages.

Because there is such a wide variety of these systems available, MetaEvents does not directly provide support for them — doing so would be a great deal of effort, and yet still unlikely to satisfy most users. Instead, MetaEvents makes it very easy to use any package you want:

class MyEventReceiver
  def track(distinct_id, event_name, event_properties)
    # Call Resque, Sidekiq, or anything else you want here; enqueue a job that, when run, will call:
    #   Mixpanel::Tracker.new($my_mixpanel_api_key).track(distinct_id, event_name, event_properties)
  end
end

MetaEvents::Tracker.default_event_receivers << MyEventReceiver.new

Voilà — asynchronous event tracking.

Mixpanel, Aliasing, and People

MetaEvents is not intended as a complete superset of a backend analytics library (like Mixpanel) — there are features of those libraries that are not implemented via MetaEvents, and which should be used by direct calls to the service in question.

For example, Mixpanel has an alias call that lets you tell it that a user with a particular distinct ID is actually the same person as a user with a different distinct ID — this is typically used at signup, when you convert from an "anonymous" distinct ID representing the unknown user who is poking around your site to the actual official user ID (typically your users table primary key) of that user. MetaEvents does not, in any way, attempt to support this; it allows you to pass whatever distinct_id you want in the #event! call, but, if you want to use alias, you should make that Mixpanel call directly. (See also the discussion above about distinct ID.)

Similarly, Mixpanel's People functionality is not in any way directly supported by MetaEvents. You may well use the Tracker's #effective_properties method to compute a set of properties that you pass to Mixpanel's People system, but there are no calls directly in MetaEvents to do this for you.

Retiring an Event

Often you'll have events that you retire — they were used in the past, but no longer. You could just delete them from your MetaEvents DSL file, but this will mean the historical record is suddenly gone. (Well, there's source control, but that's a pain.)

Rather than doing this, you can retire them:

global_events_prefix :ab

version 1, "2014-02-04" do
  category :user do
    event :logged_in_with_facebook, "2014-02-04", "user creates a brand-new account", :retired_at => "2014-06-01"
    event :signed_up, "2014-02-04", "user creates a brand-new account"
  end
end

Given the above, trying to call event!(:user, :logged_in_with_facebook) will fail with an exception, because the event has been retired. (Note that, once again, the actual date passed to :retired_at is simply for record-keeping purposes; the exception is generated if :retired_at is set to anything.)

You can retire events, categories, and entire versions; this system ensures the DSL continues to be a historical record of what things were in the past, as well as what they are today.

Adding Notes to Events

You can also add notes to events. They must be tagged with the author and the time, and they can be very useful for documenting changes:

global_events_prefix :ab

version 1, "2014-02-04" do
  category :user do
    event :signed_up, "2014-02-04", "user creates a brand-new account" do
      note "2014-03-17", "jsmith", "Moved sign-up button to the home page -- should increase signups significantly"
    end
  end
end

This allows you to record changes to events, as well as the events themselves.

Documenting Events

Currently, the documentation for the MetaEvents DSL is the source to that DSL itself — i.e., config/meta_events.rb or something similar. However, methods on the DSL objects created (accessible via a Tracker's #definitions method, or MetaEvents::Tracker's default_definitions class method) allow for introspection, and could easily be extended to, e.g., generate HTML fully documenting the events.

Patches are welcome. ;-)

Times

MetaEvents automatically adds a time property to any event you fire via #event!; this is so that you can take the set of properties in a receiver and make it asynchronous, and don't have to worry about getting the time right. You can override this, however, by simply passing a :time property with your event; it will override any time we would otherwise set. (You can even set :time => nil if you want to make sure no time is passed at all.)

MetaEvents correctly converts any Time object you pass into the correct String format for Mixpanel (e.g., 2014-02-03T15:49:17), converting it to UTC first. This should make your times much cleaner.

Adding a New Version

What is this top-level version in the DSL? Well, every once in a while, you will want to completely redo your set of events — perhaps you've learned a lot about using your analytics system, and realize you want them configured in a different way.

When you want to do this, define a new top-level version in your DSL, and pass :version => 2 (or whatever number you gave the new version) when creating your MetaEvents::Tracker. The tracker will look under that version for categories and events, and completely ignore other versions; your events will be called things like ab2_user_signup instead of ab1_user_signup, and so on. The old version can still stay present in your DSL for documentation and historical purposes.

When you're completely done with the old version, retire it — version 1, :retired_at => '2014-06-01' do ....

Often, you'll want to run two versions simultaneously, because you want to have a transition period where you fire both sets of events — this is hugely helpful in figuring out how your old events map to new events and when adjusting bases for the new events. (If you simply flash-cut from an old version to a new one on a single day, it is difficult or impossible to know if true underlying usage, etc., actually changed, or if it's just an artifact of changing events.) You can simply create two MetaEvents::Tracker instances, one for each version, and use them in parallel.

Customizing the Event Name

Developers love names like "xyz1_user_signed_up" but sometimes it's not a developer doing the analysis. Depending on what the back-end analytics library supports, event names in external systems are frequently not given a lot of real estate.

In cases like these, you can override the default external event name behavior. There are three ways to override these external names.

First, you can override them globally for all MetaEvents::Tracker instances:

MetaEvents::Tracker.default_external_name = lambda { |event| "#{event.category_name} #{event.name}" }

Second, you can override them for a specific MetaEvents::Tracker instance:

MetaEvents::Tracker.new(current_user.try(:id),
                    request.remote_ip,
                    :external_name => lambda { |event| "#{event.category_name} #{event.name}" }
                   )

Finally, you can override each event's external name in the events DSL:

global_events_prefix :ab

version 1, "2014-02-11" do
  category :example_category do
    event :example_event, "2014-02-11", "Example was exampled!", :external_name => 'ex. was ex.'
  end
end

The order of precedence for determining the external event name is the DSL's event :external_name => 'foo', MetaEvents::Tracker.new, MetaEvents::Tracker.default_external_name, built-in default.

Customizing the Nested Property Separator

Similarly, while developers might be perfectly comfortable with (and even prefer) expanded properties named things like user_age, user_name, and so on, others might want a different separator (like a space character). When defining a version, you can set this, as follows:

global_events_prefix :ab

version 1, "2014-02-11", :property_separator => ' ' do
  category :example_category do
    event :example_event, "2014-02-11", "Example was exampled!"
  end
end

Now, assuming @user is a User object that responds to #to_event_properties (or is just a Hash):

tracker.event!(:example_category, :example_event, :user => @user)

...you'll get properties named user age, user name, and so on, rather than user_age and user_name.

Note that this is defined on the version, not the category, event, or even #event! call, because changing this is a big deal — changing property names almost always breaks all kinds of analysis you might want to do with your analytics tool. However, the idea is that changing versions is a breaking change to your analytics system anyway, so you can certainly set it on a new version to something different.

Contributing

  1. Fork it ( http://github.com/swiftype/meta_events/fork )
  2. Create your feature branch (git checkout -b my-new-feature)
  3. Commit your changes (git commit -am 'Add some feature')
  4. Push to the branch (git push origin my-new-feature)
  5. Create new Pull Request

meta_events's People

Contributors

aaronlerch avatar aerlinger avatar ageweke avatar gauravtiwari avatar look avatar markquezada avatar pete280z avatar yogodoshi avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

meta_events's Issues

meta_events 1.1.0 gem dependencies

I went to upgrade meta_events to 1.1.0 in my Rails 4 app and it refused:

Bundler could not find compatible versions for gem "activesupport":
  In Gemfile:
    meta_events (~> 1.1.0) ruby depends on
      activesupport (< 4.0, >= 3.0) ruby

    rails (= 4.0.4) ruby depends on
      activesupport (4.0.4)

(yes, I know I need to upgrade rails for security reasons ... first things first though ;) )

Looking at the meta_events gemspec I see the conditional used to reference activesupport:

if RUBY_VERSION =~ /^1\.8\./
  spec.add_dependency "activesupport", ">= 3.0", "< 4.0"
else
  spec.add_dependency "activesupport", ">= 3.0", "<= 4.99.99"
end

I'm running with Ruby 2.1.1, and it appears that the gem dependencies are saying "too bad, sucker". :) Looks like the dependencies are evaluated when the gem is built, and that conditional references like this perhaps aren't possible. Though all I did was investigate the issue, I haven't investigated any solutions.

Here's a good SO writeup I found: http://stackoverflow.com/questions/8940271/build-a-ruby-gem-and-conditionally-specify-dependencies

New release?

The last release of this gem is 1.2.1 - November 06, 2014. Can you cut a new release so we can pick up the Rails 5 compatibility changes from #20? Would avoid us having to pull from GitHub.

Clarification on installation?

With meta_events, do I still need to use the mixpanel-ruby Gem or load mixpanel javascript separately on the front-end? It wasn't clear in the instructions.

undefined method `watch' for Spring:Module (NoMethodError)

getting this after going through the first few steps in README:

  • add meta_events gem in development group of Gemfile
  • setup config/meta_events.rb exactly the same
  • add meta_events_tracker to ApplicationController
  • call meta_events_tracker.event!(:user, :signed_up... on AccountsController on create
  • add MetaEvents::Tracker.default_event_receivers << MetaEvents::TestReceiver.new to config/environments/development.rb
  • run $ rails s
➜  blah git:(meta_events_for_mixpanel) ✗ rails s
=> Booting WEBrick
=> Rails 3.2.19 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
Exiting
/opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/meta_events-1.2.0/lib/meta_events/railtie.rb:24:in `block in <class:Railtie>': undefined method `watch' for Spring:Module (NoMethodError)
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/initializable.rb:30:in `instance_exec'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/initializable.rb:30:in `run'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/initializable.rb:55:in `block in run_initializers'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/initializable.rb:54:in `each'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/initializable.rb:54:in `run_initializers'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/application.rb:136:in `initialize!'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/railtie/configurable.rb:30:in `method_missing'
        from /Users/blah/Repos/blah/config/environment.rb:6:in `<top (required)>'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/activesupport-3.2.19/lib/active_support/dependencies.rb:251:in `require'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/activesupport-3.2.19/lib/active_support/dependencies.rb:251:in `block in require'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/activesupport-3.2.19/lib/active_support/dependencies.rb:236:in `load_dependency'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/activesupport-3.2.19/lib/active_support/dependencies.rb:251:in `require'
        from /Users/blah/Repos/blah/config.ru:14:in `block in <main>'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/rack-1.4.5/lib/rack/builder.rb:51:in `instance_eval'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/rack-1.4.5/lib/rack/builder.rb:51:in `initialize'
        from /Users/blah/Repos/blah/config.ru:in `new'
        from /Users/blah/Repos/blah/config.ru:in `<main>'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/rack-1.4.5/lib/rack/builder.rb:40:in `eval'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/rack-1.4.5/lib/rack/builder.rb:40:in `parse_file'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/rack-1.4.5/lib/rack/server.rb:200:in `app'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/commands/server.rb:46:in `app'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/rack-1.4.5/lib/rack/server.rb:304:in `wrapped_app'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/rack-1.4.5/lib/rack/server.rb:254:in `start'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/commands/server.rb:70:in `start'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/commands.rb:55:in `block in <top (required)>'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/commands.rb:50:in `tap'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/railties-3.2.19/lib/rails/commands.rb:50:in `<top (required)>'
        from /Users/blah/Repos/blah/bin/rails:10:in `require'
        from /Users/blah/Repos/blah/bin/rails:10:in `<top (required)>'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/spring-1.1.3/lib/spring/client/rails.rb:27:in `load'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/spring-1.1.3/lib/spring/client/rails.rb:27:in `call'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/spring-1.1.3/lib/spring/client/command.rb:7:in `call'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/spring-1.1.3/lib/spring/client.rb:26:in `run'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/spring-1.1.3/bin/spring:48:in `<top (required)>'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/spring-1.1.3/lib/spring/binstub.rb:11:in `load'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/gems/2.1.0/gems/spring-1.1.3/lib/spring/binstub.rb:11:in `<top (required)>'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from /opt/boxen/rbenv/versions/2.1.2/lib/ruby/2.1.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from /Users/blah/Repos/blah/bin/spring:16:in `<top (required)>'
        from bin/rails:3:in `load'
        from bin/rails:3:in `<main>'

How do I use this gem for tracking events in a single page app (SPA)?

I have a single page app (SPA) and I'm interested in using Frontend Events. According to the docs, I have to register any events I might fire from a page using meta_events_define_frontend_event.

This seems highly impractical for a SPA, as I'd have to dump every event I declare in config/meta_events.rb into a gargantuan before_filter in some top-level controller. Wouldn't it be more practical to serialize config/meta_events.rb as JSON and somehow preload it as part of <%= meta_events_frontend_events_javascript %>?

Auto-Tracking also seems irrelevant for SPAs because I have access to none of the ActionView helpers in a JavaScript-driven SPA.

Am I missing something? How are folks integrating the meta_events gem with SPAs?

Catching errors

What's the best way to rescue a Errno::ECONNRESET error? This error can be caused from the mixpanel api being down.

Right now I'm doing something like the following in a controller which doesn't feel quite right:

def track
  MetaEvents::Tracker.new(
    distinct_id,
    request.ip,
    implicit_properties: implicit_properties
  )
rescue Errno::ECONNRESET => error
  MetaEvents::Tracker.default_event_receivers = [MetaEvents::TestReceiver.new]
end

meta_events_tracked_link_to working with blocks?

I tried meta_events_tracked_link_to with a block, such as

= meta_events_tracked_link_to searches_path(), class: 'btn btn-primary', :meta_event => {:category => :user, :event => :search, :properties => {:name => 'all'}} do
          %i.fa.fa-globe
          Search

However, I get this error: You asked for a tracked link, but you didn't provide a :meta_event: nil. I debugged a bit and tracked it down to the helper meta_events_tracked_link_to and it's showing that html_options is nil. Did I do something wrong?

Asynchronous / queuing?

Mixpanel's gem doesn't support asynchronous calls/queuing with delayed_job out of box. How does MetaEvents handle performance?

Pass human readable event names to mixpanel

I think it makes a whole lot of sense to implement a human readable names feature for events, such that when events are fired, the human readable form is sent to mixpanel, rather than one that is snake-cased and really only developer-friendly.

For instance in meta_events.rb, it would be great to see an optional last parameter, like so:

category :user do
  event :signed_up, "2014-05-15", "user creates a new account", "User Signed Up"
end

Or, alternatively, use the existing event description as the human readable event that's sent to mixpanel.

Problem with the documentation

Hi, I think your plugin is pretty awesome, I can't wait to use it and see if it is still the case in few weeks ;-)
I have trouble to set up my project using your documentation. I decided to go with the Frontend Events set up and I was misguided by the sentence:

First off, make sure you get this into your layout in a script tag somewhere at the bottom of the page is perfectly fine: <%= meta_events_frontend_events_javascript %>

To make this work, I needed to nest the code inside a jquery ready callback otherwise the registerFrontendEvent method is never fired and event are not registred inside of MetaEvents.

I took a look at the meta_event.js.erb and it seems to only manage the mixpanel.track method.
This is annoying because you need to identify and people.set your user to be able to link the event tracked with a user.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.