class TrackableSession include MongoMapper::Document # MongoMapper Setup ============================================================================== belongs_to :trackable_stat many :trackable_actions, :order => :created_at key :bounce, Boolean, :index => true key :clickthrough_destination, String key :duration, Integer key :entrance_page, String key :exit_page, String key :has_clicks, Boolean, :index => true key :has_clickthroughs, Boolean, :index => true key :has_conversions, Boolean key :has_mouseovers, Boolean, :index => true key :has_scrolls, Boolean, :index => true key :initial_request_url, String key :ip_address, String, :index => true key :kind, String, :index => true key :location, String key :new_visit, Boolean, :index => true key :referrer, String, :index => true key :referring_keywords, String key :session_id, String, :index => true key :site, String, :index => true key :spider, Boolean, :index => true key :trackable_stat_id, ObjectId key :user_agent, String key :views_count, Integer timestamps! ensure_index :created_at # 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| { :conditions => { :ip_address => ip } } } scope :of_kind, lambda { |k| { :conditions => { :kind => k } } } scope :for_site, lambda { |s| { :conditions => { :site => s } } } scope :for_month, lambda { |d| { :conditions => { :created_at => { '$gte' => d.beginning_of_month, '$lte' => d.end_of_month } } } } scope :for_week, lambda { |d| { :conditions => { :created_at => { '$gte' => d.beginning_of_week, '$lte' => d.end_of_week } } } } scope :for_date_range, lambda { |start_date,end_date| { :conditions => { :created_at => { '$gte' => start_date.beginning_of_month, '$lte' => end_date.end_of_month } } } } scope :bounces, :conditions => {:bounce => true} scope :direct_visits, :conditions => { :kind => 'direct' }, :order => 'created_at' scope :new_visits, :conditions => { :new_visit => true }, :order => 'created_at' scope :organic_visits, :conditions => { :kind => 'natural' }, :order => 'created_at' scope :ppc_visits, :conditions => { :kind => 'paid' }, :order => 'created_at' scope :spider_visits, :conditions => { :spider => true }, :order => 'created_at' scope :recent_visits, :conditions => { :created_at => { '$gte' => Time.zone.now.beginning_of_month }}, :order => 'created_at DESC', :limit => 25 scope :return_visits, :conditions => { :new_visit => false }, :order => 'created_at' scope :search_visits, :conditions => { :kind => 'search' }, :order => 'created_at' scope :with_any_actions scope :with_clicks, :conditions => { :has_clicks => true }, :order => 'created_at' scope :with_clickthroughs, :conditions => { :has_clickthroughs => true }, :order => 'created_at' scope :with_conversions, :conditions => { :has_conversions => true }, :order => 'created_at' scope :with_leads, :conditions => { :has_conversions => true }, :order => 'created_at' scope :with_mouseovers, :conditions => { :has_mouseovers => true }, :order => 'created_at' scope :with_scrolls, :conditions => { :has_scrolls => true }, :order => 'created_at' scope :with_views, :conditions => { :views_count => { '$gte' => 1 } }, :order => 'created_at' # Class Methods ================================================================================== def self.conversion_rate(args) (TrackableSession.search(args).with_conversions.count.to_f / TrackableSession.search(args).count) * 100 end def self.clickthroughs_histogram(args) conditions = TrackableSession.search(args).to_hash TrackableSession.collection.group("function(x) { return { destination: x.clickthrough_destination }; }", conditions, { :count => 0}, "function(x,y){y.count++}", true).inject({}){|h,k| h[k['destination']] = k['count'] unless k['destination'].blank?; h} end def self.entrance_pages_histogram(args) conditions = TrackableSession.search(args).to_hash TrackableSession.collection.group("function(x) { return { entrance_page: x.entrance_page }; }", conditions, { :count => 0}, "function(x,y){y.count++}", true).inject({}){|h,k| h[k['entrance_page']] = k['count']; h}.sort{|a,b| a[1] <=> b[1]}.reverse end def self.exit_pages_histogram(args) conditions = TrackableSession.search(args).to_hash TrackableSession.collection.group("function(x) { return { exit_page: x.exit_page }; }", conditions, { :count => 0}, "function(x,y){y.count++}", true).inject({}){|h,k| h[k['exit_page']] = k['count']; h}.sort{|a,b| a[1] <=> b[1]}.reverse end def self.keywords_histogram(args) conditions = TrackableSession.search(args).to_hash TrackableSession.collection.group("function(x) { return { keyword: x.referring_keywords }; }", conditions, { :count => 0}, "function(x,y){y.count++}", true).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 def self.locations_histogram(args) conditions = TrackableSession.search(args).to_hash TrackableSession.collection.group("function(x) { return { location: x.location }; }", conditions, { :count => 0}, "function(x,y){y.count++}", true).inject({}){|h,k| h[k['location']] = k['count'].to_i unless k['location'].blank? || k['location'] == 'Unknown'; h} end 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 def self.referrers_histogram(args) conditions = TrackableSession.search(args).to_hash TrackableSession.collection.group("function(x) { return { referrer: x.referrer }; }", conditions, { :count => 0}, "function(x,y){y.count++}", true).inject({}){|h,k| h[k['referrer']] = k['count'] unless k['referrer'].blank?; h} end def self.search(args = {}) args[:site] ||= 'all sites' args[:visit_kind] ||= 'all' args[:action_kind] ||= 'any_action' args[:action_kind] = args[:action_kind].pluralize [:site, :action_kind, :visit_kind, :time_period].each{|a| args[a] = args[a].downcase if args[a]} case args[:time_period] when 'past 3 months' args[:start_date] = (Time.zone.now - 2.months).beginning_of_month args[:end_date] = Time.zone.now when 'past 6 months' args[:start_date] = (Time.zone.now - 5.months).beginning_of_month args[:end_date] = Time.zone.now when 'past 12 months' args[:start_date] = (Time.zone.now - 11.months).beginning_of_month args[:end_date] = Time.zone.now else args[:start_date] = TrackableSession.first.created_at args[:end_date] = Time.zone.now end if args[:time_period] if args[:visit_kind] == 'all' if args[:site] == 'all sites' TrackableSession.send("with_#{args[:action_kind]}").for_date_range(args[:start_date], args[:end_date]) else TrackableSession.for_site(args[:site]).send("with_#{args[:action_kind]}").for_date_range(args[:start_date], args[:end_date]) end else if args[:site] == 'all sites' TrackableSession.of_kind(args[:visit_kind]).send("with_#{args[:action_kind]}").for_date_range(args[:start_date], args[:end_date]) else TrackableSession.for_site(args[:site]).of_kind(args[:visit_kind]).send("with_#{args[:action_kind]}").for_date_range(args[:start_date], args[:end_date]) end end else if args[:visit_kind] == 'all' if args[:site] == 'all sites' TrackableSession.send("with_#{args[:action_kind]}") else TrackableSession.for_site(args[:site]).send("with_#{args[:action_kind]}") end else if args[:site] == 'all sites' TrackableSession.of_kind(args[:visit_kind]).send("with_#{args[:action_kind]}") else TrackableSession.for_site(args[:site]).of_kind(args[:visit_kind]).send("with_#{args[:action_kind]}") end end end end # Can't combine date conditions, so this is needed for SessionReport def self.search_without_date(args = {}) args[:site] ||= 'all sites' args[:visit_kind] ||= 'all' args[:action_kind] ||= 'any_action' args[:action_kind] = args[:action_kind].pluralize args[:time_period] ||= 'past 3 months' [:site, :action_kind, :visit_kind].each{|a| args[a] = args[a].downcase} if args[:visit_kind] == 'all' if args[:site] == 'all sites' TrackableSession.send("with_#{args[:action_kind]}") else TrackableSession.for_site(args[:site]).send("with_#{args[:action_kind]}") end else if args[:site] == 'all sites' TrackableSession.of_kind(args[:visit_kind]).send("with_#{args[:action_kind]}") else TrackableSession.for_site(args[:site]).of_kind(args[:visit_kind]).send("with_#{args[:action_kind]}") end end end def self.user_agents_histogram(args) conditions = TrackableSession.search(args).to_hash _histogram = TrackableSession.collection.group("function(x) { return { user_agent: x.user_agent }; }", conditions, { :count => 0}, "function(x,y){y.count++}", true) _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 =============================================================================== def actions self.trackable_actions.count - self.trackable_actions.views.count end def detect_new_visit TrackableSession.find_by_ip_address(self.ip_address) ? false : true end def hr_duration return 0 unless self.duration self.duration > 60 ? sprintf("%.1f minutes", self.duration / 60.0) : sprintf("%.1f seconds", self.duration) end 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 _location = Geokit::Geocoders::MultiGeocoder.geocode(self.ip_address) self.location ||= _location.success? ? "#{_location.city}, #{_location.state}" : "Unknown" rescue self.location = "Unknown" end end 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 def pageviews self.trackable_actions.views.count end def parsed_user_agent TrackableSession.parsed_user_agent(self.user_agent) end def last_visit_date return nil if previous_visits_count.zero? TrackableSession.by_ip_address(self.ip_address).fields(:created_at).map{|s| s.created_at.to_s(:concise)}.sort[-2] end def previous_visits_count TrackableSession.by_ip_address(self.ip_address).count - 1 end def sanitize_referring_keywords self.referring_keywords.to_s.gsub('+',' ') end # Update the record field based on continued session activity 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, :updated_at => Time.zone.now, :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, :updated_at => Time.zone.now, :duration => Time.zone.now - self.created_at ) elsif action_kind && action_kind == 'clickthrough' self.update_attributes( :has_clickthroughs => true, :clickthrough_destination => destination, :bounce => false, :updated_at => Time.zone.now, :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, :updated_at => Time.zone.now, :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, :updated_at => Time.zone.now, :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 def visit_type self.new_visit? ? "New" : "Returning" end 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