├── spec ├── dummy │ ├── log │ │ └── .keep │ ├── app │ │ ├── mailers │ │ │ └── .keep │ │ ├── models │ │ │ ├── .keep │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── post.rb │ │ │ ├── company.rb │ │ │ ├── request.rb │ │ │ ├── support.rb │ │ │ └── user.rb │ │ ├── assets │ │ │ ├── images │ │ │ │ └── .keep │ │ │ ├── javascripts │ │ │ │ └── application.js │ │ │ └── stylesheets │ │ │ │ └── application.css │ │ ├── controllers │ │ │ ├── concerns │ │ │ │ └── .keep │ │ │ ├── welcome_controller.rb │ │ │ ├── application_controller.rb │ │ │ └── posts_controller.rb │ │ ├── views │ │ │ ├── welcome │ │ │ │ └── index.html.erb │ │ │ └── layouts │ │ │ │ └── application.html.erb │ │ └── helpers │ │ │ └── application_helper.rb │ ├── lib │ │ └── assets │ │ │ └── .keep │ ├── public │ │ ├── favicon.ico │ │ ├── 500.html │ │ ├── 422.html │ │ └── 404.html │ ├── .rspec │ ├── bin │ │ ├── rake │ │ ├── bundle │ │ ├── rails │ │ └── setup │ ├── spec │ │ ├── support │ │ │ └── controllers_compatibility.rb │ │ ├── controllers │ │ │ ├── users_controller_spec.rb │ │ │ ├── sessions_controller_spec.rb │ │ │ └── sources_controller_spec.rb │ │ ├── features │ │ │ ├── linking_users_spec.rb │ │ │ └── js_part_spec.rb │ │ ├── models │ │ │ ├── sources_tracked_object_spec.rb │ │ │ ├── session_spec.rb │ │ │ ├── user_spec.rb │ │ │ └── source_spec.rb │ │ ├── lib │ │ │ ├── tracked_model_additions_spec.rb │ │ │ ├── controller_additions_spec.rb │ │ │ ├── statistics_spec.rb │ │ │ ├── markup_generator_spec.rb │ │ │ └── owner_model_additions_spec.rb │ │ ├── rails_helper.rb │ │ └── spec_helper.rb │ ├── config.ru │ ├── config │ │ ├── initializers │ │ │ ├── cookies_serializer.rb │ │ │ ├── session_store.rb │ │ │ ├── mime_types.rb │ │ │ ├── filter_parameter_logging.rb │ │ │ ├── backtrace_silencers.rb │ │ │ ├── assets.rb │ │ │ ├── wrap_parameters.rb │ │ │ └── inflections.rb │ │ ├── environment.rb │ │ ├── routes.rb │ │ ├── boot.rb │ │ ├── database.yml │ │ ├── locales │ │ │ └── en.yml │ │ ├── secrets.yml │ │ ├── application.rb │ │ └── environments │ │ │ ├── development.rb │ │ │ ├── test.rb │ │ │ └── production.rb │ ├── db │ │ └── migrate │ │ │ ├── 20160806143430_create_supports.rb │ │ │ ├── 20160705112156_create_users.rb │ │ │ ├── 20160708153841_create_companies.rb │ │ │ ├── 20160723153334_create_requests.rb │ │ │ └── 20160705114108_create_posts.rb │ ├── Rakefile │ └── README.rdoc └── travis │ ├── database.travis.yml │ └── gemfiles │ ├── Gemfile-5 │ └── Gemfile-4.2 ├── lib ├── referrer │ ├── version.rb │ └── engine.rb ├── tasks │ └── referrer_tasks.rake ├── concerns │ ├── models │ │ ├── tracked_model_additions.rb │ │ └── owner_model_additions.rb │ └── controllers │ │ └── controller_additions.rb ├── referrer.rb ├── modules │ └── statistics.rb └── other │ └── markup_generator.rb ├── app ├── helpers │ └── referrer │ │ └── application_helper.rb ├── controllers │ └── referrer │ │ ├── application_controller.rb │ │ ├── users_controller.rb │ │ ├── sessions_controller.rb │ │ └── sources_controller.rb ├── models │ └── referrer │ │ ├── users_main_app_user.rb │ │ ├── sources_tracked_object.rb │ │ ├── session.rb │ │ ├── user.rb │ │ └── source.rb ├── views │ └── layouts │ │ └── referrer │ │ └── application.html.erb └── assets │ └── javascripts │ └── referrer │ ├── application.js │ └── referrer.js.erb ├── config └── routes.rb ├── .gitignore ├── bin └── rails ├── Rakefile ├── .travis.yml ├── Gemfile ├── referrer.gemspec ├── MIT-LICENSE ├── db └── migrate │ └── 20160620130847_create_referrer_init_structure.rb ├── Gemfile.lock └── README.md /spec/dummy/log/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/mailers/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/lib/assets/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/public/favicon.ico: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/images/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/models/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/concerns/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /spec/dummy/.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /spec/dummy/app/views/welcome/index.html.erb: -------------------------------------------------------------------------------- 1 |

Welcome, test

-------------------------------------------------------------------------------- /lib/referrer/version.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | VERSION = '1.0.0' 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/helpers/application_helper.rb: -------------------------------------------------------------------------------- 1 | module ApplicationHelper 2 | end 3 | -------------------------------------------------------------------------------- /spec/dummy/app/models/post.rb: -------------------------------------------------------------------------------- 1 | class Post < ActiveRecord::Base 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /app/helpers/referrer/application_helper.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | module ApplicationHelper 3 | end 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/models/company.rb: -------------------------------------------------------------------------------- 1 | class Company < ActiveRecord::Base 2 | include Referrer::OwnerModelAdditions 3 | end 4 | -------------------------------------------------------------------------------- /spec/travis/database.travis.yml: -------------------------------------------------------------------------------- 1 | test: 2 | adapter: <%= ENV['DB'] %> 3 | database: travis_ci_test 4 | timeout: 30000 -------------------------------------------------------------------------------- /spec/dummy/app/models/request.rb: -------------------------------------------------------------------------------- 1 | class Request < ActiveRecord::Base 2 | include Referrer::TrackedModelAdditions 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/support.rb: -------------------------------------------------------------------------------- 1 | class Support < ActiveRecord::Base 2 | include Referrer::TrackedModelAdditions 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require_relative '../config/boot' 3 | require 'rake' 4 | Rake.application.run 5 | -------------------------------------------------------------------------------- /lib/referrer/engine.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class Engine < ::Rails::Engine 3 | isolate_namespace Referrer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/tasks/referrer_tasks.rake: -------------------------------------------------------------------------------- 1 | # desc "Explaining what the task does" 2 | # task :referrer do 3 | # # Task goes here 4 | # end 5 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/welcome_controller.rb: -------------------------------------------------------------------------------- 1 | class WelcomeController < ApplicationController 2 | def index; end 3 | end 4 | -------------------------------------------------------------------------------- /spec/dummy/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | include Referrer::OwnerModelAdditions 3 | has_many :posts 4 | end 5 | -------------------------------------------------------------------------------- /spec/dummy/bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /spec/dummy/bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | APP_PATH = File.expand_path('../../config/application', __FILE__) 3 | require_relative '../config/boot' 4 | require 'rails/commands' 5 | -------------------------------------------------------------------------------- /spec/dummy/spec/support/controllers_compatibility.rb: -------------------------------------------------------------------------------- 1 | module ControllersCompatibility 2 | def params(hash) 3 | Rails::VERSION::MAJOR > 4 ? {params: hash} : hash 4 | end 5 | end -------------------------------------------------------------------------------- /app/controllers/referrer/application_controller.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class ApplicationController < ActionController::Base 3 | protect_from_forgery with: :exception 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/dummy/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/cookies_serializer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.action_dispatch.cookies_serializer = :json 4 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/session_store.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | Rails.application.config.session_store :cookie_store, key: '_dummy_session' 4 | -------------------------------------------------------------------------------- /spec/dummy/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /app/models/referrer/users_main_app_user.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class UsersMainAppUser < ActiveRecord::Base 3 | belongs_to :main_app_user, polymorphic: true 4 | belongs_to :user 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | resources :posts, only: [:index, :new, :create] 3 | mount Referrer::Engine => '/referrer' 4 | 5 | root to: 'welcome#index' 6 | end 7 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160806143430_create_supports.rb: -------------------------------------------------------------------------------- 1 | class CreateSupports < ActiveRecord::Migration 2 | def change 3 | create_table :supports do |t| 4 | t.text :text 5 | t.timestamps 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160705112156_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | create_table :users do |t| 4 | t.string :name 5 | 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Referrer::Engine.routes.draw do 2 | resources :users, only: :create 3 | resources :sessions, only: :create 4 | resources :sources, only: [] do 5 | collection do 6 | post :mass_create 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160708153841_create_companies.rb: -------------------------------------------------------------------------------- 1 | class CreateCompanies < ActiveRecord::Migration 2 | def change 3 | create_table :companies do |t| 4 | t.string :name 5 | 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | Gemfile.lock 3 | .bundle/ 4 | log/*.log 5 | pkg/ 6 | spec/dummy/db/*.sqlite3 7 | spec/dummy/db/*.sqlite3-journal 8 | spec/dummy/log/*.log 9 | spec/dummy/tmp/ 10 | spec/dummy/.sass-cache 11 | spec/dummy/.byebug_history 12 | spec/dummy/public/assets 13 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160723153334_create_requests.rb: -------------------------------------------------------------------------------- 1 | class CreateRequests < ActiveRecord::Migration 2 | def change 3 | create_table :requests do |t| 4 | t.text :body 5 | t.text :email 6 | t.timestamps null: false 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | $LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) 6 | -------------------------------------------------------------------------------- /spec/dummy/Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /spec/dummy/db/migrate/20160705114108_create_posts.rb: -------------------------------------------------------------------------------- 1 | class CreatePosts < ActiveRecord::Migration 2 | def change 3 | create_table :posts do |t| 4 | t.string :title 5 | t.text :body 6 | t.integer :user_id 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /app/controllers/referrer/users_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'referrer/application_controller' 2 | 3 | module Referrer 4 | class UsersController < ApplicationController 5 | def create 6 | @user = Referrer::User.create! 7 | render json: {id: @user.id, token: @user.token} 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/views/layouts/referrer/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Referrer 5 | <%= stylesheet_link_tag "referrer/application", media: "all" %> 6 | <%= javascript_include_tag "referrer/application" %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/travis/gemfiles/Gemfile-5: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'pg' 3 | gem 'mysql2' 4 | 5 | group :development do 6 | gem 'sqlite3' 7 | gem 'rspec-rails', '~> 3.5', require: false 8 | gem 'database_cleaner' 9 | gem 'byebug' 10 | gem 'uglifier' 11 | gem 'watir-webdriver' 12 | gem 'headless' 13 | end 14 | 15 | gem 'rails', '>= 5' 16 | 17 | -------------------------------------------------------------------------------- /spec/travis/gemfiles/Gemfile-4.2: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | gem 'pg' 3 | gem 'mysql2' 4 | 5 | group :development do 6 | gem 'sqlite3' 7 | gem 'rspec-rails', '~> 3.5', require: false 8 | gem 'database_cleaner' 9 | gem 'byebug' 10 | gem 'uglifier' 11 | gem 'watir-webdriver' 12 | gem 'headless' 13 | end 14 | 15 | gem 'rails', '~> 4.2' 16 | 17 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | include Referrer::ControllerAdditions 3 | 4 | # Prevent CSRF attacks by raising an exception. 5 | # For APIs, you may want to use :null_session instead. 6 | protect_from_forgery with: :exception 7 | 8 | def current_user; User.first; end 9 | end 10 | -------------------------------------------------------------------------------- /spec/dummy/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Dummy 5 | <%= stylesheet_link_tag 'application', media: 'all', 'data-turbolinks-track' => true %> 6 | <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %> 7 | <%= csrf_meta_tags %> 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /spec/dummy/app/controllers/posts_controller.rb: -------------------------------------------------------------------------------- 1 | class PostsController < ApplicationController 2 | def index 3 | render json: Post.all.to_json 4 | end 5 | 6 | def create 7 | render json: Post.create!(post_params.merge(user: current_user)).to_json 8 | end 9 | 10 | def new 11 | render json: Post.new.to_json 12 | end 13 | 14 | private 15 | 16 | def post_params 17 | params.require(:post).permit(:title, :body) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /app/models/referrer/sources_tracked_object.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class SourcesTrackedObject < ActiveRecord::Base 3 | belongs_to :trackable, polymorphic: true 4 | belongs_to :user 5 | belongs_to :source 6 | 7 | validates_presence_of :trackable, :user, :linked_at 8 | before_validation :set_fields, on: :create 9 | 10 | private 11 | 12 | def set_fields 13 | self.linked_at = Time.now unless linked_at 14 | self.source = user.try(:source_at, linked_at) if linked_at 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application. 3 | 4 | ENGINE_ROOT = File.expand_path('../..', __FILE__) 5 | ENGINE_PATH = File.expand_path('../../lib/referrer/engine', __FILE__) 6 | 7 | # Set up gems listed in the Gemfile. 8 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 9 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 10 | 11 | require 'rails/all' 12 | require 'rails/engine/commands' 13 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/assets.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Version of your assets, change this if you want to expire all your assets. 4 | Rails.application.config.assets.version = '1.0' 5 | 6 | # Add additional assets to the asset load path 7 | # Rails.application.config.assets.paths << Emoji.images_path 8 | 9 | # Precompile additional assets. 10 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 11 | # Rails.application.config.assets.precompile += %w( search.js ) 12 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | ActiveSupport.on_load(:action_controller) do 8 | wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /spec/dummy/README.rdoc: -------------------------------------------------------------------------------- 1 | == README 2 | 3 | This README would normally document whatever steps are necessary to get the 4 | application up and running. 5 | 6 | Things you may want to cover: 7 | 8 | * Ruby version 9 | 10 | * System dependencies 11 | 12 | * Configuration 13 | 14 | * Database creation 15 | 16 | * Database initialization 17 | 18 | * How to run the test suite 19 | 20 | * Services (job queues, cache servers, search engines, etc.) 21 | 22 | * Deployment instructions 23 | 24 | * ... 25 | 26 | 27 | Please feel free to use a different markup language if you do not plan to run 28 | rake doc:app. 29 | -------------------------------------------------------------------------------- /spec/dummy/spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Referrer::UsersController, type: :controller do 4 | routes { Referrer::Engine.routes } 5 | 6 | describe 'create' do 7 | it 'should be done' do 8 | expect(Referrer::User.all.count).to eq(0) 9 | post(:create) 10 | expect(Referrer::User.all.count).to eq(1) 11 | user = Referrer::User.last 12 | result = JSON.parse(response.body) 13 | expect(result.keys).to eq(%w(id token)) 14 | expect(result['id']).to eq(user.id) 15 | expect(result['token']).to eq(user.token) 16 | end 17 | end 18 | end -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | begin 2 | require 'bundler/setup' 3 | rescue LoadError 4 | puts 'You must `gem install bundler` and `bundle install` to run rake tasks' 5 | end 6 | 7 | require 'rdoc/task' 8 | 9 | RDoc::Task.new(:rdoc) do |rdoc| 10 | rdoc.rdoc_dir = 'rdoc' 11 | rdoc.title = 'Referrer' 12 | rdoc.options << '--line-numbers' 13 | rdoc.rdoc_files.include('README.rdoc') 14 | rdoc.rdoc_files.include('lib/**/*.rb') 15 | end 16 | 17 | APP_RAKEFILE = File.expand_path("../spec/dummy/Rakefile", __FILE__) 18 | load 'rails/tasks/engine.rake' 19 | 20 | 21 | load 'rails/tasks/statistics.rake' 22 | 23 | 24 | 25 | Bundler::GemHelper.install_tasks 26 | 27 | -------------------------------------------------------------------------------- /app/assets/javascripts/referrer/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | -------------------------------------------------------------------------------- /spec/dummy/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3 3 | # 4 | # Ensure the SQLite 3 gem is defined in your Gemfile 5 | # gem 'sqlite3' 6 | # 7 | default: &default 8 | adapter: sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | development: 13 | <<: *default 14 | database: db/referrer_development.sqlite3 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run "rake". 18 | # Do not set this db to the same as development or production. 19 | test: 20 | <<: *default 21 | database: db/referrer_test.sqlite3 22 | 23 | production: 24 | <<: *default 25 | database: db/referrer_production.sqlite3 26 | -------------------------------------------------------------------------------- /lib/concerns/models/tracked_model_additions.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | module TrackedModelAdditions 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | has_one :tracked_relation, as: :trackable, class_name: 'Referrer::SourcesTrackedObject' 7 | 8 | def referrer_link_with(r_user, linked_at: nil) 9 | create_tracked_relation(user: r_user, linked_at: linked_at) 10 | end 11 | 12 | def referrer_link_with!(r_user, linked_at: nil) 13 | create_tracked_relation!(user: r_user, linked_at: linked_at) 14 | end 15 | 16 | def referrer_markup 17 | tracked_relation.try(:source).try(:to_markup).try(:symbolize_keys!) 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/javascripts/application.js: -------------------------------------------------------------------------------- 1 | // This is a manifest file that'll be compiled into application.js, which will include all the files 2 | // listed below. 3 | // 4 | // Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, 5 | // or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. 6 | // 7 | // It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the 8 | // compiled file. 9 | // 10 | // Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details 11 | // about supported directives. 12 | // 13 | //= require_tree . 14 | //= require referrer/application 15 | -------------------------------------------------------------------------------- /spec/dummy/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /spec/dummy/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /spec/dummy/app/assets/stylesheets/application.css: -------------------------------------------------------------------------------- 1 | /* 2 | * This is a manifest file that'll be compiled into application.css, which will include all the files 3 | * listed below. 4 | * 5 | * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, 6 | * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. 7 | * 8 | * You're free to add application-wide styles to this file and they'll appear at the bottom of the 9 | * compiled file so the styles you add here take precedence over styles defined in any styles 10 | * defined in the other CSS/SCSS files in this directory. It is generally better to create a new 11 | * file per style scope. 12 | * 13 | *= require_tree . 14 | *= require_self 15 | */ 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | cache: bundler 3 | sudo: false 4 | rvm: 5 | - 2.2.2 6 | gemfile: 7 | - spec/travis/gemfiles/Gemfile-4.2 8 | - spec/travis/gemfiles/Gemfile-5 9 | env: 10 | matrix: 11 | - DB=mysql2 12 | - DB=postgresql 13 | - DB=sqlite3 14 | before_script: 15 | - cp spec/travis/database.travis.yml spec/dummy/config/database.yml 16 | - mysql -e 'create database travis_ci_test' 17 | - psql -c 'create database travis_ci_test' -U postgres 18 | - export DISPLAY=:99.0 19 | - sh -e /etc/init.d/xvfb start 20 | script: 21 | - cd spec/dummy 22 | - RAILS_ENV=test bundle exec rake referrer:install:migrations 23 | - bundle exec rake db:migrate 24 | - RAILS_ENV=test bundle exec rails s -d 25 | - bundle exec rspec 26 | -------------------------------------------------------------------------------- /app/models/referrer/session.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class Session < ActiveRecord::Base 3 | belongs_to :user 4 | has_many :sources 5 | 6 | validates_presence_of :user, :active_until 7 | 8 | before_validation :set_active_period 9 | 10 | scope :active_at, -> (time) { where('active_from <= :time AND active_until >= :time', {time: time}) } 11 | 12 | def active_seconds 13 | (active_until - Time.now).to_i 14 | end 15 | 16 | def source_at(time) 17 | sources.priority.where('created_at <= ?', time).last 18 | end 19 | 20 | private 21 | 22 | def set_active_period 23 | self.active_from = Time.now unless active_from 24 | self.active_until = Time.now + Referrer.session_duration unless active_until 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/referrer/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'referrer/application_controller' 2 | 3 | module Referrer 4 | class SessionsController < ApplicationController 5 | def create 6 | user = Referrer::User.where(id: session_params[:user_id], token: session_params[:user_token]).first 7 | if user 8 | @session = user.sessions.active_at(Time.now).first 9 | @session = user.sessions.create! unless @session 10 | render json: {id: @session.id, active_seconds: @session.active_seconds} 11 | else 12 | render json: {errors: ['User token is incorrect']}, status: 401 13 | end 14 | end 15 | 16 | private 17 | 18 | def session_params 19 | params.require(:session).permit(:user_id, :user_token) 20 | end 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /app/models/referrer/user.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class User < ActiveRecord::Base 3 | has_many :sessions 4 | has_many :users_main_app_users 5 | 6 | validates :token, presence: true 7 | 8 | before_validation :generate_token, on: :create 9 | 10 | def link_with(obj) 11 | users_main_app_users.create(main_app_user: obj) 12 | end 13 | 14 | def linked_with?(obj) 15 | users_main_app_users.where(main_app_user: obj).present? 16 | end 17 | 18 | def linked_objects 19 | users_main_app_users.includes(:main_app_user).map{|relation| relation.main_app_user} 20 | end 21 | 22 | def source_at(time) 23 | sessions.active_at(time).first.try(:source_at, time) 24 | end 25 | 26 | private 27 | 28 | def generate_token 29 | self.token = SecureRandom.hex(5) 30 | end 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Declare your gem's dependencies in referrer.gemspec. 4 | # Bundler will treat runtime dependencies like base dependencies, and 5 | # development dependencies will be added by default to the :development group. 6 | gemspec 7 | 8 | # Declare any dependencies that are still in development here instead of in 9 | # your gemspec. These might include edge Rails or gems from your path or 10 | # Git. Remember to move these dependencies to your gemspec before releasing 11 | # your gem to rubygems.org. 12 | 13 | # To use a debugger 14 | # gem 'byebug', group: [:development, :test] 15 | 16 | group :development do 17 | gem 'sqlite3' 18 | gem 'rspec-rails', '~> 3.5', require: false 19 | gem 'database_cleaner' 20 | gem 'byebug' 21 | gem 'uglifier' 22 | gem 'watir-webdriver' 23 | gem 'headless' 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/dummy/spec/features/linking_users_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Linking users', watir: true do 4 | 5 | # This checks based on def current_user; User.first; end hack in dummy ApplicationController 6 | 7 | it 'should be done' do 8 | user = User.create! 9 | @browser.goto(@url) 10 | Watir::Wait.until{Referrer::Source.count == 1} 11 | @browser.refresh 12 | Watir::Wait.until{Referrer::Source.count == 2} 13 | expect(Referrer::User.count).to eq(1) 14 | expect(Referrer::User.first.linked_objects).to eq([user]) 15 | end 16 | 17 | it 'should not be done' do 18 | 2.times do 19 | source_count = Referrer::Source.count 20 | @browser.goto(@url) 21 | Watir::Wait.until{Referrer::Source.count > source_count} 22 | expect(Referrer::User.first.linked_objects).to eq([]) 23 | end 24 | end 25 | end -------------------------------------------------------------------------------- /referrer.gemspec: -------------------------------------------------------------------------------- 1 | $:.push File.expand_path('../lib', __FILE__) 2 | 3 | require 'referrer/version' 4 | 5 | Gem::Specification.new do |s| 6 | s.name = 'referrer' 7 | s.version = Referrer::VERSION 8 | s.authors = ['Sergey Sokolov'] 9 | s.email = ['sokolov.sergey.a@gmail.com'] 10 | s.homepage = 'https://github.com/salkar/referrer' 11 | s.summary = "Referrer tracks sources with which users visit your site, computes priority of these sources and provides linking for sources with tracked model's records." 12 | s.description = "Tracking system for sources of site's visitors with sources priorities and linking most priority source with tracked orders/requests/etc" 13 | s.license = 'MIT' 14 | 15 | s.files = Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE', 'Rakefile', 'README.md'] 16 | 17 | s.add_dependency('rails', '>= 4', '< 6') 18 | end 19 | -------------------------------------------------------------------------------- /lib/concerns/controllers/controller_additions.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | module ControllerAdditions 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | before_action :link_referrer_user_to_app_user 7 | 8 | def link_referrer_user_to_app_user 9 | current_object = send(Referrer.current_user_method_name) 10 | if current_object && referrer_user && !referrer_user.linked_with?(current_object) 11 | referrer_user.link_with(current_object) 12 | end 13 | end 14 | 15 | def referrer_user 16 | @referrer ||= begin 17 | if cookies[:referrer_user].present? 18 | obj = JSON.parse(cookies[:referrer_user]) 19 | Referrer::User.where(id: obj['id'], token: obj['token']).first 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/dummy/bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | 4 | # path to your application root. 5 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 6 | 7 | Dir.chdir APP_ROOT do 8 | # This script is a starting point to setup your application. 9 | # Add necessary setup steps to this file: 10 | 11 | puts "== Installing dependencies ==" 12 | system "gem install bundler --conservative" 13 | system "bundle check || bundle install" 14 | 15 | # puts "\n== Copying sample files ==" 16 | # unless File.exist?("config/database.yml") 17 | # system "cp config/database.yml.sample config/database.yml" 18 | # end 19 | 20 | puts "\n== Preparing database ==" 21 | system "bin/rake db:setup" 22 | 23 | puts "\n== Removing old logs and tempfiles ==" 24 | system "rm -f log/*" 25 | system "rm -rf tmp/cache" 26 | 27 | puts "\n== Restarting application server ==" 28 | system "touch tmp/restart.txt" 29 | end 30 | -------------------------------------------------------------------------------- /lib/referrer.rb: -------------------------------------------------------------------------------- 1 | require 'referrer/engine' 2 | require 'other/markup_generator' 3 | require 'concerns/controllers/controller_additions' 4 | require 'concerns/models/tracked_model_additions' 5 | require 'concerns/models/owner_model_additions' 6 | require 'modules/statistics' 7 | 8 | module Referrer 9 | mattr_accessor(:markup_generator_settings){{}} 10 | mattr_accessor(:sources_overwriting_schema) do 11 | {direct: %w(direct), 12 | referral: %w(direct referral organic utm), 13 | organic: %w(direct referral organic utm), 14 | utm: %w(direct referral organic utm)} 15 | end 16 | mattr_accessor(:session_duration){3.months} 17 | mattr_accessor(:current_user_method_name){:current_user} 18 | mattr_accessor(:js_settings) {{}} 19 | mattr_accessor(:js_csrf_token) do 20 | <<-JS 21 | var tokenContainer = document.querySelector("meta[name=csrf-token]"); 22 | return tokenContainer ? tokenContainer.content : null; 23 | JS 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/modules/statistics.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | module Statistics 3 | class << self 4 | def sources_markup(from, to) 5 | Referrer::Source.where('created_at >= ? AND created_at <= ?', from, to).group_by do |source| 6 | source.to_markup.symbolize_keys! 7 | end.map do |markup, matches| 8 | markup.merge({count: matches.size}) 9 | end 10 | end 11 | 12 | def tracked_objects_markup(from, to, type: nil) 13 | result = Referrer::SourcesTrackedObject.where('linked_at >= ? AND linked_at <= ?', from, to).includes(:source) 14 | result = result.where(trackable_type: type) if type.present? 15 | result.map{|item| item.source}.select do |source| 16 | source.present? 17 | end.group_by do |source| 18 | source.to_markup.symbolize_keys! 19 | end.map do |markup, matches| 20 | markup.merge({count: matches.size}) 21 | end 22 | end 23 | end 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /spec/dummy/config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 13e23782dd59d092731ba667b65672f65967a2688f711a3b4a71316522d7de098d312af59aead858aca405757d9b2aab55da3f3dcd3ad7789a67b589b42e3d12 15 | 16 | test: 17 | secret_key_base: 2af9278e1e1a6e82332cee6e34818f8adcf831b48a5aecaf2006180168f4c517ca24ed828cd738d95520a65b449cf837f99cad81090290d18deb764e9ce0d955 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Sergey Sokolov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /lib/concerns/models/owner_model_additions.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | module OwnerModelAdditions 3 | extend ActiveSupport::Concern 4 | 5 | included do 6 | has_many :referrer_users_main_app_users, class_name: 'Referrer::UsersMainAppUser', as: :main_app_user 7 | 8 | def referrer_users 9 | referrer_users_main_app_users.includes(:user).map{|relation| relation.user} 10 | end 11 | 12 | def referrer_sources 13 | Referrer::Source.where( 14 | session_id: Referrer::Session.where(user_id: referrer_users_main_app_users.pluck(:user_id)).pluck(:id)) 15 | end 16 | 17 | def referrer_first_source 18 | referrer_sources.first 19 | end 20 | 21 | def referrer_priority_source 22 | referrer_sources.priority.last 23 | end 24 | 25 | def referrer_last_source 26 | referrer_sources.last 27 | end 28 | 29 | def referrer_markups 30 | Hash[{first: referrer_first_source, priority: referrer_priority_source, 31 | last: referrer_last_source}.map{|k, v| [k, v.try(:to_markup).try(:symbolize_keys!)]}] 32 | end 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /app/controllers/referrer/sources_controller.rb: -------------------------------------------------------------------------------- 1 | require_dependency 'referrer/application_controller' 2 | 3 | module Referrer 4 | class SourcesController < ApplicationController 5 | def mass_create 6 | user = Referrer::User.where(id: mass_source_params[:user_id], token: mass_source_params[:user_token]).first 7 | if user.present? 8 | sessions = user.sessions 9 | @sources = JSON.parse(mass_source_params[:values]).inject([]) do |r, pack| 10 | session ||= sessions.detect{|s| s.id == pack['session_id'].to_i} || 11 | sessions.detect{|s| s.id == mass_source_params[:current_session_id].to_i} 12 | if session.blank? || session.sources.exists?(client_duplicate_id: pack['client_duplicate_id']) 13 | r 14 | else 15 | r << session.sources.create!(entry_point: pack['entry_point'], referrer: pack['referrer'], 16 | client_duplicate_id: pack['client_duplicate_id']) 17 | end 18 | end 19 | render json: {ids: @sources.map(&:id)} 20 | else 21 | render json: {errors: ['User token is incorrect']}, status: 401 22 | end 23 | end 24 | 25 | private 26 | 27 | def mass_source_params 28 | params.require(:sources).permit(:user_id, :current_session_id, :user_token, :values) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/dummy/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | # Pick the frameworks you want: 4 | require "active_record/railtie" 5 | require "action_controller/railtie" 6 | require "action_mailer/railtie" 7 | require "action_view/railtie" 8 | require "sprockets/railtie" 9 | # require "rails/test_unit/railtie" 10 | 11 | Bundler.require(*Rails.groups) 12 | require "referrer" 13 | 14 | module Dummy 15 | class Application < Rails::Application 16 | # Settings in config/environments/* take precedence over those specified here. 17 | # Application configuration should go into files in config/initializers 18 | # -- all .rb files in that directory are automatically loaded. 19 | 20 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 21 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 22 | # config.time_zone = 'Central Time (US & Canada)' 23 | 24 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 25 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 26 | # config.i18n.default_locale = :de 27 | 28 | # Do not swallow errors in after_commit/after_rollback callbacks. 29 | config.active_record.raise_in_transactional_callbacks = true if Rails::VERSION::MAJOR < 5 30 | end 31 | end 32 | 33 | -------------------------------------------------------------------------------- /app/models/referrer/source.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class Source < ActiveRecord::Base 3 | belongs_to :session 4 | 5 | validates_presence_of :entry_point, :utm_source, :utm_campaign, :utm_medium, :utm_content, :utm_term, :session, 6 | :kind, :client_duplicate_id 7 | 8 | before_validation :fill_markup_fields, on: :create 9 | before_create :set_priority 10 | 11 | scope :priority, -> {where(priority: true)} 12 | 13 | class << self 14 | def markup_generator 15 | @markup_generator ||= begin 16 | mg = MarkupGenerator.new 17 | Referrer.markup_generator_settings.each do |k, v| 18 | mg.send(:"#{k}=", v) 19 | end 20 | mg 21 | end 22 | end 23 | end 24 | 25 | def to_markup 26 | attributes.slice(*%w{utm_source utm_campaign utm_medium utm_content utm_term kind}) 27 | end 28 | 29 | private 30 | 31 | def fill_markup_fields 32 | markup = self.class.markup_generator.generate(referrer, entry_point) 33 | %i{utm_source utm_campaign utm_medium utm_content utm_term kind}.each do |column_name| 34 | self.send(:"#{column_name}=", markup[column_name]) 35 | end 36 | end 37 | 38 | def set_priority 39 | previous_priority_source = session.sources.where(priority: true).last 40 | if previous_priority_source.blank? || 41 | Referrer.sources_overwriting_schema[kind.to_sym].include?(previous_priority_source.kind) 42 | self.priority = true 43 | end 44 | end 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /spec/dummy/spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | include ControllersCompatibility 3 | 4 | RSpec.describe Referrer::SessionsController, type: :controller do 5 | routes { Referrer::Engine.routes } 6 | 7 | before :each do 8 | @user = Referrer::User.create! 9 | end 10 | 11 | describe 'create' do 12 | it 'should be done' do 13 | expect(@user.sessions.count).to eq(0) 14 | post(:create, params(session: {user_id: @user.id, user_token: @user.token})) 15 | expect(@user.sessions.count).to eq(1) 16 | session = @user.sessions.last 17 | result = JSON.parse(response.body) 18 | expect(result.keys).to eq(%w(id active_seconds)) 19 | expect(result['id']).to eq(session.id) 20 | expect(result['active_seconds']).to be_within(1).of(session.active_seconds) 21 | end 22 | 23 | it 'should be done with already active session' do 24 | session = @user.sessions.create!(active_from: 1.day.ago, active_until: 2.days.since) 25 | expect(@user.sessions.count).to eq(1) 26 | post(:create, params(session: {user_id: @user.id, user_token: @user.token})) 27 | expect(@user.sessions.count).to eq(1) 28 | result = JSON.parse(response.body) 29 | expect(result['id']).to eq(session.id) 30 | end 31 | 32 | it 'should not be done because incorrect token' do 33 | expect(@user.sessions.count).to eq(0) 34 | post(:create, params(session: {user_id: @user.id, user_token: 'test'})) 35 | expect(@user.sessions.count).to eq(0) 36 | result = JSON.parse(response.body) 37 | expect(response.code).to eq('401') 38 | expect(result).to eq({'errors'=>['User token is incorrect']}) 39 | end 40 | end 41 | end -------------------------------------------------------------------------------- /spec/dummy/public/500.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | We're sorry, but something went wrong (500) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

We're sorry, but something went wrong.

62 |
63 |

If you are the application owner check the logs for more information.

64 |
65 | 66 | 67 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send. 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger. 20 | config.active_support.deprecation = :log 21 | 22 | # Raise an error on page load if there are pending migrations. 23 | config.active_record.migration_error = :page_load 24 | 25 | # Debug mode disables concatenation and preprocessing of assets. 26 | # This option may cause significant delays in view rendering with a large 27 | # number of complex assets. 28 | config.assets.debug = true 29 | 30 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 31 | # yet still be able to expire them through the digest params. 32 | config.assets.digest = true 33 | 34 | # Adds additional error checking when serving assets at runtime. 35 | # Checks for improperly declared sprockets dependencies. 36 | # Raises helpful error messages. 37 | config.assets.raise_runtime_errors = true 38 | 39 | # Raises error for missing translations 40 | # config.action_view.raise_on_missing_translations = true 41 | end 42 | -------------------------------------------------------------------------------- /spec/dummy/public/422.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The change you wanted was rejected (422) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The change you wanted was rejected.

62 |

Maybe you tried to change something you didn't have access to.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /spec/dummy/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | The page you were looking for doesn't exist (404) 5 | 6 | 55 | 56 | 57 | 58 | 59 |
60 |
61 |

The page you were looking for doesn't exist.

62 |

You may have mistyped the address or the page may have moved.

63 |
64 |

If you are the application owner check the logs for more information.

65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /db/migrate/20160620130847_create_referrer_init_structure.rb: -------------------------------------------------------------------------------- 1 | class CreateReferrerInitStructure < ActiveRecord::Migration 2 | def change 3 | create_table :referrer_users do |t| 4 | t.string :token 5 | 6 | t.timestamps null: false 7 | end 8 | 9 | create_table :referrer_users_main_app_users do |t| 10 | t.integer :user_id 11 | t.integer :main_app_user_id 12 | t.string :main_app_user_type 13 | 14 | t.timestamps null: false 15 | end 16 | add_index :referrer_users_main_app_users, [:main_app_user_type, :main_app_user_id], name: 'referrer_users_main_app_users_mau' 17 | add_index :referrer_users_main_app_users, :user_id 18 | 19 | create_table :referrer_sessions do |t| 20 | t.integer :user_id 21 | t.datetime :active_from 22 | t.datetime :active_until 23 | 24 | t.timestamps null: false 25 | end 26 | add_index :referrer_sessions, :user_id 27 | 28 | create_table :referrer_sources do |t| 29 | t.integer :session_id 30 | t.string :referrer 31 | t.string :entry_point 32 | t.integer :client_duplicate_id 33 | t.string :utm_source 34 | t.string :utm_campaign 35 | t.string :utm_medium 36 | t.string :utm_content 37 | t.string :utm_term 38 | t.string :kind 39 | t.boolean :priority, default: false 40 | 41 | t.timestamps null: false 42 | end 43 | add_index :referrer_sources, :session_id 44 | 45 | create_table :referrer_sources_tracked_objects do |t| 46 | t.integer :user_id 47 | t.integer :source_id 48 | t.datetime :linked_at 49 | t.integer :trackable_id 50 | t.string :trackable_type 51 | 52 | t.timestamps null: false 53 | end 54 | add_index :referrer_sources_tracked_objects, [:trackable_type, :trackable_id], name: 'referrer_sources_tracked_objects_ta' 55 | add_index :referrer_sources_tracked_objects, :user_id 56 | add_index :referrer_sources_tracked_objects, :source_id 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /spec/dummy/spec/models/sources_tracked_object_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Referrer::SourcesTrackedObject, type: :model do 4 | before :each, with_data: true do 5 | @user = Referrer::User.create! 6 | @request = Request.create! 7 | @session = @user.sessions.create!(active_from: 10.days.ago, active_until: 10.days.since) 8 | end 9 | 10 | before :each, with_source: true do 11 | @source = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 12 | client_duplicate_id: 1, created_at: 5.days.ago) 13 | end 14 | 15 | describe 'should be created', with_data: true do 16 | it 'with source', with_source: true do 17 | sto = Referrer::SourcesTrackedObject.create(trackable: @request, user: @user) 18 | expect(sto.new_record?).to eq(false) 19 | expect(sto.user).to eq(@user) 20 | expect(sto.trackable).to eq(@request) 21 | expect(sto.source).to eq(@source) 22 | expect(sto.linked_at.to_i).to be_within(2).of(Time.now.to_i) 23 | end 24 | 25 | it 'without source' do 26 | sto = Referrer::SourcesTrackedObject.create(trackable: @request, user: @user) 27 | expect(sto.new_record?).to eq(false) 28 | expect(sto.user).to eq(@user) 29 | expect(sto.trackable).to eq(@request) 30 | expect(sto.source).to eq(nil) 31 | expect(sto.linked_at.to_i).to be_within(2).of(Time.now.to_i) 32 | end 33 | 34 | it 'with custom linked_at' do 35 | sto = Referrer::SourcesTrackedObject.create(trackable: @request, user: @user, linked_at: 2.days.ago) 36 | expect(sto.new_record?).to eq(false) 37 | expect(sto.linked_at.to_i).to be_within(2).of(2.days.ago.to_i) 38 | end 39 | end 40 | 41 | describe 'should not be created' do 42 | it 'due validations errors' do 43 | sto = Referrer::SourcesTrackedObject.create 44 | expect(sto.new_record?).to eq(true) 45 | expect(sto.errors.size).to be > 0 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /spec/dummy/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static file server for tests with Cache-Control for performance. 16 | if Rails::VERSION::MAJOR > 4 17 | config.public_file_server.enabled = true 18 | config.public_file_server.headers = { 'Cache-Control' => 'public, max-age=3600' } 19 | else 20 | config.serve_static_files = true 21 | config.static_cache_control = 'public, max-age=3600' 22 | end 23 | 24 | # Show full error reports and disable caching. 25 | config.consider_all_requests_local = true 26 | config.action_controller.perform_caching = false 27 | 28 | # Raise exceptions instead of rendering exception templates. 29 | config.action_dispatch.show_exceptions = false 30 | 31 | # Disable request forgery protection in test environment. 32 | config.action_controller.allow_forgery_protection = false 33 | 34 | # Tell Action Mailer not to deliver emails to the real world. 35 | # The :test delivery method accumulates sent emails in the 36 | # ActionMailer::Base.deliveries array. 37 | config.action_mailer.delivery_method = :test 38 | 39 | # Randomize the order test cases are executed. 40 | config.active_support.test_order = :random 41 | 42 | # Print deprecation notices to the stderr. 43 | config.active_support.deprecation = :stderr 44 | 45 | # Raises error for missing translations 46 | # config.action_view.raise_on_missing_translations = true 47 | end 48 | -------------------------------------------------------------------------------- /spec/dummy/spec/lib/tracked_model_additions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Tracked model additions' do 4 | before :each do 5 | @user = Referrer::User.create! 6 | @request = Request.create! 7 | end 8 | 9 | before :each, with_current_source: true do 10 | @session = @user.sessions.create!(active_from: 10.days.ago, active_until: 10.days.since) 11 | @source = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 12 | client_duplicate_id: 1, created_at: 5.days.ago) 13 | end 14 | 15 | describe 'referrer_link_with' do 16 | describe 'should link', with_current_source: true do 17 | it 'by default' do 18 | @request.referrer_link_with(@user) 19 | expect(Referrer::SourcesTrackedObject.count).to eq(1) 20 | relation = Referrer::SourcesTrackedObject.first 21 | expect(relation.trackable).to eq(@request) 22 | expect(relation.user).to eq(@user) 23 | expect(relation.source).to eq(@source) 24 | expect(relation.linked_at.to_i).to be_within(2).of(Time.now.to_i) 25 | end 26 | 27 | it 'with custom linked_at time' do 28 | @request.referrer_link_with(@user, linked_at: 1.day.ago) 29 | expect(Referrer::SourcesTrackedObject.count).to eq(1) 30 | relation = Referrer::SourcesTrackedObject.first 31 | expect(relation.linked_at.to_i).to be_within(2).of(1.day.ago.to_i) 32 | end 33 | 34 | it 'with ! method version' do 35 | @request.referrer_link_with!(@user) 36 | expect(Referrer::SourcesTrackedObject.count).to eq(1) 37 | end 38 | end 39 | 40 | describe 'should not link' do 41 | it 'with default version' do 42 | @request.referrer_link_with(nil) 43 | expect(Referrer::SourcesTrackedObject.count).to eq(0) 44 | end 45 | 46 | it 'with ! method version' do 47 | expect{@request.referrer_link_with!(nil)}.to raise_error(/Validation/) 48 | expect(Referrer::SourcesTrackedObject.count).to eq(0) 49 | end 50 | end 51 | end 52 | 53 | describe 'referrer_markup', with_current_source: true do 54 | it 'should be' do 55 | @request.referrer_link_with(@user) 56 | expect(@request.referrer_markup).to eq({utm_source: 'test.com', utm_campaign: '(none)', utm_medium: 'referral', 57 | utm_content: '/', utm_term: '(none)', kind: 'referral'}) 58 | end 59 | 60 | it 'should not be' do 61 | expect(@request.referrer_markup).to eq(nil) 62 | end 63 | end 64 | end -------------------------------------------------------------------------------- /spec/dummy/spec/models/session_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Referrer::Session, type: :model do 4 | before :each do 5 | @user = Referrer::User.create! 6 | end 7 | 8 | before :each, with_sources: true do 9 | @session = @user.sessions.create!(active_from: 10.days.ago, active_until: 10.days.since) 10 | @source_0 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 11 | client_duplicate_id: 1, created_at: 7.days.ago) 12 | @source_1 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 13 | client_duplicate_id: 2, created_at: 5.days.ago) 14 | @source_2 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 15 | client_duplicate_id: 3, created_at: 2.days.ago) 16 | end 17 | 18 | describe 'scopes' do 19 | describe 'active_at' do 20 | it 'should work' do 21 | @user.sessions.create!(active_from: 10.days.ago, active_until: 7.days.ago) 22 | session_1 = @user.sessions.create!(active_from: 6.days.ago, active_until: 4.days.ago) 23 | @user.sessions.create!(active_from: 3.days.ago, active_until: 1.days.ago) 24 | expect(@user.sessions.active_at(5.days.ago)).to eq([session_1]) 25 | end 26 | end 27 | end 28 | 29 | describe 'active period' do 30 | it 'should be correct by default' do 31 | session = @user.sessions.create! 32 | expect(session.active_until.to_i).to be_within(2).of((Time.now + Referrer.session_duration).to_i) 33 | expect(session.active_from.to_i).to be_within(2).of(Time.now.to_i) 34 | end 35 | 36 | it 'should be correct with custom values' do 37 | session = @user.sessions.create!(active_from: 10.days.ago, active_until: 10.days.since) 38 | expect(session.active_until.to_i).to be_within(2).of(10.days.since.to_i) 39 | expect(session.active_from.to_i).to be_within(2).of(10.days.ago.to_i) 40 | end 41 | end 42 | 43 | describe 'active seconds' do 44 | it 'should be correct' do 45 | session = @user.sessions.create! 46 | expect(session.active_seconds).to be_within(2).of(session.active_until - Time.now) 47 | end 48 | end 49 | 50 | describe 'source_at', with_sources: true do 51 | it 'should return correct source' do 52 | expect(@session.source_at(4.days.ago)).to eq(@source_1) 53 | end 54 | 55 | it 'should not return source' do 56 | expect(@session.source_at(8.days.ago)).to eq(nil) 57 | end 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /spec/dummy/spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Referrer::User, type: :model do 4 | before :each do 5 | @user = Referrer::User.create! 6 | end 7 | 8 | before :each, with_main_app_user: true do 9 | @main_app_user = User.create! 10 | end 11 | 12 | before :each, with_sources: true do 13 | @session = @user.sessions.create!(active_from: 10.days.ago, active_until: 10.days.since) 14 | @source_0 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 15 | client_duplicate_id: 1, created_at: 7.days.ago) 16 | @source_1 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 17 | client_duplicate_id: 2, created_at: 5.days.ago) 18 | @source_2 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', 19 | client_duplicate_id: 3, created_at: 2.days.ago) 20 | end 21 | 22 | describe 'token' do 23 | it 'should be generated' do 24 | expect(@user.token.length).to be > 0 25 | end 26 | end 27 | 28 | describe 'link_with', with_main_app_user: true do 29 | it 'should work' do 30 | expect(@user.linked_with?(@main_app_user)).to eq(false) 31 | @user.link_with(@main_app_user) 32 | expect(@user.linked_with?(@main_app_user)).to eq(true) 33 | end 34 | end 35 | 36 | describe 'link_with?', with_main_app_user: true do 37 | it 'should return true' do 38 | @user.users_main_app_users.create(main_app_user: @main_app_user) 39 | expect(@user.users_main_app_users.size).to eq(1) 40 | expect(@user.linked_with?(@main_app_user)).to eq(true) 41 | end 42 | 43 | it 'should return false' do 44 | expect(@user.linked_with?(@main_app_user)).to eq(false) 45 | end 46 | end 47 | 48 | describe 'linked_objects' do 49 | it 'should return' do 50 | expect(@user.linked_objects).to eq([]) 51 | @user.link_with(@main_app_user) 52 | expect(@user.reload.linked_objects).to eq([@main_app_user]) 53 | end 54 | 55 | it 'should not return' do 56 | expect(@user.linked_objects).to eq([]) 57 | end 58 | end 59 | 60 | describe 'source_at(time)' do 61 | it 'should return source', with_sources: true do 62 | expect(@user.source_at(4.days.ago)).to eq(@source_1) 63 | end 64 | 65 | it 'should not return source due no suitable sources', with_sources: true do 66 | expect(@session.source_at(8.days.ago)).to eq(nil) 67 | end 68 | 69 | it 'should not return source due no sessions' do 70 | expect(@user.source_at(4.days.ago)).to eq(nil) 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /spec/dummy/spec/lib/controller_additions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | include ControllersCompatibility 3 | 4 | RSpec.describe 'Controller additions for' do 5 | describe PostsController, type: :controller do 6 | before :each, with_referrer_user: true do 7 | @referrer_user = Referrer::User.create! 8 | request.cookies['referrer_user'] = {id: @referrer_user.id, token: @referrer_user.token}.to_json 9 | end 10 | 11 | before :each, with_other_referrer_user: true do 12 | @other_referrer_user = Referrer::User.create! 13 | end 14 | 15 | before :each, with_main_app_user: true do 16 | @main_app_user = User.create! 17 | allow_any_instance_of(PostsController).to receive(:current_user).and_return(@main_app_user) 18 | end 19 | 20 | describe 'with main app user', with_main_app_user: true do 21 | describe 'with referrer user', with_referrer_user: true do 22 | describe 'on post request' do 23 | it 'should link users' do 24 | expect(@referrer_user.linked_objects).to eq([]) 25 | post(:create, params(post: {title: 'test'})) 26 | expect(@referrer_user.reload.linked_objects).to eq([@main_app_user]) 27 | end 28 | end 29 | 30 | describe 'on get request' do 31 | it 'should link users' do 32 | expect(@referrer_user.linked_objects).to eq([]) 33 | get(:new) 34 | expect(@referrer_user.reload.linked_objects).to eq([@main_app_user]) 35 | end 36 | end 37 | end 38 | 39 | describe 'without referrer user' do 40 | it 'should not link users due no current referrer user', with_other_referrer_user: true do 41 | get(:new) 42 | expect(@other_referrer_user.linked_objects).to eq([]) 43 | end 44 | 45 | it 'should not link users due no referrer users' do 46 | post(:create, params(post:{title: 'test'})) 47 | end 48 | end 49 | end 50 | 51 | describe 'without main app user' do 52 | describe 'with referrer user', with_referrer_user: true do 53 | describe 'on post request' do 54 | it 'should not link users' do 55 | expect(@referrer_user.linked_objects).to eq([]) 56 | post(:create, params(post:{title: 'test'})) 57 | expect(@referrer_user.linked_objects).to eq([]) 58 | end 59 | end 60 | 61 | describe 'on get request' do 62 | it 'should not link users' do 63 | expect(@referrer_user.linked_objects).to eq([]) 64 | get(:index) 65 | expect(@referrer_user.linked_objects).to eq([]) 66 | end 67 | end 68 | end 69 | 70 | describe 'without referrer user' do 71 | it 'should not link users' do 72 | get(:index) 73 | end 74 | end 75 | end 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /spec/dummy/spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV['RAILS_ENV'] ||= 'test' 3 | require File.expand_path('../../config/environment', __FILE__) 4 | # Prevent database truncation if the environment is production 5 | abort("The Rails environment is running in production mode!") if Rails.env.production? 6 | require 'spec_helper' 7 | require 'rspec/rails' 8 | # Add additional requires below this line. Rails is not loaded until this point! 9 | 10 | # Requires supporting ruby files with custom matchers and macros, etc, in 11 | # spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are 12 | # run as spec files by default. This means that files in spec/support that end 13 | # in _spec.rb will both be required and run as specs, causing the specs to be 14 | # run twice. It is recommended that you do not name files matching this glob to 15 | # end with _spec.rb. You can configure this pattern with the --pattern 16 | # option on the command line or in ~/.rspec, .rspec or `.rspec-local`. 17 | # 18 | # The following line is provided for convenience purposes. It has the downside 19 | # of increasing the boot-up time by auto-requiring all files in the support 20 | # directory. Alternatively, in the individual `*_spec.rb` files, manually 21 | # require only the support files necessary. 22 | 23 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 24 | 25 | # Checks for pending migration and applies them before tests are run. 26 | # If you are not using ActiveRecord, you can remove this line. 27 | ActiveRecord::Migration.maintain_test_schema! 28 | 29 | RSpec.configure do |config| 30 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 31 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 32 | 33 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 34 | # examples within a transaction, remove the following line or assign false 35 | # instead of true. 36 | config.use_transactional_fixtures = false 37 | 38 | # RSpec Rails can automatically mix in different behaviours to your tests 39 | # based on their file location, for example enabling you to call `get` and 40 | # `post` in specs under `spec/controllers`. 41 | # 42 | # You can disable this behaviour by removing the line below, and instead 43 | # explicitly tag your specs with their type, e.g.: 44 | # 45 | # RSpec.describe UsersController, :type => :controller do 46 | # # ... 47 | # end 48 | # 49 | # The different available types are documented in the features, such as in 50 | # https://relishapp.com/rspec/rspec-rails/docs 51 | config.infer_spec_type_from_file_location! 52 | 53 | # Filter lines from Rails gems in backtraces. 54 | config.filter_rails_from_backtrace! 55 | # arbitrary gems may also be filtered via: 56 | # config.filter_gems_from_backtrace("gem name") 57 | end 58 | -------------------------------------------------------------------------------- /spec/dummy/spec/features/js_part_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'JS part', watir: true do 4 | describe 'user' do 5 | it 'should be created' do 6 | expect(Referrer::User.count).to eq(0) 7 | @browser.goto(@url) 8 | Watir::Wait.until{Referrer::User.count == 1} 9 | user = Referrer::User.last 10 | Watir::Wait.until{Referrer::Session.count == 1} 11 | expect(JSON.parse(URI.decode(@browser.cookies.to_a.detect{|c| c[:name] == 'referrer_user'}[:value]))) 12 | .to eq({'id' => user.id, 'token' => user.token}) 13 | end 14 | 15 | it 'should not be overwritten' do 16 | @browser.goto(@url) 17 | Watir::Wait.until{Referrer::User.count == 1} 18 | user = Referrer::User.last 19 | Watir::Wait.until{Referrer::Session.count == 1} 20 | @browser.refresh 21 | Watir::Wait.until{Referrer::Source.count == 2} 22 | expect(Referrer::User.count).to eq(1) 23 | expect(JSON.parse(URI.decode(@browser.cookies.to_a.detect{|c| c[:name] == 'referrer_user'}[:value]))) 24 | .to eq({'id' => user.id, 'token' => user.token}) 25 | end 26 | end 27 | 28 | describe 'session' do 29 | it 'should be created' do 30 | expect(Referrer::Session.count).to eq(0) 31 | @browser.goto(@url) 32 | Watir::Wait.until{Referrer::Session.count == 1} 33 | session = Referrer::Session.last 34 | expect(session.user).to eq(Referrer::User.first) 35 | Watir::Wait.until{Referrer::Source.count == 1} 36 | expect(@browser.cookies.to_a.detect{|c| c[:name] == 'referrer_session_id'}[:value]).to eq(session.id.to_s) 37 | end 38 | 39 | it 'should not be overwritten' do 40 | @browser.goto(@url) 41 | Watir::Wait.until{Referrer::Session.count == 1} 42 | session = Referrer::Session.first 43 | Watir::Wait.until{Referrer::Source.count == 1} 44 | @browser.refresh 45 | Watir::Wait.until{Referrer::Source.count == 2} 46 | expect(Referrer::Session.count).to eq(1) 47 | expect(@browser.cookies.to_a.detect{|c| c[:name] == 'referrer_session_id'}[:value]).to eq(session.id.to_s) 48 | end 49 | 50 | it 'should be overwritten due new user' do 51 | @browser.goto(@url) 52 | Watir::Wait.until{Referrer::Session.count == 1} 53 | Watir::Wait.until{@browser.execute_script('return window.referrer.finished;');} 54 | @browser.cookies.delete('referrer_user') 55 | @browser.refresh 56 | Watir::Wait.until{Referrer::Source.count == 2} 57 | expect(Referrer::Session.count).to eq(2) 58 | new_session = Referrer::Session.last 59 | expect(Referrer::User.count).to eq(2) 60 | expect(new_session.user).to eq(Referrer::User.last) 61 | expect(@browser.cookies.to_a.detect{|c| c[:name] == 'referrer_session_id'}[:value]).to eq(new_session.id.to_s) 62 | end 63 | end 64 | 65 | describe 'source' do 66 | it 'should be created' do 67 | @browser.goto(@url) 68 | Watir::Wait.until{Referrer::Source.count == 1} 69 | source = Referrer::Source.last 70 | expect(source.session.present?).to eq(true) 71 | expect(source.referrer).to eq('') 72 | expect(source.entry_point).to eq('http://localhost:3000/') 73 | end 74 | end 75 | 76 | end -------------------------------------------------------------------------------- /spec/dummy/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like 20 | # NGINX, varnish or squid. 21 | # config.action_dispatch.rack_cache = true 22 | 23 | # Disable serving static files from the `/public` folder by default since 24 | # Apache or NGINX already handles this. 25 | if Rails::VERSION::MAJOR > 4 26 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 27 | else 28 | config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? 29 | end 30 | 31 | # Compress JavaScripts and CSS. 32 | config.assets.js_compressor = :uglifier 33 | # config.assets.css_compressor = :sass 34 | 35 | # Do not fallback to assets pipeline if a precompiled asset is missed. 36 | config.assets.compile = false 37 | 38 | # Asset digests allow you to set far-future HTTP expiration dates on all assets, 39 | # yet still be able to expire them through the digest params. 40 | config.assets.digest = true 41 | 42 | # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb 43 | 44 | # Specifies the header that your server uses for sending files. 45 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 46 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 47 | 48 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 49 | # config.force_ssl = true 50 | 51 | # Use the lowest log level to ensure availability of diagnostic information 52 | # when problems arise. 53 | config.log_level = :debug 54 | 55 | # Prepend all log lines with the following tags. 56 | # config.log_tags = [ :subdomain, :uuid ] 57 | 58 | # Use a different logger for distributed setups. 59 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 60 | 61 | # Use a different cache store in production. 62 | # config.cache_store = :mem_cache_store 63 | 64 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 65 | # config.action_controller.asset_host = 'http://assets.example.com' 66 | 67 | # Ignore bad email addresses and do not raise email delivery errors. 68 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 69 | # config.action_mailer.raise_delivery_errors = false 70 | 71 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 72 | # the I18n.default_locale when a translation cannot be found). 73 | config.i18n.fallbacks = true 74 | 75 | # Send deprecation notices to registered listeners. 76 | config.active_support.deprecation = :notify 77 | 78 | # Use default logging formatter so that PID and timestamp are not suppressed. 79 | config.log_formatter = ::Logger::Formatter.new 80 | 81 | # Do not dump schema after migrations. 82 | config.active_record.dump_schema_after_migration = false 83 | end 84 | -------------------------------------------------------------------------------- /spec/dummy/spec/models/source_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Referrer::Source, type: :model do 4 | before :all do 5 | @class = Referrer::Source 6 | end 7 | 8 | before :each do 9 | Referrer.markup_generator_settings = {} 10 | @class.instance_variable_set(:@markup_generator, nil) 11 | end 12 | 13 | before :each, with_session: true do 14 | @user = Referrer::User.create! 15 | @session = @user.sessions.create! 16 | end 17 | 18 | describe 'markup generator' do 19 | it 'should be initialized only once' do 20 | expect(@class.markup_generator.__id__).to eq(@class.markup_generator.__id__) 21 | end 22 | 23 | it 'should be itinialized with custom params' do 24 | custom_params = { 25 | organics: Referrer::MarkupGenerator::ORGANICS << {host: 'search.test.test', param: 'query'}, 26 | referrals: Referrer::MarkupGenerator::REFERRALS << {host: /^(www\.)?test1\.test$/, display: 'test1.test'}, 27 | utm_synonyms: {utm_campaign: 'custom_campaign'}, 28 | array_params_joiner: '|' 29 | } 30 | Referrer.markup_generator_settings = custom_params 31 | mg = @class.markup_generator 32 | custom_params.each do |k, v| 33 | expect(mg.send(k)).to eq(v) 34 | end 35 | end 36 | end 37 | 38 | describe 'utm markup', with_session: true do 39 | it 'should be created before creation' do 40 | source = @session.sources.new(referrer: 'http://test.com/a', entry_point: 'https://www.dummy.com/welcome', client_duplicate_id: 1) 41 | source.save! 42 | expect(source.utm_campaign).to eq('(none)') 43 | expect(source.utm_source).to eq('test.com') 44 | expect(source.utm_medium).to eq('referral') 45 | expect(source.utm_content).to eq('/a') 46 | expect(source.utm_term).to eq('(none)') 47 | expect(source.kind).to eq('referral') 48 | end 49 | end 50 | 51 | describe 'priority', with_session: true do 52 | describe 'for first source' do 53 | it 'should set' do 54 | [['', 'http://dummy.com'], 55 | ['http://test.com', 'http://dummy.com'], 56 | ['http://google.com/?q=query', 'http://dummy.com'], 57 | ['http://test.com/?utm_source=source', 'http://dummy.com']].each do |arr| 58 | @session.sources.destroy_all 59 | source = @session.sources.create!(referrer: arr[0], entry_point: arr[1], client_duplicate_id: 1) 60 | expect(source.priority).to eq(true) 61 | end 62 | end 63 | end 64 | 65 | describe 'for several sources' do 66 | it 'should set' do 67 | @session.sources.create!(referrer: '', entry_point: 'https://www.dummy.com/welcome', client_duplicate_id: 1) 68 | expect(@session.sources.create!(referrer: '', entry_point: 'https://www.dummy.com/welcome', client_duplicate_id: 2).priority).to eq(true) 69 | expect(@session.sources.create!(referrer: 'http://test.com', entry_point: 'https://www.dummy.com/welcome', client_duplicate_id: 3).priority).to eq(true) 70 | end 71 | 72 | it 'should not set' do 73 | @session.sources.create!(referrer: 'http://test.com', entry_point: 'https://www.dummy.com/welcome', client_duplicate_id: 1) 74 | expect(@session.sources.create!(referrer: '', entry_point: 'https://www.dummy.com/welcome', client_duplicate_id: 2).priority).to eq(false) 75 | end 76 | end 77 | 78 | describe 'to_markup' do 79 | it 'should be correct' do 80 | source = @session.sources.create!(referrer: 'http://test.com/a', entry_point: 'https://www.dummy.com/welcome', 81 | client_duplicate_id: 1) 82 | expect(source.to_markup).to eq({'utm_source'=>'test.com', 'utm_campaign'=>'(none)', 'utm_medium'=>'referral', 83 | 'utm_content'=>'/a', 'utm_term'=>'(none)', 'kind'=>'referral'}) 84 | end 85 | end 86 | end 87 | end -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | referrer (1.0.0) 5 | rails (>= 4, < 6) 6 | 7 | GEM 8 | remote: https://rubygems.org/ 9 | specs: 10 | actioncable (5.0.0) 11 | actionpack (= 5.0.0) 12 | nio4r (~> 1.2) 13 | websocket-driver (~> 0.6.1) 14 | actionmailer (5.0.0) 15 | actionpack (= 5.0.0) 16 | actionview (= 5.0.0) 17 | activejob (= 5.0.0) 18 | mail (~> 2.5, >= 2.5.4) 19 | rails-dom-testing (~> 2.0) 20 | actionpack (5.0.0) 21 | actionview (= 5.0.0) 22 | activesupport (= 5.0.0) 23 | rack (~> 2.0) 24 | rack-test (~> 0.6.3) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | actionview (5.0.0) 28 | activesupport (= 5.0.0) 29 | builder (~> 3.1) 30 | erubis (~> 2.7.0) 31 | rails-dom-testing (~> 2.0) 32 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 33 | activejob (5.0.0) 34 | activesupport (= 5.0.0) 35 | globalid (>= 0.3.6) 36 | activemodel (5.0.0) 37 | activesupport (= 5.0.0) 38 | activerecord (5.0.0) 39 | activemodel (= 5.0.0) 40 | activesupport (= 5.0.0) 41 | arel (~> 7.0) 42 | activesupport (5.0.0) 43 | concurrent-ruby (~> 1.0, >= 1.0.2) 44 | i18n (~> 0.7) 45 | minitest (~> 5.1) 46 | tzinfo (~> 1.1) 47 | arel (7.1.0) 48 | builder (3.2.2) 49 | byebug (9.0.5) 50 | childprocess (0.5.9) 51 | ffi (~> 1.0, >= 1.0.11) 52 | concurrent-ruby (1.0.2) 53 | database_cleaner (1.5.3) 54 | diff-lcs (1.2.5) 55 | erubis (2.7.0) 56 | execjs (2.7.0) 57 | ffi (1.9.14) 58 | globalid (0.3.7) 59 | activesupport (>= 4.1.0) 60 | headless (2.2.3) 61 | i18n (0.7.0) 62 | loofah (2.0.3) 63 | nokogiri (>= 1.5.9) 64 | mail (2.6.4) 65 | mime-types (>= 1.16, < 4) 66 | method_source (0.8.2) 67 | mime-types (3.1) 68 | mime-types-data (~> 3.2015) 69 | mime-types-data (3.2016.0521) 70 | mini_portile2 (2.1.0) 71 | minitest (5.9.0) 72 | nio4r (1.2.1) 73 | nokogiri (1.6.8) 74 | mini_portile2 (~> 2.1.0) 75 | pkg-config (~> 1.1.7) 76 | pkg-config (1.1.7) 77 | rack (2.0.1) 78 | rack-test (0.6.3) 79 | rack (>= 1.0) 80 | rails (5.0.0) 81 | actioncable (= 5.0.0) 82 | actionmailer (= 5.0.0) 83 | actionpack (= 5.0.0) 84 | actionview (= 5.0.0) 85 | activejob (= 5.0.0) 86 | activemodel (= 5.0.0) 87 | activerecord (= 5.0.0) 88 | activesupport (= 5.0.0) 89 | bundler (>= 1.3.0, < 2.0) 90 | railties (= 5.0.0) 91 | sprockets-rails (>= 2.0.0) 92 | rails-dom-testing (2.0.1) 93 | activesupport (>= 4.2.0, < 6.0) 94 | nokogiri (~> 1.6.0) 95 | rails-html-sanitizer (1.0.3) 96 | loofah (~> 2.0) 97 | railties (5.0.0) 98 | actionpack (= 5.0.0) 99 | activesupport (= 5.0.0) 100 | method_source 101 | rake (>= 0.8.7) 102 | thor (>= 0.18.1, < 2.0) 103 | rake (11.2.2) 104 | rspec-core (3.5.1) 105 | rspec-support (~> 3.5.0) 106 | rspec-expectations (3.5.0) 107 | diff-lcs (>= 1.2.0, < 2.0) 108 | rspec-support (~> 3.5.0) 109 | rspec-mocks (3.5.0) 110 | diff-lcs (>= 1.2.0, < 2.0) 111 | rspec-support (~> 3.5.0) 112 | rspec-rails (3.5.1) 113 | actionpack (>= 3.0) 114 | activesupport (>= 3.0) 115 | railties (>= 3.0) 116 | rspec-core (~> 3.5.0) 117 | rspec-expectations (~> 3.5.0) 118 | rspec-mocks (~> 3.5.0) 119 | rspec-support (~> 3.5.0) 120 | rspec-support (3.5.0) 121 | rubyzip (1.2.0) 122 | selenium-webdriver (2.53.4) 123 | childprocess (~> 0.5) 124 | rubyzip (~> 1.0) 125 | websocket (~> 1.0) 126 | sprockets (3.7.0) 127 | concurrent-ruby (~> 1.0) 128 | rack (> 1, < 3) 129 | sprockets-rails (3.1.1) 130 | actionpack (>= 4.0) 131 | activesupport (>= 4.0) 132 | sprockets (>= 3.0.0) 133 | sqlite3 (1.3.11) 134 | thor (0.19.1) 135 | thread_safe (0.3.5) 136 | tzinfo (1.2.2) 137 | thread_safe (~> 0.1) 138 | uglifier (3.0.0) 139 | execjs (>= 0.3.0, < 3) 140 | watir-webdriver (0.9.1) 141 | selenium-webdriver (>= 2.46.2) 142 | websocket (1.2.3) 143 | websocket-driver (0.6.4) 144 | websocket-extensions (>= 0.1.0) 145 | websocket-extensions (0.1.2) 146 | 147 | PLATFORMS 148 | ruby 149 | 150 | DEPENDENCIES 151 | byebug 152 | database_cleaner 153 | headless 154 | referrer! 155 | rspec-rails (~> 3.5) 156 | sqlite3 157 | uglifier 158 | watir-webdriver 159 | 160 | BUNDLED WITH 161 | 1.10.6 162 | -------------------------------------------------------------------------------- /spec/dummy/spec/lib/statistics_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Statistics' do 4 | before :each do 5 | @user = Referrer::User.create! 6 | @session = @user.sessions.create!(active_from: 10.days.ago, active_until: 5.days.ago + 10.minutes) 7 | @session_1 = @user.sessions.create!(active_from: 5.days.ago + 10.minutes) 8 | @session_source = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com/?utm_source=0-0', 9 | client_duplicate_id: 1, created_at: 9.days.ago) 10 | @session_source_1 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com/?utm_source=0-0', 11 | client_duplicate_id: 1, created_at: 7.days.ago) 12 | @session_1_source = @session_1.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com/?utm_source=1-0', 13 | client_duplicate_id: 1, created_at: 5.days.ago + 10.minutes) 14 | @session_1_source_1 = @session_1.sources.create!(referrer: '', entry_point: 'http://dummy.com/', 15 | client_duplicate_id: 1, created_at: 3.days.ago) 16 | end 17 | 18 | describe 'sources_markup' do 19 | it 'should return data' do 20 | result = Referrer::Statistics.sources_markup(10.days.ago, 2.days.ago) 21 | expect(result). 22 | to eq([{utm_source: '0-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 23 | utm_term: '(none)', kind: 'utm', count: 2}, 24 | {utm_source: '1-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 25 | utm_term: '(none)', kind: 'utm', count: 1}, 26 | {utm_source: '(direct)', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 27 | utm_term: '(none)', kind: 'direct', count: 1}]) 28 | end 29 | 30 | it 'should return part of data' do 31 | result = Referrer::Statistics.sources_markup(8.days.ago, 4.days.ago) 32 | expect(result). 33 | to eq([{utm_source: '0-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 34 | utm_term: '(none)', kind: 'utm', count: 1}, 35 | {utm_source: '1-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 36 | utm_term: '(none)', kind: 'utm', count: 1}]) 37 | end 38 | 39 | it 'should not return data' do 40 | expect(Referrer::Statistics.sources_markup(20.days.ago, 10.days.ago)).to eq([]) 41 | end 42 | end 43 | 44 | describe 'tracked_objects_markup' do 45 | before :each do 46 | @user_1 = Referrer::User.create! 47 | @request = Request.create! 48 | @request_1 = Request.create! 49 | @support = Support.create! 50 | @support_1 = Support.create! 51 | Request.create! 52 | Support.create! 53 | @request.referrer_link_with(@user, linked_at: 8.days.ago) 54 | @request_1.referrer_link_with(@user_1, linked_at: 6.days.ago) 55 | @support.referrer_link_with(@user, linked_at: 6.days.ago) 56 | @support_1.referrer_link_with(@user, linked_at: 4.days.ago) 57 | end 58 | 59 | it 'should return data' do 60 | result = Referrer::Statistics.tracked_objects_markup(10.days.ago, 2.days.ago) 61 | expect(result).to eq([{utm_source: '0-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 62 | utm_term: '(none)', kind: 'utm', count: 2}, 63 | {utm_source: '1-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 64 | utm_term: '(none)', kind: 'utm', count: 1}]) 65 | end 66 | 67 | it 'should return data only for one class' do 68 | result = Referrer::Statistics.tracked_objects_markup(10.days.ago, 2.days.ago, type: 'Request') 69 | expect(result).to eq([{utm_source: '0-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 70 | utm_term: '(none)', kind: 'utm', count: 1}]) 71 | end 72 | 73 | it 'should return part of data' do 74 | result = Referrer::Statistics.tracked_objects_markup(7.days.ago, 2.days.ago) 75 | expect(result).to eq([{utm_source: '0-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 76 | utm_term: '(none)', kind: 'utm', count: 1}, 77 | {utm_source: '1-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 78 | utm_term: '(none)', kind: 'utm', count: 1}]) 79 | end 80 | 81 | it 'should not return data' do 82 | expect(Referrer::Statistics.tracked_objects_markup(20.days.ago, 15.days.ago)).to eq([]) 83 | end 84 | end 85 | end 86 | -------------------------------------------------------------------------------- /lib/other/markup_generator.rb: -------------------------------------------------------------------------------- 1 | module Referrer 2 | class MarkupGenerator 3 | UTM_KEYS = %w(utm_source utm_medium utm_campaign utm_content utm_term) 4 | ORGANICS = [{host: 'search.daum.net', param: 'q'}, 5 | {host: 'search.naver.com', param: 'query'}, 6 | {host: 'search.yahoo.com', param: 'p'}, 7 | {host: /^(www\.)?google\.[a-z]+$/, param: 'q', display: 'google'}, 8 | {host: 'www.bing.com', param: 'q'}, 9 | {host: 'search.aol.com', params: 'q'}, 10 | {host: 'search.lycos.com', param: 'q'}, 11 | {host: 'edition.cnn.com', param: 'text'}, 12 | {host: 'index.about.com', param: 'q'}, 13 | {host: 'mamma.com', param: 'q'}, 14 | {host: 'ricerca.virgilio.it', param: 'qs'}, 15 | {host: 'www.baidu.com', param: 'wd'}, 16 | {host: /^(www\.)?yandex\.[a-z]+$/, param: 'text', display: 'yandex'}, 17 | {host: 'search.seznam.cz', param: 'oq'}, 18 | {host: 'www.search.com', param: 'q'}, 19 | {host: 'search.yam.com', param: 'k'}, 20 | {host: 'www.kvasir.no', param: 'q'}, 21 | {host: 'buscador.terra.com', param: 'query'}, 22 | {host: 'nova.rambler.ru', param: 'query'}, 23 | {host: 'go.mail.ru', param: 'q'}, 24 | {host: 'www.ask.com', param: 'q'}, 25 | {host: 'searches.globososo.com', param: 'q'}, 26 | {host: 'search.tut.by', param: 'query'}] 27 | 28 | REFERRALS = [{host: /^(www\.)?t\.co$/, display: 'twitter.com'}, 29 | {host: /^(www\.)?plus\.url\.google\.com$/, display: 'plus.google.com'}] 30 | 31 | attr_accessor :organics, :referrals, :utm_synonyms, :array_params_joiner 32 | 33 | def initialize 34 | @organics = ORGANICS 35 | @referrals = REFERRALS 36 | @utm_synonyms = UTM_KEYS.inject({}){|r, key| r.merge({key => []})} 37 | @array_params_joiner = ', ' 38 | end 39 | 40 | def generate(referrer, entry_point) 41 | referrer_uri, entry_point_uri = *[referrer, entry_point].map{|url| URI(URI::encode(url || ''))} 42 | referrer_params, entry_point_params = *[referrer_uri, entry_point_uri].map{|uri| uri_params(uri)} 43 | prepare_result(utm(entry_point_params) || organic(referrer_uri, referrer_params) || 44 | referral(referrer_uri) || direct) 45 | end 46 | 47 | private 48 | 49 | def uri_params(uri) 50 | Rack::Utils.parse_query(uri.try(:query)) 51 | end 52 | 53 | def base_result 54 | UTM_KEYS.inject({}){|r, key| r.merge!(key => '(none)')} 55 | end 56 | 57 | def check_host(option, value) 58 | case option 59 | when String 60 | option == value 61 | when Regexp 62 | option =~ value 63 | else 64 | false 65 | end 66 | end 67 | 68 | def prepare_result(markup) 69 | Hash[markup.map{|k, v| [k, v.is_a?(Array) ? v.join(array_params_joiner) : v]}].symbolize_keys 70 | end 71 | 72 | def utm(entry_point_params) 73 | if (entry_point_params.keys & (UTM_KEYS + utm_synonyms.values.flatten)).present? 74 | UTM_KEYS.inject(base_result) do |r, key| 75 | values = if utm_synonyms[key.to_sym].present? 76 | [].push(entry_point_params[key]).push([utm_synonyms[key.to_sym]].flatten.map do |synonym_key| 77 | entry_point_params[synonym_key] 78 | end) 79 | else 80 | [entry_point_params[key]] 81 | end.flatten.compact.map{|value| URI::decode(value)} 82 | values.present? ? r.merge!({key => values}) : r 83 | end.merge('kind' => 'utm') 84 | end 85 | end 86 | 87 | def organic(referrer_uri, referrer_params) 88 | if referrer_uri.to_s.present? 89 | current_organic = organics.detect{|organic| check_host(organic[:host], referrer_uri.host)} 90 | base_result.merge!({'utm_source' => current_organic[:display] || current_organic[:host].split('.')[-2], 91 | 'utm_medium' => 'organic', 92 | 'utm_term' => referrer_params[current_organic[:param]] || '(none)', 93 | 'kind' => 'organic'}) if current_organic.present? 94 | end 95 | end 96 | 97 | def referral(referrer_uri) 98 | if referrer_uri.to_s.present? 99 | custom_referral = referrals.detect{|referral| check_host(referral[:host], referrer_uri.host)} 100 | base_result.merge!( 101 | 'utm_source' => custom_referral ? custom_referral[:display] : referrer_uri.host.gsub('www.', ''), 102 | 'utm_medium' => 'referral', 103 | 'utm_content' => URI::decode(referrer_uri.request_uri) || '(none)', 104 | 'kind' => 'referral') 105 | end 106 | end 107 | 108 | def direct 109 | base_result.merge!('utm_source' => '(direct)', 'kind' => 'direct') 110 | end 111 | end 112 | end -------------------------------------------------------------------------------- /spec/dummy/spec/lib/markup_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Markup generator' do 4 | before :all do 5 | @mg = Referrer::MarkupGenerator.new 6 | end 7 | 8 | describe 'for direct channel' do 9 | it 'should work' do 10 | [[nil, nil], 11 | ['', ''], 12 | [nil, 'http:://dummy.com:3000/'], 13 | ['', 'http:://dummy.com:3000/']].each do |params| 14 | expect(@mg.generate(*params)).to eq({utm_source: '(direct)', 15 | utm_medium: '(none)', 16 | utm_campaign: '(none)', 17 | utm_content: '(none)', 18 | utm_term: '(none)', 19 | kind: 'direct'}) 20 | end 21 | end 22 | end 23 | 24 | describe 'for utm channel' do 25 | it 'should work with all params' do 26 | [nil, '', 'http://test.com'].each do |referrer| 27 | expect(@mg.generate( 28 | referrer, 29 | 'http://www.dummy.com/test/?utm_campaign=campaign&utm_medium=medium&utm_source=source&utm_term=term&utm_content=content' 30 | )).to eq({utm_source: 'source', 31 | utm_medium: 'medium', 32 | utm_campaign: 'campaign', 33 | utm_content: 'content', 34 | utm_term: 'term', 35 | kind: 'utm'}) 36 | end 37 | end 38 | 39 | it 'should work with several params' do 40 | expect(@mg.generate( 41 | 'http://www.test.com', 42 | 'http://www.dummy.com/test/?utm_campaign=campaign&utm_term=term' 43 | )).to eq({utm_source: '(none)', 44 | utm_medium: '(none)', 45 | utm_campaign: 'campaign', 46 | utm_content: '(none)', 47 | utm_term: 'term', 48 | kind: 'utm'}) 49 | end 50 | 51 | describe 'with utm synonyms' do 52 | it 'should work with synonyms and non-synonyms' do 53 | (mg = @mg.clone).utm_synonyms = {utm_campaign: 'custom_campaign', utm_source: %w(source custom_source)} 54 | expect(mg.generate( 55 | 'http://www.test.com', 56 | 'http://www.dummy.com/test/?custom_campaign=campaign&utm_term=term&source=source' 57 | )).to eq({utm_source: 'source', 58 | utm_medium: '(none)', 59 | utm_campaign: 'campaign', 60 | utm_content: '(none)', 61 | utm_term: 'term', 62 | kind: 'utm'}) 63 | end 64 | 65 | it 'should work with only synonyms' do 66 | (mg = @mg.clone).utm_synonyms = {utm_campaign: 'custom_campaign', utm_source: %w(source custom_source)} 67 | expect(mg.generate( 68 | 'http://www.test.com', 69 | 'http://www.dummy.com/test/?custom_campaign=campaign&source=source' 70 | )).to eq({utm_source: 'source', 71 | utm_medium: '(none)', 72 | utm_campaign: 'campaign', 73 | utm_content: '(none)', 74 | utm_term: '(none)', 75 | kind: 'utm'}) 76 | end 77 | end 78 | end 79 | 80 | describe 'for organic channel' do 81 | it 'should work' do 82 | expect(@mg.generate( 83 | 'https://www.google.ru/search?q=test&oq=test&sourceid=chrome&ie=UTF-8', 84 | 'http://www.dummy.com/' 85 | )).to eq({utm_source: 'google', 86 | utm_medium: 'organic', 87 | utm_campaign: '(none)', 88 | utm_content: '(none)', 89 | utm_term: 'test', 90 | kind: 'organic'}) 91 | end 92 | end 93 | 94 | describe 'for referral channel' do 95 | describe 'for default source' do 96 | it 'should work' do 97 | expect(@mg.generate( 98 | 'https://www.sub.test.com/a/b/?param=1#tag', 99 | 'http://www.dummy.com/' 100 | )).to eq({utm_source: 'sub.test.com', 101 | utm_medium: 'referral', 102 | utm_campaign: '(none)', 103 | utm_content: '/a/b/?param=1#tag', 104 | utm_term: '(none)', 105 | kind: 'referral'}) 106 | end 107 | end 108 | 109 | describe 'for custom source' do 110 | it 'should work' do 111 | expect(@mg.generate( 112 | 'https://www.t.co/test', 113 | 'http://www.dummy.com/' 114 | )).to eq({utm_source: 'twitter.com', 115 | utm_medium: 'referral', 116 | utm_campaign: '(none)', 117 | utm_content: '/test', 118 | utm_term: '(none)', 119 | kind: 'referral'}) 120 | end 121 | end 122 | end 123 | end -------------------------------------------------------------------------------- /spec/dummy/spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'database_cleaner' 2 | require 'byebug' 3 | require 'watir-webdriver' 4 | require 'headless' 5 | 6 | 7 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 8 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 9 | # The generated `.rspec` file contains `--require spec_helper` which will cause 10 | # this file to always be loaded, without a need to explicitly require it in any 11 | # files. 12 | # 13 | # Given that it is always loaded, you are encouraged to keep this file as 14 | # light-weight as possible. Requiring heavyweight dependencies from this file 15 | # will add to the boot time of your test suite on EVERY test run, even for an 16 | # individual file that may not need all of that loaded. Instead, consider making 17 | # a separate helper file that requires the additional dependencies and performs 18 | # the additional setup, and require it from the spec files that actually need 19 | # it. 20 | # 21 | # The `.rspec` file also contains a few flags that are not defaults but that 22 | # users commonly want. 23 | # 24 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 25 | RSpec.configure do |config| 26 | # rspec-expectations config goes here. You can use an alternate 27 | # assertion/expectation library such as wrong or the stdlib/minitest 28 | # assertions if you prefer. 29 | config.expect_with :rspec do |expectations| 30 | # This option will default to `true` in RSpec 4. It makes the `description` 31 | # and `failure_message` of custom matchers include text for helper methods 32 | # defined using `chain`, e.g.: 33 | # be_bigger_than(2).and_smaller_than(4).description 34 | # # => "be bigger than 2 and smaller than 4" 35 | # ...rather than: 36 | # # => "be bigger than 2" 37 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 38 | end 39 | 40 | # rspec-mocks config goes here. You can use an alternate test double 41 | # library (such as bogus or mocha) by changing the `mock_with` option here. 42 | config.mock_with :rspec do |mocks| 43 | # Prevents you from mocking or stubbing a method that does not exist on 44 | # a real object. This is generally recommended, and will default to 45 | # `true` in RSpec 4. 46 | mocks.verify_partial_doubles = true 47 | end 48 | 49 | # These two settings work together to allow you to limit a spec run 50 | # to individual examples or groups you care about by tagging them with 51 | # `:focus` metadata. When nothing is tagged with `:focus`, all examples 52 | # get run. 53 | config.filter_run :focus 54 | config.run_all_when_everything_filtered = true 55 | 56 | # Allows RSpec to persist some state between runs in order to support 57 | # the `--only-failures` and `--next-failure` CLI options. We recommend 58 | # you configure your source control system to ignore this file. 59 | # config.example_status_persistence_file_path = 'spec/examples.txt' 60 | 61 | # Limits the available syntax to the non-monkey patched syntax that is 62 | # recommended. For more details, see: 63 | # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ 64 | # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ 65 | # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode 66 | config.disable_monkey_patching! 67 | 68 | # Many RSpec users commonly either run the entire suite or an individual 69 | # file, and it's useful to allow more verbose output when running an 70 | # individual spec file. 71 | if config.files_to_run.one? 72 | # Use the documentation formatter for detailed output, 73 | # unless a formatter has already been configured 74 | # (e.g. via a command-line flag). 75 | config.default_formatter = 'doc' 76 | end 77 | 78 | # Print the 10 slowest examples and example groups at the 79 | # end of the spec run, to help surface which specs are running 80 | # particularly slow. 81 | config.profile_examples = 10 82 | 83 | # Run specs in random order to surface order dependencies. If you find an 84 | # order dependency and want to debug it, you can fix the order by providing 85 | # the seed, which is printed after each run. 86 | # --seed 1234 87 | config.order = :random 88 | 89 | # Seed global randomization in this process using the `--seed` CLI option. 90 | # Setting this allows you to use `--seed` to deterministically reproduce 91 | # test failures related to randomization by passing the same `--seed` value 92 | # as the one that triggered the failure. 93 | Kernel.srand config.seed 94 | 95 | config.before(:suite) do 96 | DatabaseCleaner.strategy = :truncation 97 | end 98 | 99 | config.before(:each) do 100 | DatabaseCleaner.clean 101 | end 102 | 103 | config.before(:all, watir: true) do 104 | @url = 'http://localhost:3000/' 105 | end 106 | 107 | config.before(:each, watir: true) do 108 | (@headless = Headless.new).start unless ENV['WITHOUT_HEADLESS'] 109 | @browser = Watir::Browser.new(:firefox) 110 | end 111 | 112 | config.after(:each, watir: true) do 113 | @browser.close 114 | @headless.destroy if @headless.present? 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /spec/dummy/spec/controllers/sources_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | include ControllersCompatibility 3 | 4 | RSpec.describe Referrer::SourcesController, type: :controller do 5 | routes { Referrer::Engine.routes } 6 | 7 | before :each do 8 | @user = Referrer::User.create! 9 | @session = @user.sessions.create! 10 | end 11 | 12 | before :each, with_other_sessions: true do 13 | @other_session = @user.sessions.create!(created_at: Time.now - 1.month) 14 | @other_session_1 = @user.sessions.create!(created_at: Time.now - 1.day) 15 | end 16 | 17 | describe 'mass create' do 18 | it 'should be done' do 19 | expect(@session.sources.count).to eq(0) 20 | post(:mass_create, params(sources: {current_session_id: @session.id, user_token: @user.token, user_id: @user.id, values: [{ 21 | referrer: 'http://test.com', entry_point: 'http://dummy.com', client_duplicate_id: 1}].to_json})) 22 | expect(@session.sources.count).to eq(1) 23 | source = @session.sources.last 24 | result = JSON.parse(response.body) 25 | expect(result.keys).to eq(%w(ids)) 26 | expect(result['ids']).to eq([source.id]) 27 | expect(source.kind).to eq('referral') 28 | end 29 | 30 | it 'should not be done because incorrect token' do 31 | expect(@session.sources.count).to eq(0) 32 | post(:mass_create, params(sources: {current_session_id: @session.id, user_token: 'test', user_id: @user.id, values: [{ 33 | referrer: 'http://test.com', entry_point: 'http://dummy.com', client_duplicate_id: 1}].to_json})) 34 | expect(@session.sources.count).to eq(0) 35 | result = JSON.parse(response.body) 36 | expect(response.code).to eq('401') 37 | expect(result).to eq({'errors'=>['User token is incorrect']}) 38 | end 39 | 40 | it 'should be done for source with session id', with_other_sessions: true do 41 | expect(@session.sources.count).to eq(0) 42 | post(:mass_create, params(sources: {current_session_id: @session.id, 43 | user_token: @user.token, 44 | user_id: @user.id, 45 | values: [{ 46 | referrer: 'http://test.com', entry_point: 'http://dummy.com', 47 | client_duplicate_id: 1, session_id: @other_session.id 48 | }].to_json})) 49 | expect(@other_session.sources.count).to eq(1) 50 | source = @other_session.sources.last 51 | expect(source.session_id).to eq(@other_session.id) 52 | end 53 | 54 | it 'should be done with several source values' do 55 | expect(@session.sources.count).to eq(0) 56 | post(:mass_create, params(sources: {current_session_id: @session.id, 57 | user_token: @user.token, 58 | user_id: @user.id, 59 | values: [{referrer: 'http://test.com', entry_point: 'http://dummy.com', client_duplicate_id: 1}, 60 | {referrer: 'http://test2.com', entry_point: 'http://dummy2.com', client_duplicate_id: 2} 61 | ].to_json})) 62 | expect(@session.sources.count).to eq(2) 63 | referrers = @session.sources.pluck(:referrer) 64 | %w{http://test2.com http://test.com}.each do |referrer| 65 | expect(referrers.include?(referrer)).to eq(true) 66 | end 67 | end 68 | 69 | describe 'with duplicates' do 70 | it 'in params should not be done' do 71 | expect(@session.sources.count).to eq(0) 72 | post(:mass_create, params(sources: {current_session_id: @session.id, 73 | user_token: @user.token, 74 | user_id: @user.id, 75 | values: [{referrer: 'http://test.com', entry_point: 'http://dummy.com', client_duplicate_id: 1}, 76 | {referrer: 'http://test2.com', entry_point: 'http://dummy2.com', client_duplicate_id: 1} 77 | ].to_json})) 78 | expect(@session.sources.count).to eq(1) 79 | source = @session.sources.last 80 | expect(source.referrer).to eq('http://test.com') 81 | end 82 | 83 | it 'in db should not be done' do 84 | source = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com', client_duplicate_id: 1) 85 | post(:mass_create, params(sources: {current_session_id: @session.id, 86 | user_token: @user.token, 87 | user_id: @user.id, 88 | values: [{ 89 | referrer: 'http://test2.com', entry_point: 'http://dummy2.com', 90 | client_duplicate_id: 1 91 | }].to_json})) 92 | @session.sources.reload 93 | expect(@session.sources.count).to eq(1) 94 | expect(Referrer::Source.count).to eq(1) 95 | s = @session.sources.last 96 | expect(s.id).to eq(source.id) 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/dummy/spec/lib/owner_model_additions_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe 'Owner model additions' do 4 | %w{User Company}.each do |main_app_user_class| 5 | describe "with main app user #{main_app_user_class}" do 6 | before :each do 7 | @main_app_user = Object.const_get(main_app_user_class).create! 8 | @user = Referrer::User.create! 9 | end 10 | 11 | before :each, with_users_relation: true do 12 | @user.link_with(@main_app_user) 13 | end 14 | 15 | before :each, with_sessions: true do 16 | @session = @user.sessions.create!(active_from: 10.days.ago) 17 | @session_1 = @user.sessions.create!(active_from: 5.days.ago) 18 | end 19 | 20 | before :each, with_sources: true do 21 | @session_source = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com/?utm_source=0-0', 22 | client_duplicate_id: 1, created_at: 9.days.ago) 23 | @session_source_1 = @session.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com/?utm_source=0-1', 24 | client_duplicate_id: 1, created_at: 7.days.ago) 25 | @session_1_source = @session_1.sources.create!(referrer: 'http://test.com', entry_point: 'http://dummy.com/?utm_source=1-0', 26 | client_duplicate_id: 1, created_at: 5.days.ago + 10.minutes) 27 | @session_1_source_1 = @session_1.sources.create!(referrer: '', entry_point: 'http://dummy.com/', 28 | client_duplicate_id: 1, created_at: 4.days.ago) 29 | end 30 | 31 | describe 'relations' do 32 | describe 'referrer_users_main_app_users' do 33 | it 'should return main app user', with_users_relation: true do 34 | expect(@main_app_user.referrer_users_main_app_users.size).to eq(1) 35 | relation = @main_app_user.referrer_users_main_app_users.first 36 | expect(relation.user_id).to eq(@user.id) 37 | end 38 | 39 | it 'should not return associated users' do 40 | expect(@main_app_user.referrer_users_main_app_users).to eq([]) 41 | end 42 | end 43 | end 44 | 45 | describe 'referrer_users' do 46 | it 'should be returned', with_users_relation: true do 47 | expect(@main_app_user.referrer_users).to eq([@user]) 48 | end 49 | 50 | it 'should not be returned' do 51 | expect(@main_app_user.referrer_users).to eq([]) 52 | end 53 | end 54 | 55 | describe 'referrer_sources' do 56 | it 'should be returned', with_users_relation: true, with_sessions: true, with_sources: true do 57 | expect(@main_app_user.referrer_sources).to eq([@session_source, @session_source_1, @session_1_source, @session_1_source_1]) 58 | end 59 | 60 | it 'should not be returned without sources', with_users_relation: true, with_sessions: true do 61 | expect(@main_app_user.referrer_sources).to eq([]) 62 | end 63 | 64 | it 'should not be returned without sessions', with_users_relation: true do 65 | expect(@main_app_user.referrer_sources).to eq([]) 66 | end 67 | 68 | it 'should not be returned without referrer users' do 69 | expect(@main_app_user.referrer_sources).to eq([]) 70 | end 71 | end 72 | 73 | describe 'referrer sources block', with_users_relation: true, with_sessions: true do 74 | describe 'referrer_first_source' do 75 | it 'should be returned', with_sources: true do 76 | expect(@main_app_user.referrer_first_source).to eq(@session_source) 77 | end 78 | 79 | it 'should not be returned' do 80 | expect(@main_app_user.referrer_first_source).to eq(nil) 81 | end 82 | end 83 | 84 | describe 'referrer_priority_source' do 85 | it 'should be returned', with_sources: true do 86 | expect(@main_app_user.referrer_priority_source).to eq(@session_1_source) 87 | end 88 | 89 | it 'should not be returned' do 90 | expect(@main_app_user.referrer_priority_source).to eq(nil) 91 | end 92 | end 93 | 94 | describe 'referrer_last_source' do 95 | it 'should be returned', with_sources: true do 96 | expect(@main_app_user.referrer_last_source).to eq(@session_1_source_1) 97 | end 98 | 99 | it 'should not be returned' do 100 | expect(@main_app_user.referrer_last_source).to eq(nil) 101 | end 102 | end 103 | 104 | describe 'referrer_markups', with_users_relation: true, with_sessions: true do 105 | it 'should return correct data', with_sources: true do 106 | expect(@main_app_user.referrer_markups). 107 | to eq({first: {utm_source: '0-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 108 | utm_term: '(none)', kind: 'utm'}, 109 | priority: {utm_source: '1-0', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 110 | utm_term: '(none)', kind: 'utm'}, 111 | last: {utm_source: '(direct)', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 112 | utm_term: '(none)', kind: 'direct'}}) 113 | end 114 | 115 | it 'should return empty data' do 116 | expect(@main_app_user.referrer_markups).to eq({first: nil, priority: nil, last: nil}) 117 | end 118 | end 119 | end 120 | end 121 | end 122 | end 123 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Referrer 2 | 3 | [![Build Status](https://api.travis-ci.org/salkar/referrer.svg?branch=master)](http://travis-ci.org/salkar/referrer) 4 | [![Code Climate](https://codeclimate.com/github/salkar/referrer.svg)](https://codeclimate.com/github/salkar/referrer) 5 | 6 | Referrer tracks sources with which users visit your site, converts them into utm markup, computes priority of these sources and provides linking for sources with tracked model's records (orders/requests/etc). 7 | 8 | Sample questions which Referrer can help to answer: 9 | 10 | - Where did this user come from? 11 | - `{utm_source: 'google', utm_medium: 'organic', utm_campaign: '(none)', utm_content: '(none)', utm_term: 'user search query', kind: 'organic'}` 12 |

13 | - Where did users come from last month? 14 | - `[{utm_source: 'google', utm_medium: 'organic', utm_campaign: '(none)', utm_content: '(none)', utm_term: 'user search query', kind: 'organic', count: 1}, {utm_source: 'google', utm_campaign: 'adv_campaign', utm_medium: 'cpc', utm_content: 'adv 1', utm_term: 'some text', kind: 'utm', count: 2}, etc...]` 15 |

16 | - Where did user who make purchase come from? 17 | - `{utm_source: 'twitter.com', utm_medium: 'referral', utm_campaign: '(none)', utm_content: '/some_path', utm_term: '(none)', kind: 'referral'}` 18 |

19 | - Where did users who left some requests come from last week? 20 | - `[{utm_source: '(direct)', utm_medium: '(none)', utm_campaign: '(none)', utm_content: '(none)', utm_term: '(none)', kind: 'direct', count: 3}, {utm_source: 'google', utm_campaign: 'adv_campaign', utm_medium: 'cpc', utm_content: 'adv 2', utm_term: 'another text', kind: 'utm', count: 5}, etc...]` 21 | 22 | ## Installation 23 | 24 | ### Basic setup 25 | 26 | 1. Add Referrer to your Gemfile: 27 | ```ruby 28 | gem 'referrer' 29 | ``` 30 | 31 | 2. Run the bundle command to install it: 32 | ```bash 33 | bundle install 34 | ``` 35 | 36 | 3. Copy migrations from engine: 37 | ```bash 38 | rake referrer:install:migrations 39 | ``` 40 | 41 | 4. Migrate your database: 42 | ```bash 43 | rake db:migrate 44 | ``` 45 | 46 | 5. Mount engine's routes at `config/routes.rb` 47 | ```ruby 48 | mount Referrer::Engine => '/referrer' 49 | ``` 50 | 51 | 6. Require engine's js in your `application.js` file: 52 | ```ruby 53 | //= require referrer/application 54 | ``` 55 | 56 | 7. Add to your ApplicationController: 57 | ```ruby 58 | include Referrer::ControllerAdditions 59 | ``` 60 | 61 | 8. If your `current_user` method has another name, add to `config/initializers/referrer.rb`: 62 | ```ruby 63 | Referrer.current_user_method_name = :your_method_name 64 | ``` 65 | 66 | ### Setup your statistics owners 67 | 68 | Add to your models which objects may be returned from `current_user` (or your same method, specified in `Referrer.current_user_method_name`) `include Referrer::OwnerModelAdditions`. For sample if your `current_user` return `User` objects: 69 | 70 | ```ruby 71 | class User < ActiveRecord::Base 72 | include Referrer::OwnerModelAdditions 73 | #... 74 | end 75 | ``` 76 | 77 | ### Setup your tracked models 78 | 79 | 1. Add to models you want to track `include Referrer::TrackedModelAdditions`. For sample if you want to track where users who make orders come from (`Order` class): 80 | ```ruby 81 | class Order < ActiveRecord::Base 82 | include Referrer::TrackedModelAdditions 83 | #... 84 | end 85 | ``` 86 | 87 | 2. After you create tracked model record next time you can link record with current source. For sample (with `Order` object): 88 | ```ruby 89 | # OrdersControllers#create 90 | #... 91 | if @order.save 92 | @order.referrer_link_with(referrer_user) # referrer_user defined in Referrer::TrackedModelAdditions 93 | else 94 | #... 95 | ``` 96 | 97 | ## Settings 98 | 99 | Referrer settings can be changed in initializers. For sample, you can create `config/initializers/referrer.rb` and add your custom settings to it. 100 | 101 | ###Settings list 102 | 103 | 1. **current_user_method_name** - method name in ApplicationController which return current logged in object. 104 | ```ruby 105 | :current_user 106 | ``` 107 | 108 | 2. **js_settings** - options passes to js part of Referrer. 109 | ```ruby 110 | {} 111 | ``` 112 | Available options: 113 | * cookies 114 | 115 | ```javascript 116 | {prefix: 'referrer', 117 | domain: null, 118 | path: '/'} 119 | ``` 120 | 121 | * object 122 | 123 | ```javascript 124 | {name: 'referrer'} 125 | ``` 126 | 127 | * callback 128 | 129 | ```javascript 130 | null 131 | ``` 132 | 133 | 3. **js_csrf_token** - js code to get CSRF token if it is used. 134 | ```ruby 135 | <<-JS 136 | var tokenContainer = document.querySelector("meta[name=csrf-token]"); 137 | return tokenContainer ? tokenContainer.content : null; 138 | JS 139 | ``` 140 | 141 | 4. **markup_generator_settings** - options for Referrer::MarkupGenerator. 142 | ```ruby 143 | {} 144 | ``` 145 | Available options: 146 | * organics 147 | 148 | ```ruby 149 | [{host: 'search.daum.net', param: 'q'}, 150 | {host: 'search.naver.com', param: 'query'}, 151 | {host: 'search.yahoo.com', param: 'p'}, 152 | {host: /^(www\.)?google\.[a-z]+$/, param: 'q', display: 'google'}, 153 | {host: 'www.bing.com', param: 'q'}, 154 | {host: 'search.aol.com', params: 'q'}, 155 | {host: 'search.lycos.com', param: 'q'}, 156 | {host: 'edition.cnn.com', param: 'text'}, 157 | {host: 'index.about.com', param: 'q'}, 158 | {host: 'mamma.com', param: 'q'}, 159 | {host: 'ricerca.virgilio.it', param: 'qs'}, 160 | {host: 'www.baidu.com', param: 'wd'}, 161 | {host: /^(www\.)?yandex\.[a-z]+$/, param: 'text', display: 'yandex'}, 162 | {host: 'search.seznam.cz', param: 'oq'}, 163 | {host: 'www.search.com', param: 'q'}, 164 | {host: 'search.yam.com', param: 'k'}, 165 | {host: 'www.kvasir.no', param: 'q'}, 166 | {host: 'buscador.terra.com', param: 'query'}, 167 | {host: 'nova.rambler.ru', param: 'query'}, 168 | {host: 'go.mail.ru', param: 'q'}, 169 | {host: 'www.ask.com', param: 'q'}, 170 | {host: 'searches.globososo.com', param: 'q'}, 171 | {host: 'search.tut.by', param: 'query'}] 172 | ``` 173 | * referrals 174 | 175 | ```ruby 176 | [{host: /^(www\.)?t\.co$/, display: 'twitter.com'}, 177 | {host: /^(www\.)?plus\.url\.google\.com$/, display: 'plus.google.com'}] 178 | ``` 179 | * utm_synonyms 180 | 181 | ```ruby 182 | {'utm_source'=>[], 'utm_medium'=>[], 'utm_campaign'=>[], 'utm_content'=>[], 'utm_term'=>[]} 183 | ``` 184 | * array_params_joiner 185 | 186 | ```ruby 187 | ', ' 188 | ``` 189 | 190 | 5. **session_duration** - after this duration left, new session will be created and its sources priorities will be computed without regard to past sessions sources. 191 | ```ruby 192 | 3.months 193 | ``` 194 | 195 | 6. **sources_overwriting_schema** - source's kind priorities for priority source computation. 196 | ```ruby 197 | {direct: %w(direct), 198 | referral: %w(direct referral organic utm), 199 | organic: %w(direct referral organic utm), 200 | utm: %w(direct referral organic utm)} 201 | ``` 202 | 203 | ## Usage 204 | 205 | ### Sources owner 206 | 207 | #### referrer_markups 208 | 209 | Get markups for application user. Returns hash where: 210 | * **first** - user's first source 211 | * **priority** - user's last priority source, computed using `sources_overwriting_schema` 212 | * **last** - user's last source 213 | 214 | ```ruby 215 | user.referrer_markups 216 | => {first: {utm_source: 'google', utm_medium: 'organic', utm_campaign: '(none)', utm_content: '(none)', utm_term: 'user search query', kind: 'organic'}, 217 | priority: {utm_source: 'google', utm_campaign: 'adv_campaign', utm_medium: 'cpc', utm_content: 'adv 1', utm_term: 'some text', kind: 'utm'}, 218 | last: {utm_source: '(direct)', utm_medium: '(none)', utm_campaign: '(none)', utm_content: '(none)', utm_term: '(none)', kind: 'direct'}} 219 | ``` 220 | 221 | ### Tracked model 222 | 223 | #### referrer_markup 224 | 225 | Get markup for tracked model's object. Returns markup hash: 226 | 227 | ```ruby 228 | user.referrer_markup 229 | => {utm_source: 'test.com', utm_campaign: '(none)', utm_medium: 'referral', utm_content: '/', 230 | utm_term: '(none)', kind: 'referral'}} 231 | ``` 232 | 233 | #### referrer_link_with(r_user, linked_at: nil) [referrer_link_with!(r_user, linked_at: nil)] 234 | 235 | Link tracked model's object to referrer's data. Parameters: 236 | 237 | * `r_user` - `Referrer::User` object, in controller can be got by `referrer_user` (requires Referrer::ControllerAdditions be included). 238 | * `linked_at` - custom linking `DateTime`. 239 | 240 | ### Statistics 241 | 242 | #### Referrer::Statistics.sources_markup(from, to) 243 | 244 | Get markup of visits occurred in the specified period. Parameters: 245 | 246 | * `from` - DateTime of period start 247 | * `to` - DateTime of period end 248 | 249 | Sample: 250 | 251 | ```ruby 252 | Referrer::Statistics.sources_markup(10.days.ago, 2.days.ago) 253 | => [{utm_source: 'first', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 254 | utm_term: '(none)', kind: 'utm', count: 2}, 255 | {utm_source: 'second', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 256 | utm_term: '(none)', kind: 'utm', count: 1}, 257 | {utm_source: '(direct)', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 258 | utm_term: '(none)', kind: 'direct', count: 1}] 259 | ``` 260 | 261 | #### Referrer::Statistics.tracked_objects_markup(from, to, type: nil) 262 | 263 | Get markup associated with tracked model objects, in the specified period. Parameters: 264 | 265 | * `from` - DateTime of period start 266 | * `to` - DateTime of period end 267 | * `type` - tracked model name. If specified, response will contain markup associated only with this model objects. 268 | 269 | Sample: 270 | 271 | ```ruby 272 | Referrer::Statistics.tracked_objects_markup(10.days.ago, 2.days.ago) 273 | => [{utm_source: 'first', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 274 | utm_term: '(none)', kind: 'utm', count: 2}, 275 | {utm_source: 'second', utm_campaign: '(none)', utm_medium: '(none)', utm_content: '(none)', 276 | utm_term: '(none)', kind: 'utm', count: 1}] 277 | ``` 278 | 279 | ## License 280 | 281 | This project rocks and uses MIT-LICENSE. 282 | -------------------------------------------------------------------------------- /app/assets/javascripts/referrer/referrer.js.erb: -------------------------------------------------------------------------------- 1 | <% urls = Referrer::Engine.routes.url_helpers %> 2 | (function() { 3 | 'use strict'; 4 | 5 | window.Referrer = function (params) { 6 | var _options = { 7 | cookies: { 8 | prefix: 'referrer', 9 | domain: null, 10 | path: '/' 11 | }, 12 | user: { 13 | methods: {create: {url: '<%= urls.users_path %>'}} 14 | }, 15 | session: { 16 | methods: {create: {url: '<%= urls.sessions_path %>'}} 17 | }, 18 | source: { 19 | methods: {massCreate: {url: '<%= urls.mass_create_sources_path %>'}} 20 | }, 21 | object: {name: 'referrer'}, 22 | callback: null, 23 | csrfTokenFunction: function () {<%= Referrer.js_csrf_token %>}, 24 | version: 1.0 25 | }; 26 | 27 | var _storageNames, _urlSupport, _storageManager, _requestResolver; 28 | 29 | var UrlSupport = function () { 30 | this.getHostname = function (url) { 31 | var tmp = document.createElement('a'); 32 | tmp.href = url; 33 | return tmp.hostname; 34 | }; 35 | this.referrerUrl = document.referrer; 36 | this.referrerHostname = this.referrerUrl ? this.getHostname(this.referrerUrl) : null; 37 | this.locationUrl = window.location.toString(); 38 | this.locationHostname = this.getHostname(this.locationUrl); 39 | }; 40 | 41 | var StorageManager = function () { 42 | var _domain = _options.cookies.domain || 43 | (_urlSupport.locationHostname == 'localhost' ? null : _urlSupport.locationHostname); 44 | 45 | this.setCookie = function (name, value, permanent, seconds) { 46 | var expires; 47 | var cookie = name + "=" + value + ';path=' + _options.cookies.path; 48 | if (_domain) {cookie = cookie + ';domain=' + _domain;} 49 | if (permanent) { 50 | expires = new Date(); 51 | expires.setFullYear(expires.getFullYear() + 20); 52 | } else { 53 | if (seconds && typeof seconds == 'number') { 54 | expires = new Date(); 55 | expires.setTime(expires.getTime() + seconds*1000); 56 | } 57 | } 58 | if (expires) { 59 | cookie = cookie + ';expires=' + expires.toUTCString(); 60 | } 61 | document.cookie = cookie; 62 | }; 63 | 64 | this.getCookie = function (name) { 65 | var pName = name + '='; 66 | var arr = document.cookie.split(';'); 67 | for(var i=0; i), finished: false}; 363 | }); 364 | })(); --------------------------------------------------------------------------------