pax_global_header 0000666 0000000 0000000 00000000064 13641751417 0014523 g ustar 00root root 0000000 0000000 52 comment=30dfbe3126c42236ae133fac2b62b0673f88c8fc
ahoy-3.0.2/ 0000775 0000000 0000000 00000000000 13641751417 0012465 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/.github/ 0000775 0000000 0000000 00000000000 13641751417 0014025 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/.github/ISSUE_TEMPLATE.md 0000664 0000000 0000000 00000000215 13641751417 0016530 0 ustar 00root root 0000000 0000000 Hi,
Before creating an issue, please check out the Contributing Guide:
https://github.com/ankane/ahoy/blob/master/CONTRIBUTING.md
Thanks!
ahoy-3.0.2/.gitignore 0000664 0000000 0000000 00000000260 13641751417 0014453 0 ustar 00root root 0000000 0000000 *.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
test/tmp
test/version_tmp
tmp
*.log
*.sqlite
*.lock
ahoy-3.0.2/.travis.yml 0000664 0000000 0000000 00000000771 13641751417 0014603 0 ustar 00root root 0000000 0000000 dist: bionic
language: ruby
jobs:
include:
- rvm: 2.7
gemfile: Gemfile
- rvm: 2.6
gemfile: test/gemfiles/rails52.gemfile
- rvm: 2.5
gemfile: test/gemfiles/rails51.gemfile
- rvm: 2.4
gemfile: test/gemfiles/rails50.gemfile
addons:
postgresql: 10
services:
- postgresql
- mysql
- mongodb
script: bundle exec rake test
before_install:
- mysqladmin create ahoy_test
- createdb ahoy_test
notifications:
email:
on_success: never
on_failure: change
ahoy-3.0.2/CHANGELOG.md 0000664 0000000 0000000 00000014566 13641751417 0014312 0 ustar 00root root 0000000 0000000 ## 3.0.2 (2020-04-03)
- Added `cookie_options`
## 3.0.1 (2019-09-21)
- Made `Ahoy::Tracker` work outside of requests
- Fixed storage of `false` values with customized store
- Fixed error with `user_method` and `Rails::InfoController`
- Gracefully handle `ActionDispatch::RemoteIp::IpSpoofAttackError`
## 3.0.0 (2019-05-29)
- Made Device Detector the default user agent parser
- Made v2 the default bot detection version
- Removed a large number of dependencies
- Removed search keyword detection (most search engines today prevent this)
- Removed support for Rails < 5
## 2.2.1 (2019-05-26)
- Updated Ahoy.js to 0.3.4
- Fixed v2 bot detection
- Added latitude and longitude to installation
## 2.2.0 (2019-01-04)
- Added `amp_event` helper
- Improved bot detection for Device Detector
## 2.1.0 (2018-05-18)
- Added option for IP masking
- Added option to use anonymity sets instead of cookies
- Added `user_agent_parser` option
- Fixed `visitable` for Rails 4.2
- Removed `search_keyword` from new installs
## 2.0.2 (2018-03-14)
- Fixed error on duplicate records
- Fixed message when visit not found for geocoding
- Better compatibility with GeoLite2
- Better browser compatibility for Ahoy.js
## 2.0.1 (2018-02-26)
- Added `Ahoy.server_side_visits = :when_needed` to automatically create visits server-side when needed for events and `visitable`
- Better handling of visit duration and expiration in JavaScript
## 2.0.0 (2018-02-25)
- 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 (2018-02-02)
- Added `gin` index on properties for events
- Fixed `visitable` options when name not provided
## 1.6.0 (2017-05-01)
- Added support for Rails 5.1
## 1.5.5 (2017-03-23)
- Added support for Rails API
- Added NATS and NSQ stores
## 1.5.4 (2017-01-22)
- Fixed issue with duplicate events
- Added support for PostGIS for `where_properties`
## 1.5.3 (2016-10-31)
- Fixed error with Rails 5 and Mongoid 6
- Fixed regression with server not generating visit and visitor tokens
- Accept UTM parameters as request parameters (for native apps)
## 1.5.2 (2016-08-26)
- Better support for Rails 5
## 1.5.1 (2016-08-19)
- Restored throttling after removing side effects
## 1.5.0 (2016-08-19)
- Removed throttling due to unintended side effects with its implementation
- Ensure basic token requirements
- Fixed visit recreation on cookie expiration
- Fixed issue where `/ahoy/visits` is called indefinitely when `Ahoy.cookie_domain = :all`
## 1.4.2 (2016-06-21)
- Fixed issues with `where_properties`
## 1.4.1 (2016-06-20)
- Added `where_properties` method
- Added Kafka store
- Added `mount` option
- Use less intrusive version of `safely`
## 1.4.0 (2016-03-23)
- Use `ActiveRecordTokenStore` by default (integer instead of uuid for id)
- Detect database for `rails g ahoy:stores:active_record` for easier installation
- Use `safely` as default exception handler
- Fixed issue with log silencer
- Use multi-column indexes on `ahoy_events` table creation
## 1.3.1 (2016-03-22)
- Raise errors in test environment
## 1.3.0 (2016-03-06)
- Added throttling
- Added `max_content_length` and `max_events_per_request`
## 1.2.2 (2016-03-05)
- Fixed issue with latest version of `browser` gem
- Added support for RabbitMQ
- Added support for Amazon Kinesis Firehose
- Fixed deprecation warnings in Rails 5
## 1.2.1 (2015-08-14)
- Fixed `SystemStackError: stack level too deep` when used with `activerecord-session_store`
## 1.2.0 (2015-06-07)
- Added support for PostgreSQL `jsonb` column type
- Added Fluentd store
- Added latitude, longitude, and postal_code to visits
- Log exclusions
## 1.1.1 (2015-01-05)
- Better support for Authlogic
- Added `screen_height` and `screen_width`
## 1.1.0 (2014-11-02)
- Added `geocode` option
- Report errors to service by default
- Fixed association mismatch
## 1.0.2 (2014-07-10)
- Fixed BSON for Mongoid 3
- Fixed Doorkeeper integration
- Fixed user tracking in overridden authenticate method
## 1.0.1 (2014-06-27)
- Fixed `visitable` outside of requests
## 1.0.0 (2014-06-18)
- Added support for any data store, and Mongoid out of the box
- Added `track_visits_immediately` option
- Added exception catching and reporting
- Visits expire after inactivity, not fixed interval
- Added `visit_duration` and `visitor_duration` options
## 0.3.2 (2014-06-15)
- Fixed bot exclusion for visits
- Fixed user method
## 0.3.1 (2014-06-12)
- Fixed visitor cookies when set on server
- Added `domain` option for server cookies
## 0.3.0 (2014-06-11)
- Added `current_visit_token` and `current_visitor_token` method
- Switched to UUIDs
- Quiet endpoint requests
- Skip server-side bot events
- Added `request` argument to `exclude_method`
## 0.2.2 (2014-05-26)
- Added `exclude_method` option
- Added support for batch events
- Fixed cookie encoding
- Fixed `options` variable from being modified
## 0.2.1 (2014-05-16)
- Fixed IE 8 error
- Added `track_bots` option
- Added `$authenticate` event
## 0.2.0 (2014-05-13)
- Added event tracking (merged ahoy_events)
- Added ahoy.js
## 0.1.8 (2014-05-11)
- Fixed bug with `user_type` set to `false` instead of `nil`
## 0.1.7 (2014-05-11)
- Made cookie functions public for ahoy_events
## 0.1.6 (2014-05-07)
- Better user agent parser
## 0.1.5 (2014-05-01)
- Added support for Doorkeeper
- Added options to `visitable`
- Added `landing_params` method
## 0.1.4 (2014-04-27)
- Added `ahoy.ready()` and `ahoy.log()` for events
## 0.1.3 (2014-04-24)
- Supports `current_user` from `ApplicationController`
- Added `ahoy.reset()`
- Added `ahoy.debug()`
- Added experimental support for native apps
- Prefer `ahoy` over `Ahoy`
## 0.1.2 (2014-04-15)
- Attach user on Devise sign up
- Ability to specify visit model
## 0.1.1 (2014-03-20)
- Made most database columns optional
- Performance hack for referer-parser
## 0.1.0 (2014-03-19)
- First major release
ahoy-3.0.2/CONTRIBUTING.md 0000664 0000000 0000000 00000002507 13641751417 0014722 0 ustar 00root root 0000000 0000000 # Contributing
First, thanks for wanting to contribute. You’re awesome! :heart:
## Help
We’re not able to provide support through GitHub Issues. If you’re looking for help with your code, try posting on [Stack Overflow](https://stackoverflow.com/).
All features should be documented. If you don’t see a feature in the docs, assume it doesn’t exist.
## Bugs
Think you’ve discovered a bug?
1. Search existing issues to see if it’s been reported.
2. Try the `master` branch to make sure it hasn’t been fixed.
```rb
gem "ahoy_matey", github: "ankane/ahoy"
```
If the above steps don’t help, create an issue. Include:
- Detailed steps to reproduce
- Complete backtraces for exceptions
## New Features
If you’d like to discuss a new feature, create an issue and start the title with `[Idea]`.
## Pull Requests
Fork the project and create a pull request. A few tips:
- Keep changes to a minimum. If you have multiple features or fixes, submit multiple pull requests.
- Follow the existing style. The code should read like it’s written by a single person.
Feel free to open an issue to get feedback on your idea before spending too much time on it.
---
This contributing guide is released under [CCO](https://creativecommons.org/publicdomain/zero/1.0/) (public domain). Use it for your own project without attribution.
ahoy-3.0.2/Gemfile 0000664 0000000 0000000 00000000162 13641751417 0013757 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
# Specify your gem's dependencies in ahoy.gemspec
gemspec
gem "rails", "~> 6.0.0"
ahoy-3.0.2/LICENSE.txt 0000664 0000000 0000000 00000002061 13641751417 0014307 0 ustar 00root root 0000000 0000000 Copyright (c) 2014-2019 Andrew Kane
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
ahoy-3.0.2/README.md 0000664 0000000 0000000 00000036311 13641751417 0013750 0 ustar 00root root 0000000 0000000 # Ahoy
:fire: Simple, powerful, first-party analytics for Rails
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.
:postbox: Check out [Ahoy Email](https://github.com/ankane/ahoy_email) for emails and [Field Test](https://github.com/ankane/field_test) for A/B testing
:tangerine: Battle-tested at [Instacart](https://www.instacart.com/opensource)
[](https://travis-ci.org/ankane/ahoy)
## Installation
Add this line to your application’s Gemfile:
```ruby
gem 'ahoy_matey'
```
And run:
```sh
bundle install
rails generate ahoy:install
rails db:migrate
```
Restart your web server, open a page in your browser, and a visit will be created :tada:
Track your first event from a controller with:
```ruby
ahoy.track "My first event", language: "Ruby"
```
### JavaScript, Native Apps, & AMP
Enable the API in `config/initializers/ahoy.rb`:
```ruby
Ahoy.api = true
```
And restart your web server.
### JavaScript
For Rails 6 / Webpacker, run:
```sh
yarn add ahoy.js
```
And add to `app/javascript/packs/application.js`:
```javascript
import ahoy from "ahoy.js";
```
For Rails 5 / Sprockets, add to `app/assets/javascripts/application.js`:
```javascript
//= require ahoy
```
Track an event with:
```javascript
ahoy.track("My second event", {language: "JavaScript"});
```
### GDPR Compliance
Ahoy provides a number of options to help with GDPR compliance. See the [GDPR section](#gdpr-compliance-1) for more info.
## 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
- **location** - country, region, city, latitude, longitude
- **technology** - browser, OS, device type
- **utm parameters** - source, medium, term, content, campaign
Use the `current_visit` method to access it.
Prevent certain Rails actions from creating visits with:
```ruby
skip_before_action :track_ahoy_visit
```
This is typically useful for APIs. If your entire Rails app is an API, you can use:
```ruby
Ahoy.api_only = true
```
You can also defer visit tracking to JavaScript. This is useful for preventing bots (that aren’t detected by their user agent) and users with cookies disabled from creating a new visit on each request. `:when_needed` will create visits server-side only when needed by events, and `false` will disable server-side creation completely, discarding events without a visit.
```ruby
Ahoy.server_side_visits = :when_needed
```
### Events
Each event has a `name` and `properties`. There are several ways to track events.
#### Ruby
```ruby
ahoy.track "Viewed book", title: "Hot, Flat, and Crowded"
```
Track actions automatically with:
```ruby
class ApplicationController < ActionController::Base
after_action :track_action
protected
def track_action
ahoy.track "Ran action", request.path_parameters
end
end
```
#### JavaScript
```javascript
ahoy.track("Viewed book", {title: "The World is Flat"});
```
Track events automatically with:
```javascript
ahoy.trackAll();
```
See [Ahoy.js](https://github.com/ankane/ahoy.js) for a complete list of features.
#### Native Apps
For Android, check out [Ahoy Android](https://github.com/instacart/ahoy-android). For other platforms, see the [API spec](#api-spec).
#### AMP
```erb
<%= amp_event "Viewed article", title: "Analytics with Rails" %>
```
### Associated Models
Say we want to associate orders with visits. Just add `visitable` to the model.
```ruby
class Order < ApplicationRecord
visitable :ahoy_visit
end
```
When a visitor places an order, the `ahoy_visit_id` column is automatically set :tada:
See where orders are coming from with simple joins:
```ruby
Order.joins(:ahoy_visit).group("referring_domain").count
Order.joins(:ahoy_visit).group("city").count
Order.joins(:ahoy_visit).group("device_type").count
```
Here’s what the migration to add the `ahoy_visit_id` column should look like:
```ruby
class AddVisitIdToOrders < ActiveRecord::Migration[6.0]
def change
add_column :orders, :ahoy_visit_id, :bigint
end
end
```
Customize the column with:
```ruby
visitable :sign_up_visit
```
### Users
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.
With other authentication frameworks, add this to the end of your sign in method:
```ruby
ahoy.authenticate(user)
```
To see the visits for a given user, create an association:
```ruby
class User < ApplicationRecord
has_many :visits, class_name: "Ahoy::Visit"
end
```
And use:
```ruby
User.find(123).visits
```
#### Custom User Method
Use a method besides `current_user`
```ruby
Ahoy.user_method = :true_user
```
or use a proc
```ruby
Ahoy.user_method = ->(controller) { controller.true_user }
```
#### Doorkeeper
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 ApplicationController < ActionController::Base
private
def current_resource_owner
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
end
```
#### Knock
To attach the user with [Knock](https://github.com/nsarno/knock), either include `Knock::Authenticable`in `ApplicationController`:
```ruby
class ApplicationController < ActionController::API
include Knock::Authenticable
end
```
Or include it in Ahoy:
```ruby
Ahoy::BaseController.include Knock::Authenticable
```
And use:
```ruby
Ahoy.user_method = ->(controller) { controller.send(:authenticate_entity, "user") }
```
### Exclusions
Bots are excluded from tracking by default. To include them, use:
```ruby
Ahoy.track_bots = true
```
Add your own rules with:
```ruby
Ahoy.exclude_method = lambda do |controller, request|
request.ip == "192.168.1.1"
end
```
### Visit Duration
By default, a new visit is created after 4 hours of inactivity. Change this with:
```ruby
Ahoy.visit_duration = 30.minutes
```
### Cookies
To track visits across multiple subdomains, use:
```ruby
Ahoy.cookie_domain = :all
```
Set other [cookie options](https://api.rubyonrails.org/classes/ActionDispatch/Cookies.html) with:
```ruby
Ahoy.cookie_options = {same_site: :lax}
```
### Geocoding
Disable geocoding with:
```ruby
Ahoy.geocode = false
```
The default job queue is `:ahoy`. Change this with:
```ruby
Ahoy.job_queue = :low_priority
```
#### Geocoding Performance
To avoid calls to a remote API, download the [GeoLite2 City database](https://dev.maxmind.com/geoip/geoip2/geolite2/) and configure Geocoder to use it.
Add this line to your application’s Gemfile:
```ruby
gem 'maxminddb'
```
And create an initializer at `config/initializers/geocoder.rb` with:
```ruby
Geocoder.configure(
ip_lookup: :geoip2,
geoip2: {
file: Rails.root.join("lib", "GeoLite2-City.mmdb")
}
)
```
If you use Heroku, you can use an unofficial buildpack like [this one](https://github.com/temedica/heroku-buildpack-maxmind-geolite2) to avoid including the database in your repo.
### 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.token_generator = -> { Druuid.gen }
```
### Throttling
You can use [Rack::Attack](https://github.com/kickstarter/rack-attack) to throttle requests to the API.
```ruby
class Rack::Attack
throttle("ahoy/ip", limit: 20, period: 1.minute) do |req|
if req.path.start_with?("/ahoy/")
req.ip
end
end
end
```
### 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. To customize this, use:
```ruby
Safely.report_exception_method = ->(e) { Rollbar.error(e) }
```
## GDPR Compliance
Ahoy provides a number of options to help with [GDPR compliance](https://en.wikipedia.org/wiki/General_Data_Protection_Regulation).
Update `config/initializers/ahoy.rb` with:
```ruby
class Ahoy::Store < Ahoy::DatabaseStore
def authenticate(data)
# disables automatic linking of visits and users
end
end
Ahoy.mask_ips = true
Ahoy.cookies = false
```
This:
- Masks IP addresses
- Switches from cookies to anonymity sets
- Disables automatic linking of visits and users
If you use JavaScript tracking, also set:
```javascript
ahoy.configure({cookies: false});
```
### IP Masking
Ahoy can mask IPs with the same approach [Google Analytics uses for IP anonymization](https://support.google.com/analytics/answer/2763052). This means:
- For IPv4, the last octet is set to 0 (`8.8.4.4` becomes `8.8.4.0`)
- For IPv6, the last 80 bits are set to zeros (`2001:4860:4860:0:0:0:0:8844` becomes `2001:4860:4860::`)
```ruby
Ahoy.mask_ips = true
```
IPs are masked before geolocation is performed.
To mask previously collected IPs, use:
```ruby
Ahoy::Visit.find_each do |visit|
visit.update_column :ip, Ahoy.mask_ip(visit.ip)
end
```
### Anonymity Sets & Cookies
Ahoy can switch from cookies to [anonymity sets](https://privacypatterns.org/patterns/Anonymity-set). Instead of cookies, visitors with the same IP mask and user agent are grouped together in an anonymity set.
```ruby
Ahoy.cookies = false
```
Previously set cookies are automatically deleted.
## Development
Ahoy is built with developers in mind. You can run the following code in your browser’s console.
Force a new visit
```javascript
ahoy.reset(); // then reload the page
```
Log messages
```javascript
ahoy.debug();
```
Turn off logging
```javascript
ahoy.debug(false);
```
Debug API requests in Ruby
```ruby
Ahoy.quiet = false
```
## Data Stores
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
class Ahoy::Store < Ahoy::DatabaseStore
end
```
There are four events data stores can subscribe to:
```ruby
class Ahoy::Store < Ahoy::BaseStore
def track_visit(data)
# new visit
end
def track_event(data)
# new event
end
def geocode(data)
# visit geocoded
end
def authenticate(data)
# user authenticates
end
end
```
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.
### Track Additional Data
```ruby
class Ahoy::Store < Ahoy::DatabaseStore
def track_visit(data)
data[:accept_language] = request.headers["Accept-Language"]
super(data)
end
end
```
Two useful methods you can use are `request` and `controller`.
You can pass additional visit data from JavaScript with:
```javascript
ahoy.configure({visitParams: {referral_code: 123}});
```
And use:
```ruby
class Ahoy::Store < Ahoy::DatabaseStore
def track_visit(data)
data[:referral_code] = request.parameters[:referral_code]
super(data)
end
end
```
### Use Different Models
```ruby
class Ahoy::Store < Ahoy::DatabaseStore
def visit_model
MyVisit
end
def event_model
MyEvent
end
end
```
## Explore the Data
[Blazer](https://github.com/ankane/blazer) is a great tool for exploring your data.
With ActiveRecord, you can do:
```ruby
Ahoy::Visit.group(:search_keyword).count
Ahoy::Visit.group(:country).count
Ahoy::Visit.group(:referring_domain).count
```
[Chartkick](https://www.chartkick.com/) and [Groupdate](https://github.com/ankane/groupdate) make it easy to visualize the data.
```erb
<%= line_chart Ahoy::Visit.group_by_day(:started_at).count %>
```
### Querying Events
Ahoy provides two methods on the event model to make querying easier.
To query on both name and properties, you can use:
```ruby
Ahoy::Event.where_event("Viewed product", product_id: 123).count
```
Or just query properties with:
```ruby
Ahoy::Event.where_props(product_id: 123).count
```
### Funnels
It’s easy to create funnels.
```ruby
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)
```
The same approach also works with visitor tokens.
## Tutorials
- [Tracking Metrics with Ahoy and Blazer](https://gorails.com/episodes/internal-metrics-with-ahoy-and-blazer)
## API Spec
### Visits
Generate visit and visitor tokens as [UUIDs](https://en.wikipedia.org/wiki/Universally_unique_identifier), and include these values in the `Ahoy-Visit` and `Ahoy-Visitor` headers with all requests.
Send a `POST` request to `/ahoy/visits` with `Content-Type: application/json` and a body like:
```json
{
"visit_token": "",
"visitor_token": "",
"platform": "iOS",
"app_version": "1.0.0",
"os_version": "11.2.6"
}
```
After 4 hours of inactivity, create another visit (use the same visitor token).
### Events
Send a `POST` request to `/ahoy/events` with `Content-Type: application/json` and a body like:
```json
{
"visit_token": "",
"visitor_token": "",
"events": [
{
"id": "",
"name": "Viewed item",
"properties": {
"item_id": 123
},
"time": "2018-01-01T00:00:00-07:00"
}
]
}
```
## Upgrading
### 3.0
If you installed Ahoy before 2.1 and want to keep legacy user agent parsing and bot detection, add to your Gemfile:
```ruby
gem "browser", "~> 2.0"
gem "user_agent_parser"
```
And add to `config/initializers/ahoy.rb`:
```ruby
Ahoy.user_agent_parser = :legacy
```
### 2.2
Ahoy now ships with better bot detection if you use Device Detector. This should be more accurate but can significantly reduce the number of visits recorded. For existing installs, it’s opt-in to start. To use it, add to `config/initializers/ahoy.rb`:
```ruby
Ahoy.bot_detection_version = 2
```
### 2.1
Ahoy recommends [Device Detector](https://github.com/podigee/device_detector) for user agent parsing and makes it the default for new installations. To switch, add to `config/initializers/ahoy.rb`:
```ruby
Ahoy.user_agent_parser = :device_detector
```
Backfill existing records with:
```ruby
Ahoy::Visit.find_each do |visit|
client = DeviceDetector.new(visit.user_agent)
device_type =
case client.device_type
when "smartphone"
"Mobile"
when "tv"
"TV"
else
client.device_type.try(:titleize)
end
visit.browser = client.name
visit.os = client.os_name
visit.device_type = device_type
visit.save(validate: false) if visit.changed?
end
```
### 2.0
See the [upgrade guide](docs/Ahoy-2-Upgrade.md)
## History
View the [changelog](https://github.com/ankane/ahoy/blob/master/CHANGELOG.md)
## Contributing
Everyone is encouraged to help improve this project. Here are a few ways you can help:
- [Report bugs](https://github.com/ankane/ahoy/issues)
- Fix bugs and [submit pull requests](https://github.com/ankane/ahoy/pulls)
- Write, clarify, or fix documentation
- Suggest or add new features
ahoy-3.0.2/Rakefile 0000664 0000000 0000000 00000000260 13641751417 0014130 0 ustar 00root root 0000000 0000000 require "bundler/gem_tasks"
require "rake/testtask"
task default: :test
Rake::TestTask.new do |t|
t.libs << "test"
t.pattern = "test/**/*_test.rb"
t.warning = false
end
ahoy-3.0.2/ahoy_matey.gemspec 0000664 0000000 0000000 00000002122 13641751417 0016166 0 ustar 00root root 0000000 0000000 require_relative "lib/ahoy/version"
Gem::Specification.new do |spec|
spec.name = "ahoy_matey"
spec.version = Ahoy::VERSION
spec.summary = "Simple, powerful, first-party analytics for Rails"
spec.homepage = "https://github.com/ankane/ahoy"
spec.license = "MIT"
spec.author = "Andrew Kane"
spec.email = "andrew@chartkick.com"
spec.files = Dir["*.{md,txt}", "{app,config,lib,vendor}/**/*"]
spec.require_path = "lib"
spec.required_ruby_version = ">= 2.4"
spec.add_dependency "activesupport", ">= 5"
spec.add_dependency "geocoder", ">= 1.4.5"
spec.add_dependency "safely_block", ">= 0.2.1"
spec.add_dependency "device_detector"
spec.add_development_dependency "bundler"
spec.add_development_dependency "rake"
spec.add_development_dependency "minitest"
spec.add_development_dependency "combustion"
spec.add_development_dependency "rails"
spec.add_development_dependency "sqlite3"
spec.add_development_dependency "pg"
spec.add_development_dependency "mysql2"
spec.add_development_dependency "mongoid"
end
ahoy-3.0.2/app/ 0000775 0000000 0000000 00000000000 13641751417 0013245 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/app/controllers/ 0000775 0000000 0000000 00000000000 13641751417 0015613 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/app/controllers/ahoy/ 0000775 0000000 0000000 00000000000 13641751417 0016553 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/app/controllers/ahoy/base_controller.rb 0000664 0000000 0000000 00000002040 13641751417 0022251 0 ustar 00root root 0000000 0000000 module Ahoy
class BaseController < ApplicationController
filters = _process_action_callbacks.map(&:filter) - Ahoy.preserve_callbacks
skip_before_action(*filters, raise: false)
skip_after_action(*filters, raise: false)
skip_around_action(*filters, raise: false)
before_action :verify_request_size
before_action :renew_cookies
if respond_to?(:protect_from_forgery)
protect_from_forgery with: :null_session, if: -> { Ahoy.protect_from_forgery }
end
protected
def ahoy
@ahoy ||= Ahoy::Tracker.new(controller: self, api: true)
end
# set proper ttl if cookie generated from JavaScript
# approach is not perfect, as user must reload the page
# for new cookie settings to take effect
def renew_cookies
set_ahoy_cookies if params[:js] && !Ahoy.api_only
end
def verify_request_size
if request.content_length > Ahoy.max_content_length
logger.info "[ahoy] Payload too large"
render plain: "Payload too large\n", status: 413
end
end
end
end
ahoy-3.0.2/app/controllers/ahoy/events_controller.rb 0000664 0000000 0000000 00000001745 13641751417 0022656 0 ustar 00root root 0000000 0000000 module Ahoy
class EventsController < Ahoy::BaseController
def create
events =
if params[:name]
# legacy API and AMP
[request.params]
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(data)
rescue ActiveSupport::JSON.parse_error
# do nothing
[]
end
end
events.first(Ahoy.max_events_per_request).each do |event|
time = Time.zone.parse(event["time"]) rescue nil
# timestamp is deprecated
time ||= Time.zone.at(event["time"].to_f) rescue nil
options = {
id: event["id"],
time: time
}
ahoy.track event["name"], event["properties"], options
end
render json: {}
end
end
end
ahoy-3.0.2/app/controllers/ahoy/visits_controller.rb 0000664 0000000 0000000 00000000472 13641751417 0022667 0 ustar 00root root 0000000 0000000 module Ahoy
class VisitsController < BaseController
def create
ahoy.track_visit
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
ahoy-3.0.2/app/jobs/ 0000775 0000000 0000000 00000000000 13641751417 0014202 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/app/jobs/ahoy/ 0000775 0000000 0000000 00000000000 13641751417 0015142 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/app/jobs/ahoy/geocode_job.rb 0000664 0000000 0000000 00000000342 13641751417 0017725 0 ustar 00root root 0000000 0000000 # 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
ahoy-3.0.2/app/jobs/ahoy/geocode_v2_job.rb 0000664 0000000 0000000 00000001377 13641751417 0020345 0 ustar 00root root 0000000 0000000 module Ahoy
class GeocodeV2Job < ActiveJob::Base
queue_as { Ahoy.job_queue }
def perform(visit_token, ip)
location =
begin
Geocoder.search(ip).first
rescue => e
Ahoy.log "Geocode error: #{e.class.name}: #{e.message}"
nil
end
if location && location.country.present?
data = {
country: location.country,
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
ahoy-3.0.2/config/ 0000775 0000000 0000000 00000000000 13641751417 0013732 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/config/routes.rb 0000664 0000000 0000000 00000000340 13641751417 0015575 0 ustar 00root root 0000000 0000000 Rails.application.routes.draw do
mount Ahoy::Engine => "/ahoy" if Ahoy.api
end
Ahoy::Engine.routes.draw do
scope module: "ahoy" do
resources :visits, only: [:create]
resources :events, only: [:create]
end
end
ahoy-3.0.2/docs/ 0000775 0000000 0000000 00000000000 13641751417 0013415 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/docs/Ahoy-2-Upgrade.md 0000664 0000000 0000000 00000006170 13641751417 0016367 0 ustar 00root root 0000000 0000000 # 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 update ahoy_matey
```
Add to `config/initializers/ahoy.rb`:
```ruby
Ahoy.api = true
Ahoy.server_side_visits = false
```
You can also try the new `Ahoy.server_side_visits = :when_needed` to automatically create visits server-side when needed for events and `visitable`.
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`
ahoy-3.0.2/docs/Data-Store-Examples.md 0000664 0000000 0000000 00000007750 13641751417 0017467 0 ustar 00root root 0000000 0000000 # 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-3.0.2/lib/ 0000775 0000000 0000000 00000000000 13641751417 0013233 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/lib/ahoy.rb 0000664 0000000 0000000 00000005175 13641751417 0014530 0 ustar 00root root 0000000 0000000 require "ipaddr"
# dependencies
require "active_support"
require "active_support/core_ext"
require "geocoder"
require "safely/core"
# modules
require "ahoy/utils"
require "ahoy/base_store"
require "ahoy/controller"
require "ahoy/database_store"
require "ahoy/helper"
require "ahoy/model"
require "ahoy/query_methods"
require "ahoy/tracker"
require "ahoy/version"
require "ahoy/visit_properties"
require "ahoy/engine" if defined?(Rails)
module Ahoy
mattr_accessor :visit_duration
self.visit_duration = 4.hours
mattr_accessor :visitor_duration
self.visitor_duration = 2.years
mattr_accessor :cookies
self.cookies = true
# TODO deprecate in favor of cookie_options
mattr_accessor :cookie_domain
mattr_accessor :cookie_options
self.cookie_options = {}
mattr_accessor :server_side_visits
self.server_side_visits = true
mattr_accessor :quiet
self.quiet = true
mattr_accessor :geocode
self.geocode = true
mattr_accessor :max_content_length
self.max_content_length = 8192
mattr_accessor :max_events_per_request
self.max_events_per_request = 10
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
self.preserve_callbacks = [:load_authlogic, :activate_authlogic]
mattr_accessor :user_method
self.user_method = lambda do |controller|
(controller.respond_to?(:current_user, true) && controller.send(:current_user)) || (controller.respond_to?(:current_resource_owner, true) && controller.send(:current_resource_owner)) || nil
end
mattr_accessor :exclude_method
mattr_accessor :track_bots
self.track_bots = false
mattr_accessor :bot_detection_version
self.bot_detection_version = 2
mattr_accessor :token_generator
self.token_generator = -> { SecureRandom.uuid }
mattr_accessor :mask_ips
self.mask_ips = false
mattr_accessor :user_agent_parser
self.user_agent_parser = :device_detector
mattr_accessor :logger
def self.log(message)
logger.info { "[ahoy] #{message}" } if logger
end
def self.mask_ip(ip)
addr = IPAddr.new(ip)
if addr.ipv4?
# set last octet to 0
addr.mask(24).to_s
else
# set last 80 bits to zeros
addr.mask(48).to_s
end
end
end
ActiveSupport.on_load(:action_controller) do
include Ahoy::Controller
end
ActiveSupport.on_load(:active_record) do
extend Ahoy::Model
end
ActiveSupport.on_load(:action_view) do
include Ahoy::Helper
end
# Mongoid
if defined?(ActiveModel)
ActiveModel::Callbacks.include(Ahoy::Model)
end
ahoy-3.0.2/lib/ahoy/ 0000775 0000000 0000000 00000000000 13641751417 0014173 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/lib/ahoy/base_store.rb 0000664 0000000 0000000 00000003630 13641751417 0016650 0 ustar 00root root 0000000 0000000 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) if controller.respond_to?(Ahoy.user_method, true)
end
end
end
def exclude?
(!Ahoy.track_bots && bot?) || exclude_by_method?
end
def generate_id
Ahoy.token_generator.call
end
def visit_or_create
visit
end
protected
def bot?
unless defined?(@bot)
@bot = begin
if request
if Ahoy.user_agent_parser == :device_detector
detector = DeviceDetector.new(request.user_agent)
if Ahoy.bot_detection_version == 2
detector.bot? || (detector.device_type.nil? && detector.os_name.nil?)
else
detector.bot?
end
else
# no need to throw friendly error if browser isn't defined
# since will error in visit_properties
Browser.new(request.user_agent).bot?
end
else
false
end
end
end
@bot
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-3.0.2/lib/ahoy/controller.rb 0000664 0000000 0000000 00000002245 13641751417 0016706 0 ustar 00root root 0000000 0000000 module Ahoy
module Controller
def self.included(base)
if base.respond_to?(:helper_method)
base.helper_method :current_visit
base.helper_method :ahoy
end
base.before_action :set_ahoy_cookies, unless: -> { Ahoy.api_only }
base.before_action :track_ahoy_visit, unless: -> { Ahoy.api_only }
base.around_action :set_ahoy_request_store
end
def ahoy
@ahoy ||= Ahoy::Tracker.new(controller: self)
end
def current_visit
ahoy.visit
end
def set_ahoy_cookies
if Ahoy.cookies
ahoy.set_visitor_cookie
ahoy.set_visit_cookie
else
# delete cookies if exist
ahoy.reset
end
end
def track_ahoy_visit
defer = Ahoy.server_side_visits != true
if defer && !Ahoy.cookies
# avoid calling new_visit?, which triggers a database call
elsif ahoy.new_visit?
ahoy.track_visit(defer: defer)
end
end
def set_ahoy_request_store
previous_value = Thread.current[:ahoy]
begin
Thread.current[:ahoy] = ahoy
yield
ensure
Thread.current[:ahoy] = previous_value
end
end
end
end
ahoy-3.0.2/lib/ahoy/database_store.rb 0000664 0000000 0000000 00000005126 13641751417 0017504 0 ustar 00root root 0000000 0000000 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)
# so next call to visit will try to fetch from DB
if defined?(@visit)
remove_instance_variable(:@visit)
end
end
def track_event(data)
visit = visit_or_create(started_at: data[:time])
if visit
event = event_model.new(slice_data(event_model, data))
event.visit = visit
event.time = visit.started_at if event.time < visit.started_at
begin
event.save!
rescue => e
raise e unless unique_exception?(e)
end
else
Ahoy.log "Event excluded since visit not created: #{data[:visit_token]}"
end
end
def geocode(data)
visit_token = data.delete(:visit_token)
data = slice_data(visit_model, data)
if defined?(Mongoid::Document) && visit_model < Mongoid::Document
# upsert since visit might not be found due to eventual consistency
visit_model.where(visit_token: visit_token).find_one_and_update({"$set": data}, {upsert: true})
elsif visit
visit.update!(data)
else
Ahoy.log "Visit for geocode not found: #{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
unless defined?(@visit)
@visit = visit_model.where(visit_token: ahoy.visit_token).first if ahoy.visit_token
end
@visit
end
# if we don't have a visit, let's try to create one first
def visit_or_create(started_at: nil)
ahoy.track_visit(started_at: started_at) if !visit && Ahoy.server_side_visits
visit
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.nil? }
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-3.0.2/lib/ahoy/engine.rb 0000664 0000000 0000000 00000001534 13641751417 0015770 0 ustar 00root root 0000000 0000000 module Ahoy
class Engine < ::Rails::Engine
initializer "ahoy", after: "sprockets.environment" do
Ahoy.logger ||= Rails.logger
# allow Devise to be loaded after Ahoy
require "ahoy/warden" if defined?(Warden)
next unless Ahoy.quiet
# Parse PATH_INFO by assets prefix
AHOY_PREFIX = "/ahoy/".freeze
# 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.silence do
call_without_quiet_ahoy(env)
end
else
call_without_quiet_ahoy(env)
end
end
alias_method :call_without_quiet_ahoy, :call
alias_method :call, :call_with_quiet_ahoy
end
end
end
end
ahoy-3.0.2/lib/ahoy/helper.rb 0000664 0000000 0000000 00000002117 13641751417 0016000 0 ustar 00root root 0000000 0000000 module Ahoy
module Helper
def amp_event(name, properties = {})
url = Ahoy::Engine.routes.url_helpers.events_url(
url_options.slice(:host, :port, :protocol).merge(
name: name,
properties: properties,
screen_width: "SCREEN_WIDTH",
screen_height: "SCREEN_HEIGHT",
platform: "Web",
landing_page: "AMPDOC_URL",
referrer: "DOCUMENT_REFERRER",
random: "RANDOM"
)
)
url = "#{url}&visit_token=${clientId(ahoy_visit)}&visitor_token=${clientId(ahoy_visitor)}"
content_tag "amp-analytics" do
content_tag "script", type: "application/json" do
json_escape({
requests: {
pageview: url
},
triggers: {
trackPageview: {
on: "visible",
request: "pageview"
}
},
transport: {
beacon: true,
xhrpost: true,
image: false
}
}.to_json).html_safe
end
end
end
end
end
ahoy-3.0.2/lib/ahoy/model.rb 0000664 0000000 0000000 00000000566 13641751417 0015627 0 ustar 00root root 0000000 0000000 module Ahoy
module Model
def visitable(name = :visit, **options)
class_eval do
belongs_to(name, class_name: "Ahoy::Visit", optional: true, **options)
before_create :set_ahoy_visit
end
class_eval %{
def set_ahoy_visit
self.#{name} ||= Thread.current[:ahoy].try(:visit_or_create)
end
}
end
end
end
ahoy-3.0.2/lib/ahoy/query_methods.rb 0000664 0000000 0000000 00000004623 13641751417 0017415 0 ustar 00root root 0000000 0000000 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}", 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-3.0.2/lib/ahoy/tracker.rb 0000664 0000000 0000000 00000016050 13641751417 0016155 0 ustar 00root root 0000000 0000000 require "active_support/core_ext/digest/uuid"
module Ahoy
class Tracker
UUID_NAMESPACE = "a82ae811-5011-45ab-a728-569df7499c5f"
attr_reader :request, :controller
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]
@user = options[:user]
@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
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(defer: false, started_at: nil)
if exclude?
debug "Visit excluded"
elsif missing_params?
debug "Missing required parameters"
else
if defer
set_cookie("ahoy_track", true, nil, false)
else
delete_cookie("ahoy_track")
data = {
visit_token: visit_token,
visitor_token: visitor_token,
user_id: user.try(:id),
started_at: trusted_time(started_at),
}.merge(visit_properties).select { |_, v| v }
@store.track_visit(data)
Ahoy::GeocodeV2Job.perform_later(visit_token, data[:ip]) if Ahoy.geocode && data[:ip]
end
end
true
rescue => e
report_exception(e)
end
def geocode(data)
if exclude?
debug "Geocode excluded"
else
data = {
visit_token: visit_token
}.merge(data).select { |_, v| v }
@store.geocode(data)
true
end
rescue => e
report_exception(e)
end
def authenticate(user)
if exclude?
debug "Authentication excluded"
else
@store.user = user
data = {
visit_token: visit_token,
user_id: user.try(:id)
}
@store.authenticate(data)
end
true
rescue => e
report_exception(e)
end
def visit
@visit ||= @store.visit
end
def visit_or_create
@visit ||= @store.visit_or_create
end
def new_visit?
Ahoy.cookies ? !existing_visit_token : visit.nil?
end
def new_visitor?
!existing_visitor_token
end
def set_visit_cookie
set_cookie("ahoy_visit", visit_token, Ahoy.visit_duration)
end
def set_visitor_cookie
if new_visitor?
set_cookie("ahoy_visitor", visitor_token, Ahoy.visitor_duration)
end
end
def user
@user ||= @store.user
end
def visit_properties
@visit_properties ||= request ? 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
delete_cookie("ahoy_visitor")
end
def reset_visit
delete_cookie("ahoy_visit")
delete_cookie("ahoy_events")
delete_cookie("ahoy_track")
end
protected
def api?
@options[:api]
end
def missing_params?
if Ahoy.cookies && api? && Ahoy.protect_from_forgery
!(existing_visit_token && existing_visitor_token)
else
false
end
end
def set_cookie(name, value, duration = nil, use_domain = true)
# safety net
return unless Ahoy.cookies && request
cookie = Ahoy.cookie_options.merge(value: value)
cookie[:expires] = duration.from_now if duration
# prefer cookie_options[:domain] over cookie_domain
cookie[:domain] ||= Ahoy.cookie_domain if Ahoy.cookie_domain
cookie.delete(:domain) unless use_domain
request.cookie_jar[name] = cookie
end
def delete_cookie(name)
request.cookie_jar.delete(name) if request && request.cookie_jar[name]
end
def trusted_time(time = nil)
if !time || (api? && !(1.minute.ago..Time.now).cover?(time))
Time.current
else
time
end
end
def exclude?
@store.exclude?
end
def report_exception(e)
if defined?(ActionDispatch::RemoteIp::IpSpoofAttackError) && e.is_a?(ActionDispatch::RemoteIp::IpSpoofAttackError)
debug "Tracking excluded due to IP spoofing"
else
raise e if !defined?(Rails) || Rails.env.development? || Rails.env.test?
Safely.report_exception(e)
end
end
def generate_id
@store.generate_id
end
def visit_token_helper
@visit_token_helper ||= begin
token = existing_visit_token
token ||= visit_anonymity_set unless Ahoy.cookies
token ||= generate_id unless Ahoy.api_only
token
end
end
def visitor_token_helper
@visitor_token_helper ||= begin
token = existing_visitor_token
token ||= visitor_anonymity_set unless Ahoy.cookies
token ||= generate_id unless Ahoy.api_only
token
end
end
def existing_visit_token
@existing_visit_token ||= begin
token = visit_header
token ||= visit_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
token ||= visit_param if api?
token
end
end
def existing_visitor_token
@existing_visitor_token ||= begin
token = visitor_header
token ||= visitor_cookie if Ahoy.cookies && !(api? && Ahoy.protect_from_forgery)
token ||= visitor_param if api?
token
end
end
def visit_anonymity_set
@visit_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visit", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
end
def visitor_anonymity_set
@visitor_anonymity_set ||= Digest::UUID.uuid_v5(UUID_NAMESPACE, ["visitor", Ahoy.mask_ip(request.remote_ip), request.user_agent].join("/"))
end
def visit_cookie
@visit_cookie ||= request && request.cookies["ahoy_visit"]
end
def visitor_cookie
@visitor_cookie ||= request && request.cookies["ahoy_visitor"]
end
def visit_header
@visit_header ||= request && request.headers["Ahoy-Visit"]
end
def visitor_header
@visitor_header ||= request && request.headers["Ahoy-Visitor"]
end
def visit_param
@visit_param ||= request && request.params["visit_token"]
end
def visitor_param
@visitor_param ||= request && request.params["visitor_token"]
end
def ensure_token(token)
token = Ahoy::Utils.ensure_utf8(token)
token.to_s.gsub(/[^a-z0-9\-]/i, "").first(64) if token
end
def debug(message)
Ahoy.log message
end
end
end
ahoy-3.0.2/lib/ahoy/utils.rb 0000664 0000000 0000000 00000000247 13641751417 0015663 0 ustar 00root root 0000000 0000000 module Ahoy
module Utils
def self.ensure_utf8(str)
str.encode("UTF-8", "binary", invalid: :replace, undef: :replace, replace: "") if str
end
end
end
ahoy-3.0.2/lib/ahoy/version.rb 0000664 0000000 0000000 00000000044 13641751417 0016203 0 ustar 00root root 0000000 0000000 module Ahoy
VERSION = "3.0.2"
end
ahoy-3.0.2/lib/ahoy/visit_properties.rb 0000664 0000000 0000000 00000006272 13641751417 0020141 0 ustar 00root root 0000000 0000000 require "cgi"
require "device_detector"
require "uri"
module Ahoy
class VisitProperties
attr_reader :request, :params, :referrer, :landing_page
def initialize(request, api:)
@request = request
@params = request.params
@referrer = api ? params["referrer"] : request.referer
@landing_page = api ? params["landing_page"] : request.original_url
end
def generate
@generate ||= request_properties.merge(tech_properties).merge(traffic_properties).merge(utm_properties)
end
private
def utm_properties
landing_params = {}
begin
landing_uri = URI.parse(landing_page)
# could also use Rack::Utils.parse_nested_query
landing_params = CGI.parse(landing_uri.query) if landing_uri
rescue
# do nothing
end
props = {}
%w(utm_source utm_medium utm_term utm_content utm_campaign).each do |name|
props[name.to_sym] = params[name] || landing_params[name].try(:first)
end
props
end
def traffic_properties
uri = URI.parse(referrer) rescue nil
{
referring_domain: uri.try(:host).try(:first, 255)
}
end
def tech_properties
if Ahoy.user_agent_parser == :device_detector
client = DeviceDetector.new(request.user_agent)
device_type =
case client.device_type
when "smartphone"
"Mobile"
when "tv"
"TV"
else
client.device_type.try(:titleize)
end
{
browser: client.name,
os: client.os_name,
device_type: device_type
}
else
raise "Add browser to your Gemfile to use legacy user agent parsing" unless defined?(Browser)
raise "Add user_agent_parser to your Gemfile to use legacy user agent parsing" unless defined?(UserAgentParser)
# cache for performance
@@user_agent_parser ||= UserAgentParser::Parser.new
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
{
browser: agent.name,
os: agent.os.name,
device_type: device_type
}
end
end
# masking based on Google Analytics anonymization
# https://support.google.com/analytics/answer/2763052
def ip
ip = request.remote_ip
if ip && Ahoy.mask_ips
Ahoy.mask_ip(ip)
else
ip
end
end
def request_properties
{
ip: ip,
user_agent: Ahoy::Utils.ensure_utf8(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
ahoy-3.0.2/lib/ahoy/warden.rb 0000664 0000000 0000000 00000000276 13641751417 0016005 0 ustar 00root root 0000000 0000000 Warden::Manager.after_set_user except: :fetch do |user, auth, _|
request = ActionDispatch::Request.new(auth.env)
ahoy = Ahoy::Tracker.new(request: request)
ahoy.authenticate(user)
end
ahoy-3.0.2/lib/ahoy_matey.rb 0000664 0000000 0000000 00000000017 13641751417 0015715 0 ustar 00root root 0000000 0000000 require "ahoy"
ahoy-3.0.2/lib/generators/ 0000775 0000000 0000000 00000000000 13641751417 0015404 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/lib/generators/ahoy/ 0000775 0000000 0000000 00000000000 13641751417 0016344 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/lib/generators/ahoy/activerecord_generator.rb 0000664 0000000 0000000 00000002456 13641751417 0023420 0 ustar 00root root 0000000 0000000 require "rails/generators/active_record"
module Ahoy
module Generators
class ActiverecordGenerator < Rails::Generators::Base
include ActiveRecord::Generators::Migration
source_root File.join(__dir__, "templates")
class_option :database, type: :string, aliases: "-d"
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
puts "\nAlmost set! Last, run:\n\n rails 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 rails52?
ActiveRecord::VERSION::STRING >= "5.2"
end
def migration_version
"[#{ActiveRecord::VERSION::MAJOR}.#{ActiveRecord::VERSION::MINOR}]"
end
end
end
end
ahoy-3.0.2/lib/generators/ahoy/base_generator.rb 0000664 0000000 0000000 00000000442 13641751417 0021651 0 ustar 00root root 0000000 0000000 require "rails/generators"
module Ahoy
module Generators
class BaseGenerator < Rails::Generators::Base
source_root File.join(__dir__, "templates")
def copy_templates
template "base_store_initializer.rb", "config/initializers/ahoy.rb"
end
end
end
end
ahoy-3.0.2/lib/generators/ahoy/install_generator.rb 0000664 0000000 0000000 00000001576 13641751417 0022416 0 ustar 00root root 0000000 0000000 require "rails/generators"
module Ahoy
module Generators
class InstallGenerator < Rails::Generators::Base
source_root File.join(__dir__, "templates")
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
ahoy-3.0.2/lib/generators/ahoy/mongoid_generator.rb 0000664 0000000 0000000 00000001003 13641751417 0022365 0 ustar 00root root 0000000 0000000 require "rails/generators"
module Ahoy
module Generators
class MongoidGenerator < Rails::Generators::Base
source_root File.join(__dir__, "templates")
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
end
end
end
ahoy-3.0.2/lib/generators/ahoy/templates/ 0000775 0000000 0000000 00000000000 13641751417 0020342 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/lib/generators/ahoy/templates/active_record_event_model.rb.tt 0000664 0000000 0000000 00000000354 13641751417 0026511 0 ustar 00root root 0000000 0000000 class Ahoy::Event < ApplicationRecord
include Ahoy::QueryMethods
self.table_name = "ahoy_events"
belongs_to :visit
belongs_to :user, optional: true<% if properties_type == "text" %>
serialize :properties, JSON<% end %>
end
ahoy-3.0.2/lib/generators/ahoy/templates/active_record_migration.rb.tt 0000664 0000000 0000000 00000002755 13641751417 0026210 0 ustar 00root root 0000000 0000000 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.text :landing_page
# technology
t.string :browser
t.string :os
t.string :device_type
# location
t.string :country
t.string :region
t.string :city
t.float :latitude
t.float :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 :app_version
t.string :os_version
t.string :platform
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" %><% if rails52? %>
add_index :ahoy_events, :properties, using: :gin, opclass: :jsonb_path_ops<% else %>
add_index :ahoy_events, "properties jsonb_path_ops", using: "gin"<% end %><% end %>
end
end
ahoy-3.0.2/lib/generators/ahoy/templates/active_record_visit_model.rb.tt 0000664 0000000 0000000 00000000236 13641751417 0026525 0 ustar 00root root 0000000 0000000 class Ahoy::Visit < ApplicationRecord
self.table_name = "ahoy_visits"
has_many :events, class_name: "Ahoy::Event"
belongs_to :user, optional: true
end
ahoy-3.0.2/lib/generators/ahoy/templates/base_store_initializer.rb.tt 0000664 0000000 0000000 00000000411 13641751417 0026042 0 ustar 00root root 0000000 0000000 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
# set to true for JavaScript tracking
Ahoy.api = false
ahoy-3.0.2/lib/generators/ahoy/templates/database_store_initializer.rb.tt 0000664 0000000 0000000 00000000144 13641751417 0026677 0 ustar 00root root 0000000 0000000 class Ahoy::Store < Ahoy::DatabaseStore
end
# set to true for JavaScript tracking
Ahoy.api = false
ahoy-3.0.2/lib/generators/ahoy/templates/mongoid_event_model.rb.tt 0000664 0000000 0000000 00000000424 13641751417 0025332 0 ustar 00root root 0000000 0000000 class Ahoy::Event
include Mongoid::Document
# associations
belongs_to :visit, index: true
belongs_to :user, index: true, optional: true
# fields
field :name, type: String
field :properties, type: Hash
field :time, type: Time
index({name: 1, time: 1})
end
ahoy-3.0.2/lib/generators/ahoy/templates/mongoid_visit_model.rb.tt 0000664 0000000 0000000 00000002231 13641751417 0025345 0 ustar 00root root 0000000 0000000 class Ahoy::Visit
include Mongoid::Document
# associations
has_many :events, class_name: "Ahoy::Event"
belongs_to :user, index: true, optional: true
# 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 :referring_domain, type: String
field :landing_page, type: String
# technology
field :browser, type: String
field :os, type: String
field :device_type, type: String
# location
field :country, type: String
field :region, type: String
field :city, type: String
field :latitude, type: Float
field :longitude, type: Float
# 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
# native apps
field :app_version, type: String
field :os_version, type: String
field :platform, type: String
field :started_at, type: Time
index({visit_token: 1}, {unique: true})
end
ahoy-3.0.2/test/ 0000775 0000000 0000000 00000000000 13641751417 0013444 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/controller_test.rb 0000664 0000000 0000000 00000002244 13641751417 0017215 0 ustar 00root root 0000000 0000000 require_relative "test_helper"
class ControllerTest < ActionDispatch::IntegrationTest
def setup
Ahoy::Visit.delete_all
Ahoy::Event.delete_all
end
def test_works
get products_url
assert :success
assert_equal 1, Ahoy::Visit.count
assert_equal 1, Ahoy::Event.count
event = Ahoy::Event.last
assert_equal "Viewed products", event.name
end
def test_bad_visit_cookie
make_request(cookies: {"ahoy_visit" => "badtoken\255"})
assert_equal ahoy.visit_token, "badtoken"
end
def test_bad_visitor_cookie
make_request(cookies: {"ahoy_visitor" => "badtoken\255"})
assert_equal ahoy.visitor_token, "badtoken"
end
def test_bad_visit_header
make_request(headers: {"Ahoy-Visit" => "badtoken\255"})
assert_equal ahoy.visit_token, "badtoken"
end
def test_bad_visitor_header
make_request(headers: {"Ahoy-Visitor" => "badtoken\255"})
assert_equal ahoy.visitor_token, "badtoken"
end
private
def make_request(cookies: {}, headers: {})
cookies.each do |k, v|
self.cookies[k] = v
end
get products_url, headers: headers
assert_response :success
end
def ahoy
controller.ahoy
end
end
ahoy-3.0.2/test/gemfiles/ 0000775 0000000 0000000 00000000000 13641751417 0015237 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/gemfiles/rails50.gemfile 0000664 0000000 0000000 00000000151 13641751417 0020045 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec path: "../../"
gem "rails", "~> 5.0.0"
gem "sqlite3", "~> 1.3.0"
ahoy-3.0.2/test/gemfiles/rails51.gemfile 0000664 0000000 0000000 00000000117 13641751417 0020050 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec path: "../../"
gem "rails", "~> 5.1.0"
ahoy-3.0.2/test/gemfiles/rails52.gemfile 0000664 0000000 0000000 00000000117 13641751417 0020051 0 ustar 00root root 0000000 0000000 source "https://rubygems.org"
gemspec path: "../../"
gem "rails", "~> 5.2.0"
ahoy-3.0.2/test/internal/ 0000775 0000000 0000000 00000000000 13641751417 0015260 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/app/ 0000775 0000000 0000000 00000000000 13641751417 0016040 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/app/controllers/ 0000775 0000000 0000000 00000000000 13641751417 0020406 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/app/controllers/products_controller.rb 0000664 0000000 0000000 00000000175 13641751417 0025044 0 ustar 00root root 0000000 0000000 class ProductsController < ActionController::Base
def index
ahoy.track "Viewed products"
render json: {}
end
end
ahoy-3.0.2/test/internal/app/models/ 0000775 0000000 0000000 00000000000 13641751417 0017323 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/app/models/ahoy/ 0000775 0000000 0000000 00000000000 13641751417 0020263 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/app/models/ahoy/event.rb 0000664 0000000 0000000 00000000301 13641751417 0021723 0 ustar 00root root 0000000 0000000 class Ahoy::Event < ApplicationRecord
include Ahoy::QueryMethods
self.table_name = "ahoy_events"
belongs_to :visit
belongs_to :user, optional: true
serialize :properties, JSON
end
ahoy-3.0.2/test/internal/app/models/ahoy/visit.rb 0000664 0000000 0000000 00000000236 13641751417 0021747 0 ustar 00root root 0000000 0000000 class Ahoy::Visit < ApplicationRecord
self.table_name = "ahoy_visits"
has_many :events, class_name: "Ahoy::Event"
belongs_to :user, optional: true
end
ahoy-3.0.2/test/internal/app/models/application_record.rb 0000664 0000000 0000000 00000000116 13641751417 0023507 0 ustar 00root root 0000000 0000000 class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
end
ahoy-3.0.2/test/internal/config/ 0000775 0000000 0000000 00000000000 13641751417 0016525 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/config/database.yml 0000664 0000000 0000000 00000000100 13641751417 0021003 0 ustar 00root root 0000000 0000000 test:
adapter: sqlite3
database: db/combustion_test.sqlite
ahoy-3.0.2/test/internal/config/initializers/ 0000775 0000000 0000000 00000000000 13641751417 0021233 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/config/initializers/ahoy.rb 0000664 0000000 0000000 00000000104 13641751417 0022513 0 ustar 00root root 0000000 0000000 class Ahoy::Store < Ahoy::DatabaseStore
end
Ahoy.track_bots = true
ahoy-3.0.2/test/internal/config/routes.rb 0000664 0000000 0000000 00000000113 13641751417 0020366 0 ustar 00root root 0000000 0000000 Rails.application.routes.draw do
resources :products, only: [:index]
end
ahoy-3.0.2/test/internal/db/ 0000775 0000000 0000000 00000000000 13641751417 0015645 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/internal/db/schema.rb 0000664 0000000 0000000 00000002126 13641751417 0017433 0 ustar 00root root 0000000 0000000 ActiveRecord::Schema.define do
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.text :landing_page
# technology
t.string :browser
t.string :os
t.string :device_type
# location
t.string :country
t.string :region
t.string :city
t.float :latitude
t.float :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 :app_version
t.string :os_version
t.string :platform
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.text :properties
t.timestamp :time
end
add_index :ahoy_events, [:name, :time]
end
ahoy-3.0.2/test/query_methods/ 0000775 0000000 0000000 00000000000 13641751417 0016334 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/query_methods/mongoid_test.rb 0000664 0000000 0000000 00000000373 13641751417 0021357 0 ustar 00root root 0000000 0000000 require_relative "../test_helper"
class MongoidEvent
include Mongoid::Document
include Ahoy::QueryMethods
field :properties, type: Hash
end
class MongoidTest < Minitest::Test
include QueryMethodsTest
def model
MongoidEvent
end
end
ahoy-3.0.2/test/query_methods/mysql_json_test.rb 0000664 0000000 0000000 00000000357 13641751417 0022123 0 ustar 00root root 0000000 0000000 require_relative "../test_helper"
class MysqlJsonEvent < MysqlBase
serialize :properties, JSON if connection.send(:mariadb?)
end
class MysqlJsonTest < Minitest::Test
include QueryMethodsTest
def model
MysqlJsonEvent
end
end
ahoy-3.0.2/test/query_methods/mysql_text_test.rb 0000664 0000000 0000000 00000000321 13641751417 0022125 0 ustar 00root root 0000000 0000000 require_relative "../test_helper"
class MysqlTextEvent < MysqlBase
serialize :properties, JSON
end
class MysqlTextTest < Minitest::Test
include QueryMethodsTest
def model
MysqlTextEvent
end
end
ahoy-3.0.2/test/query_methods/postgresql_hstore_test.rb 0000664 0000000 0000000 00000000315 13641751417 0023506 0 ustar 00root root 0000000 0000000 require_relative "../test_helper"
class PostgresqlHstoreEvent < PostgresqlBase
end
class PostgresqlHstoreTest < Minitest::Test
include QueryMethodsTest
def model
PostgresqlHstoreEvent
end
end
ahoy-3.0.2/test/query_methods/postgresql_json_test.rb 0000664 0000000 0000000 00000000307 13641751417 0023154 0 ustar 00root root 0000000 0000000 require_relative "../test_helper"
class PostgresqlJsonEvent < PostgresqlBase
end
class PostgresqlJsonTest < Minitest::Test
include QueryMethodsTest
def model
PostgresqlJsonEvent
end
end
ahoy-3.0.2/test/query_methods/postgresql_jsonb_test.rb 0000664 0000000 0000000 00000000312 13641751417 0023312 0 ustar 00root root 0000000 0000000 require_relative "../test_helper"
class PostgresqlJsonbEvent < PostgresqlBase
end
class PostgresqlJsonbTest < Minitest::Test
include QueryMethodsTest
def model
PostgresqlJsonbEvent
end
end
ahoy-3.0.2/test/query_methods/postgresql_text_test.rb 0000664 0000000 0000000 00000000345 13641751417 0023171 0 ustar 00root root 0000000 0000000 require_relative "../test_helper"
class PostgresqlTextEvent < PostgresqlBase
serialize :properties, JSON
end
class PostgresqlTextTest < Minitest::Test
include QueryMethodsTest
def model
PostgresqlTextEvent
end
end
ahoy-3.0.2/test/support/ 0000775 0000000 0000000 00000000000 13641751417 0015160 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/test/support/mongoid.rb 0000664 0000000 0000000 00000000257 13641751417 0017145 0 ustar 00root root 0000000 0000000 Mongoid.logger.level = Logger::WARN
Mongo::Logger.logger.level = Logger::WARN
Mongoid.configure do |config|
config.connect_to("ahoy_test", server_selection_timeout: 1)
end
ahoy-3.0.2/test/support/mysql.rb 0000664 0000000 0000000 00000000745 13641751417 0016660 0 ustar 00root root 0000000 0000000 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
ActiveRecord::Migration.create_table :mysql_json_events, force: true do |t|
t.json :properties
end
class MysqlBase < ActiveRecord::Base
include Ahoy::QueryMethods
establish_connection adapter: "mysql2", username: "root", database: "ahoy_test"
self.abstract_class = true
end
ahoy-3.0.2/test/support/postgresql.rb 0000664 0000000 0000000 00000001412 13641751417 0017706 0 ustar 00root root 0000000 0000000 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
ActiveRecord::Migration.create_table :postgresql_json_events, force: true do |t|
t.json :properties
end
ActiveRecord::Migration.create_table :postgresql_jsonb_events, force: true do |t|
t.jsonb :properties
t.index :properties, using: :gin
end
ActiveRecord::Migration.create_table :postgresql_text_events, force: true do |t|
t.text :properties
end
class PostgresqlBase < ActiveRecord::Base
include Ahoy::QueryMethods
establish_connection adapter: "postgresql", database: "ahoy_test"
self.abstract_class = true
end
ahoy-3.0.2/test/support/query_methods_test.rb 0000664 0000000 0000000 00000003124 13641751417 0021434 0 ustar 00root root 0000000 0000000 module QueryMethodsTest
def setup
model.delete_all
end
def test_empty
assert_equal 0, count_events({})
end
def test_string
create_event value: "world"
assert_equal 1, count_events(value: "world")
end
def test_number
create_event value: 1
assert_equal 1, count_events(value: 1)
end
def test_date
today = Date.today
create_event value: today
assert_equal 1, count_events(value: today)
end
def test_time
now = Time.now
create_event value: now
assert_equal 1, count_events(value: now)
end
def test_true
create_event value: true
assert_equal 1, count_events(value: true)
end
def test_false
create_event value: false
assert_equal 1, count_events(value: false)
end
def test_nil
create_event value: nil
assert_equal 1, count_events(value: nil)
end
def test_any
create_event hello: "world", prop2: "hi"
assert_equal 1, count_events(hello: "world")
end
def test_multiple
create_event prop1: "hi", prop2: "bye"
assert_equal 1, count_events(prop1: "hi", prop2: "bye")
end
def test_multiple_order
create_event prop2: "bye", prop1: "hi"
assert_equal 1, count_events(prop1: "hi", prop2: "bye")
end
def test_partial
create_event hello: "world"
assert_equal 0, count_events(hello: "world", prop2: "hi")
end
def test_prefix
create_event value: 123
assert_equal 0, count_events(value: 1)
end
def create_event(properties)
model.create(properties: properties)
end
def count_events(properties)
model.where_properties(properties).count
end
end
ahoy-3.0.2/test/test_helper.rb 0000664 0000000 0000000 00000001552 13641751417 0016312 0 ustar 00root root 0000000 0000000 require "bundler/setup"
require "combustion"
Bundler.require(:default)
require "minitest/autorun"
require "minitest/pride"
require "active_record"
require "mongoid"
Combustion.path = "test/internal"
Combustion.initialize! :active_record, :action_controller, :active_job do
if ActiveRecord::VERSION::MAJOR < 6 && config.active_record.sqlite3.respond_to?(:represent_boolean_as_integer)
config.active_record.sqlite3.represent_boolean_as_integer = true
end
logger = ActiveSupport::Logger.new(STDOUT)
config.active_record.logger = logger if ENV["VERBOSE"]
config.action_mailer.logger = logger if ENV["VERBOSE"]
end
# run setup / migrations
require_relative "support/mysql"
require_relative "support/postgresql"
require_relative "support/mongoid"
# restore connection
ActiveRecord::Base.establish_connection(:test)
require_relative "support/query_methods_test"
ahoy-3.0.2/test/tracker_test.rb 0000664 0000000 0000000 00000000510 13641751417 0016457 0 ustar 00root root 0000000 0000000 require_relative "test_helper"
class TrackerTest < Minitest::Test
def test_no_request
ahoy = Ahoy::Tracker.new
assert ahoy.track("Some event", some_prop: true)
end
def test_user_option
user = OpenStruct.new(id: "123")
ahoy = Ahoy::Tracker.new(user: user)
assert_equal ahoy.user.id, user.id
end
end
ahoy-3.0.2/vendor/ 0000775 0000000 0000000 00000000000 13641751417 0013762 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/vendor/assets/ 0000775 0000000 0000000 00000000000 13641751417 0015264 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/vendor/assets/javascripts/ 0000775 0000000 0000000 00000000000 13641751417 0017615 5 ustar 00root root 0000000 0000000 ahoy-3.0.2/vendor/assets/javascripts/ahoy.js 0000664 0000000 0000000 00000035273 13641751417 0021125 0 ustar 00root root 0000000 0000000 /*
* Ahoy.js
* Simple, powerful JavaScript analytics
* https://github.com/ankane/ahoy.js
* v0.3.4
* MIT License
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.ahoy = factory());
}(this, (function () { 'use strict';
function isUndefined(value) {
return value === undefined;
}
function isNull(value) {
return value === null;
}
function isObject(value) {
return value === Object(value);
}
function isArray(value) {
return Array.isArray(value);
}
function isDate(value) {
return value instanceof Date;
}
function isBlob(value) {
return (
value &&
typeof value.size === 'number' &&
typeof value.type === 'string' &&
typeof value.slice === 'function'
);
}
function isFile(value) {
return (
isBlob(value) &&
(typeof value.lastModifiedDate === 'object' ||
typeof value.lastModified === 'number') &&
typeof value.name === 'string'
);
}
function isFormData(value) {
return value instanceof FormData;
}
function objectToFormData(obj, cfg, fd, pre) {
if (isFormData(cfg)) {
pre = fd;
fd = cfg;
cfg = null;
}
cfg = cfg || {};
cfg.indices = isUndefined(cfg.indices) ? false : cfg.indices;
cfg.nulls = isUndefined(cfg.nulls) ? true : cfg.nulls;
fd = fd || new FormData();
if (isUndefined(obj)) {
return fd;
} else if (isNull(obj)) {
if (cfg.nulls) {
fd.append(pre, '');
}
} else if (isArray(obj)) {
if (!obj.length) {
var key = pre + '[]';
fd.append(key, '');
} else {
obj.forEach(function(value, index) {
var key = pre + '[' + (cfg.indices ? index : '') + ']';
objectToFormData(value, cfg, fd, key);
});
}
} else if (isDate(obj)) {
fd.append(pre, obj.toISOString());
} else if (isObject(obj) && !isFile(obj) && !isBlob(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, cfg, fd, key);
});
} else {
fd.append(pre, obj);
}
return fd;
}
var objectToFormdata = objectToFormData;
// https://www.quirksmode.org/js/cookies.html
var Cookies = {
set: function (name, value, ttl, domain) {
var expires = "";
var cookieDomain = "";
if (ttl) {
var date = new Date();
date.setTime(date.getTime() + (ttl * 60 * 1000));
expires = "; expires=" + date.toGMTString();
}
if (domain) {
cookieDomain = "; domain=" + domain;
}
document.cookie = name + "=" + escape(value) + expires + cookieDomain + "; path=/";
},
get: function (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;
}
};
var config = {
urlPrefix: "",
visitsUrl: "/ahoy/visits",
eventsUrl: "/ahoy/events",
page: null,
platform: "Web",
useBeacon: true,
startOnReady: true,
trackVisits: true,
cookies: true,
cookieDomain: null,
headers: {},
visitParams: {},
withCredentials: false
};
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;
}
function isEmpty(obj) {
return Object.keys(obj).length === 0;
}
function canTrackNow() {
return (config.useBeacon || config.trackNow) && isEmpty(config.headers) && canStringify && typeof(window.navigator.sendBeacon) !== "undefined" && !config.withCredentials;
}
// cookies
function setCookie(name, value, ttl) {
Cookies.set(name, value, ttl, config.cookieDomain || config.domain);
}
function getCookie(name) {
return Cookies.get(name);
}
function destroyCookie(name) {
Cookies.set(name, "", -1);
}
function log(message) {
if (getCookie("ahoy_debug")) {
window.console.log(message);
}
}
function setReady() {
var callback;
while ((callback = queue.shift())) {
callback();
}
isReady = true;
}
function ready(callback) {
if (isReady) {
callback();
} else {
queue.push(callback);
}
}
function matchesSelector(element, selector) {
var matches = element.matches ||
element.matchesSelector ||
element.mozMatchesSelector ||
element.msMatchesSelector ||
element.oMatchesSelector ||
element.webkitMatchesSelector;
if (matches) {
return matches.apply(element, [selector]);
} else {
log("Unable to match");
return false;
}
}
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);
}
// https://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() {
if (config.cookies && canStringify) {
setCookie("ahoy_events", JSON.stringify(eventQueue), 1);
}
}
// from rails-ujs
function csrfToken() {
var meta = document.querySelector("meta[name=csrf-token]");
return meta && meta.content;
}
function csrfParam() {
var meta = document.querySelector("meta[name=csrf-param]");
return meta && meta.content;
}
function CSRFProtection(xhr) {
var token = csrfToken();
if (token) { xhr.setRequestHeader("X-CSRF-Token", token); }
}
function sendRequest(url, data, success) {
if (canStringify) {
if ($) {
$.ajax({
type: "POST",
url: url,
data: JSON.stringify(data),
contentType: "application/json; charset=utf-8",
dataType: "json",
beforeSend: CSRFProtection,
success: success,
headers: config.headers,
xhrFields: {
withCredentials: config.withCredentials
}
});
} else {
var xhr = new XMLHttpRequest();
xhr.open("POST", url, true);
xhr.withCredentials = config.withCredentials;
xhr.setRequestHeader("Content-Type", "application/json");
for (var header in config.headers) {
if (config.headers.hasOwnProperty(header)) {
xhr.setRequestHeader(header, config.headers[header]);
}
}
xhr.onload = function() {
if (xhr.status === 200) {
success();
}
};
CSRFProtection(xhr);
xhr.send(JSON.stringify(data));
}
}
}
function eventData(event) {
var data = {
events: [event]
};
if (config.cookies) {
data.visit_token = event.visit_token;
data.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();
});
});
}
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(), objectToFormdata(data));
});
}
function page() {
return config.page || window.location.pathname;
}
function presence(str) {
return (str && str.length > 0) ? str : null;
}
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 getClosestSection(element) {
for ( ; element && element !== document; element = element.parentNode) {
if (element.hasAttribute('data-section')) {
return element.getAttribute('data-section');
}
}
return null;
}
function createVisit() {
isReady = false;
visitId = ahoy.getVisitId();
visitorId = ahoy.getVisitorId();
track = getCookie("ahoy_track");
if (config.cookies === false || config.trackVisits === false) {
log("Visit tracking disabled");
setReady();
} else if (visitId && visitorId && !track) {
// TODO keep visit alive?
log("Active visit");
setReady();
} else {
if (!visitId) {
visitId = generateId();
setCookie("ahoy_visit", visitId, visitTtl);
}
// make sure cookies are enabled
if (getCookie("ahoy_visit")) {
log("Visit started");
if (!visitorId) {
visitorId = generateId();
setCookie("ahoy_visitor", visitorId, visitorTtl);
}
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,
js: true
};
// referrer
if (document.referrer.length > 0) {
data.referrer = document.referrer;
}
for (var key in config.visitParams) {
if (config.visitParams.hasOwnProperty(key)) {
data[key] = config.visitParams[key];
}
}
log(data);
sendRequest(visitsUrl(), data, function () {
// wait until successful to destroy
destroyCookie("ahoy_track");
setReady();
});
} else {
log("Cookies disabled");
setReady();
}
}
}
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(),
js: true
};
ready( function () {
if (config.cookies && !ahoy.getVisitId()) {
createVisit();
}
ready( function () {
log(event);
event.visit_token = ahoy.getVisitId();
event.visitor_token = ahoy.getVisitorId();
if (canTrackNow()) {
trackEventNow(event);
} else {
eventQueue.push(event);
saveEventQueue();
// wait in case navigating to reduce duplicate events
setTimeout( function () {
trackEvent(event);
}, 1000);
}
});
});
return true;
};
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);
};
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);
});
};
ahoy.trackSubmits = function () {
onEvent("submit", "form", function (e) {
var properties = eventProperties(e);
ahoy.track("$submit", properties);
});
};
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();
}
});
return ahoy;
})));