{"id":13880111,"url":"https://github.com/swiftype/meta_events","last_synced_at":"2025-07-16T16:31:13.769Z","repository":{"id":13827197,"uuid":"16523861","full_name":"swiftype/meta_events","owner":"swiftype","description":"Structured, documented, powerful event emitting library for Mixpanel and other such systems","archived":true,"fork":false,"pushed_at":"2020-01-09T22:50:30.000Z","size":516,"stargazers_count":75,"open_issues_count":3,"forks_count":31,"subscribers_count":4,"default_branch":"master","last_synced_at":"2024-05-14T01:04:26.402Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/swiftype.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGES.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2014-02-04T20:02:12.000Z","updated_at":"2024-04-12T14:20:29.000Z","dependencies_parsed_at":"2022-08-21T00:20:19.772Z","dependency_job_id":null,"html_url":"https://github.com/swiftype/meta_events","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiftype%2Fmeta_events","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiftype%2Fmeta_events/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiftype%2Fmeta_events/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/swiftype%2Fmeta_events/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/swiftype","download_url":"https://codeload.github.com/swiftype/meta_events/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":226148441,"owners_count":17581047,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-08-06T08:02:47.652Z","updated_at":"2024-11-24T09:30:30.831Z","avatar_url":"https://github.com/swiftype.png","language":"Ruby","funding_links":[],"categories":["Ruby","Analytics"],"sub_categories":[],"readme":"`MetaEvents` is a Ruby gem that sits on top of a user-centric analytics system like\n[Mixpanel](https://www.mixpanel.com/) and provides structure, documentation, and a historical record to events,\nand a powerful properties system that makes it easy to pass large numbers of consistent properties with your events.\n\nMetaEvents supports:\n\n* 1.9.3, 2.0.0, 2.1.2, or JRuby 1.7.12\n\nThese are, however, just the versions it's tested against; MetaEvents contains no code that should be at all\nparticularly dependent on exact Ruby versions, and should be compatible with a broad set of versions.\n\nCurrent build status: ![Current Build Status](https://api.travis-ci.org/swiftype/meta_events.png?branch=master)\n\nBrought to you by the folks at [Swiftype](https://www.swiftype.com/). First version written by\n[Andrew Geweke](https://www.github.com/ageweke). For additional contributors, see [CONTRIBUTORS](CONTRIBUTORS.md).\n\n### Installation\n\nIf you're in a project using [Bundler](http://bundler.io/) \u0026mdash; for example, any Rails project, most Gems, and\nprobably most other Ruby software these days \u0026mdash; and therefore have a [`Gemfile`](http://bundler.io/gemfile.html),\nsimply add this to the end of your `Gemfile`:\n\n```ruby\ngem 'meta_events'\n```\n\nAlternatively, if you aren't using Bundler:\n\n```ruby\ngem install meta_events\n```\n\n### Background\n\nSending user-centric events to (_e.g._) Mixpanel is far from difficult; it's a single method call. However, in a\nlarge project, adding calls to Mixpanel all over eventually starts causing issues:\n\n* Understanding what, exactly, an event is tracking, including when it was introduced and when it was changed, is\n_paramount_ to doing correct analysis. But once events have been around for a while, whoever put them there has\nlong-since forgotten (or they may even not be around any more), and trying to understand what `User Upgraded Account`\nmeans, eighteen months later, involves an awful lot of spelunking. (Why did it suddenly triple, permanently, on\nFebruary 19th? Is that because we changed what the event means or because we improved the product?)\n* Getting a holistic view of what events there are and how they interact becomes basically impossible; all you can do\nis look at the output (_i.e._, Mixpanel) and hope you can put the pieces together from there.\n* Critical to using Mixpanel well is to pass lots and lots of properties; engineers being the lazy folks that we are,\nwe often don't do this, and, when we do, they're named inconsistently and may mean different things in different\nplaces.\n* Often you want certain properties of the currently-logged-in user (for example) passed on every single event, and\nthere's not always a clean way to do this.\n\n### MetaEvents\n\n`MetaEvents` helps solve this problem by adding a few critical features:\n\n1. The **MetaEvents DSL** requires developers to declare and document events as they add them (and if they don't, they\ncan't fire them); this is quick and easy, but enormously powerful as it gives you a holistic view of your events, a\nhistorical record, and detailed documentation on each one.\n1. **Object properties support** means you can define the set of event properties an object in your system (like a\nUser) should expose, and then simply pass that object in your event \u0026mdash; this makes it vastly easier to include\nlots of properties, and be consistent about them.\n1. **Implicit properties support** means you can add contextual properties (like the currently-logged-in user) in a\nsingle place, and then have every event include those properties.\n1. **Front-end integration** lets you very easily track events from DOM elements (like links) using JavaScript, and\nuse a powerful mechanism to fire front-end events in any way you want.\n\n# Getting Started\n\nLet'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`).\n\n**Note**: You will also need to provide whatever API your analytics system uses; for example, in the case of Mixpanel,\nyou must also add `gem mixpanel-ruby` to your `Gemfile`.\n\n### Declaring Events\n\nFirst, let's declare an event that we want to fire. Create `config/meta_events.rb` (MetaEvents automatically\nconfigures this as your events file if you're using Rails; if not, use `MetaEvents::Tracker.default_definitions =` to\nset the path to whatever file you like):\n\n```ruby\nglobal_events_prefix :ab\n\nversion 1, \"2014-02-04\" do\n  category :user do\n    event :signed_up, \"2014-02-04\", \"user creates a brand-new account\"\n  end\nend\n```\n\nLet's walk through this:\n\n* `global_events_prefix` is a short string that gets added before every single event; this helps discriminate events\n  coming from MetaEvents from events coming from other systems. Choose this carefully, don't ever change it, and keep\n  it short \u0026mdash; most tools, like Mixpanel, have limited screen real estate for displaying event names.\n* `version 1` defines a version of _your entire events system_; this is useful in the case where you want to rework\n  the entire set of events you fire \u0026mdash; which is not an uncommon thing. But, for a while, we'll only need a single\n  version, and we'll call it 1.\n* `2014-02-04` is when this version first was used; this can be any date (and time, if you _really_ want to be precise)\n  that you want \u0026mdash; it just has to be parseable by Ruby's `Time.parse` method. (MetaEvents never, ever compares\n  this date to `Time.now` or otherwise uses it; it's just for documentation.)\n* `category :user` is just a grouping and namespacing of events; the category name is included in every event name\n  when fired.\n* `event :signed_up` declares an event with a name; `2014-02-04` is required and is the date (and time) that this\n  event was introduced. (Again, this is just for documentation purposes.) `user creates a brand-new account` is also\n  just for documentation purposes (and also is required), and describes the exact purpose of this event.\n\n### Firing Events\n\nTo fire an event, we need an instance of `MetaEvents::Tracker`. For reasons to be explained shortly, we'll want an\ninstance of this class to be created at a level where we may have things in common (like the current user) \u0026mdash; so,\nin a Rails application, our `ApplicationController` is a good place. We need to pass it the _distinct ID_ of the user\nthat's signed in, which is almost always just the primary key from the `users` table \u0026mdash; or `nil` if no user is\ncurrently signed in. We also pass it the IP address of the user (which can safely be `nil`); Mixpanel, for example,\nuses this for doing geolocation of users:\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  ...\n  def meta_events_tracker\n    @meta_events_tracker ||= MetaEvents::Tracker.new(current_user.try(:id), request.remote_ip)\n  end\n  ...\nend\n```\n\nNow, from the controller, we can fire an event and pass a couple of properties:\n\n```ruby\nclass UsersController \u003c ApplicationController\n  ...\n  def create\n    ...\n    meta_events_tracker.event!(:user, :signed_up, { :user_gender =\u003e @new_user.gender, :user_age =\u003e @new_user.age })\n    ...\n  end\n  ...\nend\n```\n\nWe're just about all done; but, right now, the event isn't actually going anywhere, because we haven't configured any\n_event receivers_.\n\n### Hooking Up Mixpanel and a Test Receiver\n\nAn _event receiver_ is any object that responds to a method `#track(distinct_id, event_name, event_properties)`, where\n`distinct_id` is the distinct ID of the user, `event_name` is a `String` and `event_properties` is a Hash mapping `String`\nproperty names to simple scalar values \u0026mdash; `true`, `false`, `nil`, numbers (all `Numeric`s, including both\nintegers and floating-point numbers, are supported), `String`s (and `Symbol`s will be converted to `String`s\ntransparently), and `Time` objects.\n\nFortunately, the [Mixpanel](https://github.com/mixpanel/mixpanel-ruby) Gem complies with this interface perfectly.\nSo, in `config/environments/production.rb` (or any other file that loads before your first event gets fired):\n\n```ruby\nMetaEvents::Tracker.default_event_receivers \u003c\u003c Mixpanel::Tracker.new(\"0123456789abcdef\")\n```\n\n(where `0123456789abcdef` is actually your Mixpanel API token)\n\nIn our development environment, we may or may not want to include Mixpanel itself (so we can either add or not add the\nMixpanel event receiver, above); however, we might also want to print events to the console or some other file as\nthey are fired. So, in `config/environments/development.rb`:\n\n```ruby\nMetaEvents::Tracker.default_event_receivers \u003c\u003c MetaEvents::TestReceiver.new\n```\n\nThis will print events as they are fired to your Rails log (_e.g._, `log/development.log`); you can pass an argument\nto the constructor of `TestReceiver` that's a `Logger`, an `IO` (_e.g._, `STDOUT`, `STDERR`, an open `File` object),\nor a block (or anything responding to `call`), if you want it to go elsewhere.\n\n### Testing It Out\n\nNow, when you fire an event, you should get output like this in your Rails log:\n\n    Tracked event: user 483123, \"ab1_user_signed_up\"\n                              user_age: 27\n                           user_gender: female\n\n...and, if you have configured Mixpanel properly, it will have been sent to Mixpanel, too!\n\n### Firing Front-End Events\n\nGenerally speaking, firing events from the back end (your application server talking to Mixpanel or some other service\ndirectly) is more reliable, while firing events from the front end (JavaScript in your users' browsers talking to\nMixpanel or some other service) is more scalable \u0026mdash; so you may wish to fire events from the front end, too.\nFurther, there are certain events (scrolling, JavaScript manipulation in the browser, and so on) that simply don't\nexist on the back end and can't be tracked from there \u0026mdash; at least, not without adding calls back to your server\nfrom the front-end JavaScript.\n\n**IMPORTANT**: In case it isn't obvious, _any property you include in a front-end event is visible to your users_.\nNo matter what tricks you might include to obscure that data, it fundamentally will be present on your users' computers\nand thus visible to them if they want to take a look. This is no different  than the situation would be without\nMetaEvents, but, because MetaEvents makes it so easy to add large amounts of properties (which is a good thing!),\nyou should take extra care with your `#to_event_properties` methods once you start firing front-end events.\n\nYou can fire front-end events with MetaEvents in two ways: _auto-tracking_ and _frontend events_. Both methods require\nthe use of Rails (because `MetaEvents::ControllerMethods` is intended for use with `ActionController`, and\n`MetaEvents::Helpers` is intended for use with `ActionView`), although the techniques are generally applicable and\neasy enough to use with any framework.\n\n**Note**: Again, because MetaEvents does not directly require any one user-centric analytics system, you must make sure\nthe JavaScript API to whatever system you're using is loaded, too. So, for example, if you're using Mixpanel, make sure\nthe JavaScript code Mixpanel provides you to use its API is loaded on your page _as well as_ the MetaEvents JavaScript\ncode.\n\n#### Auto-Tracking\n\nAuto-tracking is the easiest way of triggering front-end events. MetaEvents provides a Rails helper method that adds\ncertain attributes to any DOM element you wish (like a link); it then provides a JavaScript function that automatically\npicks up these attributes, decodes them, and calls any function you want with them.\n\nAs an example, in a view, you simply convert:\n\n```html+erb\n\u003c%= link_to(\"go here\", user_awesome_path, :class =\u003e \"my_class\") %\u003e\n```\n\n...to:\n\n```html+erb\n\u003c%= meta_events_tracked_link_to(\"go here\", user_awesome_path, :class =\u003e \"my_class\",\n                                :meta_event =\u003e { :category =\u003e :user, :event =\u003e :awesome,\n                                                 :properties =\u003e { :color =\u003e 'green' } }) %\u003e\n```\n\n(Not immediately obvious: the `:meta_event` attribute is just part of the `html_options` `Hash` that\n`link_to` accepts, not an additional parameter. `meta_events_tracked_link_to` accepts exactly the same parameters as\n`link_to`.)\n\nThis automatically turns the generated HTML from:\n\n```html\n\u003ca href=\"/users/awesome\" class=\"my_class\"\u003ego here\u003c/a\u003e\n```\n\nto something like this:\n\n```html\n\u003ca href=\"/users/awesome\" class=\"my_class mejtp_trk\" data-mejtp-event=\"ab1_user_awesome\"\n   data-mejtp-prp=\"{\u0026quot;ip\u0026quot;:\u0026quot;127.0.0.1\u0026quot;,\u0026quot;color\u0026quot;:\u0026quot;green\u0026quot;,\u0026quot;implicit_prop_1\u0026quot;:\u0026quot;someValue\u0026quot;}\"\u003ego here\u003c/a\u003e\n```\n\n`mejtp` stands for \"MetaEvents JavaScript Tracking Prefix\", and is simply a likely-unique prefix for these values.\n(You can change it with `MetaEvents::Helpers.meta_events_javascript_tracking_prefix 'foo'`.) `mejtp_trk` is the class\nthat allows us to easily detect which elements are set up for tracking; the two data attributes pass the full name\nof the event, and a JSON-encoded string of all the properties (both implicit and explicit) to pass with the event.\n\nNow, add this to a Javascript file in your application:\n\n```javascript\n//= require meta_events\n```\n\nAnd, finally, call something like this:\n\n```javascript\n$(document).ready(function() {\n  MetaEvents.forAllTrackableElements(document, function(id, element, eventName, properties) {\n    mixpanel.track_links(\"#\" + id, eventName, properties);\n  })\n});\n```\n\n`MetaEvents.forAllTrackableElements` accepts a root element to start searching at, and a callback function. It finds\nall elements with class `mejtp_trk` on them underneath that element, extracts the event name and properties, and adds\na generated DOM ID to that element if it doesn't have one already. It then calls your callback function, passing that\n(existing or generated) DOM ID, the element itself, the name of the event, and the full set of properties (decoded, as\na JavaScript Object here). You can then (as above) easily use this to do anything you want, like telling Mixpanel to\ntrack that link properly.\n\n`forAllTrackableElements` also sets a certain data attribute on each element as it processes it, and knows to skip\nelements that already have that attribute set, so it's safe to call as often as you wish \u0026mdash; for example, if\nthe DOM changes. It does _not_ know when the DOM changes, however, so, if you add content to your page, you will\nneed to re-call it.\n\n#### Frontend Events\n\nUse Frontend Events only if Auto-Tracking isn't flexible enough for your purposes; Auto-Tracking is simpler in\nmost ways.\n\nBecause MetaEvents leverages the events DSL to define events, and calls methods on your Ruby models (and other objects)\nto create large numbers of properties, you cannot simply fire an event by name from the front-end without a _little_\nextra work \u0026mdash; otherwise, how would we get those properties? However, it's not much more work.\n\nFirst off, make sure you get this into your layout in a `\u003cscript\u003e` tag somewhere \u0026mdash; at the bottom of the page is\nperfectly fine:\n\n```html+erb\n\u003c%= meta_events_frontend_events_javascript %\u003e\n```\n\nThis allows MetaEvents to pass event data properly from the backend to the frontend for any events you'll be firing.\n\nNow, as an example, let's imagine we implement a JavaScript game on our site, and want to fire events when the user\nwins, loses, or gets a new high score. First, let's define those in our DSL:\n\n```ruby\nglobal_events_prefix :ab\n\nversion 1, \"2014-02-11\" do\n  category :jsgame do\n    event :won, \"2014-02-11\", \"user won a game!\"\n    event :lost, \"2014-02-11\", \"user lost a game\"\n    event :new_high_score, \"2014-02-11\", \"user got a new high score\"\n  end\nend\n```\n\nNow, in whatever controller action renders the page that the game is on, we need to _register_ these events. This\ntells the front-end integration that we might fire them from the resulting page; it therefore embeds JavaScript in the\npage that defines the set of properties for those events, so that the front end has access to the data it needs:\n\n```ruby\nclass GameController \u003c ApplicationController\n  def game\n    ...\n    meta_events_define_frontend_event(:jsgame, :won, { :winning_streak =\u003e current_winning_streak })\n    meta_events_define_frontend_event(:jsgame, :lost, { :losing_streak =\u003e current_losing_streak })\n    meta_events_define_frontend_event(:jsgame, :new_high_score, { :previous_high_score =\u003e current_high_score })\n    ...\n  end\nend\n```\n\nThis will allow us to make the following calls in the frontend, from our game code:\n\n```javascript\nif (wonGame) {\n  MetaEvents.event('jsgame_won');\n} else {\n  MetaEvents.event('jsgame_lost');\n}\n\nif (currentScore \u003e highScore) {\n  MetaEvents.event('jsgame_new_high_score', { score: currentScore });\n}\n```\n\nWhat's happened here is that `meta_events_define_frontend_event` took the set of properties you passed, merged them\nwith any implicit properties defined, and passed them to the frontend via the `meta_events_frontend_events_javascript`\noutput we added above. It binds each event to an _event alias_, which, by default, is just the category name and the\nevent name, joined with an underscore. So when you call `MetaEvents.event`, it simply takes the string you pass it,\nlooks up the event stored under that alias, merges any properties you supply with the ones passed from the backend,\nand fires it off. (You can, in fact, supply as many additional JavaScript objects/hashes as you want after the\nevent alias; they will all be merged together, along with the properties supplied by the backend.)\n\n##### Aliasing Event Names\n\nIf you need to be able to fire the exact same event with _different_ sets of properties from different places in a\nsingle page, you can alias the event using the `:name` property:\n\n```ruby\nclass GameController \u003c ApplicationController\n  def game\n    ...\n    meta_events_define_frontend_event(:jsgame, :paused_game, { :while =\u003e :winning }, { :name =\u003e :paused_while_winning })\n    meta_events_define_frontend_event(:jsgame, :paused_game, { :while =\u003e :losing }, { :name =\u003e :paused_while_losing })\n    ...\n  end\nend\n```\n```javascript\n...\nif (winning) {\n  MetaEvents.event('paused_while_winning');\n} else {\n  MetaEvents.event('paused_while_losing');\n}\n```\n\nBoth calls from the JavaScript will fire the event `ab1_jsgame_paused_game`, but one of them will pass\n`while: 'winning'`, and the other `while: 'losing'`.\n\n##### Definition Cycle\n\nCalls to `meta_events_define_frontend_event` get aggregated on the current controller object, during the request\ncycle. If you have events that can get fired on any page, then, for example, use a `before_filter` to always\ndefine them, or a method you mix in and call, or any other mechanism you want.\n\n##### The Frontend Events Handler\n\n`MetaEvents.event` calls the current _frontend event handler_ on the `MetaEvents` JavaScript object; by default this\njust calls `mixpanel.track`. By calling `MetaEvents.setEventHandler(myFunction)`, you can set it to anything you want;\nit gets passed the fully-qualified event name and set of all properties.\n\n### More About Distinct IDs\n\nWe glossed over the discussion of the distinct ID above. In short, it is a unique identifier (of no particular format;\nboth Strings and integers are acceptable) that is unique to the user in question, based on your application's\ndefinition of 'user'. Using the primary key from your `users` table is typically a great way to do it.\n\nThere are a few situations where you need to take special care, however:\n\n* **What about visitors who aren't signed in yet?** In this case, you will want to generate a unique ID and assign it\n  to the visitor anyway; generating a very large random number and putting it in a cookie in their browser is a good\n  way to do this, as well as using something like nginx's `ngx_http_userid_module`.\n  (Note that Mixpanel has facilities to do this automatically; however, it uses cookies set on their\n  domain, which means you can't read them, which limits it unacceptably \u0026mdash; server-side code and even your own\n  Javascript will be unable to use this ID.) If you do this, take a look at [`web_server_uid`](https://github.com/swiftype/web_server_uid), a gem that makes manipulating the resulting IDs vastly easier in Ruby.\n* **What do I do when a user logs in?** Typically, you simply want to switch completely from using their old\n  (cookie-based) unique ID to using the primary key of your `users` table (or whatever you use for tracking logged-in\n  users). This may seem counterintuitive, but it makes sense, particularly in broad consumer applications: until\n  someone logs in, all you really know is which _browser_ is hitting your site, not which _user_. Activity that happens\n  in the signed-out state might be the user who eventually logs in...but it also might not be, in the case of shared\n  machines; further, activity that happens before the user logs in is unlikely to be particularly interesting to you\n  \u0026mdash; you already have the user as a registered user, and so this isn't a conversion or sign-up funnel. Effectively\n  treating the activity that happens before they sign in as a completely separate user is actually exactly the right\n  thing to do. The correct code structure is simply to call `#distinct_id=` on your `MetaEvents::Tracker` at exactly\n  the point at which you log them in (using your session, or a cookie, or whatever), and be done with it.\n* **What do I do when a user signs up?** This is the tricky case. You really want to correlate all the activity that\n  happened before the signup process with the activity afterwards, so that you can start seeing things like \"users who\n  come in through funnel X convert to truly active/paid/whatever users at a higher rate than those through funnel Y\".\n  This requires support from your back-end analytics provider; Mixpanel calls it _aliasing_, and it's accessed via\n  their `alias` call. It effectively says \"the user with autogenerated ID X is the exact same user as the user with\n  primary-key ID Y\". Making this call is beyond the scope of MetaEvents, but is quite easy to do assuming your\n  analytics provider supports it.\n\nYou may also wish to see Mixpanel's documentation about distinct ID, [here](https://mixpanel.com/docs/managing-users/what-the-unique-identifer-does-and-why-its-important), [here](https://mixpanel.com/docs/managing-users/assigning-your-own-unique-identifiers-to-users), and [here](https://mixpanel.com/docs/integration-libraries/using-mixpanel-alias).\n\n# The Real Power of MetaEvents\n\nNow that we've gotten the basics out of the way, we can start using the real power of MetaEvents.\n\n### Adding Implicit Properties\n\nVery often, just by being in some particular part of code, you already know a fair amount of data that you want to\npass as events. For example, if you're inside a Rails controller action, and you have a current user, you're probably\ngoing to want to pass properties about that user to any event that happens in the controller action.\n\nYou could add these to every single call to `#event!`, but MetaEvents has a better way. When you create the\n`MetaEvents::Tracker` instance, you can define _implicit properties_. Let's add some now:\n\n```ruby\nclass ApplicationController \u003c ActionController::Base\n  ...\n  def meta_events_tracker\n    implicit_properties = { }\n    if current_user\n      implicit_properties.merge!(\n        :user_gender =\u003e current_user.gender,\n        :user_age =\u003e current_user.age\n      )\n    end\n    @meta_events_tracker ||= MetaEvents::Tracker.new(current_user.try(:id), request.remote_ip,\n                                                    :implicit_properties =\u003e implicit_properties)\n  end\n  ...\nend\n```\n\nNow, these properties will get passed on every event fired by this Tracker. (This is, in fact, the biggest\nconsideration when deciding when and where you'll create new `MetaEvents::Tracker` instances: implicit properties are\nextremely useful, so you'll want the lifecycle of a Tracker to match closely the lifecycle of something in your\napplication that has implicit properties.)\n\n### Multi-Object Events\n\nWe're also going to face another problem: many events involve multiple underlying objects, each of which has many\nproperties that are defined on it. For example, imagine we have an event triggered when a user sends a message to\nanother user. We have at least three entities: the 'from' user, the 'to' user, and the message itself. If we really\nwant to instrument this event properly, we're going to want something like this:\n\n```ruby\nmeta_events_tracker.event!(:user, :sent_message, {\n  :from_user_country =\u003e from_user.country,\n  :from_user_state =\u003e from_user.state,\n  :from_user_postcode =\u003e from_user.postcode,\n  :from_user_city =\u003e from_user.city,\n  :from_user_language =\u003e from_user.language,\n  :from_user_referred_from =\u003e from_user.referred_from,\n  :from_user_gender =\u003e from_user.gender,\n  :from_user_age =\u003e from_user.age,\n\n  :to_user_country =\u003e to_user.country,\n  :to_user_state =\u003e to_user.state,\n  :to_user_postcode =\u003e to_user.postcode,\n  :to_user_city =\u003e to_user.city,\n  :to_user_language =\u003e to_user.language,\n  :to_user_referred_from =\u003e to_user.referred_from,\n  :to_user_gender =\u003e to_user.gender,\n  :to_user_age =\u003e to_user.age,\n\n  :message_sent_at =\u003e message.sent_at,\n  :message_type =\u003e message.type,\n  :message_length =\u003e message.length,\n  :message_language =\u003e message.language,\n  :message_attachments =\u003e message.attachments?\n  })\n```\n\nNeedless to say, this kind of sucks. Either we're going to end up with a ton of duplicate, unmaintainable code, or\nwe'll just cut back and only pass a few properties \u0026mdash; greatly reducing the possibilities of our analytics\nsystem.\n\n### Using Hashes to Factor Out Naming\n\nWe can improve this situation by using a feature of MetaEvents: when properties are nested in sub-hashes, they get\nautomatically expanded and their names prefixed by the outer hash key. So let's define a couple of methods on models:\n\n```ruby\nclass User \u003c ActiveRecord::Base\n  def to_event_properties\n    {\n      :country =\u003e country,\n      :state =\u003e state,\n      :postcode =\u003e postcode,\n      :city =\u003e city,\n      :language =\u003e language,\n      :referred_from =\u003e referred_from,\n      :gender =\u003e gender,\n      :age =\u003e age\n    }\n  end\nend\n\nclass Message \u003c ActiveRecord::Base\n  def to_event_properties\n    {\n      :sent_at =\u003e sent_at,\n      :type =\u003e type,\n      :length =\u003e length,\n      :language =\u003e language,\n      :attachments =\u003e attachments?\n    }\n  end\nend\n```\n\nNow, we can pass the exact same set of properties as the above example, by simply doing:\n\n```ruby\nmeta_events_tracker.event!(:user, :sent_message, {\n  :from_user =\u003e from_user.to_event_properties,\n  :to_user =\u003e to_user.to_event_properties,\n  :message =\u003e message.to_event_properties\n  })\n```\n\n**SO** much better.\n\n### Moving Hash Generation To Objects\n\nAnd \u0026mdash; tah-dah! \u0026mdash; MetaEvents supports this syntax automatically. If you pass an object as a property, and\nthat object defines a method called `#to_event_properties`, then it will be called automatically, and replaced.\nOur code now looks like:\n\n```ruby\nmeta_events_tracker.event!(:user, :sent_message, { :from_user =\u003e from_user, :to_user =\u003e to_user, :message =\u003e message })\n```\n\n### How to Take the Most Advantage\n\nTo make the most use of MetaEvents, define `#to_event_properties` very liberally on objects in your system, make them\nreturn any properties you even think might be useful, and pass them to events. MetaEvents will expand them for you,\nallowing large numbers of properties on events, which allows Mixpanel and other such systems to be of the most use\nto you.\n\n# Miscellaneous and Trivia\n\nA few things before we're done:\n\n### Performance and Asynchronousness\n\nWhen deploying Mixpanel or other such systems in a live application, you very often want to dispatch event reporting\nin the background, so that any slowdown in Mixpanel's servers doesn't slow down your application (or, in the worst\ncase, bring it down entirely). Typically, this is done using something like [Resque](https://github.com/resque/resque),\n[Sidekiq](http://sidekiq.org/), or another queue-based system. There are literally dozens of these systems available\nfor Ruby, and it's highly likely that your codebase either uses one already or will soon. Additionally, many users\nlayer additional features like logging, durability, or other infrastructure services on top of the base functionality\nof these packages.\n\nBecause there is such a wide variety of these systems available, MetaEvents does not _directly_ provide support for\nthem \u0026mdash; doing so would be a great deal of effort, and yet still unlikely to satisfy most users. Instead,\nMetaEvents makes it very easy to use any package you want:\n\n```ruby\nclass MyEventReceiver\n  def track(distinct_id, event_name, event_properties)\n    # Call Resque, Sidekiq, or anything else you want here; enqueue a job that, when run, will call:\n    #   Mixpanel::Tracker.new($my_mixpanel_api_key).track(distinct_id, event_name, event_properties)\n  end\nend\n\nMetaEvents::Tracker.default_event_receivers \u003c\u003c MyEventReceiver.new\n```\n\nVoilà \u0026mdash; asynchronous event tracking.\n\n### Mixpanel, Aliasing, and People\n\nMetaEvents is _not_ intended as a complete superset of a backend analytics library (like Mixpanel) \u0026mdash; there are\nfeatures of those libraries that are not implemented via MetaEvents, and which should be used by direct calls to the\nservice in question.\n\nFor example, Mixpanel has an `alias` call that lets you tell it that a user with a particular distinct ID is actually\nthe same person as a user with a different distinct ID \u0026mdash; this is typically used at signup, when you convert from\nan \"anonymous\" distinct ID representing the unknown user who is poking around your site to the actual official user ID\n(typically your `users` table primary key) of that user. MetaEvents does not, in any way, attempt to support this; it\nallows you to pass whatever `distinct_id` you want in the `#event!` call, but, if you want to use `alias`, you should\nmake that Mixpanel call directly. (See also the discussion above about _distinct ID_.)\n\nSimilarly, Mixpanel's People functionality is not in any way directly supported by MetaEvents. You may well use the\nTracker's `#effective_properties` method to compute a set of properties that you pass to Mixpanel's People system,\nbut there are no calls directly in MetaEvents to do this for you.\n\n### Retiring an Event\n\nOften you'll have events that you _retire_ \u0026mdash; they were used in the past, but no longer. You could just delete\nthem from your MetaEvents DSL file, but this will mean the historical record is suddenly gone. (Well, there's source\ncontrol, but that's a pain.)\n\nRather than doing this, you can retire them:\n\n```ruby\nglobal_events_prefix :ab\n\nversion 1, \"2014-02-04\" do\n  category :user do\n    event :logged_in_with_facebook, \"2014-02-04\", \"user creates a brand-new account\", :retired_at =\u003e \"2014-06-01\"\n    event :signed_up, \"2014-02-04\", \"user creates a brand-new account\"\n  end\nend\n```\n\nGiven the above, trying to call `event!(:user, :logged_in_with_facebook)` will fail with an exception, because the\nevent has been retired. (Note that, once again, the actual date passed to `:retired_at` is simply for record-keeping\npurposes; the exception is generated if `:retired_at` is set to _anything_.)\n\nYou can retire events, categories, and entire versions; this system ensures the DSL continues to be a historical record\nof what things were in the past, as well as what they are today.\n\n### Adding Notes to Events\n\nYou can also add notes to events. They must be tagged with the author and the time, and they can be very useful for\ndocumenting changes:\n\n```ruby\nglobal_events_prefix :ab\n\nversion 1, \"2014-02-04\" do\n  category :user do\n    event :signed_up, \"2014-02-04\", \"user creates a brand-new account\" do\n      note \"2014-03-17\", \"jsmith\", \"Moved sign-up button to the home page -- should increase signups significantly\"\n    end\n  end\nend\n```\n\nThis allows you to record changes to events, as well as the events themselves.\n\n### Documenting Events\n\nCurrently, the documentation for the MetaEvents DSL is the source to that DSL itself \u0026mdash; _i.e._,\n`config/meta_events.rb` or something similar. However, methods on the DSL objects created (accessible via\na `Tracker`'s `#definitions` method, or `MetaEvents::Tracker`'s `default_definitions` class method) allow for\nintrospection, and could easily be extended to, _e.g._, generate HTML fully documenting the events.\n\nPatches are welcome. ;-)\n\n### Times\n\nMetaEvents automatically adds a `time` property to any event you fire via `#event!`; this is so that you can take the\nset of properties in a receiver and make it asynchronous, and don't have to worry about getting the time right. You\ncan override this, however, by simply passing a `:time` property with your event; it will override any time we would\notherwise set. (You can even set `:time =\u003e nil` if you want to make sure no time is passed at all.)\n\nMetaEvents correctly converts any `Time` object you pass into the correct String format for Mixpanel (_e.g._,\n`2014-02-03T15:49:17`), converting it to UTC first. This should make your times much cleaner.\n\n### Adding a New Version\n\nWhat is this top-level `version` in the DSL? Well, every once in a while, you will want to completely redo your set of\nevents \u0026mdash; perhaps you've learned a lot about using your analytics system, and realize you want them configured\nin a different way.\n\nWhen you want to do this, define a new top-level `version` in your DSL, and pass `:version =\u003e 2` (or whatever number\nyou gave the new version) when creating your `MetaEvents::Tracker`. The tracker will look under that version for\ncategories and events, and completely ignore other versions; your events will be called things like `ab2_user_signup`\ninstead of `ab1_user_signup`, and so on. The old version can still stay present in your DSL for documentation and\nhistorical purposes.\n\nWhen you're completely done with the old version, retire it \u0026mdash; `version 1, :retired_at =\u003e '2014-06-01' do ...`.\n\nOften, you'll want to run two versions simultaneously, because you want to have a transition period where you fire\n_both_ sets of events \u0026mdash; this is hugely helpful in figuring out how your old events map to new events and\nwhen adjusting bases for the new events. (If you simply flash-cut from an old version to a new one on a single day,\nit is difficult or impossible to know if true underlying usage, etc., _actually_ changed, or if it's just an artifact\nof changing events.) You can simply create two `MetaEvents::Tracker` instances, one for each version, and use them\nin parallel.\n\n### Customizing the Event Name\n\nDevelopers love names like \"xyz1_user_signed_up\" but sometimes it's not a developer doing the analysis.\nDepending on what the back-end analytics library supports, event names in external systems are frequently not\ngiven a lot of real estate.\n\nIn cases like these, you can override the default external event name behavior. There are three ways to\noverride these external names.\n\nFirst, you can override them globally for all `MetaEvents::Tracker` instances:\n\n```ruby\nMetaEvents::Tracker.default_external_name = lambda { |event| \"#{event.category_name} #{event.name}\" }\n```\n\nSecond, you can override them for a specific `MetaEvents::Tracker` instance:\n\n```ruby\nMetaEvents::Tracker.new(current_user.try(:id),\n                    request.remote_ip,\n                    :external_name =\u003e lambda { |event| \"#{event.category_name} #{event.name}\" }\n                   )\n```\n\nFinally, you can override each event's external name in the events DSL:\n\n```ruby\nglobal_events_prefix :ab\n\nversion 1, \"2014-02-11\" do\n  category :example_category do\n    event :example_event, \"2014-02-11\", \"Example was exampled!\", :external_name =\u003e 'ex. was ex.'\n  end\nend\n```\n\nThe order of precedence for determining the external event name is the DSL's `event :external_name =\u003e 'foo'`,\n`MetaEvents::Tracker.new`, `MetaEvents::Tracker.default_external_name`, built-in default.\n\n### Customizing the Nested Property Separator\n\nSimilarly, while developers might be perfectly comfortable with (and even prefer) expanded properties named things\nlike `user_age`, `user_name`, and so on, others might want a different separator (like a space character). When\ndefining a version, you can set this, as follows:\n\n```ruby\nglobal_events_prefix :ab\n\nversion 1, \"2014-02-11\", :property_separator =\u003e ' ' do\n  category :example_category do\n    event :example_event, \"2014-02-11\", \"Example was exampled!\"\n  end\nend\n```\n\nNow, assuming `@user` is a `User` object that responds to `#to_event_properties` (or is just a `Hash`):\n\n```ruby\ntracker.event!(:example_category, :example_event, :user =\u003e @user)\n```\n\n...you'll get properties named `user age`, `user name`, and so on, rather than `user_age` and `user_name`.\n\nNote that this is defined on the `version`, not the `category`, `event`, or even `#event!` call, because changing\nthis is _a big deal_ \u0026mdash; changing property names almost always breaks all kinds of analysis you might want to do\nwith your analytics tool. However, the idea is that changing `version`s is a breaking change to your analytics\nsystem anyway, so you can certainly set it on a new `version` to something different.\n\n## Contributing\n\n1. Fork it ( http://github.com/swiftype/meta_events/fork )\n2. Create your feature branch (`git checkout -b my-new-feature`)\n3. Commit your changes (`git commit -am 'Add some feature'`)\n4. Push to the branch (`git push origin my-new-feature`)\n5. Create new Pull Request\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswiftype%2Fmeta_events","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fswiftype%2Fmeta_events","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fswiftype%2Fmeta_events/lists"}