# Tracks user activity to a given site. class TrackableSession < TrackableBase # Mongoid Config ================================================================================= include Mongoid::Document include Mongoid::Timestamps referenced_in :trackable_stat references_many :trackable_actions field :bounce, :type => Boolean field :clickthrough_destination field :duration, :type => Integer field :entrance_page field :exit_page field :has_clicks, :type => Boolean field :has_clickthroughs, :type => Boolean field :has_conversions, :type => Boolean field :has_mouseovers, :type => Boolean field :has_scrolls, :type => Boolean field :initial_request_url field :ip_address field :kind field :location field :new_visit, :type => Boolean field :referrer field :referring_keywords field :session_id field :site field :spider, :type => Boolean field :user_agent field :views_count, :type => Integer field :visit_kind index :bounce, :unique => false index :created_at, :unique => false index :has_clicks, :unique => false index :has_clickthroughs, :unique => false index :has_mouseovers, :unique => false index :has_scrolls, :unique => false index :ip_address, :unique => false index :kind, :unique => false index :new_visit, :unique => false index :referrer, :unique => false index :session_id, :unique => false index :site, :unique => false index :spider, :unique => false index :visit_kind, :unique => false # Callbacks ====================================================================================== before_create :init # Constants ====================================================================================== KINDS = ['direct', 'natural', 'paid', 'search'] HUMAN_USER_AGENTS = ['Chrome', 'Firefox', 'Gecko', 'Mozilla', 'MSIE', 'Opera','Safari'] # Scopes ========================================================================================= scope :by_ip_address, lambda { |ip| { :where => { :ip_address => ip } } } scope :of_kind, lambda { |k| { :where => { :kind => k } } } scope :for_site, lambda { |s| { :where => { :site => s } } } scope :for_month, lambda { |d| { :where => { :created_at => { '$gte' => d.beginning_of_month, '$lte' => d.end_of_month } } } } scope :for_week, lambda { |d| { :where => { :created_at => { '$gte' => d.beginning_of_week, '$lte' => d.end_of_week } } } } scope :for_date_range, lambda { |start_date,end_date| { :where => { :created_at => { '$gte' => start_date.beginning_of_month, '$lte' => end_date.end_of_month } } } } scope :bounces, :where => {:bounce => true} scope :direct_visits, :where => { :kind => 'direct' }, :order => 'created_at' scope :new_visits, :where => { :new_visit => true }, :order => 'created_at' scope :organic_visits, :where => { :kind => 'natural' }, :order => 'created_at' scope :ppc_visits, :where => { :kind => 'paid' }, :order => 'created_at' scope :spider_visits, :where => { :spider => true }, :order => 'created_at' scope :recent_visits, :where => { :created_at => { '$gte' => Time.zone.now.beginning_of_month }}, :order => 'created_at DESC', :limit => 25 scope :return_visits, :where => { :new_visit => false }, :order => 'created_at' scope :search_visits, :where => { :kind => 'search' }, :order => 'created_at' scope :with_any_actions scope :with_clicks, :where => { :has_clicks => true }, :order => 'created_at' scope :with_clickthroughs, :where => { :has_clickthroughs => true } scope :with_conversions, :where => { :has_conversions => true }, :order => 'created_at' scope :with_leads, :where => { :has_conversions => true }, :order => 'created_at' scope :with_mouseovers, :where => { :has_mouseovers => true }, :order => 'created_at' scope :with_scrolls, :where => { :has_scrolls => true }, :order => 'created_at' scope :with_views, :where => { :views_count => { '$gte' => 1 } }, :order => 'created_at' # Class Methods ================================================================================== # Returns the conversion rate for sessions matching the specified arguments, ie: percentage of the total number of visits that were conversions. # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.conversion_rate( :site => 'foo.com' ) def self.conversion_rate(args) (TrackableSession.search(args).with_conversions.count.to_f / TrackableSession.search(args).count) * 100 end # Returns a histogram for the number of clickthroughs to each referred site(s). # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.clickthroughs_histogram( :site => 'foo.com' ) def self.clickthroughs_histogram(args) conditions = TrackableSession.search(args).selector TrackableSession.collection.group(:keyf => "function(x) { return { destination: x.clickthrough_destination }; }", :cond => conditions, :initial => { :count => 0}, :reduce => "function(x,y){y.count++}").inject({}){|h,k| h[k['destination']] = k['count'] unless k['destination'].blank?; h} end # Returns a histogram for the first page users visited for the specified site(s). # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.entrance_pages_histogram( :site => 'foo.com' ) def self.entrance_pages_histogram(args) conditions = TrackableSession.search(args).selector TrackableSession.collection.group(:keyf => "function(x) { return { entrance_page: x.entrance_page }; }", :cond => conditions, :initial => { :count => 0}, :reduce => "function(x,y){y.count++}").inject({}){|h,k| h[k['entrance_page']] = k['count']; h}.sort{|a,b| a[1] <=> b[1]}.reverse end # Returns a histogram for the last page users visited for the specified site(s). # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.exit_pages_histogram( :site => 'foo.com' ) def self.exit_pages_histogram(args) conditions = TrackableSession.search(args).selector TrackableSession.collection.group(:keyf => "function(x) { return { exit_page: x.exit_page }; }", :cond => conditions, :initial => { :count => 0}, :reduce => "function(x,y){y.count++}").inject({}){|h,k| h[k['exit_page']] = k['count']; h}.sort{|a,b| a[1] <=> b[1]}.reverse end # Returns a histogram for the search terms users used to find the specified site(s). # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.keywords_histogram( :site => 'foo.com' ) def self.keywords_histogram(args) conditions = TrackableSession.search(args).selector TrackableSession.collection.group(:keyf => "function(x) { return { keyword: x.referring_keywords }; }", :cond => conditions, :initial => { :count => 0}, :reduce => "function(x,y){y.count++}").inject({}){|h,k| h[k['keyword']] = k['count'] unless k['keyword'].blank?; h} end def self.kinds_for_select [['All', 'all']] | TrackableSession::KINDS.sort.map{|k| [k.titleize, k]} end # Returns a histogram for the geocoded locations of visitors to the specified site(s). # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.locations_histogram( :site => 'foo.com' ) def self.locations_histogram(args) conditions = TrackableSession.search(args).selector TrackableSession.collection.group(:keyf => "function(x) { return { location: x.location }; }", :cond => conditions, :initial => { :count => 0}, :reduce => "function(x,y){y.count++}").inject({}){|h,k| h[k['location']] = k['count'].to_i unless k['location'].blank? || k['location'] == 'Unknown'; h} end # Returns a human-readable user agent for the browser. def self.parsed_user_agent(user_agent) begin _ua = Agent.new(user_agent) _user_agent = "#{_ua.name} #{_ua.version} (#{_ua.os})" rescue _user_agent = "Unknown" end _user_agent end # Returns a histogram for the referrers for site(s) matching the specified arguments. # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.referrers_histogram( :site => 'foo.com' ) def self.referrers_histogram(args) conditions = TrackableSession.search(args).selector TrackableSession.collection.group(:keyf => "function(x) { return { referrer: x.referrer }; }", :cond => conditions, :initial => { :count => 0}, :reduce => "function(x,y){y.count++}").inject({}){|h,k| h[k['referrer']] = k['count'] unless k['referrer'].blank?; h} end # Returns a scope based on time period, site, and visit kind. Null arguments returns all. # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.search( :site => 'foo.com' ) def self.search(args) (args[:start_date], args[:end_date]) = dates_from_time_period(args[:time_period]) sessions = args[:time_period] ? TrackableSession.for_date_range(args[:start_date], args[:end_date]) : TrackableSession.all sessions = sessions.send("with_#{args[:action_kind].pluralize.downcase}") if args[:action_kind] sessions = sessions.for_site(args[:site].downcase) unless args[:site].downcase == 'all sites' sessions = sessions.of_kind(args[:visit_kind].downcase) if args[:visit_kind] && args[:visit_kind].downcase != 'all' sessions.desc(:created_at) end # Returns a scope based on site and visit kind only. # # ==== Attributes # # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.search_without_date( :site => 'foo.com' ) def self.search_without_date(args = {}) args = args.clone args.delete(:time_period) self.search(args) end # Returns a histogram for the user agents for site(s) matching the specified arguments. # # ==== Attributes # # * +:time_period+ - date range as a string: 'past 3 months', 'past 6 months', 'past 12 months' # * +:site+ - name of a site # * +:visit_kind+ - kind of visit as a string: 'direct', 'natural', 'paid', 'search' # # ==== Examples # # TrackableSession.user_agents_histogram( :site => 'foo.com' ) def self.user_agents_histogram(args) conditions = TrackableSession.search(args).selector _histogram = TrackableSession.collection.group(:keyf => "function(x) { return { user_agent: x.user_agent }; }", :cond => conditions, :initial => { :count => 0}, :reduce => "function(x,y){y.count++}") _final = {} _histogram.each do |ua| _ua = parsed_user_agent(ua['user_agent']) _final[_ua] ||= 0 _final[_ua] += ua['count'].to_i unless ua['user_agent'].blank? || ua['user_agent'] == 'Unknown' end _final end # Instance Methods =============================================================================== # Returns the number of non-view actions for this session. def actions self.trackable_actions.count - self.trackable_actions.views.count end # Returns true if this is a new visit. def detect_new_visit TrackableSession.where(:ip_address => self.ip_address) ? false : true end # Returns this session's duration in X minutes or Y seconds. def hr_duration return 0 unless self.duration self.duration > 60 ? sprintf("%.1f minutes", self.duration / 60.0) : sprintf("%.1f seconds", self.duration) end # Initializes a new session. def init self.bounce = true self.kind ||= self.kind_by_referrer self.new_visit ||= self.detect_new_visit self.referring_keywords = self.sanitize_referring_keywords self.spider ||= spider_visit? begin raise ArgumentError, "No IP address available" unless self.ip_address _location = Geokit::Geocoders::MultiGeocoder.geocode(self.ip_address) self.location ||= _location.success? ? "#{_location.city}, #{_location.state}" : "Unknown" rescue self.location = "Unknown" end end # Returns whether this session was initiated by a direct (bookmark or typed URL), search, paid (landing page), or natural (non-search link) referrer. def kind_by_referrer return "direct" unless self.referrer unless self.referrer.blank? return 'search' if ['google','bing','yahoo'].include?(self.referrer.split('.')[-2]) return 'paid' if ppc_visit?(self.initial_request_url) end "natural" end # Returns the number of page views associated with this session. def pageviews self.trackable_actions.views.count end # Returns this session's human-readable user agent. def parsed_user_agent TrackableSession.parsed_user_agent(self.user_agent) end # Returns the date of the last visit if this user visited before. def last_visit_date return nil if previous_visits_count.zero? TrackableSession.by_ip_address(self.ip_address).asc(:created_at).only(:created_at).map{|s| s.created_at}.sort[-2] end # Returns the number of times this user has been here before. def previous_visits_count TrackableSession.by_ip_address(self.ip_address).count - 1 end # Unescapes referring keywords for display. def sanitize_referring_keywords self.referring_keywords.to_s.gsub('+',' ') end # Updates this session with whatever the user just did. # # ==== Attributes # # * +:action_kind+ - action as a string: 'click', 'clickthrough', 'conversion', 'mouseover', 'scroll', 'view' # * +:destination+ - clickthrough destination # * +:last_url+ - the referring URL # # ==== Examples # # trackable_session.touch( 'click', 'cnn.com', 'foo.com/cnn' ) def touch(action_kind = nil, destination = nil, last_url = nil) if action_kind && action_kind == 'scroll' self.update_attributes( :has_scrolls => true, :bounce => self.trackable_actions.count <= 1, :duration => Time.zone.now - self.created_at ) elsif action_kind && action_kind == 'click' self.update_attributes( :has_clicks => true, :bounce => self.trackable_actions.count <= 1, :duration => Time.zone.now - self.created_at ) elsif action_kind && action_kind == 'clickthrough' self.update_attributes( :has_clickthroughs => true, :clickthrough_destination => destination, :bounce => false, :duration => Time.zone.now - self.created_at ) elsif action_kind && action_kind == 'conversion' self.update_attributes( :has_conversions => true, :bounce => self.trackable_actions.count <= 1, :duration => Time.zone.now - self.created_at ) elsif action_kind && action_kind == 'mouseover' self.update_attributes( :has_mouseovers => true, :bounce => self.trackable_actions.count <= 1, :duration => Time.zone.now - self.created_at ) else # View self.update_attributes(:updated_at => Time.zone.now, :duration => Time.zone.now - self.created_at, :views_count => self.views_count.to_i + 1, :exit_page => last_url, :bounce => self.trackable_actions.count > 1 ? false : true) end touch_stat end # Returns whether this session is new or existing. def visit_type self.new_visit? ? "New" : "Returning" end # Upserts stats associated with this session. Called every time this session is touched. def touch_stat # Upsert associated stat if trackable_stat = TrackableStat.by_site(self.site).by_date(self.created_at).first trackable_stat.touch(self, self.trackable_actions[-1], self.trackable_actions[-2]) else trackable_stat = TrackableStat.create(:site => self.site, :created_at => self.created_at) trackable_stat.touch(self, self.trackable_actions.last) end end private def ppc_visit?(url) begin _params = URI.parse(url).query.to_s.downcase rescue false end return false if _params.blank? return true if _params.include?('gclid') # Google AdWords return true if _params.include?('keyword') # AdCenter return true if _params.include?('adid') # AdCenter? return true if _params.include?('matchtype') # Generic return true if _params.include?('ppc') # Generic return true if _params.include?('cpc') # Generic return false end def spider_visit? _spider = true return false if self.user_agent == "Unknown" || self.user_agent.nil? unless self.user_agent.to_s.include?('bot') HUMAN_USER_AGENTS.each{ |ua| _spider = false if self.user_agent.to_s.include?(ua) } end _spider end end