Merged 2.0
Andrew
committed Feb 26, 2018
commit 20261fd584da51f552799364aa72b93da287e94d
Showing 99
changed files with
2046 additions
and 2690 deletions
CHANGELOG.md
+19
-1
| @@ | @@ -1,6 +1,24 @@ |
| - | ## 1.6.2 [unreleased] |
| + | ## 2.0.0 [unreleased] |
| + | - Removed dependency on jQuery |
| + | - Use `navigator.sendBeacon` by default in supported browsers |
| + | - Added `geocode` event |
| + | - Added `where_event` method for querying events |
| + | - Added support for `visitable` and `where_props` to Mongoid |
| - Added `preserve_callbacks` option | |
| + | - Use `json` for MySQL by default |
| + | - Fixed log silencing |
| + | |
| + | Breaking changes |
| + | |
| + | - Simpler interface for data stores |
| + | - Renamed `track_visits_immediately` to `server_side_visits` and enabled by default |
| + | - Renamed `mount` option to `api` and disabled by default |
| + | - Enabled `protect_from_forgery` by default |
| + | - Removed deprecated options |
| + | - Removed throttling |
| + | - Removed most built-in stores |
| + | - Removed support for Rails < 4.2 |
| ## 1.6.1 | |
README.md
+210
-489
| @@ | @@ -1,12 +1,14 @@ |
| # Ahoy | |
| - | Ahoy provides a solid foundation to track visits and events in Ruby, JavaScript, and native apps. Works with any data store so you can easily scale. |
| + | :fire: Simple, powerful analytics for Rails |
| - | :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) |
| + | Track visits and events in Ruby, JavaScript, and native apps. Data is stored in your database by default so you can easily combine it with other data. |
| + | |
| + | **Ahoy 2.0 was recently released!** See [how to upgrade](docs/Ahoy-2-Upgrade.md) |
| - | :postbox: To track emails, check out [Ahoy Email](https://github.com/ankane/ahoy_email). |
| + | :postbox: To track emails, check out [Ahoy Email](https://github.com/ankane/ahoy_email), and for A/B testing, check out [Field Test](https://github.com/ankane/field_test) |
| - | :maple_leaf: For A/B testing, check out [Field Test](https://github.com/ankane/field_test). |
| + | :tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource) |
| ## Installation | |
| @@ | @@ -16,177 +18,73 @@ Add this line to your application’s Gemfile: |
| gem 'ahoy_matey' | |
| ``` | |
| - | And add the javascript file in `app/assets/javascripts/application.js` after jQuery. |
| - | |
| - | ```javascript |
| - | //= require jquery |
| - | //= require ahoy |
| - | ``` |
| - | |
| - | ## Choose a Data Store |
| - | |
| - | Ahoy supports a number of data stores out of the box. You can start with one of them and customize as needed, or create your own store from scratch. |
| - | |
| - | - [PostgreSQL, MySQL, or SQLite](#postgresql-mysql-or-sqlite) |
| - | - [MongoDB](#mongodb) |
| - | - [Kafka](#kafka), [Fluentd](#fluentd), [RabbitMQ](#rabbitmq), [NATS](#nats), [NSQ](#nsq), or [Amazon Kinesis Firehose](#amazon-kinesis-firehose) |
| - | - [Logs](#logs) |
| - | - [Custom](#custom) |
| - | |
| - | ### PostgreSQL, MySQL, or SQLite |
| - | |
| - | Run: |
| - | |
| - | ```sh |
| - | rails generate ahoy:stores:active_record |
| - | rake db:migrate |
| - | ``` |
| - | |
| - | ### MongoDB |
| - | |
| - | Run: |
| - | |
| - | ```sh |
| - | rails generate ahoy:stores:mongoid |
| - | ``` |
| - | |
| - | ### Kafka |
| - | |
| - | Add [ruby-kafka](https://github.com/zendesk/ruby-kafka) to your Gemfile. |
| - | |
| - | ```ruby |
| - | gem 'ruby-kafka' |
| - | ``` |
| - | |
| And run: | |
| ```sh | |
| - | rails generate ahoy:stores:kafka |
| + | bundle install |
| + | rails generate ahoy:install |
| + | rails db:migrate |
| ``` | |
| - | Use `ENV["KAFKA_URL"]` to configure. |
| + | Restart your web server, open a page in your browser, and a visit will be created :tada: |
| - | ### Fluentd |
| - | |
| - | Add [fluent-logger](https://github.com/fluent/fluent-logger-ruby) to your Gemfile. |
| + | Track your first event from a controller with: |
| ```ruby | |
| - | gem 'fluent-logger' |
| - | ``` |
| - | |
| - | And run: |
| - | |
| - | ```sh |
| - | rails generate ahoy:stores:fluentd |
| + | ahoy.track "My first event", {language: "Ruby"} |
| ``` | |
| - | Use `ENV["FLUENTD_HOST"]` and `ENV["FLUENTD_PORT"]` to configure. |
| - | |
| - | ### RabbitMQ |
| + | ### JavaScript & Native Apps |
| - | Add [bunny](https://github.com/ruby-amqp/bunny) to your Gemfile. |
| + | First, enable the API in `config/initializers/ahoy.rb`: |
| ```ruby | |
| - | gem 'bunny' |
| - | ``` |
| - | |
| - | And run: |
| - | |
| - | ```sh |
| - | rails generate ahoy:stores:bunny |
| + | Ahoy.api = true |
| ``` | |
| - | Use `ENV["RABBITMQ_URL"]` to configure. |
| - | |
| - | ### NATS |
| + | And restart your web server. |
| - | Add [nats-pure](https://github.com/nats-io/pure-ruby-nats) to your Gemfile. |
| + | For JavaScript, add to `app/assets/javascripts/application.js`: |
| - | ```ruby |
| - | gem 'nats-pure' |
| + | ```javascript |
| + | //= require ahoy |
| ``` | |
| - | And run: |
| + | And track an event with: |
| - | ```sh |
| - | rails generate ahoy:stores:nats |
| + | ```javascript |
| + | ahoy.track("My second event", {language: "JavaScript"}); |
| ``` | |
| - | Use `ENV["NATS_URL"]` to configure. |
| - | |
| - | ### NSQ |
| - | |
| - | Add [nsq-ruby](https://github.com/wistia/nsq-ruby) to your Gemfile. |
| + | For native apps, see the [API spec](#api-spec). |
| - | ```ruby |
| - | gem 'nsq-ruby' |
| - | ``` |
| + | ## How It Works |
| - | And run: |
| + | ### Visits |
| - | ```sh |
| - | rails generate ahoy:stores:nsq |
| - | ``` |
| + | When someone visits your website, Ahoy creates a visit with lots of useful information. |
| - | Use `ENV["NSQ_URL"]` to configure. |
| + | - **traffic source** - referrer, referring domain, landing page, search keyword |
| + | - **location** - country, region, and city |
| + | - **technology** - browser, OS, and device type |
| + | - **utm parameters** - source, medium, term, content, campaign |
| - | ### Amazon Kinesis Firehose |
| + | Use the `current_visit` method to access it. |
| - | Add [aws-sdk](https://github.com/aws/aws-sdk-ruby) to your Gemfile. |
| + | Prevent certain Rails actions from creating visits with: |
| ```ruby | |
| - | gem 'aws-sdk', '>= 2.0.0' |
| - | ``` |
| - | |
| - | And run: |
| - | |
| - | ```sh |
| - | rails generate ahoy:stores:kinesis_firehose |
| - | ``` |
| - | |
| - | Configure delivery streams and credentials in the initializer. |
| - | |
| - | ### Logs |
| - | |
| - | ```sh |
| - | rails generate ahoy:stores:log |
| + | skip_before_action :track_ahoy_visit |
| ``` | |
| - | This logs visits to `log/visits.log` and events to `log/events.log`. |
| - | |
| - | ### Custom |
| + | This is typically useful for APIs. |
| - | ```sh |
| - | rails generate ahoy:stores:custom |
| - | ``` |
| - | |
| - | This creates a class for you to fill out. |
| + | You can also defer visit tracking to JavaScript (Ahoy 1.0 behavior) with: |
| ```ruby | |
| - | class Ahoy::Store < Ahoy::Stores::BaseStore |
| - | def track_visit(options) |
| - | end |
| - | |
| - | def track_event(name, properties, options) |
| - | end |
| - | end |
| + | Ahoy.server_side_visits = false |
| ``` | |
| - | See the [ActiveRecordTokenStore](https://github.com/ankane/ahoy/blob/master/lib/ahoy/stores/active_record_token_store.rb) for an example. |
| - | |
| - | ## How It Works |
| - | |
| - | ### Visits |
| - | |
| - | When someone visits your website, Ahoy creates a visit with lots of useful information. |
| - | |
| - | - **traffic source** - referrer, referring domain, landing page, search keyword |
| - | - **location** - country, region, and city |
| - | - **technology** - browser, OS, and device type |
| - | - **utm parameters** - source, medium, term, content, campaign |
| - | |
| - | Use the `current_visit` method to access it. |
| - | |
| ### Events | |
| Each event has a `name` and `properties`. | |
| @@ | @@ -213,239 +111,193 @@ See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features |
| ahoy.track "Viewed book", title: "Hot, Flat, and Crowded" | |
| ``` | |
| - | #### Native Apps |
| - | |
| - | See the [HTTP spec](#native-apps-1) until libraries are built. |
| - | |
| - | ### Users |
| - | |
| - | Ahoy automatically attaches the `current_user` to the visit. |
| - | |
| - | With [Devise](https://github.com/plataformatec/devise), it will attach the user even if he or she signs in after the visit starts. |
| - | |
| - | With other authentication frameworks, add this to the end of your sign in method: |
| + | or track actions automatically with: |
| ```ruby | |
| - | ahoy.authenticate(user) |
| - | ``` |
| - | |
| - | ## Customize the Store |
| + | class ApplicationController < ActionController::Base |
| + | after_action :track_action |
| - | Stores are built to be highly customizable. |
| + | protected |
| - | ```ruby |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | # add methods here |
| + | def track_action |
| + | ahoy.track "Viewed action", request.path_parameters |
| + | end |
| end | |
| ``` | |
| - | ### Exclude Bots and More |
| + | #### Native Apps |
| - | Exclude visits and events from being tracked with: |
| + | See the [API spec](#api-spec). |
| - | ```ruby |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | def exclude? |
| - | bot? || request.ip == "192.168.1.1" |
| - | end |
| - | end |
| - | ``` |
| + | ### Associated Models |
| - | Bots are excluded by default. |
| + | Say we want to associate orders with visits. Ahoy can do this automatically. |
| - | ### Track Additional Values |
| + | First, generate a migration and add a `visit_id` column (not needed for Mongoid). |
| ```ruby | |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | def track_visit(options) |
| - | super do |visit| |
| - | visit.gclid = visit_properties.landing_params["gclid"] |
| - | end |
| - | end |
| - | |
| - | def track_event(name, properties, options) |
| - | super do |event| |
| - | event.ip = request.ip |
| - | end |
| + | class AddVisitIdToOrders < ActiveRecord::Migration[5.1] |
| + | def change |
| + | add_column :orders, :visit_id, :bigint |
| end | |
| end | |
| ``` | |
| - | Some methods you can use are `request`, `controller`, `visit_properties`, and `ahoy`. |
| - | |
| - | ### Customize User |
| - | |
| - | If you use a method other than `current_user`, set it here: |
| + | Then, add `visitable` to the model. |
| ```ruby | |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | def user |
| - | controller.true_user |
| - | end |
| + | class Order < ApplicationRecord |
| + | visitable |
| end | |
| ``` | |
| - | ### Report Exceptions |
| - | |
| - | Exceptions are rescued so analytics do not break your app. Ahoy uses [Safely](https://github.com/ankane/safely) to try to report them to a service by default. |
| + | When a visitor places an order, the `visit_id` column is automatically set :tada: |
| - | To customize this, use: |
| + | See where orders are coming from with simple joins: |
| ```ruby | |
| - | Safely.report_exception_method = -> (e) { Rollbar.error(e) } |
| + | Order.joins(:visit).group("referring_domain").count |
| + | Order.joins(:visit).group("city").count |
| + | Order.joins(:visit).group("device_type").count |
| ``` | |
| - | ### Use Different Models |
| - | |
| - | For ActiveRecord and Mongoid stores |
| + | Customize the column and class name with: |
| ```ruby | |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | def visit_model |
| - | CustomVisit |
| - | end |
| - | |
| - | def event_model |
| - | CustomEvent |
| - | end |
| - | end |
| + | visitable :sign_up_visit, class_name: "Visit" |
| ``` | |
| - | ## More Features |
| + | ### Users |
| - | ### Automatic Tracking |
| + | Ahoy automatically attaches the `current_user` to the visit. With [Devise](https://github.com/plataformatec/devise), it attaches the user even if he or she signs in after the visit starts. |
| - | Page views |
| + | With other authentication frameworks, add this to the end of your sign in method: |
| - | ```javascript |
| - | ahoy.trackView(); |
| + | ```ruby |
| + | ahoy.authenticate(user) |
| ``` | |
| - | Clicks |
| + | To see the visits for a given user, create an association: |
| - | ```javascript |
| - | ahoy.trackClicks(); |
| + | ```ruby |
| + | class User < ApplicationRecord |
| + | has_many :visits, class_name: "Ahoy::Visit" |
| + | end |
| ``` | |
| - | Rails actions |
| + | And use: |
| ```ruby | |
| - | class ApplicationController < ActionController::Base |
| - | after_action :track_action |
| - | |
| - | protected |
| - | |
| - | def track_action |
| - | ahoy.track "Viewed #{controller_path}##{action_name}", params: request.path_parameters |
| - | end |
| - | end |
| + | User.find(123).visits |
| ``` | |
| - | ### Multiple Subdomains |
| + | #### Custom User Method |
| - | To track visits across multiple subdomains, use: |
| + | Use a method besides `current_user` |
| ```ruby | |
| - | Ahoy.cookie_domain = :all |
| + | Ahoy.user_method = :true_user |
| ``` | |
| - | ### Visit Duration |
| - | |
| - | By default, a new visit is created after 4 hours of inactivity. |
| - | |
| - | Change this with: |
| + | or use a proc |
| ```ruby | |
| - | Ahoy.visit_duration = 30.minutes |
| + | Ahoy.user_method = ->(controller) { controller.true_user } |
| ``` | |
| - | ### ActiveRecord |
| - | |
| - | Let’s associate orders with visits. |
| + | ### Doorkeeper |
| - | First, generate a migration and add a `visit_id` column. |
| + | To attach the user with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), be sure you have a `current_resource_owner` method in `ApplicationController`. |
| ```ruby | |
| - | class AddVisitIdToOrders < ActiveRecord::Migration |
| - | def change |
| - | add_column :orders, :visit_id, :integer |
| + | class ApplicationController < ActionController::Base |
| + | private |
| + | |
| + | def current_resource_owner |
| + | User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token |
| end | |
| end | |
| ``` | |
| - | **Note**: Use the `uuid` column type if the `id` column on `visits` is a `uuid`. |
| + | ### Exclusions |
| - | Then, add `visitable` to the model. |
| + | Bots are excluded from tracking by default. To enable, use: |
| ```ruby | |
| - | class Order < ActiveRecord::Base |
| - | visitable |
| + | Ahoy.track_bots = true |
| + | ``` |
| + | |
| + | Add your own rules with: |
| + | |
| + | ```ruby |
| + | Ahoy.exclude_method = lambda do |controller, request| |
| + | request.ip == "192.168.1.1" |
| end | |
| ``` | |
| - | When a visitor places an order, the `visit_id` column is automatically set. :tada: |
| + | ### Visit Duration |
| - | Customize the column and class name with: |
| + | By default, a new visit is created after 4 hours of inactivity. Change this with: |
| ```ruby | |
| - | visitable :sign_up_visit, class_name: "Visit" |
| + | Ahoy.visit_duration = 30.minutes |
| ``` | |
| - | ### Doorkeeper |
| + | ### Multiple Subdomains |
| - | To attach the user with [Doorkeeper](https://github.com/doorkeeper-gem/doorkeeper), be sure you have a `current_resource_owner` method in `ApplicationController`. |
| + | To track visits across multiple subdomains, use: |
| ```ruby | |
| - | class ApplicationController < ActionController::Base |
| - | private |
| - | |
| - | def current_resource_owner |
| - | User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token |
| - | end |
| - | end |
| + | Ahoy.cookie_domain = :all |
| ``` | |
| ### Geocoding | |
| - | By default, geocoding is performed inline. For performance, move it to the background with: |
| + | Disable geocoding with: |
| ```ruby | |
| - | Ahoy.geocode = :async |
| + | Ahoy.geocode = false |
| ``` | |
| - | For Rails 4.0 and 4.1, you’ll need to add [activejob_backport](https://github.com/ankane/activejob_backport). |
| - | |
| - | To change the queue name (`ahoy` by default), use: |
| + | Change the job queue with: |
| ```ruby | |
| Ahoy.job_queue = :low_priority | |
| ``` | |
| - | Or disable geocoding with: |
| + | ### Token Generation |
| + | |
| + | Ahoy uses random UUIDs for visit and visitor tokens by default, but you can use your own generator like [Druuid](https://github.com/recurly/druuid). |
| ```ruby | |
| - | Ahoy.geocode = false |
| + | Ahoy.token_generator = -> { Druuid.gen } |
| ``` | |
| - | ### Track Visits Immediately |
| + | ### Throttling |
| - | Visitor and visit ids are generated on the first request (so you can use them immediately), but the `track_visit` method isn’t called until the JavaScript library posts to the server. This prevents browsers with cookies disabled from creating multiple visits and ensures visits are not created for API endpoints. Change this with: |
| + | You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API. |
| ```ruby | |
| - | Ahoy.track_visits_immediately = true |
| + | class Rack::Attack |
| + | throttle("ahoy/ip", limit: 20, period: 1.minute) do |req| |
| + | if req.path.start_with?("/ahoy/") |
| + | req.ip |
| + | end |
| + | end |
| + | end |
| ``` | |
| - | **Note:** It’s highly recommended to perform geocoding in the background with this option. |
| + | ### Exceptions |
| - | You can exclude API endpoints and other actions with: |
| + | Exceptions are rescued so analytics do not break your app. Ahoy uses [Safely](https://github.com/ankane/safely) to try to report them to a service by default. To customize this, use: |
| ```ruby | |
| - | skip_before_action :track_ahoy_visit |
| + | Safely.report_exception_method = ->(e) { Rollbar.error(e) } |
| ``` | |
| ## Development | |
| - | Ahoy is built with developers in mind. You can run the following code in your browser’s console. |
| + | Ahoy is built with developers in mind. You can run the following code in your browser’s console. |
| Force a new visit | |
| @@ | @@ -465,293 +317,162 @@ Turn off logging |
| ahoy.debug(false); | |
| ``` | |
| - | Debug endpoint requests in Ruby |
| + | Debug API requests in Ruby |
| ```ruby | |
| Ahoy.quiet = false | |
| ``` | |
| - | ## Explore the Data |
| - | |
| - | How you explore the data depends on the data store used. |
| - | |
| - | For SQL databases, you can use [Blazer](https://github.com/ankane/blazer) to easily generate charts and dashboards. |
| + | ## Data Stores |
| - | With ActiveRecord, you can do: |
| + | Data tracked by Ahoy is sent to your data store. Ahoy ships with a data store that uses your Rails database by default. You can find it in `config/initializers/ahoy.rb`: |
| ```ruby | |
| - | Visit.group(:search_keyword).count |
| - | Visit.group(:country).count |
| - | Visit.group(:referring_domain).count |
| - | ``` |
| - | |
| - | [Chartkick](http://chartkick.com/) and [Groupdate](https://github.com/ankane/groupdate) make it easy to visualize the data. |
| - | |
| - | ```erb |
| - | <%= line_chart Visit.group_by_day(:started_at).count %> |
| - | ``` |
| - | |
| - | See where orders are coming from with simple joins: |
| - | |
| - | ```ruby |
| - | Order.joins(:visit).group("referring_domain").count |
| - | Order.joins(:visit).group("city").count |
| - | Order.joins(:visit).group("device_type").count |
| - | ``` |
| - | |
| - | To see the visits for a given user, create an association: |
| - | |
| - | ```ruby |
| - | class User < ActiveRecord::Base |
| - | has_many :visits |
| + | class Ahoy::Store < Ahoy::DatabaseStore |
| end | |
| ``` | |
| - | And use: |
| - | |
| - | ```ruby |
| - | user = User.first |
| - | user.visits |
| - | ``` |
| - | |
| - | ### Create Funnels |
| + | There are four events data stores can subscribe to: |
| ```ruby | |
| - | viewed_store_ids = Ahoy::Event.where(name: "Viewed store").uniq.pluck(:user_id) |
| - | added_item_ids = Ahoy::Event.where(user_id: viewed_store_ids, name: "Added item to cart").uniq.pluck(:user_id) |
| - | viewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: "Viewed checkout").uniq.pluck(:user_id) |
| - | ``` |
| - | |
| - | The same approach also works with visitor tokens. |
| - | |
| - | ### Querying Properties |
| - | |
| - | With ActiveRecord, use: |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | # new visit |
| + | end |
| - | ```ruby |
| - | Ahoy::Event.where(name: "Viewed product").where_properties(product_id: 123).count |
| - | ``` |
| + | def track_event(data) |
| + | # new event |
| + | end |
| - | **Note:** If you get a `NoMethodError`, upgrade Ahoy and add `include Ahoy::Properties` to the Ahoy::Event class: |
| + | def geocode(data) |
| + | # visit geocoded |
| + | end |
| - | ```ruby |
| - | module Ahoy |
| - | class Event < ActiveRecord::Base |
| - | include Ahoy::Properties |
| - | ... |
| + | def authenticate(data) |
| + | # user authenticates |
| end | |
| end | |
| ``` | |
| - | ### Throttling |
| + | Data stores are designed to be highly customizable so you can scale as you grow. Check out [examples](docs/Data-Store-Examples.md) for Kafka, RabbitMQ, Fluentd, NATS, NSQ, and Amazon Kinesis Firehose. |
| - | By default, Ahoy uses [rack-attack](https://github.com/kickstarter/rack-attack) to throttle requests to Ahoy endpoints. Turn this off with: |
| + | ### Track Additional Data |
| ```ruby | |
| - | Ahoy.throttle = false |
| - | ``` |
| - | |
| - | The default limit is 20 requests per minute. This can be overridden with: |
| - | |
| - | ```ruby |
| - | # limit number of requests to 100 requests every 5 minutes |
| - | Ahoy.throttle_limit = 100 |
| - | Ahoy.throttle_period = 5.minutes |
| - | ``` |
| - | |
| - | ## Tutorials |
| - | |
| - | - [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer) |
| - | |
| - | ## Native Apps |
| - | |
| - | ### Visits |
| - | |
| - | When a user launches the app, create a visit. |
| - | |
| - | Generate a `visit_token` and `visitor_token` as [UUIDs](http://en.wikipedia.org/wiki/Universally_unique_identifier). |
| - | |
| - | Send these values in the `Ahoy-Visit` and `Ahoy-Visitor` headers with all requests. |
| - | |
| - | Send a `POST` request to `/ahoy/visits` with: |
| - | |
| - | - platform - `iOS`, `Android`, etc. |
| - | - app_version - `1.0.0` |
| - | - os_version - `7.0.6` |
| - | |
| - | After 4 hours of inactivity, create another visit and use the updated visit id. |
| - | |
| - | ### Events |
| - | |
| - | Send a `POST` request as `Content-Type: application/json` to `/ahoy/events` with: |
| - | |
| - | - id - `5aea7b70-182d-4070-b062-b0a09699ad5e` - UUID |
| - | - name - `Viewed item` |
| - | - properties - `{"item_id": 123}` |
| - | - time - `2014-06-17T00:00:00-07:00` - [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) |
| - | - `Ahoy-Visit` and `Ahoy-Visitor` headers |
| - | - user token (depends on your authentication framework) |
| - | |
| - | Use an array to pass multiple events at once. |
| - | |
| - | ## Reference |
| - | |
| - | By default, Ahoy create endpoints at `/ahoy/visits` and `/ahoy/events`. To disable, use: |
| - | |
| - | ```ruby |
| - | Ahoy.mount = false |
| - | ``` |
| - | |
| - | ## Upgrading |
| - | |
| - | ### 1.4.0 |
| - | |
| - | There’s nothing mandatory to do, but it’s worth noting the default store was changed from `ActiveRecordStore` to `ActiveRecordTokenStore` for new installations. `ActiveRecordStore` will continue to be supported. |
| - | |
| - | **Optional** Consider migrating `ahoy_events` table to have the following multi-column index as this *may* benefit |
| - | query performance depending on your usage: |
| - | |
| - | ```ruby |
| - | add_index :ahoy_events, [:name, :time] |
| + | class Ahoy::Store < Ahoy::DatabaseStore |
| + | def track_visit(data) |
| + | data[:accept_language] = request.headers["Accept-Language"] |
| + | super(data) |
| + | end |
| + | end |
| ``` | |
| - | ### json -> jsonb |
| + | Two useful methods you can use are `request` and `controller`. |
| - | Create a migration to add a new `jsonb` column. |
| + | ### Use Different Models |
| ```ruby | |
| - | rename_column :ahoy_events, :properties, :properties_json |
| - | add_column :ahoy_events, :properties, :jsonb |
| - | ``` |
| - | |
| - | Restart your web server immediately afterwards, as Ahoy will rescue and report errors until then. |
| - | |
| - | Sync the new column. |
| + | class Ahoy::Store < Ahoy::DatabaseStore |
| + | def visit_model |
| + | MyVisit |
| + | end |
| - | ```ruby |
| - | Ahoy::Event.where(properties: nil).select(:id).find_in_batches do |events| |
| - | Ahoy::Event.where(id: events.map(&:id)).update_all("properties = properties_json::jsonb") |
| + | def event_model |
| + | MyEvent |
| + | end |
| end | |
| ``` | |
| - | Then create a migration to drop the old column. |
| - | |
| - | ```ruby |
| - | remove_column :ahoy_events, :properties_json |
| - | ``` |
| + | ## Explore the Data |
| - | ### 1.0.0 |
| + | [Blazer](https://github.com/ankane/blazer) is a great tool for exploring your data. |
| - | Add the following code to the end of `config/initializers/ahoy.rb`. |
| + | With ActiveRecord, you can do: |
| ```ruby | |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | uses_deprecated_subscribers |
| - | end |
| + | Ahoy::Visit.group(:search_keyword).count |
| + | Ahoy::Visit.group(:country).count |
| + | Ahoy::Visit.group(:referring_domain).count |
| ``` | |
| - | If you use `Ahoy::Event` to track events, copy it into your project. |
| - | |
| - | ```ruby |
| - | module Ahoy |
| - | class Event < ActiveRecord::Base |
| - | self.table_name = "ahoy_events" |
| - | |
| - | belongs_to :visit |
| - | belongs_to :user, polymorphic: true |
| + | [Chartkick](http://chartkick.com/) and [Groupdate](https://github.com/ankane/groupdate) make it easy to visualize the data. |
| - | serialize :properties, JSON |
| - | end |
| - | end |
| + | ```erb |
| + | <%= line_chart Ahoy::Visit.group_by_day(:started_at).count %> |
| ``` | |
| - | That’s it! To fix deprecations, keep reading. |
| + | ### Querying Events |
| - | #### Visits |
| + | Ahoy provides two methods on the event model to make querying easier. |
| - | Remove `ahoy_visit` from your visit model and replace it with: |
| + | To query on both name and properties, you can use: |
| ```ruby | |
| - | class Visit < ActiveRecord::Base |
| - | belongs_to :user, polymorphic: true |
| - | end |
| + | Ahoy::Event.where_event("Viewed product", product_id: 123).count |
| ``` | |
| - | #### Subscribers |
| - | |
| - | Remove `uses_deprecated_subscribers` from `Ahoy::Store`. |
| - | |
| - | If you have a custom subscriber, copy the `track` method to `track_event` in `Ahoy::Store`. |
| + | Or just query properties with: |
| ```ruby | |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | def track_event(name, properties, options) |
| - | # code copied from the track method in your subscriber |
| - | end |
| - | end |
| + | Ahoy::Event.where_props(product_id: 123).count |
| ``` | |
| - | #### Authentication |
| + | ### Funnels |
| - | Ahoy no longer tracks the `$authenticate` event automatically. |
| - | |
| - | To restore this behavior, use: |
| + | It’s easy to create funnels. |
| ```ruby | |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | def authenticate(user) |
| - | super |
| - | ahoy.track "$authenticate" |
| - | end |
| - | end |
| + | viewed_store_ids = Ahoy::Event.where(name: "Viewed store").distinct.pluck(:user_id) |
| + | added_item_ids = Ahoy::Event.where(user_id: viewed_store_ids, name: "Added item to cart").distinct.pluck(:user_id) |
| + | viewed_checkout_ids = Ahoy::Event.where(user_id: added_item_ids, name: "Viewed checkout").distinct.pluck(:user_id) |
| ``` | |
| - | #### Global Options |
| - | |
| - | Replace the `Ahoy.user_method` with `user` method, and replace `Ahoy.track_bots` and `Ahoy.exclude_method` with `exclude?` method. |
| - | |
| - | Skip this step if you do not use these options. |
| - | |
| - | ```ruby |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | def user |
| - | # logic from Ahoy.user_method goes here |
| - | controller.true_user |
| - | end |
| + | The same approach also works with visitor tokens. |
| - | def exclude? |
| - | # logic from Ahoy.track_bots and Ahoy.exclude_method goes here |
| - | bot? || request.ip == "192.168.1.1" |
| - | end |
| - | end |
| - | ``` |
| + | ## Tutorials |
| - | You made it! Now, take advantage of Ahoy’s awesome new features, like easy customization and exception reporting. |
| + | - [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer) |
| - | ### 0.3.0 |
| + | ## API Spec |
| - | Starting with `0.3.0`, visit and visitor tokens are now UUIDs. |
| + | ### Visits |
| - | ### 0.1.6 |
| + | Generate visit and visitor tokens as [UUIDs](http://en.wikipedia.org/wiki/Universally_unique_identifier), and include these values in the `Ahoy-Visit` and `Ahoy-Visitor` headers with all requests. |
| - | In `0.1.6`, a big improvement was made to `browser` and `os`. Update existing visits with: |
| + | Send a `POST` request to `/ahoy/visits` with `Content-Type: application/json` and a body like: |
| - | ```ruby |
| - | Visit.find_each do |visit| |
| - | visit.set_technology |
| - | visit.save! if visit.changed? |
| - | end |
| + | ```json |
| + | { |
| + | "visit_token": "<visit-token>", |
| + | "visitor_token": "<visitor-token>", |
| + | "platform": "iOS", |
| + | "app_version": "1.0.0", |
| + | "os_version": "11.2.6" |
| + | } |
| ``` | |
| - | ## TODO |
| - | |
| - | - real-time dashboard of visits and events |
| - | - more events for append only stores |
| - | - turn off modules |
| + | After 4 hours of inactivity, create another visit (use the same visitor token). |
| - | ## No Ruby? |
| + | ### Events |
| - | Check out [Ahoy.js](https://github.com/ankane/ahoy.js). |
| + | Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` and a body like: |
| + | |
| + | ```json |
| + | { |
| + | "visit_token": "<visit-token>", |
| + | "visitor_token": "<visitor-token>", |
| + | "events": [ |
| + | { |
| + | "id": "<optional-random-id>", |
| + | "name": "Viewed item", |
| + | "properties": { |
| + | "item_id": 123 |
| + | }, |
| + | "time": "2018-01-01T00:00:00-07:00" |
| + | } |
| + | ] |
| + | } |
| + | ``` |
| ## History | |
Rakefile
+1
-0
| @@ | @@ -5,4 +5,5 @@ task default: :test |
| Rake::TestTask.new do |t| | |
| t.libs << "test" | |
| t.pattern = "test/**/*_test.rb" | |
| + | t.warning = false |
| end | |
ahoy_matey.gemspec
+6
-8
| @@ | @@ -9,7 +9,6 @@ Gem::Specification.new do |spec| |
| spec.authors = ["Andrew Kane"] | |
| spec.email = ["andrew@chartkick.com"] | |
| spec.summary = "Simple, powerful visit tracking for Rails" | |
| - | spec.description = "Simple, powerful visit tracking for Rails" |
| spec.homepage = "https://github.com/ankane/ahoy" | |
| spec.license = "MIT" | |
| @@ | @@ -18,21 +17,20 @@ Gem::Specification.new do |spec| |
| spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) | |
| spec.require_paths = ["lib"] | |
| - | spec.add_dependency "railties" |
| + | spec.add_dependency "railties", ">= 4.2" |
| spec.add_dependency "addressable" | |
| - | spec.add_dependency "browser", "~> 2.0" |
| spec.add_dependency "geocoder" | |
| - | spec.add_dependency "referer-parser", ">= 0.3.0" |
| + | spec.add_dependency "browser", "~> 2.0" |
| + | spec.add_dependency "referer-parser", ">= 0.3" |
| spec.add_dependency "user_agent_parser" | |
| spec.add_dependency "request_store" | |
| - | spec.add_dependency "uuidtools" |
| - | spec.add_dependency "safely_block", ">= 0.1.1" |
| - | spec.add_dependency "rack-attack", "< 6" |
| + | spec.add_dependency "safely_block", ">= 0.2.1" |
| - | spec.add_development_dependency "bundler", "~> 1.5" |
| + | spec.add_development_dependency "bundler" |
| spec.add_development_dependency "rake" | |
| spec.add_development_dependency "minitest" | |
| spec.add_development_dependency "activerecord" | |
| spec.add_development_dependency "pg" | |
| spec.add_development_dependency "mysql2" | |
| + | spec.add_development_dependency "mongoid" |
| end | |
app/controllers/ahoy/base_controller.rb
+1
-4
| @@ | @@ -6,12 +6,9 @@ module Ahoy |
| skip_after_action(*filters, raise: false) | |
| skip_around_action(*filters, raise: false) | |
| before_action :verify_request_size | |
| - | elsif respond_to?(:skip_action_callback) |
| + | else |
| skip_action_callback *filters | |
| before_action :verify_request_size | |
| - | else |
| - | skip_filter *filters |
| - | before_filter :verify_request_size |
| end | |
| if respond_to?(:protect_from_forgery) | |
app/controllers/ahoy/events_controller.rb
+7
-1
| @@ | @@ -8,8 +8,14 @@ module Ahoy |
| elsif params[:events] | |
| request.params[:events] | |
| else | |
| + | data = |
| + | if params[:events_json] |
| + | request.params[:events_json] |
| + | else |
| + | request.body.read |
| + | end |
| begin | |
| - | ActiveSupport::JSON.decode(request.body.read) |
| + | ActiveSupport::JSON.decode(data) |
| rescue ActiveSupport::JSON.parse_error | |
| # do nothing | |
| [] | |
app/controllers/ahoy/visits_controller.rb
+7
-1
| @@ | @@ -2,7 +2,13 @@ module Ahoy |
| class VisitsController < BaseController | |
| def create | |
| ahoy.track_visit | |
| - | render json: {visit_id: ahoy.visit_id, visitor_id: ahoy.visitor_id} |
| + | render json: { |
| + | visit_token: ahoy.visit_token, |
| + | visitor_token: ahoy.visitor_token, |
| + | # legacy |
| + | visit_id: ahoy.visit_token, |
| + | visitor_id: ahoy.visitor_token |
| + | } |
| end | |
| end | |
| end | |
app/jobs/ahoy/geocode_job.rb
+10
-0
| @@ | @@ -0,0 +1,10 @@ |
| + | # for smooth update from Ahoy 1 -> 2 |
| + | module Ahoy |
| + | class GeocodeJob < ActiveJob::Base |
| + | queue_as { Ahoy.job_queue } |
| + | |
| + | def perform(visit) |
| + | Ahoy::GeocodeV2Job.perform_now(visit.visit_token, visit.ip) |
| + | end |
| + | end |
| + | end |
app/jobs/ahoy/geocode_v2_job.rb
+29
-0
| @@ | @@ -0,0 +1,29 @@ |
| + | module Ahoy |
| + | class GeocodeV2Job < ActiveJob::Base |
| + | queue_as { Ahoy.job_queue } |
| + | |
| + | def perform(visit_token, ip) |
| + | location = |
| + | begin |
| + | Geocoder.search(ip).first |
| + | rescue => e |
| + | $stderr.puts e.message |
| + | nil |
| + | end |
| + | |
| + | if location |
| + | data = { |
| + | visit_token: visit_token, |
| + | country: location.try(:country).presence, |
| + | region: location.try(:state).presence, |
| + | city: location.try(:city).presence, |
| + | postal_code: location.try(:postal_code).presence, |
| + | latitude: location.try(:latitude).presence, |
| + | longitude: location.try(:longitude).presence |
| + | } |
| + | |
| + | Ahoy::Tracker.new(visit_token: visit_token).geocode(data) |
| + | end |
| + | end |
| + | end |
| + | end |
config/routes.rb
+1
-1
| @@ | @@ -1,5 +1,5 @@ |
| Rails.application.routes.draw do | |
| - | mount Ahoy::Engine => "/ahoy" if Ahoy.mount |
| + | mount Ahoy::Engine => "/ahoy" if Ahoy.api |
| end | |
| Ahoy::Engine.routes.draw do | |
docs/Ahoy-2-Upgrade.md
+147
-0
| @@ | @@ -0,0 +1,147 @@ |
| + | # Ahoy 2 Upgrade |
| + | |
| + | Ahoy 2.0 brings a number of exciting changes: |
| + | |
| + | - jQuery is no longer required |
| + | - Uses `navigator.sendBeacon` by default in supported browsers |
| + | - Simpler interface for data stores |
| + | |
| + | ## How to Upgrade |
| + | |
| + | Update your Gemfile: |
| + | |
| + | ```ruby |
| + | gem 'ahoy_matey', '~> 2' |
| + | ``` |
| + | |
| + | And run: |
| + | |
| + | ```sh |
| + | bundle install |
| + | ``` |
| + | |
| + | Add to `config/initializers/ahoy.rb`: |
| + | |
| + | ```ruby |
| + | Ahoy.api = true |
| + | Ahoy.server_side_visits = false |
| + | ``` |
| + | |
| + | If you use `visitable`, add `class_name` to each instance: |
| + | |
| + | ```ruby |
| + | visitable class_name: "Visit" |
| + | ``` |
| + | |
| + | Then follow the instructions for your data store. |
| + | |
| + | - [ActiveRecordTokenStore](#activerecordtokenstore) |
| + | - [ActiveRecordStore](#activerecordstore) |
| + | - [MongoidStore](#mongoidstore) |
| + | - [Others](#others) |
| + | |
| + | ## Data Stores |
| + | |
| + | ### ActiveRecordTokenStore |
| + | |
| + | In `config/initializers/ahoy.rb`, replace `Ahoy::Store` with: |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::DatabaseStore |
| + | def visit_model |
| + | Visit |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### ActiveRecordStore |
| + | |
| + | Add [uuidtools](https://github.com/sporkmonger/uuidtools) to your Gemfile. |
| + | |
| + | In `config/initializers/ahoy.rb`, replace `Ahoy::Store` with: |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::DatabaseStore |
| + | def track_visit(data) |
| + | data[:id] = ensure_uuid(data.delete(:visit_token)) |
| + | data[:visitor_id] = ensure_uuid(data.delete(:visitor_token)) |
| + | super(data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | data[:id] = ensure_uuid(data.delete(:event_id)) |
| + | super(data) |
| + | end |
| + | |
| + | def visit |
| + | @visit ||= visit_model.find_by(id: ensure_uuid(ahoy.visit_token)) if ahoy.visit_token |
| + | end |
| + | |
| + | def visit_model |
| + | Visit |
| + | end |
| + | |
| + | UUID_NAMESPACE = UUIDTools::UUID.parse("a82ae811-5011-45ab-a728-569df7499c5f") |
| + | |
| + | def ensure_uuid(id) |
| + | UUIDTools::UUID.parse(id).to_s |
| + | rescue |
| + | UUIDTools::UUID.sha1_create(UUID_NAMESPACE, id).to_s |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### MongoidStore |
| + | |
| + | In `config/initializers/ahoy.rb`, replace `Ahoy::Store` with: |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::DatabaseStore |
| + | def track_visit(data) |
| + | data[:_id] = binary_uuid(data.delete(:visit_token)) |
| + | data[:visitor_id] = binary_uuid(data.delete(:visitor_token)) |
| + | super(data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | data[:_id] = binary_uuid(data.delete(:event_id)) |
| + | super(data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | visit_model.where(id: binary_uuid(ahoy.visit_token)).find_one_and_update({"$set": data}, {upsert: true}) |
| + | end |
| + | |
| + | def visit |
| + | @visit ||= visit_model.where(id: binary_uuid(ahoy.visit_token)).first if ahoy.visit_token |
| + | end |
| + | |
| + | def visit_model |
| + | Visit |
| + | end |
| + | |
| + | def binary_uuid(token) |
| + | token = token.delete("-") |
| + | if defined?(::BSON) |
| + | ::BSON::Binary.new(token, :uuid) |
| + | elsif defined?(::Moped::BSON) |
| + | ::Moped::BSON::Binary.new(:uuid, token) |
| + | else |
| + | token |
| + | end |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### Others |
| + | |
| + | Check out the [data store examples](Data-Store-Examples.md). |
| + | |
| + | ## Throttling |
| + | |
| + | Throttling was removed due to limited practical usefulness. See [instructions for adding it back](../README.md#throttling) if you need it. |
| + | |
| + | ## Options |
| + | |
| + | - The `mount` option was renamed to `api` |
| + | - The `track_visits_immediately` option was renamed to `server_side_visits` |
docs/Data-Store-Examples.md
+240
-0
| @@ | @@ -0,0 +1,240 @@ |
| + | # Data Store Examples |
| + | |
| + | - [Kafka](#kafka) |
| + | - [RabbitMQ](#rabbitmq) |
| + | - [Fluentd](#fluentd) |
| + | - [NATS](#nats) |
| + | - [NSQ](#nsq) |
| + | - [Amazon Kinesis Firehose](#amazon-kinesis-firehose) |
| + | |
| + | ### Kafka |
| + | |
| + | Add [ruby-kafka](https://github.com/zendesk/ruby-kafka) to your Gemfile. |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | post("ahoy_visits", data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | post("ahoy_events", data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | post("ahoy_geocode", data) |
| + | end |
| + | |
| + | def authenticate(data) |
| + | post("ahoy_auth", data) |
| + | end |
| + | |
| + | private |
| + | |
| + | def post(topic, data) |
| + | producer.produce(data.to_json, topic: topic) |
| + | end |
| + | |
| + | def producer |
| + | @producer ||= begin |
| + | client = |
| + | Kafka.new( |
| + | seed_brokers: ENV["KAFKA_URL"] || "localhost:9092", |
| + | logger: Rails.logger |
| + | ) |
| + | producer = client.async_producer(delivery_interval: 3) |
| + | at_exit { producer.shutdown } |
| + | producer |
| + | end |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### RabbitMQ |
| + | |
| + | Add [bunny](https://github.com/ruby-amqp/bunny) to your Gemfile. |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | post("ahoy_visits", data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | post("ahoy_events", data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | post("ahoy_geocode", data) |
| + | end |
| + | |
| + | def authenticate(data) |
| + | post("ahoy_auth", data) |
| + | end |
| + | |
| + | private |
| + | |
| + | def post(topic, message) |
| + | channel.queue(topic, durable: true).publish(message.to_json) |
| + | end |
| + | |
| + | def channel |
| + | @channel ||= begin |
| + | conn = Bunny.new |
| + | conn.start |
| + | conn.create_channel |
| + | end |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### Fluentd |
| + | |
| + | Add [fluent-logger](https://github.com/fluent/fluent-logger-ruby) to your Gemfile. |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | post("ahoy_visits", data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | post("ahoy_events", data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | post("ahoy_geocode", data) |
| + | end |
| + | |
| + | def authenticate(data) |
| + | post("ahoy_auth", data) |
| + | end |
| + | |
| + | private |
| + | |
| + | def post(topic, message) |
| + | logger.post(topic, message) |
| + | end |
| + | |
| + | def logger |
| + | @logger ||= Fluent::Logger::FluentLogger.new("ahoy", host: "localhost", port: 24224) |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### NATS |
| + | |
| + | Add [nats-pure](https://github.com/nats-io/pure-ruby-nats) to your Gemfile. |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | post("ahoy_visits", data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | post("ahoy_events", data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | post("ahoy_geocode", data) |
| + | end |
| + | |
| + | def authenticate(data) |
| + | post("ahoy_auth", data) |
| + | end |
| + | |
| + | private |
| + | |
| + | def post(topic, data) |
| + | client.publish(topic, data.to_json) |
| + | end |
| + | |
| + | def client |
| + | @client ||= begin |
| + | require "nats/io/client" |
| + | client = NATS::IO::Client.new |
| + | client.connect(servers: (ENV["NATS_URL"] || "nats://127.0.0.1:4222").split(",")) |
| + | client |
| + | end |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### NSQ |
| + | |
| + | Add [nsq-ruby](https://github.com/wistia/nsq-ruby) to your Gemfile. |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | post("ahoy_visits", data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | post("ahoy_events", data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | post("ahoy_geocode", data) |
| + | end |
| + | |
| + | def authenticate(data) |
| + | post("ahoy_auth", data) |
| + | end |
| + | |
| + | private |
| + | |
| + | def post(topic, data) |
| + | client.write_to_topic(topic, data.to_json) |
| + | end |
| + | |
| + | def client |
| + | @client ||= begin |
| + | require "nsq" |
| + | client = Nsq::Producer.new( |
| + | nsqd: ENV["NSQ_URL"] || "127.0.0.1:4150" |
| + | ) |
| + | at_exit { client.terminate } |
| + | client |
| + | end |
| + | end |
| + | end |
| + | ``` |
| + | |
| + | ### Amazon Kinesis Firehose |
| + | |
| + | Add [aws-sdk-firehose](https://github.com/aws/aws-sdk-ruby) to your Gemfile. |
| + | |
| + | ```ruby |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | post("ahoy_visits", data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | post("ahoy_events", data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | post("ahoy_geocode", data) |
| + | end |
| + | |
| + | def authenticate(data) |
| + | post("ahoy_auth", data) |
| + | end |
| + | |
| + | private |
| + | |
| + | def post(topic, data) |
| + | client.put_record( |
| + | delivery_stream_name: topic, |
| + | record: {data: "#{data.to_json}\n"} |
| + | ) |
| + | end |
| + | |
| + | def client |
| + | @client ||= Aws::Firehose::Client.new |
| + | end |
| + | end |
| + | ``` |
ahoy.rb b/lib/ahoy.rb
+29
-93
| @@ | @@ -1,53 +1,21 @@ |
| require "active_support" | |
| require "active_support/core_ext" | |
| require "addressable/uri" | |
| - | require "browser" |
| require "geocoder" | |
| - | require "referer-parser" |
| - | require "user_agent_parser" |
| - | require "request_store" |
| - | require "uuidtools" |
| require "safely/core" | |
| - | require "ahoy/version" |
| - | require "ahoy/tracker" |
| + | require "ahoy/base_store" |
| require "ahoy/controller" | |
| + | require "ahoy/database_store" |
| require "ahoy/model" | |
| + | require "ahoy/query_methods" |
| + | require "ahoy/tracker" |
| + | require "ahoy/version" |
| require "ahoy/visit_properties" | |
| - | require "ahoy/properties" |
| - | require "ahoy/deckhands/location_deckhand" |
| - | require "ahoy/deckhands/request_deckhand" |
| - | require "ahoy/deckhands/technology_deckhand" |
| - | require "ahoy/deckhands/traffic_source_deckhand" |
| - | require "ahoy/deckhands/utm_parameter_deckhand" |
| - | require "ahoy/stores/base_store" |
| - | require "ahoy/stores/active_record_store" |
| - | require "ahoy/stores/active_record_token_store" |
| - | require "ahoy/stores/log_store" |
| - | require "ahoy/stores/fluentd_store" |
| - | require "ahoy/stores/mongoid_store" |
| - | require "ahoy/stores/kafka_store" |
| - | require "ahoy/stores/nats_store" |
| - | require "ahoy/stores/nsq_store" |
| - | require "ahoy/stores/kinesis_firehose_store" |
| - | require "ahoy/stores/bunny_store" |
| - | require "ahoy/engine" if defined?(Rails) |
| - | require "ahoy/warden" if defined?(Warden) |
| - | |
| - | # background jobs |
| - | begin |
| - | require "active_job" |
| - | rescue LoadError |
| - | # do nothing |
| - | end |
| - | require "ahoy/geocode_job" if defined?(ActiveJob) |
| - | # deprecated |
| - | require "ahoy/subscribers/active_record" |
| + | require "ahoy/engine" if defined?(Rails) |
| module Ahoy | |
| - | UUID_NAMESPACE = UUIDTools::UUID.parse("a82ae811-5011-45ab-a728-569df7499c5f") |
| - | |
| mattr_accessor :visit_duration | |
| self.visit_duration = 4.hours | |
| @@ | @@ -56,8 +24,8 @@ module Ahoy |
| mattr_accessor :cookie_domain | |
| - | mattr_accessor :track_visits_immediately |
| - | self.track_visits_immediately = false |
| + | mattr_accessor :server_side_visits |
| + | self.server_side_visits = true |
| mattr_accessor :quiet | |
| self.quiet = true | |
| @@ | @@ -71,76 +39,44 @@ module Ahoy |
| mattr_accessor :max_events_per_request | |
| self.max_events_per_request = 10 | |
| - | mattr_accessor :mount |
| - | self.mount = true |
| - | |
| - | mattr_accessor :throttle |
| - | self.throttle = true |
| - | |
| - | mattr_accessor :throttle_limit |
| - | self.throttle_limit = 20 |
| - | |
| - | mattr_accessor :throttle_period |
| - | self.throttle_period = 1.minute |
| - | |
| mattr_accessor :job_queue | |
| self.job_queue = :ahoy | |
| + | mattr_accessor :api |
| + | self.api = false |
| + | |
| mattr_accessor :api_only | |
| self.api_only = false | |
| + | mattr_accessor :protect_from_forgery |
| + | self.protect_from_forgery = true |
| + | |
| mattr_accessor :preserve_callbacks | |
| - | # Preserve Authlogic activation. Users of Authlogic will likely need this to properly obtain current_user. |
| self.preserve_callbacks = [:load_authlogic, :activate_authlogic] | |
| - | mattr_accessor :protect_from_forgery |
| - | self.protect_from_forgery = false |
| - | |
| - | def self.ensure_uuid(id) |
| - | valid = UUIDTools::UUID.parse(id) rescue nil |
| - | if valid |
| - | id |
| - | else |
| - | UUIDTools::UUID.sha1_create(UUID_NAMESPACE, id).to_s |
| - | end |
| + | mattr_accessor :user_method |
| + | self.user_method = lambda do |controller| |
| + | (controller.respond_to?(:current_user) && controller.current_user) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil |
| end | |
| - | # deprecated |
| - | |
| - | mattr_accessor :domain |
| - | mattr_accessor :visit_model |
| - | mattr_accessor :user_method |
| mattr_accessor :exclude_method | |
| - | mattr_accessor :subscribers |
| - | self.subscribers = [] |
| - | |
| mattr_accessor :track_bots | |
| self.track_bots = false | |
| + | |
| + | mattr_accessor :token_generator |
| + | self.token_generator = -> { SecureRandom.uuid } |
| end | |
| - | if defined?(Rails) |
| - | ActiveSupport.on_load(:action_controller) do |
| - | include Ahoy::Controller |
| - | end |
| + | ActiveSupport.on_load(:action_controller) do |
| + | include Ahoy::Controller |
| + | end |
| - | ActiveSupport.on_load(:active_record) do |
| - | extend Ahoy::Model |
| - | end |
| + | ActiveSupport.on_load(:active_record) do |
| + | extend Ahoy::Model |
| + | end |
| - | if Rails.version < "4.2" |
| - | # ensure logger silence will not be added by activerecord-session_store |
| - | # otherwise, we get SystemStackError: stack level too deep |
| - | begin |
| - | require "active_record/session_store/extension/logger_silencer" |
| - | rescue LoadError |
| - | require "ahoy/logger_silencer" |
| - | Logger.send :include, Ahoy::LoggerSilencer |
| - | |
| - | begin |
| - | require "syslog/logger" |
| - | Syslog::Logger.send :include, Ahoy::LoggerSilencer |
| - | rescue LoadError; end |
| - | end |
| - | end |
| + | # Mongoid |
| + | if defined?(ActiveModel) |
| + | ActiveModel::Callbacks.include(Ahoy::Model) |
| end | |
ahoy/base_store.rb b/lib/ahoy/base_store.rb
+72
-0
| @@ | @@ -0,0 +1,72 @@ |
| + | module Ahoy |
| + | class BaseStore |
| + | attr_writer :user |
| + | |
| + | def initialize(options) |
| + | @options = options |
| + | end |
| + | |
| + | def track_visit(data) |
| + | end |
| + | |
| + | def track_event(data) |
| + | end |
| + | |
| + | def geocode(data) |
| + | end |
| + | |
| + | def authenticate(data) |
| + | end |
| + | |
| + | def visit |
| + | end |
| + | |
| + | def user |
| + | @user ||= begin |
| + | if Ahoy.user_method.respond_to?(:call) |
| + | Ahoy.user_method.call(controller) |
| + | else |
| + | controller.send(Ahoy.user_method) |
| + | end |
| + | end |
| + | end |
| + | |
| + | def exclude? |
| + | (!Ahoy.track_bots && bot?) || exclude_by_method? |
| + | end |
| + | |
| + | def generate_id |
| + | Ahoy.token_generator.call |
| + | end |
| + | |
| + | protected |
| + | |
| + | def bot? |
| + | @bot ||= request ? Browser.new(request.user_agent).bot? : false |
| + | end |
| + | |
| + | def exclude_by_method? |
| + | if Ahoy.exclude_method |
| + | if Ahoy.exclude_method.arity == 1 |
| + | Ahoy.exclude_method.call(controller) |
| + | else |
| + | Ahoy.exclude_method.call(controller, request) |
| + | end |
| + | else |
| + | false |
| + | end |
| + | end |
| + | |
| + | def request |
| + | @request ||= @options[:request] || controller.try(:request) |
| + | end |
| + | |
| + | def controller |
| + | @controller ||= @options[:controller] |
| + | end |
| + | |
| + | def ahoy |
| + | @ahoy ||= @options[:ahoy] |
| + | end |
| + | end |
| + | end |
ahoy/controller.rb b/lib/ahoy/controller.rb
+4
-10
| @@ | @@ -7,15 +7,9 @@ module Ahoy |
| base.helper_method :current_visit | |
| base.helper_method :ahoy | |
| end | |
| - | if base.respond_to?(:before_action) |
| - | base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only } |
| - | base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only } |
| - | base.before_action :set_ahoy_request_store |
| - | else |
| - | base.before_filter :set_ahoy_cookies, unless: -> { Ahoy.api_only } |
| - | base.before_filter :track_ahoy_visit, unless: -> { Ahoy.api_only } |
| - | base.before_filter :set_ahoy_request_store |
| - | end |
| + | base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only } |
| + | base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only } |
| + | base.before_action :set_ahoy_request_store |
| end | |
| def ahoy | |
| @@ | @@ -33,7 +27,7 @@ module Ahoy |
| def track_ahoy_visit | |
| if ahoy.new_visit? | |
| - | ahoy.track_visit(defer: !Ahoy.track_visits_immediately) |
| + | ahoy.track_visit(defer: !Ahoy.server_side_visits) |
| end | |
| end | |
ahoy/database_store.rb b/lib/ahoy/database_store.rb
+72
-0
| @@ | @@ -0,0 +1,72 @@ |
| + | module Ahoy |
| + | class DatabaseStore < BaseStore |
| + | def track_visit(data) |
| + | @visit = visit_model.create!(slice_data(visit_model, data)) |
| + | rescue => e |
| + | raise e unless unique_exception?(e) |
| + | @visit = nil |
| + | end |
| + | |
| + | def track_event(data) |
| + | # if we don't have a visit, let's try to create one first |
| + | ahoy.track_visit unless visit |
| + | |
| + | event = event_model.new(slice_data(event_model, data)) |
| + | event.visit = visit |
| + | begin |
| + | event.save! |
| + | rescue => e |
| + | raise e unless unique_exception?(e) |
| + | end |
| + | end |
| + | |
| + | def geocode(data) |
| + | data = slice_data(visit_model, data.except(:visit_token)) |
| + | if defined?(Mongoid::Document) && visit_model < Mongoid::Document |
| + | # upsert since visit might not be found due to eventual consistency |
| + | visit_model.where(visit_token: ahoy.visit_token).find_one_and_update({"$set": data}, {upsert: true}) |
| + | elsif visit |
| + | visit.update_attributes(data) |
| + | else |
| + | $stderr.puts "[ahoy] Visit for geocode not found: #{data[:visit_token]}" |
| + | end |
| + | end |
| + | |
| + | def authenticate(_) |
| + | if visit && visit.respond_to?(:user) && !visit.user |
| + | begin |
| + | visit.user = user |
| + | visit.save! |
| + | rescue ActiveRecord::AssociationTypeMismatch |
| + | # do nothing |
| + | end |
| + | end |
| + | end |
| + | |
| + | def visit |
| + | @visit ||= visit_model.where(visit_token: ahoy.visit_token).first if ahoy.visit_token |
| + | end |
| + | |
| + | protected |
| + | |
| + | def visit_model |
| + | ::Ahoy::Visit |
| + | end |
| + | |
| + | def event_model |
| + | ::Ahoy::Event |
| + | end |
| + | |
| + | def slice_data(model, data) |
| + | column_names = model.try(:column_names) || model.attribute_names |
| + | data.slice(*column_names.map(&:to_sym)).select { |_, v| v } |
| + | end |
| + | |
| + | def unique_exception?(e) |
| + | return true if defined?(ActiveRecord::RecordNotUnique) && e.is_a?(ActiveRecord::RecordNotUnique) |
| + | return true if defined?(PG::UniqueViolation) && e.is_a?(PG::UniqueViolation) |
| + | return true if defined?(Mongo::Error::OperationFailure) && e.is_a?(Mongo::Error::OperationFailure) && e.message.include?("duplicate key error") |
| + | false |
| + | end |
| + | end |
| + | end |
ahoy/deckhands/location_deckhand.rb b/lib/ahoy/deckhands/location_deckhand.rb
+0
-49
| @@ | @@ -1,49 +0,0 @@ |
| - | module Ahoy |
| - | module Deckhands |
| - | class LocationDeckhand |
| - | def initialize(ip) |
| - | @ip = ip |
| - | end |
| - | |
| - | def country |
| - | location.try(:country).presence |
| - | end |
| - | |
| - | def region |
| - | location.try(:state).presence |
| - | end |
| - | |
| - | def postal_code |
| - | location.try(:postal_code).presence |
| - | end |
| - | |
| - | def city |
| - | location.try(:city).presence |
| - | end |
| - | |
| - | def latitude |
| - | location.try(:latitude).presence |
| - | end |
| - | |
| - | def longitude |
| - | location.try(:longitude).presence |
| - | end |
| - | |
| - | protected |
| - | |
| - | def location |
| - | unless @checked |
| - | @location = |
| - | begin |
| - | Geocoder.search(@ip).first |
| - | rescue => e |
| - | $stderr.puts e.message |
| - | nil |
| - | end |
| - | @checked = true |
| - | end |
| - | @location |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/deckhands/request_deckhand.rb b/lib/ahoy/deckhands/request_deckhand.rb
+0
-52
| @@ | @@ -1,52 +0,0 @@ |
| - | module Ahoy |
| - | module Deckhands |
| - | class RequestDeckhand |
| - | attr_reader :request |
| - | |
| - | def initialize(request, options = {}) |
| - | @request = request |
| - | @options = options |
| - | end |
| - | |
| - | def ip |
| - | request.remote_ip |
| - | end |
| - | |
| - | def user_agent |
| - | request.user_agent |
| - | end |
| - | |
| - | def referrer |
| - | @options[:api] ? params["referrer"] : request.referer |
| - | end |
| - | |
| - | def landing_page |
| - | @options[:api] ? params["landing_page"] : request.original_url |
| - | end |
| - | |
| - | def platform |
| - | params["platform"] |
| - | end |
| - | |
| - | def app_version |
| - | params["app_version"] |
| - | end |
| - | |
| - | def os_version |
| - | params["os_version"] |
| - | end |
| - | |
| - | def screen_height |
| - | params["screen_height"] |
| - | end |
| - | |
| - | def screen_width |
| - | params["screen_width"] |
| - | end |
| - | |
| - | def params |
| - | @params ||= request.params |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/deckhands/technology_deckhand.rb b/lib/ahoy/deckhands/technology_deckhand.rb
+0
-47
| @@ | @@ -1,47 +0,0 @@ |
| - | module Ahoy |
| - | module Deckhands |
| - | class TechnologyDeckhand |
| - | def initialize(user_agent) |
| - | @user_agent = user_agent |
| - | end |
| - | |
| - | def browser |
| - | agent.name |
| - | end |
| - | |
| - | def os |
| - | agent.os.name |
| - | end |
| - | |
| - | def device_type |
| - | @device_type ||= begin |
| - | browser = Browser.new(@user_agent) |
| - | if browser.bot? |
| - | "Bot" |
| - | elsif browser.device.tv? |
| - | "TV" |
| - | elsif browser.device.console? |
| - | "Console" |
| - | elsif browser.device.tablet? |
| - | "Tablet" |
| - | elsif browser.device.mobile? |
| - | "Mobile" |
| - | else |
| - | "Desktop" |
| - | end |
| - | end |
| - | end |
| - | |
| - | protected |
| - | |
| - | def agent |
| - | @agent ||= self.class.user_agent_parser.parse(@user_agent) |
| - | end |
| - | |
| - | # performance |
| - | def self.user_agent_parser |
| - | @user_agent_parser ||= UserAgentParser::Parser.new |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/deckhands/traffic_source_deckhand.rb b/lib/ahoy/deckhands/traffic_source_deckhand.rb
+0
-22
| @@ | @@ -1,22 +0,0 @@ |
| - | module Ahoy |
| - | module Deckhands |
| - | class TrafficSourceDeckhand |
| - | def initialize(referrer) |
| - | @referrer = referrer |
| - | end |
| - | |
| - | def referring_domain |
| - | @referring_domain ||= Addressable::URI.parse(@referrer).host.first(255) rescue nil |
| - | end |
| - | |
| - | def search_keyword |
| - | @search_keyword ||= (self.class.referrer_parser.parse(@referrer)[:term][0..255] rescue nil).presence |
| - | end |
| - | |
| - | # performance hack for referer-parser |
| - | def self.referrer_parser |
| - | @referrer_parser ||= RefererParser::Parser.new |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/deckhands/utm_parameter_deckhand.rb b/lib/ahoy/deckhands/utm_parameter_deckhand.rb
+0
-23
| @@ | @@ -1,23 +0,0 @@ |
| - | module Ahoy |
| - | module Deckhands |
| - | class UtmParameterDeckhand |
| - | def initialize(landing_page, params = nil) |
| - | @landing_page = landing_page |
| - | @params = params || {} |
| - | end |
| - | |
| - | def landing_params |
| - | @landing_params ||= begin |
| - | landing_uri = Addressable::URI.parse(@landing_page) rescue nil |
| - | (landing_uri && landing_uri.query_values) || {} |
| - | end |
| - | end |
| - | |
| - | %w(utm_source utm_medium utm_term utm_content utm_campaign).each do |name| |
| - | define_method name do |
| - | @params[name] || landing_params[name] |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/engine.rb b/lib/ahoy/engine.rb
+5
-7
| @@ | @@ -1,10 +1,8 @@ |
| module Ahoy | |
| class Engine < ::Rails::Engine | |
| - | initializer "ahoy.middleware", after: "sprockets.environment" do |app| |
| - | if Ahoy.throttle |
| - | require "ahoy/throttle" |
| - | app.middleware.use Ahoy::Throttle |
| - | end |
| + | initializer "ahoy", after: "sprockets.environment" do |app| |
| + | # allow Devise to be loaded after Ahoy |
| + | require "ahoy/warden" if defined?(Warden) |
| next unless Ahoy.quiet | |
| @@ | @@ -14,8 +12,8 @@ module Ahoy |
| # Just create an alias for call in middleware | |
| Rails::Rack::Logger.class_eval do | |
| def call_with_quiet_ahoy(env) | |
| - | if env["PATH_INFO"].start_with?(AHOY_PREFIX) && logger.respond_to?(:silence_logger) |
| - | logger.silence_logger do |
| + | if env["PATH_INFO"].start_with?(AHOY_PREFIX) && logger.respond_to?(:silence) |
| + | logger.silence do |
| call_without_quiet_ahoy(env) | |
| end | |
| else | |
ahoy/geocode_job.rb b/lib/ahoy/geocode_job.rb
+0
-13
| @@ | @@ -1,13 +0,0 @@ |
| - | module Ahoy |
| - | class GeocodeJob < ActiveJob::Base |
| - | queue_as :ahoy |
| - | |
| - | def perform(visit) |
| - | deckhand = Deckhands::LocationDeckhand.new(visit.ip) |
| - | Ahoy::VisitProperties::LOCATION_KEYS.each do |key| |
| - | visit.send(:"#{key}=", deckhand.send(key)) if visit.respond_to?(:"#{key}=") |
| - | end |
| - | visit.save! |
| - | end |
| - | end |
| - | end |
ahoy/logger_silencer.rb b/lib/ahoy/logger_silencer.rb
+0
-75
| @@ | @@ -1,75 +0,0 @@ |
| - | # from https://github.com/rails/activerecord-session_store/blob/master/lib/active_record/session_store/extension/logger_silencer.rb |
| - | require "thread" |
| - | require "active_support/core_ext/class/attribute_accessors" |
| - | require "active_support/core_ext/module/aliasing" |
| - | require "active_support/core_ext/module/attribute_accessors" |
| - | require "active_support/concern" |
| - | |
| - | module Ahoy |
| - | module LoggerSilencer |
| - | extend ActiveSupport::Concern |
| - | |
| - | included do |
| - | cattr_accessor :silencer |
| - | self.silencer = true |
| - | alias_method :level_without_threadsafety, :level |
| - | alias_method :level, :level_with_threadsafety |
| - | alias_method :add_without_threadsafety, :add |
| - | alias_method :add, :add_with_threadsafety |
| - | end |
| - | |
| - | def thread_level |
| - | Thread.current[thread_hash_level_key] |
| - | end |
| - | |
| - | def thread_level=(level) |
| - | Thread.current[thread_hash_level_key] = level |
| - | end |
| - | |
| - | def level_with_threadsafety |
| - | thread_level || level_without_threadsafety |
| - | end |
| - | |
| - | def add_with_threadsafety(severity, message = nil, progname = nil, &block) |
| - | if (defined?(@logdev) && @logdev.nil?) || (severity || UNKNOWN) < level |
| - | true |
| - | else |
| - | add_without_threadsafety(severity, message, progname, &block) |
| - | end |
| - | end |
| - | |
| - | # Silences the logger for the duration of the block. |
| - | def silence_logger(temporary_level = Logger::ERROR) |
| - | if silencer |
| - | begin |
| - | self.thread_level = temporary_level |
| - | yield self |
| - | ensure |
| - | self.thread_level = nil |
| - | end |
| - | else |
| - | yield self |
| - | end |
| - | end |
| - | |
| - | for severity in Logger::Severity.constants |
| - | class_eval <<-EOT, __FILE__, __LINE__ + 1 |
| - | def #{severity.downcase}? # def debug? |
| - | Logger::#{severity} >= level # DEBUG >= level |
| - | end # end |
| - | EOT |
| - | end |
| - | |
| - | private |
| - | |
| - | def thread_hash_level_key |
| - | @thread_hash_level_key ||= :"ThreadSafeLogger##{object_id}@level" |
| - | end |
| - | end |
| - | end |
| - | |
| - | class NilLogger |
| - | def self.silence_logger |
| - | yield |
| - | end |
| - | end |
ahoy/model.rb b/lib/ahoy/model.rb
+4
-26
| @@ | @@ -1,37 +1,15 @@ |
| module Ahoy | |
| module Model | |
| - | def visitable(name = nil, options = {}) |
| - | if name.is_a?(Hash) |
| - | options = name |
| - | name = nil |
| - | end |
| - | name ||= :visit |
| + | def visitable(name = :visit, **options) |
| class_eval do | |
| - | belongs_to name, options |
| - | before_create :set_visit |
| + | belongs_to(name, optional: true, class_name: "Ahoy::Visit", **options) |
| + | before_create :set_ahoy_visit |
| end | |
| class_eval %{ | |
| - | def set_visit |
| + | def set_ahoy_visit |
| self.#{name} ||= RequestStore.store[:ahoy].try(:visit) | |
| end | |
| } | |
| end | |
| - | |
| - | # deprecated |
| - | |
| - | def ahoy_visit |
| - | class_eval do |
| - | warn "[DEPRECATION] ahoy_visit is deprecated" |
| - | |
| - | belongs_to :user, polymorphic: true |
| - | |
| - | def landing_params |
| - | @landing_params ||= begin |
| - | warn "[DEPRECATION] landing_params is deprecated" |
| - | Deckhands::UtmParameterDeckhand.new(landing_page).landing_params |
| - | end |
| - | end |
| - | end |
| - | end |
| end | |
| end | |
ahoy/properties.rb b/lib/ahoy/properties.rb
+0
-60
| @@ | @@ -1,60 +0,0 @@ |
| - | module Ahoy |
| - | module Properties |
| - | extend ActiveSupport::Concern |
| - | |
| - | module ClassMethods |
| - | def where_properties(properties) |
| - | relation = self |
| - | column_type = columns_hash["properties"].type |
| - | adapter_name = connection.adapter_name.downcase |
| - | case adapter_name |
| - | when /mysql/ |
| - | if column_type == :json |
| - | properties.each do |k, v| |
| - | if v.nil? |
| - | v = "null" |
| - | elsif v == true |
| - | v = "true" |
| - | end |
| - | |
| - | relation = relation.where("JSON_UNQUOTE(properties -> ?) = ?", "$.#{k.to_s}", v.as_json) |
| - | end |
| - | else |
| - | properties.each do |k, v| |
| - | relation = relation.where("properties REGEXP ?", "[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]") |
| - | end |
| - | end |
| - | when /postgres|postgis/ |
| - | if column_type == :jsonb |
| - | relation = relation.where("properties @> ?", properties.to_json) |
| - | elsif column_type == :json |
| - | properties.each do |k, v| |
| - | relation = |
| - | if v.nil? |
| - | relation.where("properties ->> ? IS NULL", k.to_s) |
| - | else |
| - | relation.where("properties ->> ? = ?", k.to_s, v.as_json.to_s) |
| - | end |
| - | end |
| - | elsif column_type == :hstore |
| - | properties.each do |k, v| |
| - | relation = |
| - | if v.nil? |
| - | relation.where("properties -> ? IS NULL", k.to_s) |
| - | else |
| - | relation.where("properties -> ? = ?", k.to_s, v.to_s) |
| - | end |
| - | end |
| - | else |
| - | properties.each do |k, v| |
| - | relation = relation.where("properties SIMILAR TO ?", "%[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]%") |
| - | end |
| - | end |
| - | else |
| - | raise "Adapter not supported: #{adapter_name}" |
| - | end |
| - | relation |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/query_methods.rb b/lib/ahoy/query_methods.rb
+74
-0
| @@ | @@ -0,0 +1,74 @@ |
| + | module Ahoy |
| + | module QueryMethods |
| + | extend ActiveSupport::Concern |
| + | |
| + | module ClassMethods |
| + | def where_event(name, properties = {}) |
| + | where(name: name).where_props(properties) |
| + | end |
| + | |
| + | def where_props(properties) |
| + | relation = self |
| + | if respond_to?(:columns_hash) |
| + | column_type = columns_hash["properties"].type |
| + | adapter_name = connection.adapter_name.downcase |
| + | else |
| + | adapter_name = "mongoid" |
| + | end |
| + | case adapter_name |
| + | when "mongoid" |
| + | relation = where(Hash[properties.map { |k, v| ["properties.#{k}", v] }]) |
| + | when /mysql/ |
| + | if column_type == :json |
| + | properties.each do |k, v| |
| + | if v.nil? |
| + | v = "null" |
| + | elsif v == true |
| + | v = "true" |
| + | end |
| + | |
| + | relation = relation.where("JSON_UNQUOTE(properties -> ?) = ?", "$.#{k.to_s}", v.as_json) |
| + | end |
| + | else |
| + | properties.each do |k, v| |
| + | relation = relation.where("properties REGEXP ?", "[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]") |
| + | end |
| + | end |
| + | when /postgres|postgis/ |
| + | if column_type == :jsonb |
| + | relation = relation.where("properties @> ?", properties.to_json) |
| + | elsif column_type == :json |
| + | properties.each do |k, v| |
| + | relation = |
| + | if v.nil? |
| + | relation.where("properties ->> ? IS NULL", k.to_s) |
| + | else |
| + | relation.where("properties ->> ? = ?", k.to_s, v.as_json.to_s) |
| + | end |
| + | end |
| + | elsif column_type == :hstore |
| + | properties.each do |k, v| |
| + | relation = |
| + | if v.nil? |
| + | relation.where("properties -> ? IS NULL", k.to_s) |
| + | else |
| + | relation.where("properties -> ? = ?", k.to_s, v.to_s) |
| + | end |
| + | end |
| + | else |
| + | properties.each do |k, v| |
| + | relation = relation.where("properties SIMILAR TO ?", "%[{,]#{{k.to_s => v}.to_json.sub(/\A\{/, "").sub(/\}\z/, "").gsub("+", "\\\\+")}[,}]%") |
| + | end |
| + | end |
| + | else |
| + | raise "Adapter not supported: #{adapter_name}" |
| + | end |
| + | relation |
| + | end |
| + | alias_method :where_properties, :where_props |
| + | end |
| + | end |
| + | end |
| + | |
| + | # backward compatibility |
| + | Ahoy::Properties = Ahoy::QueryMethods |
ahoy/stores/active_record_store.rb b/lib/ahoy/stores/active_record_store.rb
+0
-61
| @@ | @@ -1,61 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class ActiveRecordStore < BaseStore |
| - | def track_visit(options, &block) |
| - | @visit = |
| - | visit_model.new do |v| |
| - | v.id = ahoy.visit_id |
| - | v.visitor_id = ahoy.visitor_id |
| - | v.user = user if v.respond_to?(:user=) |
| - | v.started_at = options[:started_at] |
| - | end |
| - | |
| - | set_visit_properties(visit) |
| - | |
| - | yield(visit) if block_given? |
| - | |
| - | begin |
| - | visit.save! |
| - | geocode(visit) |
| - | rescue *unique_exception_classes |
| - | # reset to nil so subsequent calls to track_event will load visit from DB |
| - | @visit = nil |
| - | end |
| - | end |
| - | |
| - | def track_event(name, properties, options, &block) |
| - | event = |
| - | event_model.new do |e| |
| - | e.id = options[:id] |
| - | e.visit_id = ahoy.visit_id |
| - | e.user = user if e.respond_to?(:user=) |
| - | e.name = name |
| - | e.properties = properties |
| - | e.time = options[:time] |
| - | end |
| - | |
| - | yield(event) if block_given? |
| - | |
| - | begin |
| - | event.save! |
| - | rescue *unique_exception_classes |
| - | # do nothing |
| - | end |
| - | end |
| - | |
| - | def visit |
| - | @visit ||= visit_model.where(id: ahoy.visit_id).first if ahoy.visit_id |
| - | end |
| - | |
| - | protected |
| - | |
| - | def visit_model |
| - | ::Visit |
| - | end |
| - | |
| - | def event_model |
| - | ::Ahoy::Event |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/active_record_token_store.rb b/lib/ahoy/stores/active_record_token_store.rb
+0
-114
| @@ | @@ -1,114 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class ActiveRecordTokenStore < BaseStore |
| - | def track_visit(options, &block) |
| - | @visit = |
| - | visit_model.new do |v| |
| - | v.visit_token = ahoy.visit_token |
| - | v.visitor_token = ahoy.visitor_token |
| - | v.user = user if v.respond_to?(:user=) |
| - | v.started_at = options[:started_at] if v.respond_to?(:started_at) |
| - | v.created_at = options[:started_at] if v.respond_to?(:created_at) |
| - | end |
| - | |
| - | set_visit_properties(visit) |
| - | |
| - | yield(visit) if block_given? |
| - | |
| - | begin |
| - | visit.save! |
| - | geocode(visit) |
| - | rescue *unique_exception_classes |
| - | # reset to nil so subsequent calls to track_event will load visit from DB |
| - | @visit = nil |
| - | end |
| - | end |
| - | |
| - | def track_event(name, properties, options, &block) |
| - | if self.class.uses_deprecated_subscribers? |
| - | options[:controller] ||= controller |
| - | options[:user] ||= user |
| - | options[:visit] ||= visit |
| - | options[:visit_token] ||= ahoy.visit_token |
| - | options[:visitor_token] ||= ahoy.visitor_token |
| - | |
| - | subscribers = Ahoy.subscribers |
| - | if subscribers.any? |
| - | subscribers.each do |subscriber| |
| - | subscriber.track(name, properties, options.dup) |
| - | end |
| - | else |
| - | $stderr.puts "No subscribers" |
| - | end |
| - | else |
| - | event = |
| - | event_model.new do |e| |
| - | e.visit_id = visit.try(:id) |
| - | e.user = user if e.respond_to?(:user=) |
| - | e.name = name |
| - | e.properties = properties |
| - | e.time = options[:time] |
| - | end |
| - | |
| - | yield(event) if block_given? |
| - | |
| - | event.save! |
| - | end |
| - | end |
| - | |
| - | def visit |
| - | @visit ||= (visit_model.where(visit_token: ahoy.visit_token).first if ahoy.visit_token) |
| - | end |
| - | |
| - | def exclude? |
| - | (!Ahoy.track_bots && bot?) || |
| - | ( |
| - | if Ahoy.exclude_method |
| - | warn "[DEPRECATION] Ahoy.exclude_method is deprecated - use exclude? instead" |
| - | if Ahoy.exclude_method.arity == 1 |
| - | Ahoy.exclude_method.call(controller) |
| - | else |
| - | Ahoy.exclude_method.call(controller, request) |
| - | end |
| - | else |
| - | false |
| - | end |
| - | ) |
| - | end |
| - | |
| - | def user |
| - | @user ||= begin |
| - | user_method = Ahoy.user_method |
| - | if user_method.respond_to?(:call) |
| - | user_method.call(controller) |
| - | elsif user_method |
| - | controller.send(user_method) |
| - | else |
| - | super |
| - | end |
| - | end |
| - | end |
| - | |
| - | class << self |
| - | def uses_deprecated_subscribers |
| - | warn "[DEPRECATION] Ahoy subscribers are deprecated" |
| - | @uses_deprecated_subscribers = true |
| - | end |
| - | |
| - | def uses_deprecated_subscribers? |
| - | @uses_deprecated_subscribers || false |
| - | end |
| - | end |
| - | |
| - | protected |
| - | |
| - | def visit_model |
| - | Ahoy.visit_model || ::Visit |
| - | end |
| - | |
| - | def event_model |
| - | ::Ahoy::Event |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/base_store.rb b/lib/ahoy/stores/base_store.rb
+0
-88
| @@ | @@ -1,88 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class BaseStore |
| - | def initialize(options) |
| - | @options = options |
| - | end |
| - | |
| - | def track_visit(options) |
| - | end |
| - | |
| - | def track_event(name, properties, options) |
| - | end |
| - | |
| - | def visit |
| - | end |
| - | |
| - | def authenticate(user) |
| - | @user = user |
| - | if visit && visit.respond_to?(:user) && !visit.user |
| - | begin |
| - | visit.user = user |
| - | visit.save! |
| - | rescue ActiveRecord::AssociationTypeMismatch |
| - | # do nothing |
| - | end |
| - | end |
| - | end |
| - | |
| - | def report_exception(e) |
| - | raise e |
| - | end |
| - | |
| - | def user |
| - | @user ||= (controller.respond_to?(:current_user) && controller.current_user) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil |
| - | end |
| - | |
| - | def exclude? |
| - | bot? |
| - | end |
| - | |
| - | def generate_id |
| - | SecureRandom.uuid |
| - | end |
| - | |
| - | protected |
| - | |
| - | def bot? |
| - | @bot ||= request ? Browser.new(request.user_agent).bot? : false |
| - | end |
| - | |
| - | def request |
| - | @request ||= @options[:request] || controller.try(:request) |
| - | end |
| - | |
| - | def controller |
| - | @controller ||= @options[:controller] |
| - | end |
| - | |
| - | def ahoy |
| - | @ahoy ||= @options[:ahoy] |
| - | end |
| - | |
| - | def visit_properties |
| - | ahoy.visit_properties |
| - | end |
| - | |
| - | def set_visit_properties(visit) |
| - | keys = visit_properties.keys |
| - | keys.each do |key| |
| - | visit.send(:"#{key}=", visit_properties[key]) if visit.respond_to?(:"#{key}=") && visit_properties[key] |
| - | end |
| - | end |
| - | |
| - | def geocode(visit) |
| - | if Ahoy.geocode == :async |
| - | Ahoy::GeocodeJob.set(queue: Ahoy.job_queue).perform_later(visit) |
| - | end |
| - | end |
| - | |
| - | def unique_exception_classes |
| - | classes = [] |
| - | classes << ActiveRecord::RecordNotUnique if defined?(ActiveRecord::RecordNotUnique) |
| - | classes << PG::UniqueViolation if defined?(PG::UniqueViolation) |
| - | classes |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/bunny_store.rb b/lib/ahoy/stores/bunny_store.rb
+0
-33
| @@ | @@ -1,33 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class BunnyStore < LogStore |
| - | def log_visit(data) |
| - | post(visits_queue, data) |
| - | end |
| - | |
| - | def log_event(data) |
| - | post(events_queue, data) |
| - | end |
| - | |
| - | def channel |
| - | @channel ||= begin |
| - | conn = Bunny.new |
| - | conn.start |
| - | conn.create_channel |
| - | end |
| - | end |
| - | |
| - | def post(queue, message) |
| - | channel.queue(queue, durable: true).publish(message.to_json) |
| - | end |
| - | |
| - | def visits_queue |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_queue |
| - | "ahoy_events" |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/fluentd_store.rb b/lib/ahoy/stores/fluentd_store.rb
+0
-17
| @@ | @@ -1,17 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class FluentdStore < LogStore |
| - | def log_visit(data) |
| - | logger.post("visit", data) |
| - | end |
| - | |
| - | def log_event(data) |
| - | logger.post("event", data) |
| - | end |
| - | |
| - | def logger |
| - | @logger ||= Fluent::Logger::FluentLogger.new("ahoy", host: ENV["FLUENTD_HOST"] || "localhost", port: ENV["FLUENTD_PORT"] || 24224) |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/kafka_store.rb b/lib/ahoy/stores/kafka_store.rb
+0
-42
| @@ | @@ -1,42 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class KafkaStore < LogStore |
| - | def log_visit(data) |
| - | post(visits_topic, data) |
| - | end |
| - | |
| - | def log_event(data) |
| - | post(events_topic, data) |
| - | end |
| - | |
| - | def client |
| - | @client ||= begin |
| - | Kafka.new( |
| - | seed_brokers: ENV["KAFKA_URL"] || "localhost:9092", |
| - | logger: Rails.logger |
| - | ) |
| - | end |
| - | end |
| - | |
| - | def producer |
| - | @producer ||= begin |
| - | producer = client.async_producer(delivery_interval: 3) |
| - | at_exit { producer.shutdown } |
| - | producer |
| - | end |
| - | end |
| - | |
| - | def post(topic, data) |
| - | producer.produce(data.to_json, topic: topic) |
| - | end |
| - | |
| - | def visits_topic |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_topic |
| - | "ahoy_events" |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/kinesis_firehose_store.rb b/lib/ahoy/stores/kinesis_firehose_store.rb
+0
-42
| @@ | @@ -1,42 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class KinesisFirehoseStore < LogStore |
| - | def log_visit(data) |
| - | post(visits_stream, data) |
| - | end |
| - | |
| - | def log_event(data) |
| - | post(events_stream, data) |
| - | end |
| - | |
| - | def client |
| - | @client ||= Aws::Firehose::Client.new(credentials) |
| - | end |
| - | |
| - | def post(stream, data) |
| - | client.put_record( |
| - | delivery_stream_name: stream, |
| - | record: { |
| - | data: "#{data.to_json}\n" |
| - | } |
| - | ) |
| - | end |
| - | |
| - | def credentials |
| - | { |
| - | access_key_id: ENV["AWS_ACCESS_KEY_ID"], |
| - | secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"], |
| - | region: "us-east-1" |
| - | } |
| - | end |
| - | |
| - | def visits_stream |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_stream |
| - | "ahoy_events" |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/log_store.rb b/lib/ahoy/stores/log_store.rb
+0
-53
| @@ | @@ -1,53 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class LogStore < BaseStore |
| - | def track_visit(options, &block) |
| - | data = { |
| - | id: ahoy.visit_id, |
| - | visitor_id: ahoy.visitor_id |
| - | }.merge(visit_properties.to_hash) |
| - | data[:user_id] = user.id if user |
| - | data[:started_at] = options[:started_at] |
| - | |
| - | yield(data) if block_given? |
| - | |
| - | log_visit(data) |
| - | end |
| - | |
| - | def track_event(name, properties, options, &block) |
| - | data = { |
| - | id: options[:id], |
| - | name: name, |
| - | properties: properties, |
| - | visit_id: ahoy.visit_id, |
| - | visitor_id: ahoy.visitor_id |
| - | } |
| - | data[:user_id] = user.id if user |
| - | data[:time] = options[:time] |
| - | |
| - | yield(data) if block_given? |
| - | |
| - | log_event(data) |
| - | end |
| - | |
| - | protected |
| - | |
| - | def log_visit(data) |
| - | visit_logger.info data.to_json |
| - | end |
| - | |
| - | def log_event(data) |
| - | event_logger.info data.to_json |
| - | end |
| - | |
| - | # TODO disable header |
| - | def visit_logger |
| - | @visit_logger ||= ActiveSupport::Logger.new(Rails.root.join("log/visits.log")) |
| - | end |
| - | |
| - | def event_logger |
| - | @event_logger ||= ActiveSupport::Logger.new(Rails.root.join("log/events.log")) |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/mongoid_store.rb b/lib/ahoy/stores/mongoid_store.rb
+0
-63
| @@ | @@ -1,63 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class MongoidStore < BaseStore |
| - | def track_visit(options, &block) |
| - | @visit = |
| - | visit_model.new do |v| |
| - | v.id = binary(ahoy.visit_id) |
| - | v.visitor_id = binary(ahoy.visitor_id) |
| - | v.user = user if v.respond_to?(:user=) && user |
| - | v.started_at = options[:started_at] |
| - | end |
| - | |
| - | set_visit_properties(visit) |
| - | |
| - | yield(visit) if block_given? |
| - | |
| - | visit.upsert |
| - | geocode(visit) |
| - | end |
| - | |
| - | def track_event(name, properties, options, &block) |
| - | event = |
| - | event_model.new do |e| |
| - | e.id = binary(options[:id]) |
| - | e.visit_id = binary(ahoy.visit_id) |
| - | e.user = user if e.respond_to?(:user) |
| - | e.name = name |
| - | e.properties = properties |
| - | e.time = options[:time] |
| - | end |
| - | |
| - | yield(event) if block_given? |
| - | |
| - | event.upsert |
| - | end |
| - | |
| - | def visit |
| - | @visit ||= visit_model.where(_id: binary(ahoy.visit_id)).first if ahoy.visit_id |
| - | end |
| - | |
| - | protected |
| - | |
| - | def visit_model |
| - | ::Visit |
| - | end |
| - | |
| - | def event_model |
| - | ::Ahoy::Event |
| - | end |
| - | |
| - | def binary(token) |
| - | token = token.delete("-") |
| - | if defined?(::BSON) |
| - | ::BSON::Binary.new(token, :uuid) |
| - | elsif defined?(::Moped::BSON) |
| - | ::Moped::BSON::Binary.new(:uuid, token) |
| - | else |
| - | token |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/nats_store.rb b/lib/ahoy/stores/nats_store.rb
+0
-34
| @@ | @@ -1,34 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class NatsStore < LogStore |
| - | def log_visit(data) |
| - | publish(visits_subject, data) |
| - | end |
| - | |
| - | def log_event(data) |
| - | publish(events_subject, data) |
| - | end |
| - | |
| - | def publish(subject, data) |
| - | client.publish(subject, data.to_json) |
| - | end |
| - | |
| - | def client |
| - | @client ||= begin |
| - | require "nats/io/client" |
| - | client = NATS::IO::Client.new |
| - | client.connect(servers: (ENV["NATS_URL"] || "nats://127.0.0.1:4222").split(",")) |
| - | client |
| - | end |
| - | end |
| - | |
| - | def visits_subject |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_subject |
| - | "ahoy_events" |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/stores/nsq_store.rb b/lib/ahoy/stores/nsq_store.rb
+0
-36
| @@ | @@ -1,36 +0,0 @@ |
| - | module Ahoy |
| - | module Stores |
| - | class NsqStore < LogStore |
| - | def log_visit(data) |
| - | post(visits_topic, data) |
| - | end |
| - | |
| - | def log_event(data) |
| - | post(events_topic, data) |
| - | end |
| - | |
| - | def client |
| - | @client ||= begin |
| - | require "nsq" |
| - | client = Nsq::Producer.new( |
| - | nsqd: ENV["NSQ_URL"] || "127.0.0.1:4150" |
| - | ) |
| - | at_exit { client.terminate } |
| - | client |
| - | end |
| - | end |
| - | |
| - | def post(topic, data) |
| - | client.write_to_topic(topic, data.to_json) |
| - | end |
| - | |
| - | def visits_topic |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_topic |
| - | "ahoy_events" |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/subscribers/active_record.rb b/lib/ahoy/subscribers/active_record.rb
+0
-19
| @@ | @@ -1,19 +0,0 @@ |
| - | module Ahoy |
| - | module Subscribers |
| - | class ActiveRecord |
| - | def initialize(options = {}) |
| - | @model = options[:model] || Ahoy::Event |
| - | end |
| - | |
| - | def track(name, properties, options = {}) |
| - | @model.create! do |e| |
| - | e.visit = options[:visit] |
| - | e.user = options[:user] |
| - | e.name = name |
| - | e.properties = properties |
| - | e.time = options[:time] |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
ahoy/throttle.rb b/lib/ahoy/throttle.rb
+0
-17
| @@ | @@ -1,17 +0,0 @@ |
| - | require "rack/attack" |
| - | |
| - | module Ahoy |
| - | class Throttle < Rack::Attack |
| - | throttle("ahoy/ip", limit: Ahoy.throttle_limit, period: Ahoy.throttle_period) do |req| |
| - | if req.path.start_with?("/ahoy/") |
| - | req.ip |
| - | end |
| - | end |
| - | |
| - | def_delegators self, :whitelisted?, :blacklisted?, :throttled?, :tracked?, :blocklisted?, :safelisted? |
| - | |
| - | def self.throttled_response |
| - | Rack::Attack.throttled_response |
| - | end |
| - | end |
| - | end |
ahoy/tracker.rb b/lib/ahoy/tracker.rb
+60
-38
| @@ | @@ -2,45 +2,56 @@ module Ahoy |
| class Tracker | |
| attr_reader :request, :controller | |
| - | def initialize(options = {}) |
| + | def initialize(**options) |
| @store = Ahoy::Store.new(options.merge(ahoy: self)) | |
| @controller = options[:controller] | |
| @request = options[:request] || @controller.try(:request) | |
| + | @visit_token = options[:visit_token] |
| @options = options | |
| end | |
| + | # can't use keyword arguments here |
| def track(name, properties = {}, options = {}) | |
| if exclude? | |
| debug "Event excluded" | |
| elsif missing_params? | |
| debug "Missing required parameters" | |
| else | |
| - | options = options.dup |
| - | |
| - | options[:time] = trusted_time(options[:time]) |
| - | options[:id] = ensure_uuid(options[:id] || generate_id) |
| - | |
| - | @store.track_event(name, properties, options) |
| + | data = { |
| + | visit_token: visit_token, |
| + | user_id: user.try(:id), |
| + | name: name.to_s, |
| + | properties: properties, |
| + | time: trusted_time(options[:time]), |
| + | event_id: options[:id] || generate_id |
| + | }.select { |_, v| v } |
| + | |
| + | @store.track_event(data) |
| end | |
| true | |
| rescue => e | |
| report_exception(e) | |
| end | |
| - | def track_visit(options = {}) |
| + | def track_visit(defer: false) |
| if exclude? | |
| debug "Visit excluded" | |
| elsif missing_params? | |
| debug "Missing required parameters" | |
| else | |
| - | if options[:defer] |
| + | if defer |
| set_cookie("ahoy_track", true, nil, false) | |
| else | |
| - | options = options.dup |
| + | data = { |
| + | visit_token: visit_token, |
| + | visitor_token: visitor_token, |
| + | user_id: user.try(:id), |
| + | started_at: trusted_time, |
| + | }.merge(visit_properties).select { |_, v| v } |
| - | options[:started_at] ||= Time.zone.now |
| + | @store.track_visit(data) |
| - | @store.track_visit(options) |
| + | Ahoy::GeocodeV2Job.perform_later(visit_token, data[:ip]) if Ahoy.geocode |
| end | |
| end | |
| true | |
| @@ | @@ -48,11 +59,28 @@ module Ahoy |
| report_exception(e) | |
| end | |
| + | def geocode(data) |
| + | if exclude? |
| + | debug "Geocode excluded" |
| + | else |
| + | @store.geocode(data.select { |_, v| v }) |
| + | true |
| + | end |
| + | rescue => e |
| + | report_exception(e) |
| + | end |
| + | |
| def authenticate(user) | |
| if exclude? | |
| debug "Authentication excluded" | |
| else | |
| - | @store.authenticate(user) |
| + | @store.user = user |
| + | |
| + | data = { |
| + | visit_token: visit_token, |
| + | user_id: user.try(:id) |
| + | } |
| + | @store.authenticate(data) |
| end | |
| true | |
| rescue => e | |
| @@ | @@ -63,14 +91,6 @@ module Ahoy |
| @visit ||= @store.visit | |
| end | |
| - | def visit_id |
| - | @visit_id ||= ensure_uuid(visit_token_helper) |
| - | end |
| - | |
| - | def visitor_id |
| - | @visitor_id ||= ensure_uuid(visitor_token_helper) |
| - | end |
| - | |
| def new_visit? | |
| !existing_visit_token | |
| end | |
| @@ | @@ -80,12 +100,12 @@ module Ahoy |
| end | |
| def set_visit_cookie | |
| - | set_cookie("ahoy_visit", visit_id, Ahoy.visit_duration) |
| + | set_cookie("ahoy_visit", visit_token, Ahoy.visit_duration) |
| end | |
| def set_visitor_cookie | |
| if new_visitor? | |
| - | set_cookie("ahoy_visitor", visitor_id, Ahoy.visitor_duration) |
| + | set_cookie("ahoy_visitor", visitor_token, Ahoy.visitor_duration) |
| end | |
| end | |
| @@ | @@ -95,16 +115,29 @@ module Ahoy |
| # TODO better name | |
| def visit_properties | |
| - | @visit_properties ||= Ahoy::VisitProperties.new(request, api: api?) |
| + | @visit_properties ||= Ahoy::VisitProperties.new(request, api: api?).generate |
| end | |
| def visit_token | |
| @visit_token ||= ensure_token(visit_token_helper) | |
| end | |
| + | alias_method :visit_id, :visit_token |
| def visitor_token | |
| @visitor_token ||= ensure_token(visitor_token_helper) | |
| end | |
| + | alias_method :visitor_id, :visitor_token |
| + | |
| + | def reset |
| + | reset_visit |
| + | request.cookie_jar.delete("ahoy_visitor") |
| + | end |
| + | |
| + | def reset_visit |
| + | request.cookie_jar.delete("ahoy_visit") |
| + | request.cookie_jar.delete("ahoy_events") |
| + | request.cookie_jar.delete("ahoy_track") |
| + | end |
| protected | |
| @@ | @@ -125,12 +158,12 @@ module Ahoy |
| value: value | |
| } | |
| cookie[:expires] = duration.from_now if duration | |
| - | domain = Ahoy.cookie_domain || Ahoy.domain |
| + | domain = Ahoy.cookie_domain |
| cookie[:domain] = domain if domain && use_domain | |
| request.cookie_jar[name] = cookie | |
| end | |
| - | def trusted_time(time) |
| + | def trusted_time(time = nil) |
| if !time || (api? && !(1.minute.ago..Time.now).cover?(time)) | |
| Time.zone.now | |
| else | |
| @@ | @@ -142,15 +175,8 @@ module Ahoy |
| @store.exclude? | |
| end | |
| - | # odd pattern for backwards compatibility |
| - | # TODO remove this method in next major release |
| def report_exception(e) | |
| - | Safely.safely do |
| - | @store.report_exception(e) |
| - | if Rails.env.development? || Rails.env.test? |
| - | raise e |
| - | end |
| - | end |
| + | Safely.report_exception(e) |
| end | |
| def generate_id | |
| @@ | @@ -215,10 +241,6 @@ module Ahoy |
| @visitor_param ||= request && request.params["visitor_token"] | |
| end | |
| - | def ensure_uuid(id) |
| - | Ahoy.ensure_uuid(id) if id |
| - | end |
| - | |
| def ensure_token(token) | |
| token.to_s.gsub(/[^a-z0-9\-]/i, "").first(64) if token | |
| end | |
ahoy/visit_properties.rb b/lib/ahoy/visit_properties.rb
+65
-39
| @@ | @@ -1,60 +1,86 @@ |
| + | require "browser" |
| + | require "referer-parser" |
| + | require "user_agent_parser" |
| + | |
| module Ahoy | |
| class VisitProperties | |
| - | REQUEST_KEYS = [:ip, :user_agent, :referrer, :landing_page, :platform, :app_version, :os_version, :screen_height, :screen_width] |
| - | TRAFFIC_SOURCE_KEYS = [:referring_domain, :search_keyword] |
| - | UTM_PARAMETER_KEYS = [:utm_source, :utm_medium, :utm_term, :utm_content, :utm_campaign] |
| - | TECHNOLOGY_KEYS = [:browser, :os, :device_type] |
| - | LOCATION_KEYS = [:country, :region, :city, :postal_code, :latitude, :longitude] |
| - | |
| - | KEYS = REQUEST_KEYS + TRAFFIC_SOURCE_KEYS + UTM_PARAMETER_KEYS + TECHNOLOGY_KEYS + LOCATION_KEYS |
| - | |
| - | delegate(*REQUEST_KEYS, to: :request_deckhand) |
| - | delegate(*TRAFFIC_SOURCE_KEYS, to: :traffic_source_deckhand) |
| - | delegate(*(UTM_PARAMETER_KEYS + [:landing_params]), to: :utm_parameter_deckhand) |
| - | delegate(*TECHNOLOGY_KEYS, to: :technology_deckhand) |
| - | delegate(*LOCATION_KEYS, to: :location_deckhand) |
| + | attr_reader :request, :params, :referrer, :landing_page |
| - | def initialize(request, options = {}) |
| + | def initialize(request, api:) |
| @request = request | |
| - | @options = options |
| + | @params = request.params |
| + | @referrer = api ? params["referrer"] : request.referer |
| + | @landing_page = api ? params["landing_page"] : request.original_url |
| end | |
| - | def [](key) |
| - | send(key) |
| + | def generate |
| + | @generate ||= request_properties.merge(tech_properties).merge(traffic_properties).merge(utm_properties) |
| end | |
| - | def keys |
| - | if Ahoy.geocode == true # no location keys for :async |
| - | KEYS |
| - | else |
| - | KEYS - LOCATION_KEYS |
| - | end |
| - | end |
| + | private |
| - | def to_hash |
| - | keys.inject({}) { |memo, key| memo[key] = send(key); memo } |
| + | def utm_properties |
| + | landing_uri = Addressable::URI.parse(landing_page) rescue nil |
| + | landing_params = (landing_uri && landing_uri.query_values) || {} |
| + | |
| + | props = {} |
| + | %w(utm_source utm_medium utm_term utm_content utm_campaign).each do |name| |
| + | props[name.to_sym] = params[name] || landing_params[name] |
| + | end |
| + | props |
| end | |
| - | protected |
| + | def traffic_properties |
| + | # cache for performance |
| + | @@referrer_parser ||= RefererParser::Parser.new |
| - | def request_deckhand |
| - | @request_deckhand ||= Deckhands::RequestDeckhand.new(@request, @options) |
| + | { |
| + | referring_domain: (Addressable::URI.parse(referrer).host.first(255) rescue nil), |
| + | search_keyword: (@@referrer_parser.parse(@referrer)[:term][0..255] rescue nil).presence |
| + | } |
| end | |
| - | def traffic_source_deckhand |
| - | @traffic_source_deckhand ||= Deckhands::TrafficSourceDeckhand.new(request_deckhand.referrer) |
| - | end |
| + | def tech_properties |
| + | # cache for performance |
| + | @@user_agent_parser ||= UserAgentParser::Parser.new |
| - | def utm_parameter_deckhand |
| - | @utm_parameter_deckhand ||= Deckhands::UtmParameterDeckhand.new(request_deckhand.landing_page, request_deckhand.params) |
| - | end |
| + | user_agent = request.user_agent |
| + | agent = @@user_agent_parser.parse(user_agent) |
| + | browser = Browser.new(user_agent) |
| + | device_type = |
| + | if browser.bot? |
| + | "Bot" |
| + | elsif browser.device.tv? |
| + | "TV" |
| + | elsif browser.device.console? |
| + | "Console" |
| + | elsif browser.device.tablet? |
| + | "Tablet" |
| + | elsif browser.device.mobile? |
| + | "Mobile" |
| + | else |
| + | "Desktop" |
| + | end |
| - | def technology_deckhand |
| - | @technology_deckhand ||= Deckhands::TechnologyDeckhand.new(request_deckhand.user_agent) |
| + | { |
| + | browser: agent.name, |
| + | os: agent.os.name, |
| + | device_type: device_type, |
| + | } |
| end | |
| - | def location_deckhand |
| - | @location_deckhand ||= Deckhands::LocationDeckhand.new(request_deckhand.ip) |
| + | def request_properties |
| + | { |
| + | ip: request.remote_ip, |
| + | user_agent: request.user_agent, |
| + | referrer: referrer, |
| + | landing_page: landing_page, |
| + | platform: params["platform"], |
| + | app_version: params["app_version"], |
| + | os_version: params["os_version"], |
| + | screen_height: params["screen_height"], |
| + | screen_width: params["screen_width"] |
| + | } |
| end | |
| end | |
| end | |
generators/ahoy/activerecord_generator.rb b/lib/generators/ahoy/activerecord_generator.rb
+58
-0
| @@ | @@ -0,0 +1,58 @@ |
| + | # taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb |
| + | require "rails/generators" |
| + | require "rails/generators/migration" |
| + | require "active_record" |
| + | require "rails/generators/active_record" |
| + | |
| + | module Ahoy |
| + | module Generators |
| + | class ActiverecordGenerator < Rails::Generators::Base |
| + | include Rails::Generators::Migration |
| + | source_root File.expand_path("../templates", __FILE__) |
| + | |
| + | class_option :database, type: :string, aliases: "-d" |
| + | |
| + | # Implement the required interface for Rails::Generators::Migration. |
| + | def self.next_migration_number(dirname) #:nodoc: |
| + | next_migration_number = current_migration_number(dirname) + 1 |
| + | if ::ActiveRecord::Base.timestamped_migrations |
| + | [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max |
| + | else |
| + | "%.3d" % next_migration_number |
| + | end |
| + | end |
| + | |
| + | def copy_templates |
| + | template "database_store_initializer.rb", "config/initializers/ahoy.rb" |
| + | template "active_record_visit_model.rb", "app/models/ahoy/visit.rb" |
| + | template "active_record_event_model.rb", "app/models/ahoy/event.rb" |
| + | migration_template "active_record_migration.rb", "db/migrate/create_ahoy_visits_and_events.rb", migration_version: migration_version |
| + | migrate_command = rails5? ? "rails" : "rake" |
| + | puts "\nAlmost set! Last, run:\n\n #{migrate_command} db:migrate" |
| + | end |
| + | |
| + | def properties_type |
| + | # use connection_config instead of connection.adapter |
| + | # so database connection isn't needed |
| + | case ActiveRecord::Base.connection_config[:adapter].to_s |
| + | when /postg/i # postgres, postgis |
| + | "jsonb" |
| + | when /mysql/i |
| + | "json" |
| + | else |
| + | "text" |
| + | end |
| + | end |
| + | |
| + | def rails5? |
| + | Rails::VERSION::MAJOR >= 5 |
| + | end |
| + | |
| + | def migration_version |
| + | if rails5? |
| + | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
generators/ahoy/base_generator.rb b/lib/generators/ahoy/base_generator.rb
+13
-0
| @@ | @@ -0,0 +1,13 @@ |
| + | require "rails/generators" |
| + | |
| + | module Ahoy |
| + | module Generators |
| + | class BaseGenerator < Rails::Generators::Base |
| + | source_root File.expand_path("../templates", __FILE__) |
| + | |
| + | def copy_templates |
| + | template "base_store_initializer.rb", "config/initializers/ahoy.rb" |
| + | end |
| + | end |
| + | end |
| + | end |
generators/ahoy/install_generator.rb b/lib/generators/ahoy/install_generator.rb
+44
-0
| @@ | @@ -0,0 +1,44 @@ |
| + | require "rails/generators" |
| + | |
| + | module Ahoy |
| + | module Generators |
| + | class InstallGenerator < Rails::Generators::Base |
| + | source_root File.expand_path("../templates", __FILE__) |
| + | |
| + | def copy_templates |
| + | activerecord = defined?(ActiveRecord) |
| + | mongoid = defined?(Mongoid) |
| + | |
| + | selection = |
| + | if activerecord && mongoid |
| + | puts <<-MSG |
| + | |
| + | Which data store would you like to use? |
| + | 1. ActiveRecord (default) |
| + | 2. Mongoid |
| + | 3. Neither |
| + | MSG |
| + | |
| + | ask(">") |
| + | elsif activerecord |
| + | "1" |
| + | elsif mongoid |
| + | "2" |
| + | else |
| + | "3" |
| + | end |
| + | |
| + | case selection |
| + | when "", "1" |
| + | invoke "ahoy:activerecord" |
| + | when "2" |
| + | invoke "ahoy:mongoid" |
| + | when "3" |
| + | invoke "ahoy:base" |
| + | else |
| + | abort "Error: must enter a number [1-3]" |
| + | end |
| + | end |
| + | end |
| + | end |
| + | end |
generators/ahoy/mongoid_generator.rb b/lib/generators/ahoy/mongoid_generator.rb
+20
-0
| @@ | @@ -0,0 +1,20 @@ |
| + | require "rails/generators" |
| + | |
| + | module Ahoy |
| + | module Generators |
| + | class MongoidGenerator < Rails::Generators::Base |
| + | source_root File.expand_path("../templates", __FILE__) |
| + | |
| + | def copy_templates |
| + | template "database_store_initializer.rb", "config/initializers/ahoy.rb" |
| + | template "mongoid_visit_model.rb", "app/models/ahoy/visit.rb" |
| + | template "mongoid_event_model.rb", "app/models/ahoy/event.rb" |
| + | puts "\nAlmost set! Last, run:\n\n rake db:mongoid:create_indexes" |
| + | end |
| + | |
| + | def rails5? |
| + | Rails::VERSION::MAJOR >= 5 |
| + | end |
| + | end |
| + | end |
| + | end |
generators/ahoy/stores/active_record_events_generator.rb b/lib/generators/ahoy/stores/active_record_events_generator.rb
+0
-59
| @@ | @@ -1,59 +0,0 @@ |
| - | # taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb |
| - | require "rails/generators" |
| - | require "rails/generators/migration" |
| - | require "active_record" |
| - | require "rails/generators/active_record" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class ActiveRecordEventsGenerator < Rails::Generators::Base |
| - | include Rails::Generators::Migration |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | class_option :database, type: :string, aliases: "-d" |
| - | |
| - | # Implement the required interface for Rails::Generators::Migration. |
| - | def self.next_migration_number(dirname) #:nodoc: |
| - | next_migration_number = current_migration_number(dirname) + 1 |
| - | if ::ActiveRecord::Base.timestamped_migrations |
| - | [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max |
| - | else |
| - | "%.3d" % next_migration_number |
| - | end |
| - | end |
| - | |
| - | def copy_migration |
| - | @database = options["database"] || detect_database |
| - | unless @database.in?([nil, "postgresql", "postgresql-jsonb", "mysql", "sqlite"]) |
| - | raise Thor::Error, "Unknown database option" |
| - | end |
| - | migration_template "active_record_events_migration.rb", "db/migrate/create_ahoy_events.rb", migration_version: migration_version |
| - | end |
| - | |
| - | def generate_model |
| - | template "active_record_event_model.rb", "app/models/ahoy/event.rb" |
| - | end |
| - | |
| - | def create_initializer |
| - | template "active_record_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | |
| - | def detect_database |
| - | postgresql_version = ActiveRecord::Base.connection.send(:postgresql_version) rescue 0 |
| - | if postgresql_version >= 90400 |
| - | "postgresql-jsonb" |
| - | elsif postgresql_version >= 90200 |
| - | "postgresql" |
| - | end |
| - | end |
| - | |
| - | def migration_version |
| - | if ActiveRecord::VERSION::MAJOR >= 5 |
| - | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/active_record_generator.rb b/lib/generators/ahoy/stores/active_record_generator.rb
+0
-16
| @@ | @@ -1,16 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class ActiveRecordGenerator < Rails::Generators::Base |
| - | class_option :database, type: :string, aliases: "-d" |
| - | |
| - | def boom |
| - | invoke "ahoy:stores:active_record_visits", nil, options |
| - | invoke "ahoy:stores:active_record_events", nil, options |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/active_record_visits_generator.rb b/lib/generators/ahoy/stores/active_record_visits_generator.rb
+0
-49
| @@ | @@ -1,49 +0,0 @@ |
| - | # taken from https://github.com/collectiveidea/audited/blob/master/lib/generators/audited/install_generator.rb |
| - | require "rails/generators" |
| - | require "rails/generators/migration" |
| - | require "active_record" |
| - | require "rails/generators/active_record" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class ActiveRecordVisitsGenerator < Rails::Generators::Base |
| - | include Rails::Generators::Migration |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | class_option :database, type: :string, aliases: "-d" |
| - | |
| - | # Implement the required interface for Rails::Generators::Migration. |
| - | def self.next_migration_number(dirname) #:nodoc: |
| - | next_migration_number = current_migration_number(dirname) + 1 |
| - | if ::ActiveRecord::Base.timestamped_migrations |
| - | [Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % next_migration_number].max |
| - | else |
| - | "%.3d" % next_migration_number |
| - | end |
| - | end |
| - | |
| - | def copy_migration |
| - | unless options["database"].in?([nil, "postgresql", "postgresql-jsonb"]) |
| - | raise Thor::Error, "Unknown database option" |
| - | end |
| - | migration_template "active_record_visits_migration.rb", "db/migrate/create_visits.rb", migration_version: migration_version |
| - | end |
| - | |
| - | def generate_model |
| - | template "active_record_visit_model.rb", "app/models/visit.rb" |
| - | end |
| - | |
| - | def create_initializer |
| - | template "active_record_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | |
| - | def migration_version |
| - | if ActiveRecord::VERSION::MAJOR >= 5 |
| - | "[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/bunny_generator.rb b/lib/generators/ahoy/stores/bunny_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class BunnyGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "bunny_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/custom_generator.rb b/lib/generators/ahoy/stores/custom_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class CustomGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "custom_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/fluentd_generator.rb b/lib/generators/ahoy/stores/fluentd_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class FluentdGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "fluentd_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/kafka_generator.rb b/lib/generators/ahoy/stores/kafka_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class KafkaGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "kafka_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/kinesis_firehose_generator.rb b/lib/generators/ahoy/stores/kinesis_firehose_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class KinesisFirehoseGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "kinesis_firehose_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/log_generator.rb b/lib/generators/ahoy/stores/log_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class LogGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "log_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/mongoid_events_generator.rb b/lib/generators/ahoy/stores/mongoid_events_generator.rb
+0
-19
| @@ | @@ -1,19 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class MongoidEventsGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def generate_model |
| - | template "mongoid_event_model.rb", "app/models/ahoy/event.rb" |
| - | end |
| - | |
| - | def create_initializer |
| - | template "mongoid_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/mongoid_generator.rb b/lib/generators/ahoy/stores/mongoid_generator.rb
+0
-14
| @@ | @@ -1,14 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class MongoidGenerator < Rails::Generators::Base |
| - | def boom |
| - | invoke "ahoy:stores:mongoid_visits" |
| - | invoke "ahoy:stores:mongoid_events" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/mongoid_visits_generator.rb b/lib/generators/ahoy/stores/mongoid_visits_generator.rb
+0
-27
| @@ | @@ -1,27 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class MongoidVisitsGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def generate_model |
| - | @visitor_id_type = |
| - | if defined?(::BSON) |
| - | "BSON::Binary" |
| - | elsif defined?(::Moped::BSON) |
| - | "Moped::BSON::Binary" |
| - | else |
| - | "String" |
| - | end |
| - | template "mongoid_visit_model.rb", "app/models/visit.rb" |
| - | end |
| - | |
| - | def create_initializer |
| - | template "mongoid_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/nats_generator.rb b/lib/generators/ahoy/stores/nats_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class NatsGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "nats_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/nsq_generator.rb b/lib/generators/ahoy/stores/nsq_generator.rb
+0
-15
| @@ | @@ -1,15 +0,0 @@ |
| - | require "rails/generators" |
| - | |
| - | module Ahoy |
| - | module Stores |
| - | module Generators |
| - | class NsqGenerator < Rails::Generators::Base |
| - | source_root File.expand_path("../templates", __FILE__) |
| - | |
| - | def create_initializer |
| - | template "nsq_initializer.rb", "config/initializers/ahoy.rb" |
| - | end |
| - | end |
| - | end |
| - | end |
| - | end |
generators/ahoy/stores/templates/active_record_event_model.rb b/lib/generators/ahoy/stores/templates/active_record_event_model.rb
+0
-12
| @@ | @@ -1,12 +0,0 @@ |
| - | module Ahoy |
| - | class Event < ActiveRecord::Base |
| - | include Ahoy::Properties |
| - | |
| - | self.table_name = "ahoy_events" |
| - | |
| - | belongs_to :visit |
| - | belongs_to :user<%= Rails::VERSION::MAJOR >= 5 ? ", optional: true" : nil %><% unless %w(postgresql postgresql-jsonb).include?(@database) %> |
| - | |
| - | serialize :properties, JSON<% end %> |
| - | end |
| - | end |
generators/ahoy/stores/templates/active_record_events_migration.rb b/lib/generators/ahoy/stores/templates/active_record_events_migration.rb
+0
-20
| @@ | @@ -1,20 +0,0 @@ |
| - | class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %> |
| - | def change |
| - | create_table :ahoy_events do |t| |
| - | t.integer :visit_id |
| - | |
| - | # user |
| - | t.integer :user_id |
| - | # add t.string :user_type if polymorphic |
| - | |
| - | t.string :name |
| - | t.<% case @database when "postgresql" %>json<% when "postgresql-jsonb" %>jsonb<% else %>text<% end %> :properties |
| - | t.timestamp :time |
| - | end |
| - | |
| - | add_index :ahoy_events, [:visit_id, :name] |
| - | add_index :ahoy_events, [:user_id, :name] |
| - | add_index :ahoy_events, [:name, :time] |
| - | <% if @database == "postgresql-jsonb" && ActiveRecord::VERSION::MAJOR >= 5 %>add_index :ahoy_events, 'properties jsonb_path_ops', using: 'gin'<% end %> |
| - | end |
| - | end |
generators/ahoy/stores/templates/active_record_initializer.rb b/lib/generators/ahoy/stores/templates/active_record_initializer.rb
+0
-3
| @@ | @@ -1,3 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::ActiveRecordTokenStore |
| - | # customize here |
| - | end |
generators/ahoy/stores/templates/active_record_visit_model.rb b/lib/generators/ahoy/stores/templates/active_record_visit_model.rb
+0
-4
| @@ | @@ -1,4 +0,0 @@ |
| - | class Visit < ActiveRecord::Base |
| - | has_many :ahoy_events, class_name: "Ahoy::Event" |
| - | belongs_to :user<%= Rails::VERSION::MAJOR >= 5 ? ", optional: true" : nil %> |
| - | end |
generators/ahoy/stores/templates/active_record_visits_migration.rb b/lib/generators/ahoy/stores/templates/active_record_visits_migration.rb
+0
-57
| @@ | @@ -1,57 +0,0 @@ |
| - | class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %> |
| - | def change |
| - | create_table :visits do |t| |
| - | t.string :visit_token |
| - | t.string :visitor_token |
| - | |
| - | # the rest are recommended but optional |
| - | # simply remove the columns you don't want |
| - | |
| - | # standard |
| - | t.string :ip |
| - | t.text :user_agent |
| - | t.text :referrer |
| - | t.text :landing_page |
| - | |
| - | # user |
| - | t.integer :user_id |
| - | # add t.string :user_type if polymorphic |
| - | |
| - | # traffic source |
| - | t.string :referring_domain |
| - | t.string :search_keyword |
| - | |
| - | # technology |
| - | t.string :browser |
| - | t.string :os |
| - | t.string :device_type |
| - | t.integer :screen_height |
| - | t.integer :screen_width |
| - | |
| - | # location |
| - | t.string :country |
| - | t.string :region |
| - | t.string :city |
| - | t.string :postal_code |
| - | t.decimal :latitude |
| - | t.decimal :longitude |
| - | |
| - | # utm parameters |
| - | t.string :utm_source |
| - | t.string :utm_medium |
| - | t.string :utm_term |
| - | t.string :utm_content |
| - | t.string :utm_campaign |
| - | |
| - | # native apps |
| - | # t.string :platform |
| - | # t.string :app_version |
| - | # t.string :os_version |
| - | |
| - | t.timestamp :started_at |
| - | end |
| - | |
| - | add_index :visits, [:visit_token], unique: true |
| - | add_index :visits, [:user_id] |
| - | end |
| - | end |
generators/ahoy/stores/templates/bunny_initializer.rb b/lib/generators/ahoy/stores/templates/bunny_initializer.rb
+0
-9
| @@ | @@ -1,9 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::BunnyStore |
| - | def visits_queue |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_queue |
| - | "ahoy_events" |
| - | end |
| - | end |
generators/ahoy/stores/templates/custom_initializer.rb b/lib/generators/ahoy/stores/templates/custom_initializer.rb
+0
-10
| @@ | @@ -1,10 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::BaseStore |
| - | def track_visit(options) |
| - | end |
| - | |
| - | def track_event(name, properties, options) |
| - | end |
| - | |
| - | def current_visit |
| - | end |
| - | end |
generators/ahoy/stores/templates/fluentd_initializer.rb b/lib/generators/ahoy/stores/templates/fluentd_initializer.rb
+0
-3
| @@ | @@ -1,3 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::FluentdStore |
| - | # customize here |
| - | end |
generators/ahoy/stores/templates/kafka_initializer.rb b/lib/generators/ahoy/stores/templates/kafka_initializer.rb
+0
-9
| @@ | @@ -1,9 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::KafkaStore |
| - | def visits_topic |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_topic |
| - | "ahoy_events" |
| - | end |
| - | end |
generators/ahoy/stores/templates/kinesis_firehose_initializer.rb b/lib/generators/ahoy/stores/templates/kinesis_firehose_initializer.rb
+0
-17
| @@ | @@ -1,17 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::KinesisFirehoseStore |
| - | def credentials |
| - | { |
| - | access_key_id: ENV["AWS_ACCESS_KEY_ID"], |
| - | secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"], |
| - | region: "us-east-1" |
| - | } |
| - | end |
| - | |
| - | def visits_stream |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_stream |
| - | "ahoy_events" |
| - | end |
| - | end |
generators/ahoy/stores/templates/log_initializer.rb b/lib/generators/ahoy/stores/templates/log_initializer.rb
+0
-3
| @@ | @@ -1,3 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::LogStore |
| - | # customize here |
| - | end |
generators/ahoy/stores/templates/mongoid_event_model.rb b/lib/generators/ahoy/stores/templates/mongoid_event_model.rb
+0
-12
| @@ | @@ -1,12 +0,0 @@ |
| - | class Ahoy::Event |
| - | include Mongoid::Document |
| - | |
| - | # associations |
| - | belongs_to :visit |
| - | belongs_to :user |
| - | |
| - | # fields |
| - | field :name, type: String |
| - | field :properties, type: Hash |
| - | field :time, type: Time |
| - | end |
generators/ahoy/stores/templates/mongoid_initializer.rb b/lib/generators/ahoy/stores/templates/mongoid_initializer.rb
+0
-3
| @@ | @@ -1,3 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::MongoidStore |
| - | # customize here |
| - | end |
generators/ahoy/stores/templates/mongoid_visit_model.rb b/lib/generators/ahoy/stores/templates/mongoid_visit_model.rb
+0
-43
| @@ | @@ -1,43 +0,0 @@ |
| - | class Visit |
| - | include Mongoid::Document |
| - | |
| - | # associations |
| - | belongs_to :user |
| - | |
| - | # required |
| - | field :visitor_id, type: <%= @visitor_id_type %> |
| - | |
| - | # the rest are recommended but optional |
| - | # simply remove the columns you don't want |
| - | |
| - | # standard |
| - | field :ip, type: String |
| - | field :user_agent, type: String |
| - | field :referrer, type: String |
| - | field :landing_page, type: String |
| - | |
| - | # traffic source |
| - | field :referring_domain, type: String |
| - | field :search_keyword, type: String |
| - | |
| - | # technology |
| - | field :browser, type: String |
| - | field :os, type: String |
| - | field :device_type, type: String |
| - | field :screen_height, type: Integer |
| - | field :screen_width, type: Integer |
| - | |
| - | # location |
| - | field :country, type: String |
| - | field :region, type: String |
| - | field :city, type: String |
| - | |
| - | # utm parameters |
| - | field :utm_source, type: String |
| - | field :utm_medium, type: String |
| - | field :utm_term, type: String |
| - | field :utm_content, type: String |
| - | field :utm_campaign, type: String |
| - | |
| - | field :started_at, type: Time |
| - | end |
generators/ahoy/stores/templates/nats_initializer.rb b/lib/generators/ahoy/stores/templates/nats_initializer.rb
+0
-9
| @@ | @@ -1,9 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::NatsStore |
| - | def visits_subject |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_subject |
| - | "ahoy_events" |
| - | end |
| - | end |
generators/ahoy/stores/templates/nsq_initializer.rb b/lib/generators/ahoy/stores/templates/nsq_initializer.rb
+0
-9
| @@ | @@ -1,9 +0,0 @@ |
| - | class Ahoy::Store < Ahoy::Stores::NsqStore |
| - | def visits_topic |
| - | "ahoy_visits" |
| - | end |
| - | |
| - | def events_topic |
| - | "ahoy_events" |
| - | end |
| - | end |
generators/ahoy/templates/active_record_event_model.rb b/lib/generators/ahoy/templates/active_record_event_model.rb
+10
-0
| @@ | @@ -0,0 +1,10 @@ |
| + | class Ahoy::Event < <%= rails5? ? "ApplicationRecord" : "ActiveRecord::Base" %> |
| + | include Ahoy::QueryMethods |
| + | |
| + | self.table_name = "ahoy_events" |
| + | |
| + | belongs_to :visit |
| + | belongs_to :user<%= rails5? ? ", optional: true" : nil %><% if properties_type == "text" %> |
| + | |
| + | serialize :properties, JSON<% end %> |
| + | end |
generators/ahoy/templates/active_record_migration.rb b/lib/generators/ahoy/templates/active_record_migration.rb
+55
-0
| @@ | @@ -0,0 +1,55 @@ |
| + | class <%= migration_class_name %> < ActiveRecord::Migration<%= migration_version %> |
| + | def change |
| + | create_table :ahoy_visits do |t| |
| + | t.string :visit_token |
| + | t.string :visitor_token |
| + | |
| + | # the rest are recommended but optional |
| + | # simply remove any you don't want |
| + | |
| + | # user |
| + | t.references :user |
| + | |
| + | # standard |
| + | t.string :ip |
| + | t.text :user_agent |
| + | t.text :referrer |
| + | t.string :referring_domain |
| + | t.string :search_keyword |
| + | t.text :landing_page |
| + | |
| + | # technology |
| + | t.string :browser |
| + | t.string :os |
| + | t.string :device_type |
| + | |
| + | # location |
| + | t.string :country |
| + | t.string :region |
| + | t.string :city |
| + | |
| + | # utm parameters |
| + | t.string :utm_source |
| + | t.string :utm_medium |
| + | t.string :utm_term |
| + | t.string :utm_content |
| + | t.string :utm_campaign |
| + | |
| + | t.timestamp :started_at |
| + | end |
| + | |
| + | add_index :ahoy_visits, [:visit_token], unique: true |
| + | |
| + | create_table :ahoy_events do |t| |
| + | t.references :visit |
| + | t.references :user |
| + | |
| + | t.string :name |
| + | t.<%= properties_type %> :properties |
| + | t.timestamp :time |
| + | end |
| + | |
| + | add_index :ahoy_events, [:name, :time]<% if properties_type == "jsonb" && rails5? %> |
| + | add_index :ahoy_events, "properties jsonb_path_ops", using: "gin"<% end %> |
| + | end |
| + | end |
generators/ahoy/templates/active_record_visit_model.rb b/lib/generators/ahoy/templates/active_record_visit_model.rb
+6
-0
| @@ | @@ -0,0 +1,6 @@ |
| + | class Ahoy::Visit < <%= rails5? ? "ApplicationRecord" : "ActiveRecord::Base" %> |
| + | self.table_name = "ahoy_visits" |
| + | |
| + | has_many :events, class_name: "Ahoy::Event" |
| + | belongs_to :user<%= rails5? ? ", optional: true" : nil %> |
| + | end |
generators/ahoy/templates/base_store_initializer.rb b/lib/generators/ahoy/templates/base_store_initializer.rb
+17
-0
| @@ | @@ -0,0 +1,17 @@ |
| + | class Ahoy::Store < Ahoy::BaseStore |
| + | def track_visit(data) |
| + | # do |
| + | end |
| + | |
| + | def track_event(data) |
| + | # something |
| + | end |
| + | |
| + | def geocode(data) |
| + | # amazing |
| + | end |
| + | |
| + | def authenticate(data) |
| + | # !!! |
| + | end |
| + | end |
generators/ahoy/templates/database_store_initializer.rb b/lib/generators/ahoy/templates/database_store_initializer.rb
+5
-0
| @@ | @@ -0,0 +1,5 @@ |
| + | class Ahoy::Store < Ahoy::DatabaseStore |
| + | end |
| + | |
| + | # set to true for JavaScript tracking |
| + | Ahoy.api = false |
generators/ahoy/templates/mongoid_event_model.rb b/lib/generators/ahoy/templates/mongoid_event_model.rb
+16
-0
| @@ | @@ -0,0 +1,16 @@ |
| + | class Ahoy::Event |
| + | include Mongoid::Document |
| + | |
| + | # associations |
| + | belongs_to :visit |
| + | belongs_to :user<%= rails5? ? ", optional: true" : nil %> |
| + | |
| + | # fields |
| + | field :name, type: String |
| + | field :properties, type: Hash |
| + | field :time, type: Time |
| + | |
| + | index({visit_id: 1, name: 1}) |
| + | index({user_id: 1, name: 1}) |
| + | index({name: 1, time: 1}) |
| + | end |
generators/ahoy/templates/mongoid_visit_model.rb b/lib/generators/ahoy/templates/mongoid_visit_model.rb
+48
-0
| @@ | @@ -0,0 +1,48 @@ |
| + | class Ahoy::Visit |
| + | include Mongoid::Document |
| + | |
| + | # associations |
| + | has_many :events, class_name: "Ahoy::Event" |
| + | belongs_to :user<%= Rails::VERSION::MAJOR >= 5 ? ", optional: true" : nil %> |
| + | |
| + | # required |
| + | field :visit_token, type: String |
| + | field :visitor_token, type: String |
| + | |
| + | # the rest are recommended but optional |
| + | # simply remove the columns you don't want |
| + | |
| + | # standard |
| + | field :ip, type: String |
| + | field :user_agent, type: String |
| + | field :referrer, type: String |
| + | field :landing_page, type: String |
| + | |
| + | # traffic source |
| + | field :referring_domain, type: String |
| + | field :search_keyword, type: String |
| + | |
| + | # technology |
| + | field :browser, type: String |
| + | field :os, type: String |
| + | field :device_type, type: String |
| + | field :screen_height, type: Integer |
| + | field :screen_width, type: Integer |
| + | |
| + | # location |
| + | field :country, type: String |
| + | field :region, type: String |
| + | field :city, type: String |
| + | |
| + | # utm parameters |
| + | field :utm_source, type: String |
| + | field :utm_medium, type: String |
| + | field :utm_term, type: String |
| + | field :utm_content, type: String |
| + | field :utm_campaign, type: String |
| + | |
| + | field :started_at, type: Time |
| + | |
| + | index({visit_token: 1}, {unique: true}) |
| + | index({user_id: 1}) |
| + | end |
test/properties/mysql_json_test.rb
+0
-18
| @@ | @@ -1,18 +0,0 @@ |
| - | require_relative "../test_helper" |
| - | |
| - | ActiveRecord::Base.establish_connection adapter: "mysql2", username: "root", database: "ahoy_test" |
| - | |
| - | ActiveRecord::Migration.create_table :mysql_json_events, force: true do |t| |
| - | t.json :properties |
| - | end |
| - | |
| - | class MysqlJsonEvent < MysqlBase |
| - | end |
| - | |
| - | class MysqlJsonTest < Minitest::Test |
| - | include PropertiesTest |
| - | |
| - | def model |
| - | MysqlJsonEvent |
| - | end |
| - | end |
test/properties/mysql_text_test.rb
+0
-19
| @@ | @@ -1,19 +0,0 @@ |
| - | require_relative "../test_helper" |
| - | |
| - | ActiveRecord::Base.establish_connection adapter: "mysql2", username: "root", database: "ahoy_test" |
| - | |
| - | ActiveRecord::Migration.create_table :mysql_text_events, force: true do |t| |
| - | t.text :properties |
| - | end |
| - | |
| - | class MysqlTextEvent < MysqlBase |
| - | serialize :properties, JSON |
| - | end |
| - | |
| - | class MysqlTextTest < Minitest::Test |
| - | include PropertiesTest |
| - | |
| - | def model |
| - | MysqlTextEvent |
| - | end |
| - | end |
test/properties/postgresql_hstore_test.rb
+0
-20
| @@ | @@ -1,20 +0,0 @@ |
| - | require_relative "../test_helper" |
| - | |
| - | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| - | |
| - | ActiveRecord::Migration.enable_extension "hstore" |
| - | |
| - | ActiveRecord::Migration.create_table :postgresql_hstore_events, force: true do |t| |
| - | t.hstore :properties |
| - | end |
| - | |
| - | class PostgresqlHstoreEvent < PostgresqlBase |
| - | end |
| - | |
| - | class PostgresqlHstoreTest < Minitest::Test |
| - | include PropertiesTest |
| - | |
| - | def model |
| - | PostgresqlHstoreEvent |
| - | end |
| - | end |
test/properties/postgresql_json_test.rb
+0
-18
| @@ | @@ -1,18 +0,0 @@ |
| - | require_relative "../test_helper" |
| - | |
| - | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| - | |
| - | ActiveRecord::Migration.create_table :postgresql_json_events, force: true do |t| |
| - | t.json :properties |
| - | end |
| - | |
| - | class PostgresqlJsonEvent < PostgresqlBase |
| - | end |
| - | |
| - | class PostgresqlJsonTest < Minitest::Test |
| - | include PropertiesTest |
| - | |
| - | def model |
| - | PostgresqlJsonEvent |
| - | end |
| - | end |
test/properties/postgresql_jsonb_test.rb
+0
-19
| @@ | @@ -1,19 +0,0 @@ |
| - | require_relative "../test_helper" |
| - | |
| - | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| - | |
| - | ActiveRecord::Migration.create_table :postgresql_jsonb_events, force: true do |t| |
| - | t.jsonb :properties |
| - | t.index :properties, using: 'gin' |
| - | end |
| - | |
| - | class PostgresqlJsonbEvent < PostgresqlBase |
| - | end |
| - | |
| - | class PostgresqlJsonbTest < Minitest::Test |
| - | include PropertiesTest |
| - | |
| - | def model |
| - | PostgresqlJsonbEvent |
| - | end |
| - | end |
test/properties/postgresql_text_test.rb
+0
-19
| @@ | @@ -1,19 +0,0 @@ |
| - | require_relative "../test_helper" |
| - | |
| - | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| - | |
| - | ActiveRecord::Migration.create_table :postgresql_text_events, force: true do |t| |
| - | t.text :properties |
| - | end |
| - | |
| - | class PostgresqlTextEvent < PostgresqlBase |
| - | serialize :properties, JSON |
| - | end |
| - | |
| - | class PostgresqlTextTest < Minitest::Test |
| - | include PropertiesTest |
| - | |
| - | def model |
| - | PostgresqlTextEvent |
| - | end |
| - | end |
test/query_methods/mongoid_test.rb
+23
-0
| @@ | @@ -0,0 +1,23 @@ |
| + | require_relative "../test_helper" |
| + | |
| + | Mongoid.logger.level = Logger::WARN |
| + | Mongo::Logger.logger.level = Logger::WARN |
| + | |
| + | Mongoid.configure do |config| |
| + | config.connect_to("ahoy_test") |
| + | end |
| + | |
| + | class MongoidEvent |
| + | include Mongoid::Document |
| + | include Ahoy::QueryMethods |
| + | |
| + | field :properties, type: Hash |
| + | end |
| + | |
| + | class MongoidTest < Minitest::Test |
| + | include QueryMethodsTest |
| + | |
| + | def model |
| + | MongoidEvent |
| + | end |
| + | end |
test/query_methods/mysql_json_test.rb
+18
-0
| @@ | @@ -0,0 +1,18 @@ |
| + | require_relative "../test_helper" |
| + | |
| + | ActiveRecord::Base.establish_connection adapter: "mysql2", username: "root", database: "ahoy_test" |
| + | |
| + | ActiveRecord::Migration.create_table :mysql_json_events, force: true do |t| |
| + | t.json :properties |
| + | end |
| + | |
| + | class MysqlJsonEvent < MysqlBase |
| + | end |
| + | |
| + | class MysqlJsonTest < Minitest::Test |
| + | include QueryMethodsTest |
| + | |
| + | def model |
| + | MysqlJsonEvent |
| + | end |
| + | end |
test/query_methods/mysql_text_test.rb
+19
-0
| @@ | @@ -0,0 +1,19 @@ |
| + | require_relative "../test_helper" |
| + | |
| + | ActiveRecord::Base.establish_connection adapter: "mysql2", username: "root", database: "ahoy_test" |
| + | |
| + | ActiveRecord::Migration.create_table :mysql_text_events, force: true do |t| |
| + | t.text :properties |
| + | end |
| + | |
| + | class MysqlTextEvent < MysqlBase |
| + | serialize :properties, JSON |
| + | end |
| + | |
| + | class MysqlTextTest < Minitest::Test |
| + | include QueryMethodsTest |
| + | |
| + | def model |
| + | MysqlTextEvent |
| + | end |
| + | end |
test/query_methods/postgresql_hstore_test.rb
+20
-0
| @@ | @@ -0,0 +1,20 @@ |
| + | require_relative "../test_helper" |
| + | |
| + | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| + | |
| + | ActiveRecord::Migration.enable_extension "hstore" |
| + | |
| + | ActiveRecord::Migration.create_table :postgresql_hstore_events, force: true do |t| |
| + | t.hstore :properties |
| + | end |
| + | |
| + | class PostgresqlHstoreEvent < PostgresqlBase |
| + | end |
| + | |
| + | class PostgresqlHstoreTest < Minitest::Test |
| + | include QueryMethodsTest |
| + | |
| + | def model |
| + | PostgresqlHstoreEvent |
| + | end |
| + | end |
test/query_methods/postgresql_json_test.rb
+18
-0
| @@ | @@ -0,0 +1,18 @@ |
| + | require_relative "../test_helper" |
| + | |
| + | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| + | |
| + | ActiveRecord::Migration.create_table :postgresql_json_events, force: true do |t| |
| + | t.json :properties |
| + | end |
| + | |
| + | class PostgresqlJsonEvent < PostgresqlBase |
| + | end |
| + | |
| + | class PostgresqlJsonTest < Minitest::Test |
| + | include QueryMethodsTest |
| + | |
| + | def model |
| + | PostgresqlJsonEvent |
| + | end |
| + | end |
test/query_methods/postgresql_jsonb_test.rb
+19
-0
| @@ | @@ -0,0 +1,19 @@ |
| + | require_relative "../test_helper" |
| + | |
| + | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| + | |
| + | ActiveRecord::Migration.create_table :postgresql_jsonb_events, force: true do |t| |
| + | t.jsonb :properties |
| + | t.index :properties, using: 'gin' |
| + | end |
| + | |
| + | class PostgresqlJsonbEvent < PostgresqlBase |
| + | end |
| + | |
| + | class PostgresqlJsonbTest < Minitest::Test |
| + | include QueryMethodsTest |
| + | |
| + | def model |
| + | PostgresqlJsonbEvent |
| + | end |
| + | end |
test/query_methods/postgresql_text_test.rb
+19
-0
| @@ | @@ -0,0 +1,19 @@ |
| + | require_relative "../test_helper" |
| + | |
| + | ActiveRecord::Base.establish_connection adapter: "postgresql", database: "ahoy_test" |
| + | |
| + | ActiveRecord::Migration.create_table :postgresql_text_events, force: true do |t| |
| + | t.text :properties |
| + | end |
| + | |
| + | class PostgresqlTextEvent < PostgresqlBase |
| + | serialize :properties, JSON |
| + | end |
| + | |
| + | class PostgresqlTextTest < Minitest::Test |
| + | include QueryMethodsTest |
| + | |
| + | def model |
| + | PostgresqlTextEvent |
| + | end |
| + | end |
test/test_helper.rb
+4
-3
| @@ | @@ -3,22 +3,23 @@ Bundler.require(:default) |
| require "minitest/autorun" | |
| require "minitest/pride" | |
| require "active_record" | |
| + | require "mongoid" |
| ActiveRecord::Base.logger = ActiveSupport::Logger.new(STDOUT) if ENV["VERBOSE"] | |
| class PostgresqlBase < ActiveRecord::Base | |
| - | include Ahoy::Properties |
| + | include Ahoy::QueryMethods |
| establish_connection adapter: "postgresql", database: "ahoy_test" | |
| self.abstract_class = true | |
| end | |
| class MysqlBase < ActiveRecord::Base | |
| - | include Ahoy::Properties |
| + | include Ahoy::QueryMethods |
| establish_connection adapter: "mysql2", username: "root", database: "ahoy_test" | |
| self.abstract_class = true | |
| end | |
| - | module PropertiesTest |
| + | module QueryMethodsTest |
| def setup | |
| model.delete_all | |
| end | |
test/visit_properties_test.rb
+0
-44
| @@ | @@ -1,44 +0,0 @@ |
| - | require_relative "test_helper" |
| - | |
| - | class TestVisitProperties < Minitest::Test |
| - | def setup |
| - | request = MiniTest::Mock.new |
| - | @visit_properties = Ahoy::VisitProperties.new(request) |
| - | end |
| - | |
| - | def test_keys |
| - | with_geocode(true) do |
| - | assert_equal @visit_properties.keys, Ahoy::VisitProperties::KEYS |
| - | end |
| - | end |
| - | |
| - | def test_keys_when_geocode_disabled |
| - | with_geocode(false) do |
| - | keys = @visit_properties.keys |
| - | |
| - | refute keys.include?(:country) |
| - | refute keys.include?(:region) |
| - | refute keys.include?(:city) |
| - | end |
| - | end |
| - | |
| - | def test_keys_when_geocode_async |
| - | with_geocode(:async) do |
| - | keys = @visit_properties.keys |
| - | |
| - | refute keys.include?(:country) |
| - | refute keys.include?(:region) |
| - | refute keys.include?(:city) |
| - | end |
| - | end |
| - | |
| - | private |
| - | |
| - | def with_geocode(enabled) |
| - | original = Ahoy.geocode |
| - | Ahoy.geocode = enabled |
| - | yield |
| - | ensure |
| - | Ahoy.geocode = original |
| - | end |
| - | end |
vendor/assets/javascripts/ahoy.js
+551
-325
| @@ | @@ -1,153 +1,266 @@ |
| - | /* |
| - | * Ahoy.js |
| - | * Simple, powerful JavaScript analytics |
| - | * https://github.com/ankane/ahoy.js |
| - | * v0.2.1 |
| - | * MIT License |
| - | */ |
| - | |
| - | /*jslint browser: true, indent: 2, plusplus: true, vars: true */ |
| - | |
| - | (function (window) { |
| - | "use strict"; |
| - | |
| - | var config = { |
| - | urlPrefix: "", |
| - | visitsUrl: "/ahoy/visits", |
| - | eventsUrl: "/ahoy/events", |
| - | cookieDomain: null, |
| - | page: null, |
| - | platform: "Web", |
| - | useBeacon: false, |
| - | startOnReady: true |
| - | }; |
| - | |
| - | var ahoy = window.ahoy || window.Ahoy || {}; |
| - | |
| - | ahoy.configure = function (options) { |
| - | for (var key in options) { |
| - | if (options.hasOwnProperty(key)) { |
| - | config[key] = options[key]; |
| - | } |
| + | (function webpackUniversalModuleDefinition(root, factory) { |
| + | if(typeof exports === 'object' && typeof module === 'object') |
| + | module.exports = factory(); |
| + | else if(typeof define === 'function' && define.amd) |
| + | define([], factory); |
| + | else if(typeof exports === 'object') |
| + | exports["ahoy"] = factory(); |
| + | else |
| + | root["ahoy"] = factory(); |
| + | })(typeof self !== 'undefined' ? self : this, function() { |
| + | return /******/ (function(modules) { // webpackBootstrap |
| + | /******/ // The module cache |
| + | /******/ var installedModules = {}; |
| + | /******/ |
| + | /******/ // The require function |
| + | /******/ function __webpack_require__(moduleId) { |
| + | /******/ |
| + | /******/ // Check if module is in cache |
| + | /******/ if(installedModules[moduleId]) { |
| + | /******/ return installedModules[moduleId].exports; |
| + | /******/ } |
| + | /******/ // Create a new module (and put it into the cache) |
| + | /******/ var module = installedModules[moduleId] = { |
| + | /******/ i: moduleId, |
| + | /******/ l: false, |
| + | /******/ exports: {} |
| + | /******/ }; |
| + | /******/ |
| + | /******/ // Execute the module function |
| + | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); |
| + | /******/ |
| + | /******/ // Flag the module as loaded |
| + | /******/ module.l = true; |
| + | /******/ |
| + | /******/ // Return the exports of the module |
| + | /******/ return module.exports; |
| + | /******/ } |
| + | /******/ |
| + | /******/ |
| + | /******/ // expose the modules object (__webpack_modules__) |
| + | /******/ __webpack_require__.m = modules; |
| + | /******/ |
| + | /******/ // expose the module cache |
| + | /******/ __webpack_require__.c = installedModules; |
| + | /******/ |
| + | /******/ // define getter function for harmony exports |
| + | /******/ __webpack_require__.d = function(exports, name, getter) { |
| + | /******/ if(!__webpack_require__.o(exports, name)) { |
| + | /******/ Object.defineProperty(exports, name, { |
| + | /******/ configurable: false, |
| + | /******/ enumerable: true, |
| + | /******/ get: getter |
| + | /******/ }); |
| + | /******/ } |
| + | /******/ }; |
| + | /******/ |
| + | /******/ // getDefaultExport function for compatibility with non-harmony modules |
| + | /******/ __webpack_require__.n = function(module) { |
| + | /******/ var getter = module && module.__esModule ? |
| + | /******/ function getDefault() { return module['default']; } : |
| + | /******/ function getModuleExports() { return module; }; |
| + | /******/ __webpack_require__.d(getter, 'a', getter); |
| + | /******/ return getter; |
| + | /******/ }; |
| + | /******/ |
| + | /******/ // Object.prototype.hasOwnProperty.call |
| + | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; |
| + | /******/ |
| + | /******/ // __webpack_public_path__ |
| + | /******/ __webpack_require__.p = ""; |
| + | /******/ |
| + | /******/ // Load entry module and return exports |
| + | /******/ return __webpack_require__(__webpack_require__.s = 0); |
| + | /******/ }) |
| + | /************************************************************************/ |
| + | /******/ ([ |
| + | /* 0 */ |
| + | /***/ (function(module, exports, __webpack_require__) { |
| + | |
| + | "use strict"; |
| + | |
| + | |
| + | Object.defineProperty(exports, "__esModule", { |
| + | value: true |
| + | }); |
| + | |
| + | var _objectToFormdata = __webpack_require__(1); |
| + | |
| + | var _objectToFormdata2 = _interopRequireDefault(_objectToFormdata); |
| + | |
| + | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } |
| + | |
| + | var config = { |
| + | urlPrefix: "", |
| + | visitsUrl: "/ahoy/visits", |
| + | eventsUrl: "/ahoy/events", |
| + | cookieDomain: null, |
| + | page: null, |
| + | platform: "Web", |
| + | useBeacon: true, |
| + | startOnReady: true |
| + | }; /* |
| + | * Ahoy.js |
| + | * Simple, powerful JavaScript analytics |
| + | * https://github.com/ankane/ahoy.js |
| + | * v0.3.0 |
| + | * MIT License |
| + | */ |
| + | |
| + | var ahoy = window.ahoy || window.Ahoy || {}; |
| + | |
| + | ahoy.configure = function (options) { |
| + | for (var key in options) { |
| + | if (options.hasOwnProperty(key)) { |
| + | config[key] = options[key]; |
| } | |
| - | }; |
| - | |
| - | // legacy |
| - | ahoy.configure(ahoy); |
| - | |
| - | var $ = window.jQuery || window.Zepto || window.$; |
| - | var visitId, visitorId, track; |
| - | var visitTtl = 4 * 60; // 4 hours |
| - | var visitorTtl = 2 * 365 * 24 * 60; // 2 years |
| - | var isReady = false; |
| - | var queue = []; |
| - | var canStringify = typeof(JSON) !== "undefined" && typeof(JSON.stringify) !== "undefined"; |
| - | var eventQueue = []; |
| - | |
| - | function visitsUrl() { |
| - | return config.urlPrefix + config.visitsUrl; |
| } | |
| - | |
| - | function eventsUrl() { |
| - | return config.urlPrefix + config.eventsUrl; |
| + | }; |
| + | |
| + | // legacy |
| + | ahoy.configure(ahoy); |
| + | |
| + | var $ = window.jQuery || window.Zepto || window.$; |
| + | var visitId = void 0, |
| + | visitorId = void 0, |
| + | track = void 0; |
| + | var visitTtl = 4 * 60; // 4 hours |
| + | var visitorTtl = 2 * 365 * 24 * 60; // 2 years |
| + | var isReady = false; |
| + | var queue = []; |
| + | var canStringify = typeof JSON !== "undefined" && typeof JSON.stringify !== "undefined"; |
| + | var eventQueue = []; |
| + | |
| + | function visitsUrl() { |
| + | return config.urlPrefix + config.visitsUrl; |
| + | } |
| + | |
| + | function eventsUrl() { |
| + | return config.urlPrefix + config.eventsUrl; |
| + | } |
| + | |
| + | function canTrackNow() { |
| + | return (config.useBeacon || config.trackNow) && canStringify && typeof window.navigator.sendBeacon !== "undefined"; |
| + | } |
| + | |
| + | // cookies |
| + | |
| + | // http://www.quirksmode.org/js/cookies.html |
| + | function setCookie(name, value, ttl) { |
| + | var expires = ""; |
| + | var cookieDomain = ""; |
| + | if (ttl) { |
| + | var date = new Date(); |
| + | date.setTime(date.getTime() + ttl * 60 * 1000); |
| + | expires = "; expires=" + date.toGMTString(); |
| } | |
| - | |
| - | function canTrackNow() { |
| - | return (config.useBeacon || config.trackNow) && canStringify && typeof(window.navigator.sendBeacon) !== "undefined"; |
| + | var domain = config.cookieDomain || config.domain; |
| + | if (domain) { |
| + | cookieDomain = "; domain=" + domain; |
| } | |
| - | |
| - | // cookies |
| - | |
| - | // http://www.quirksmode.org/js/cookies.html |
| - | function setCookie(name, value, ttl) { |
| - | var expires = ""; |
| - | var cookieDomain = ""; |
| - | if (ttl) { |
| - | var date = new Date(); |
| - | date.setTime(date.getTime() + (ttl * 60 * 1000)); |
| - | expires = "; expires=" + date.toGMTString(); |
| + | document.cookie = name + "=" + escape(value) + expires + cookieDomain + "; path=/"; |
| + | } |
| + | |
| + | function getCookie(name) { |
| + | var i = void 0, |
| + | c = void 0; |
| + | var nameEQ = name + "="; |
| + | var ca = document.cookie.split(';'); |
| + | for (i = 0; i < ca.length; i++) { |
| + | c = ca[i]; |
| + | while (c.charAt(0) === ' ') { |
| + | c = c.substring(1, c.length); |
| } | |
| - | var domain = config.cookieDomain || config.domain; |
| - | if (domain) { |
| - | cookieDomain = "; domain=" + domain; |
| + | if (c.indexOf(nameEQ) === 0) { |
| + | return unescape(c.substring(nameEQ.length, c.length)); |
| } | |
| - | document.cookie = name + "=" + escape(value) + expires + cookieDomain + "; path=/"; |
| } | |
| + | return null; |
| + | } |
| - | function getCookie(name) { |
| - | var i, c; |
| - | var nameEQ = name + "="; |
| - | var ca = document.cookie.split(';'); |
| - | for (i = 0; i < ca.length; i++) { |
| - | c = ca[i]; |
| - | while (c.charAt(0) === ' ') { |
| - | c = c.substring(1, c.length); |
| - | } |
| - | if (c.indexOf(nameEQ) === 0) { |
| - | return unescape(c.substring(nameEQ.length, c.length)); |
| - | } |
| - | } |
| - | return null; |
| - | } |
| + | function destroyCookie(name) { |
| + | setCookie(name, "", -1); |
| + | } |
| - | function destroyCookie(name) { |
| - | setCookie(name, "", -1); |
| + | function log(message) { |
| + | if (getCookie("ahoy_debug")) { |
| + | window.console.log(message); |
| } | |
| + | } |
| - | function log(message) { |
| - | if (getCookie("ahoy_debug")) { |
| - | window.console.log(message); |
| - | } |
| + | function setReady() { |
| + | var callback = void 0; |
| + | while (callback = queue.shift()) { |
| + | callback(); |
| + | } |
| + | isReady = true; |
| + | } |
| + | |
| + | function ready(callback) { |
| + | if (isReady) { |
| + | callback(); |
| + | } else { |
| + | queue.push(callback); |
| } | |
| + | } |
| - | function setReady() { |
| - | var callback; |
| - | while (callback = queue.shift()) { |
| - | callback(); |
| - | } |
| - | isReady = true; |
| + | function matchesSelector(element, selector) { |
| + | if (element.matches) { |
| + | return element.matches(selector); |
| + | } else { |
| + | return element.msMatchesSelector(selector); |
| } | |
| + | } |
| - | function ready(callback) { |
| - | if (isReady) { |
| - | callback(); |
| - | } else { |
| - | queue.push(callback); |
| + | function onEvent(eventName, selector, callback) { |
| + | document.addEventListener(eventName, function (e) { |
| + | if (matchesSelector(e.target, selector)) { |
| + | callback(e); |
| } | |
| - | } |
| + | }); |
| + | } |
| + | |
| + | // http://beeker.io/jquery-document-ready-equivalent-vanilla-javascript |
| + | function documentReady(callback) { |
| + | document.readyState === "interactive" || document.readyState === "complete" ? callback() : document.addEventListener("DOMContentLoaded", callback); |
| + | } |
| + | |
| + | // http://stackoverflow.com/a/2117523/1177228 |
| + | function generateId() { |
| + | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { |
| + | var r = Math.random() * 16 | 0, |
| + | v = c == 'x' ? r : r & 0x3 | 0x8; |
| + | return v.toString(16); |
| + | }); |
| + | } |
| - | // http://stackoverflow.com/a/2117523/1177228 |
| - | function generateId() { |
| - | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { |
| - | var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8); |
| - | return v.toString(16); |
| - | }); |
| + | function saveEventQueue() { |
| + | // TODO add stringify method for IE 7 and under |
| + | if (canStringify) { |
| + | setCookie("ahoy_events", JSON.stringify(eventQueue), 1); |
| } | |
| + | } |
| - | function saveEventQueue() { |
| - | // TODO add stringify method for IE 7 and under |
| - | if (canStringify) { |
| - | setCookie("ahoy_events", JSON.stringify(eventQueue), 1); |
| - | } |
| - | } |
| + | // from rails-ujs |
| - | // from jquery-ujs |
| + | function csrfToken() { |
| + | var meta = document.querySelector("meta[name=csrf-token]"); |
| + | return meta && meta.content; |
| + | } |
| - | function csrfToken() { |
| - | return $("meta[name=csrf-token]").attr("content"); |
| - | } |
| + | function csrfParam() { |
| + | var meta = document.querySelector("meta[name=csrf-param]"); |
| + | return meta && meta.content; |
| + | } |
| - | function csrfParam() { |
| - | return $("meta[name=csrf-param]").attr("content"); |
| - | } |
| + | function CSRFProtection(xhr) { |
| + | var token = csrfToken(); |
| + | if (token) xhr.setRequestHeader("X-CSRF-Token", token); |
| + | } |
| - | function CSRFProtection(xhr) { |
| - | var token = csrfToken(); |
| - | if (token) xhr.setRequestHeader("X-CSRF-Token", token); |
| - | } |
| - | |
| - | function sendRequest(url, data, success) { |
| - | if (canStringify) { |
| + | function sendRequest(url, data, success) { |
| + | if (canStringify) { |
| + | if ($) { |
| $.ajax({ | |
| type: "POST", | |
| url: url, | |
| @@ | @@ -157,246 +270,359 @@ |
| beforeSend: CSRFProtection, | |
| success: success | |
| }); | |
| + | } else { |
| + | var xhr = new XMLHttpRequest(); |
| + | xhr.open("POST", url, true); |
| + | xhr.setRequestHeader("Content-Type", "application/json"); |
| + | xhr.onload = function () { |
| + | if (xhr.status === 200) { |
| + | success(); |
| + | } |
| + | }; |
| + | CSRFProtection(xhr); |
| + | xhr.send(JSON.stringify(data)); |
| } | |
| } | |
| + | } |
| - | function eventData(event) { |
| - | var data = { |
| - | events: [event], |
| - | visit_token: event.visit_token, |
| - | visitor_token: event.visitor_token |
| - | }; |
| - | delete event.visit_token; |
| - | delete event.visitor_token; |
| - | return data; |
| - | } |
| - | |
| - | function trackEvent(event) { |
| - | ready( function () { |
| - | sendRequest(eventsUrl(), eventData(event), function() { |
| - | // remove from queue |
| - | for (var i = 0; i < eventQueue.length; i++) { |
| - | if (eventQueue[i].id == event.id) { |
| - | eventQueue.splice(i, 1); |
| - | break; |
| - | } |
| + | function eventData(event) { |
| + | var data = { |
| + | events: [event], |
| + | visit_token: event.visit_token, |
| + | visitor_token: event.visitor_token |
| + | }; |
| + | delete event.visit_token; |
| + | delete event.visitor_token; |
| + | return data; |
| + | } |
| + | |
| + | function trackEvent(event) { |
| + | ready(function () { |
| + | sendRequest(eventsUrl(), eventData(event), function () { |
| + | // remove from queue |
| + | for (var i = 0; i < eventQueue.length; i++) { |
| + | if (eventQueue[i].id == event.id) { |
| + | eventQueue.splice(i, 1); |
| + | break; |
| } | |
| - | saveEventQueue(); |
| - | }); |
| + | } |
| + | saveEventQueue(); |
| }); | |
| - | } |
| + | }); |
| + | } |
| - | function trackEventNow(event) { |
| - | ready( function () { |
| - | var data = eventData(event); |
| - | var param = csrfParam(); |
| - | var token = csrfToken(); |
| - | if (param && token) data[param] = token; |
| - | var payload = new Blob([JSON.stringify(data)], {type : "application/json; charset=utf-8"}); |
| - | navigator.sendBeacon(eventsUrl(), payload); |
| - | }); |
| - | } |
| + | function trackEventNow(event) { |
| + | ready(function () { |
| + | var data = eventData(event); |
| + | var param = csrfParam(); |
| + | var token = csrfToken(); |
| + | if (param && token) data[param] = token; |
| + | // stringify so we keep the type |
| + | data.events_json = JSON.stringify(data.events); |
| + | delete data.events; |
| + | window.navigator.sendBeacon(eventsUrl(), (0, _objectToFormdata2.default)(data)); |
| + | }); |
| + | } |
| - | function page() { |
| - | return config.page || window.location.pathname; |
| - | } |
| + | function page() { |
| + | return config.page || window.location.pathname; |
| + | } |
| + | |
| + | function presence(str) { |
| + | return str && str.length > 0 ? str : null; |
| + | } |
| - | function eventProperties(e) { |
| - | var $target = $(e.currentTarget); |
| - | return { |
| - | tag: $target.get(0).tagName.toLowerCase(), |
| - | id: $target.attr("id"), |
| - | "class": $target.attr("class"), |
| - | page: page(), |
| - | section: $target.closest("*[data-section]").attr("data-section") |
| - | }; |
| + | function cleanObject(obj) { |
| + | for (var key in obj) { |
| + | if (obj.hasOwnProperty(key)) { |
| + | if (obj[key] === null) { |
| + | delete obj[key]; |
| + | } |
| + | } |
| } | |
| + | return obj; |
| + | } |
| + | |
| + | function eventProperties(e) { |
| + | var target = e.target; |
| + | return cleanObject({ |
| + | tag: target.tagName.toLowerCase(), |
| + | id: presence(target.id), |
| + | "class": presence(target.className), |
| + | page: page(), |
| + | section: getClosestSection(target) |
| + | }); |
| + | } |
| - | function createVisit() { |
| - | isReady = false; |
| + | function getClosestSection(element) { |
| + | for (; element && element !== document; element = element.parentNode) { |
| + | if (element.hasAttribute('data-section')) { |
| + | return element.getAttribute('data-section'); |
| + | } |
| + | } |
| - | visitId = ahoy.getVisitId(); |
| - | visitorId = ahoy.getVisitorId(); |
| - | track = getCookie("ahoy_track"); |
| + | return null; |
| + | } |
| - | if (visitId && visitorId && !track) { |
| - | // TODO keep visit alive? |
| - | log("Active visit"); |
| - | setReady(); |
| - | } else { |
| - | if (track) { |
| - | destroyCookie("ahoy_track"); |
| - | } |
| + | function createVisit() { |
| + | isReady = false; |
| - | if (!visitId) { |
| - | visitId = generateId(); |
| - | setCookie("ahoy_visit", visitId, visitTtl); |
| - | } |
| + | visitId = ahoy.getVisitId(); |
| + | visitorId = ahoy.getVisitorId(); |
| + | track = getCookie("ahoy_track"); |
| - | // make sure cookies are enabled |
| - | if (getCookie("ahoy_visit")) { |
| - | log("Visit started"); |
| + | if (visitId && visitorId && !track) { |
| + | // TODO keep visit alive? |
| + | log("Active visit"); |
| + | setReady(); |
| + | } else { |
| + | if (track) { |
| + | destroyCookie("ahoy_track"); |
| + | } |
| - | if (!visitorId) { |
| - | visitorId = generateId(); |
| - | setCookie("ahoy_visitor", visitorId, visitorTtl); |
| - | } |
| + | if (!visitId) { |
| + | visitId = generateId(); |
| + | setCookie("ahoy_visit", visitId, visitTtl); |
| + | } |
| - | var data = { |
| - | visit_token: visitId, |
| - | visitor_token: visitorId, |
| - | platform: config.platform, |
| - | landing_page: window.location.href, |
| - | screen_width: window.screen.width, |
| - | screen_height: window.screen.height |
| - | }; |
| - | |
| - | // referrer |
| - | if (document.referrer.length > 0) { |
| - | data.referrer = document.referrer; |
| - | } |
| + | // make sure cookies are enabled |
| + | if (getCookie("ahoy_visit")) { |
| + | log("Visit started"); |
| - | log(data); |
| + | if (!visitorId) { |
| + | visitorId = generateId(); |
| + | setCookie("ahoy_visitor", visitorId, visitorTtl); |
| + | } |
| - | sendRequest(visitsUrl(), data, setReady); |
| - | } else { |
| - | log("Cookies disabled"); |
| - | setReady(); |
| + | var data = { |
| + | visit_token: visitId, |
| + | visitor_token: visitorId, |
| + | platform: config.platform, |
| + | landing_page: window.location.href, |
| + | screen_width: window.screen.width, |
| + | screen_height: window.screen.height |
| + | }; |
| + | |
| + | // referrer |
| + | if (document.referrer.length > 0) { |
| + | data.referrer = document.referrer; |
| } | |
| + | |
| + | log(data); |
| + | |
| + | sendRequest(visitsUrl(), data, setReady); |
| + | } else { |
| + | log("Cookies disabled"); |
| + | setReady(); |
| } | |
| } | |
| - | |
| - | ahoy.getVisitId = ahoy.getVisitToken = function () { |
| - | return getCookie("ahoy_visit"); |
| + | } |
| + | |
| + | ahoy.getVisitId = ahoy.getVisitToken = function () { |
| + | return getCookie("ahoy_visit"); |
| + | }; |
| + | |
| + | ahoy.getVisitorId = ahoy.getVisitorToken = function () { |
| + | return getCookie("ahoy_visitor"); |
| + | }; |
| + | |
| + | ahoy.reset = function () { |
| + | destroyCookie("ahoy_visit"); |
| + | destroyCookie("ahoy_visitor"); |
| + | destroyCookie("ahoy_events"); |
| + | destroyCookie("ahoy_track"); |
| + | return true; |
| + | }; |
| + | |
| + | ahoy.debug = function (enabled) { |
| + | if (enabled === false) { |
| + | destroyCookie("ahoy_debug"); |
| + | } else { |
| + | setCookie("ahoy_debug", "t", 365 * 24 * 60); // 1 year |
| + | } |
| + | return true; |
| + | }; |
| + | |
| + | ahoy.track = function (name, properties) { |
| + | // generate unique id |
| + | var event = { |
| + | name: name, |
| + | properties: properties || {}, |
| + | time: new Date().getTime() / 1000.0, |
| + | id: generateId() |
| }; | |
| - | ahoy.getVisitorId = ahoy.getVisitorToken = function () { |
| - | return getCookie("ahoy_visitor"); |
| - | }; |
| + | // wait for createVisit to log |
| + | documentReady(function () { |
| + | log(event); |
| + | }); |
| - | ahoy.reset = function () { |
| - | destroyCookie("ahoy_visit"); |
| - | destroyCookie("ahoy_visitor"); |
| - | destroyCookie("ahoy_events"); |
| - | destroyCookie("ahoy_track"); |
| - | return true; |
| - | }; |
| + | ready(function () { |
| + | if (!ahoy.getVisitId()) { |
| + | createVisit(); |
| + | } |
| + | |
| + | event.visit_token = ahoy.getVisitId(); |
| + | event.visitor_token = ahoy.getVisitorId(); |
| - | ahoy.debug = function (enabled) { |
| - | if (enabled === false) { |
| - | destroyCookie("ahoy_debug"); |
| + | if (canTrackNow()) { |
| + | trackEventNow(event); |
| } else { | |
| - | setCookie("ahoy_debug", "t", 365 * 24 * 60); // 1 year |
| + | eventQueue.push(event); |
| + | saveEventQueue(); |
| + | |
| + | // wait in case navigating to reduce duplicate events |
| + | setTimeout(function () { |
| + | trackEvent(event); |
| + | }, 1000); |
| } | |
| - | return true; |
| - | }; |
| + | }); |
| + | }; |
| - | ahoy.track = function (name, properties) { |
| - | // generate unique id |
| - | var event = { |
| - | id: generateId(), |
| - | name: name, |
| - | properties: properties || {}, |
| - | time: (new Date()).getTime() / 1000.0 |
| - | }; |
| - | |
| - | // wait for createVisit to log |
| - | $( function () { |
| - | log(event); |
| - | }); |
| + | ahoy.trackView = function (additionalProperties) { |
| + | var properties = { |
| + | url: window.location.href, |
| + | title: document.title, |
| + | page: page() |
| + | }; |
| - | ready( function () { |
| - | if (!ahoy.getVisitId()) { |
| - | createVisit(); |
| + | if (additionalProperties) { |
| + | for (var propName in additionalProperties) { |
| + | if (additionalProperties.hasOwnProperty(propName)) { |
| + | properties[propName] = additionalProperties[propName]; |
| } | |
| + | } |
| + | } |
| + | ahoy.track("$view", properties); |
| + | }; |
| + | |
| + | ahoy.trackClicks = function () { |
| + | onEvent("click", "a, button, input[type=submit]", function (e) { |
| + | var target = e.target; |
| + | var properties = eventProperties(e); |
| + | properties.text = properties.tag == "input" ? target.value : (target.textContent || target.innerText || target.innerHTML).replace(/[\s\r\n]+/g, " ").trim(); |
| + | properties.href = target.href; |
| + | ahoy.track("$click", properties); |
| + | }); |
| + | }; |
| - | event.visit_token = ahoy.getVisitId(); |
| - | event.visitor_token = ahoy.getVisitorId(); |
| + | ahoy.trackSubmits = function () { |
| + | onEvent("submit", "form", function (e) { |
| + | var properties = eventProperties(e); |
| + | ahoy.track("$submit", properties); |
| + | }); |
| + | }; |
| - | if (canTrackNow()) { |
| - | trackEventNow(event); |
| - | } else { |
| - | eventQueue.push(event); |
| - | saveEventQueue(); |
| + | ahoy.trackChanges = function () { |
| + | onEvent("change", "input, textarea, select", function (e) { |
| + | var properties = eventProperties(e); |
| + | ahoy.track("$change", properties); |
| + | }); |
| + | }; |
| + | |
| + | ahoy.trackAll = function () { |
| + | ahoy.trackView(); |
| + | ahoy.trackClicks(); |
| + | ahoy.trackSubmits(); |
| + | ahoy.trackChanges(); |
| + | }; |
| + | |
| + | // push events from queue |
| + | try { |
| + | eventQueue = JSON.parse(getCookie("ahoy_events") || "[]"); |
| + | } catch (e) { |
| + | // do nothing |
| + | } |
| + | |
| + | for (var i = 0; i < eventQueue.length; i++) { |
| + | trackEvent(eventQueue[i]); |
| + | } |
| + | |
| + | ahoy.start = function () { |
| + | createVisit(); |
| + | |
| + | ahoy.start = function () {}; |
| + | }; |
| + | |
| + | documentReady(function () { |
| + | if (config.startOnReady) { |
| + | ahoy.start(); |
| + | } |
| + | }); |
| - | // wait in case navigating to reduce duplicate events |
| - | setTimeout( function () { |
| - | trackEvent(event); |
| - | }, 1000); |
| - | } |
| - | }); |
| - | }; |
| + | exports.default = ahoy; |
| - | ahoy.trackView = function (additionalProperties) { |
| - | var properties = { |
| - | url: window.location.href, |
| - | title: document.title, |
| - | page: page() |
| - | }; |
| - | |
| - | if (additionalProperties) { |
| - | for(var propName in additionalProperties) { |
| - | if (additionalProperties.hasOwnProperty(propName)) { |
| - | properties[propName] = additionalProperties[propName]; |
| - | } |
| - | } |
| - | } |
| - | ahoy.track("$view", properties); |
| - | }; |
| + | /***/ }), |
| + | /* 1 */ |
| + | /***/ (function(module, exports, __webpack_require__) { |
| - | ahoy.trackClicks = function () { |
| - | $(document).on("click", "a, button, input[type=submit]", function (e) { |
| - | var $target = $(e.currentTarget); |
| - | var properties = eventProperties(e); |
| - | properties.text = properties.tag == "input" ? $target.val() : $.trim($target.text().replace(/[\s\r\n]+/g, " ")); |
| - | properties.href = $target.attr("href"); |
| - | ahoy.track("$click", properties); |
| - | }); |
| - | }; |
| + | "use strict"; |
| - | ahoy.trackSubmits = function () { |
| - | $(document).on("submit", "form", function (e) { |
| - | var properties = eventProperties(e); |
| - | ahoy.track("$submit", properties); |
| - | }); |
| - | }; |
| - | ahoy.trackChanges = function () { |
| - | $(document).on("change", "input, textarea, select", function (e) { |
| - | var properties = eventProperties(e); |
| - | ahoy.track("$change", properties); |
| - | }); |
| - | }; |
| + | function isUndefined (value) { |
| + | return value === undefined |
| + | } |
| - | ahoy.trackAll = function() { |
| - | ahoy.trackView(); |
| - | ahoy.trackClicks(); |
| - | ahoy.trackSubmits(); |
| - | ahoy.trackChanges(); |
| - | }; |
| + | function isObject (value) { |
| + | return value === Object(value) |
| + | } |
| - | // push events from queue |
| - | try { |
| - | eventQueue = JSON.parse(getCookie("ahoy_events") || "[]"); |
| - | } catch (e) { |
| - | // do nothing |
| - | } |
| + | function isArray (value) { |
| + | return Array.isArray(value) |
| + | } |
| - | for (var i = 0; i < eventQueue.length; i++) { |
| - | trackEvent(eventQueue[i]); |
| + | function isBlob (value) { |
| + | return value != null && |
| + | typeof value.size === 'number' && |
| + | typeof value.type === 'string' && |
| + | typeof value.slice === 'function' |
| + | } |
| + | |
| + | function isFile (value) { |
| + | return isBlob(value) && |
| + | typeof value.lastModified === 'number' && |
| + | typeof value.name === 'string' |
| + | } |
| + | |
| + | function isDate (value) { |
| + | return value instanceof Date |
| + | } |
| + | |
| + | function objectToFormData (obj, fd, pre) { |
| + | fd = fd || new FormData() |
| + | |
| + | if (isUndefined(obj)) { |
| + | return fd |
| + | } else if (isArray(obj)) { |
| + | obj.forEach(function (value) { |
| + | var key = pre + '[]' |
| + | |
| + | objectToFormData(value, fd, key) |
| + | }) |
| + | } else if (isObject(obj) && !isFile(obj) && !isDate(obj)) { |
| + | Object.keys(obj).forEach(function (prop) { |
| + | var value = obj[prop] |
| + | |
| + | if (isArray(value)) { |
| + | while (prop.length > 2 && prop.lastIndexOf('[]') === prop.length - 2) { |
| + | prop = prop.substring(0, prop.length - 2) |
| + | } |
| + | } |
| + | |
| + | var key = pre ? (pre + '[' + prop + ']') : prop |
| + | |
| + | objectToFormData(value, fd, key) |
| + | }) |
| + | } else { |
| + | fd.append(pre, obj) |
| } | |
| - | ahoy.start = function () { |
| - | createVisit(); |
| + | return fd |
| + | } |
| - | ahoy.start = function () {}; |
| - | }; |
| + | module.exports = objectToFormData |
| - | $( function () { |
| - | if (config.startOnReady) { |
| - | ahoy.start(); |
| - | } |
| - | }); |
| - | window.ahoy = ahoy; |
| - | }(window)); |
| + | /***/ }) |
| + | /******/ ])["default"]; |
| + | }); |