Class: Visit
- Inherits:
-
ApplicationRecord
- Object
- ActiveRecord::Base
- ApplicationRecord
- Visit
- Defined in:
- app/models/visit.rb
Overview
== Schema Information
Table name: visits
Database name: primary
id :bigint not null, primary key
city :string
country :string
device_meta :jsonb not null
ip :string(39)
landing_page :text
latitude :decimal(10, 6)
legacy_browser :string
legacy_device_type :string
legacy_dnt :integer
legacy_gbraid :string
legacy_gclid :string
legacy_locale :string(5)
legacy_os :string
legacy_screen_height :integer
legacy_screen_width :integer
legacy_wbraid :string
longitude :decimal(10, 6)
marketing_meta :jsonb not null
postal_code :string
processed :boolean
referral_code :string
referrer :text
referring_domain :string
region :string
region_code :string(10)
search_keyword :string
started_at :datetime
user_agent :text
utm_campaign :string
utm_content :string
utm_medium :string
utm_source :string
utm_term :string
visitor_token :uuid
session_id :string
source_id :integer
user_id :integer
utm_id :string
Indexes
index_visits_on_marketing_meta (marketing_meta) USING gin
index_visits_on_referral_code (referral_code)
index_visits_on_referring_domain (referring_domain) USING gin
index_visits_on_source_id (source_id)
index_visits_on_started_at (started_at)
index_visits_on_user_id (user_id)
Foreign Keys
fk_rails_... (source_id => sources.id)
fk_rails_... (user_id => parties.id) ON DELETE => nullify
Constant Summary collapse
- SQL_CONVERSIONS =
Sql conversions.
%w[orders quotes opportunities room_configurations].map { |t| "exists(select 1 from #{t} where #{t}.visit_id = visits.id)" }.join(' or ').freeze
- GOOGLE_ADS_MARKETING_KEYS =
%w[gclid gbraid wbraid].freeze
Constants included from Models::Schedulable
Models::Schedulable::SIMPLE_FORM_OPTIONS
Belongs to collapse
Has one collapse
Has many collapse
- #opportunities ⇒ ActiveRecord::Relation<Opportunity>
- #orders ⇒ ActiveRecord::Relation<Order>
- #quotes ⇒ ActiveRecord::Relation<Quote>
- #room_configurations ⇒ ActiveRecord::Relation<RoomConfiguration>
- #visit_events ⇒ ActiveRecord::Relation<VisitEvent>
Class Method Summary collapse
-
.by_source_id_with_descendants ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are by source id with descendants.
-
.last_marketing_value(key) ⇒ Object
Returns the value at
marketing_meta[key]for the most-recent visit in the current scope (highest id). - .populate_empty_sources(visits_query = nil) ⇒ Object
- .populate_utm_id ⇒ Object
- .ransackable_scopes(_auth_object = nil) ⇒ Object
-
.reconcile_with_parties(start_time:, end_time: nil) ⇒ Object
This method is under development.
-
.with_any_marketing_key ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with any marketing key.
-
.with_conversions ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with conversions.
-
.with_events_count ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with events count.
-
.with_marketing_key ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with marketing key.
-
.with_marketing_meta ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with marketing meta.
-
.with_order_conversions ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with order conversions.
Instance Method Summary collapse
- #country_iso3 ⇒ Object
- #country_iso3166 ⇒ Object
-
#find_source ⇒ Object
Smart source lookup.
- #geocode ⇒ Object
- #keywords ⇒ Object
- #landing_page_addressable ⇒ Object
- #landing_page_params ⇒ Object
- #landing_page_path ⇒ Object
- #location_summary ⇒ Object
- #remove_self_referrer ⇒ Object
- #set_source ⇒ Object
- #state_code ⇒ Object
- #update_associated_customer_source ⇒ Object
Methods inherited from ApplicationRecord
ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation
Methods included from Models::Schedulable
Methods included from Models::AfterCommittable
Methods included from Models::EventPublishable
Class Method Details
.by_source_id_with_descendants ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are by source id with descendants. Active Record Scope
79 |
# File 'app/models/visit.rb', line 79 scope :by_source_id_with_descendants, ->(source_id) { where(source_id: Source.self_and_descendants_ids(source_id)) } |
.last_marketing_value(key) ⇒ Object
Returns the value at marketing_meta[key] for the most-recent visit
in the current scope (highest id). Nil when the scope is empty.
Combines existence filter + ordering + jsonb_pick so callers don't
repeat the .with_marketing_key(...).order(id: :desc).jsonb_pick(...)
chain.
154 155 156 157 |
# File 'app/models/visit.rb', line 154 def self.last_marketing_value(key) k = key.to_s with_marketing_key(k).reorder(id: :desc).jsonb_pick(column_name: :marketing_meta, key: k) end |
.populate_empty_sources(visits_query = nil) ⇒ Object
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 |
# File 'app/models/visit.rb', line 252 def self.populate_empty_sources(visits_query = nil) counter = 0 visits_query ||= Visit.all visits_query = visits_query.where(source_id: nil) max_counter = visits_query.size visits_query.find_in_batches do |visits| Visit.transaction do visits.each do |v| counter += 1 new_source = v.find_source puts "[#{counter} / #{max_counter}] visit id #{v.id} -> #{new_source&.full_name || 'No Match'} [#{new_source&.id || '?'}]" v.update_column(:source_id, new_source.id) if new_source end end end end |
.populate_utm_id ⇒ Object
163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'app/models/visit.rb', line 163 def self.populate_utm_id visits = Visit.where(utm_id: nil).where("landing_page LIKE '%utm_id%'") total = visits.count puts "Populating utm_id for #{total} visits" visits.select(:id, :landing_page).find_in_batches.with_index do |group, batch| visit_ids_per_utm_id = {} offset = batch * 1000 position = offset group.each_with_index do |v, _index| position += 1 progress = ((position.to_f / total) * 100).round(2) if total > 0 puts "[#{progress} % - #{position} / #{total}] #{v.id} -> #{v.landing_page}" utm_id = v.landing_page_params[:utm_id] if utm_id.present? visit_ids_per_utm_id[utm_id] ||= [] visit_ids_per_utm_id[utm_id] << v.id end end # Perform a batch update visit_ids_per_utm_id.each do |utm_id, ids| puts "#{utm_id} : #{ids.inspect}" Visit.where(id: ids).update_all(utm_id: utm_id) end end end |
.ransackable_scopes(_auth_object = nil) ⇒ Object
159 160 161 |
# File 'app/models/visit.rb', line 159 def self.ransackable_scopes(_auth_object = nil) [:by_source_id_with_descendants] end |
.reconcile_with_parties(start_time:, end_time: nil) ⇒ Object
This method is under development
294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 |
# File 'app/models/visit.rb', line 294 def self.reconcile_with_parties(start_time:, end_time: nil) visits = Visit.where(user_id: nil).where(Visit[:started_at].gteq(start_time)) visits = visits.where(Visit[:started_at].lteq(end_time)) if end_time visits.find_each do |visit| customer = Customer.find_by(visit_id: visit.id) # Find a customer created within 5 seconds of that range if customer.nil? puts 'Customer not found by visit id, attempting time range and ip lookup' time_range = (visit.started_at - 5.seconds)..(visit.started_at + 5.seconds) # Inspect the customer created in that time range with that IP, the last one will get the visit_id Customer.where(creation_method: 'web', created_at: time_range).order(Customer[:created_at].desc).find_each do |lookup_customer| if lookup_customer.versions.where(event: 'create').where(ip: visit.ip).exists? customer = lookup_customer puts "Customer #{customer.id} found based on ip creation event" break end end else puts "Found customer #{customer.id} via Visit id" end if customer visit.update_column(:user_id, customer.id) customer.update_column(:source_id, visit.source_id) if (customer.source.nil? || customer.source.unknown?) && visit.source_id end end end |
.with_any_marketing_key ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with any marketing key. Active Record Scope
135 136 137 138 139 |
# File 'app/models/visit.rb', line 135 scope :with_any_marketing_key, ->(*keys) { cleaned_keys = keys.flatten.map { |key| key.to_s.strip }.compact_blank cleaned_keys.empty? ? none : jsonb_where_exists_any(column_name: :marketing_meta, keys: cleaned_keys) } |
.with_conversions ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with conversions. Active Record Scope
76 |
# File 'app/models/visit.rb', line 76 scope :with_conversions, -> { select("*,(#{SQL_CONVERSIONS}) as has_conversions") } |
.with_events_count ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with events count. Active Record Scope
77 |
# File 'app/models/visit.rb', line 77 scope :with_events_count, -> { select('visits.*, (SELECT COUNT(*) FROM visit_events WHERE visit_events.visit_id = visits.id) as events_count') } |
.with_marketing_key ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with marketing key. Active Record Scope
130 131 132 |
# File 'app/models/visit.rb', line 130 scope :with_marketing_key, ->(key) { jsonb_where_exists(column_name: :marketing_meta, key: key.to_s) } |
.with_marketing_meta ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with marketing meta. Active Record Scope
142 143 144 |
# File 'app/models/visit.rb', line 142 scope :with_marketing_meta, ->(hash) { jsonb_where(column_name: :marketing_meta, operator: :contains, value: hash) } |
.with_order_conversions ⇒ ActiveRecord::Relation<Visit>
A relation of Visits that are with order conversions. Active Record Scope
78 |
# File 'app/models/visit.rb', line 78 scope :with_order_conversions, -> { joins(:orders).where("orders.state = 'invoiced'") } |
Instance Method Details
#country_iso3 ⇒ Object
205 206 207 |
# File 'app/models/visit.rb', line 205 def country_iso3 country_iso3166&.alpha3 end |
#country_iso3166 ⇒ Object
199 200 201 202 203 |
# File 'app/models/visit.rb', line 199 def country_iso3166 return if country.blank? ISO3166::Country.find_country_by_any_name(country) end |
#find_source ⇒ Object
Smart source lookup
236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 |
# File 'app/models/visit.rb', line 236 def find_source # Merge persisted UTM/referral attributes with landing page params so that # referral/campaign codes take precedence even if the landing URL is missing them params = landing_page_params.dup params[:referral_code] ||= referral_code if respond_to?(:referral_code) params[:utm_id] ||= utm_id if respond_to?(:utm_id) params[:utm_campaign] ||= utm_campaign if respond_to?(:utm_campaign) params[:utm_source] ||= utm_source if respond_to?(:utm_source) params[:utm_medium] ||= utm_medium if respond_to?(:utm_medium) params[:gclid] ||= params[:gbraid] ||= params[:wbraid] ||= params[:oppref] ||= Source.find_from_params(params, referrer: referrer) end |
#geocode ⇒ Object
321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 |
# File 'app/models/visit.rb', line 321 def geocode return if latitude && longitude location = begin Geocoder.search(ip).first rescue StandardError Rails.logger.error "Geocode error: #{e.class.name}: #{e.}" nil end return unless location && location.country.present? data = { country: location.country, # country_code: location.try(:country_code).presence, region: location.try(:state).presence, city: location.try(:city).presence, postal_code: location.try(:postal_code).presence, latitude: location.try(:latitude).presence, longitude: location.try(:longitude).presence } update(data) end |
#keywords ⇒ Object
189 190 191 |
# File 'app/models/visit.rb', line 189 def keywords utm_term || search_keyword end |
#landing_page_addressable ⇒ Object
213 214 215 216 217 218 219 |
# File 'app/models/visit.rb', line 213 def landing_page_addressable @landing_page_addressable ||= begin Addressable::URI.parse(landing_page) rescue StandardError nil end end |
#landing_page_params ⇒ Object
225 226 227 228 229 230 231 232 233 |
# File 'app/models/visit.rb', line 225 def landing_page_params params = {} return params unless (query = landing_page_addressable&.query) params = Rack::Utils.parse_nested_query query params.with_indifferent_access rescue StandardError {} end |
#landing_page_path ⇒ Object
221 222 223 |
# File 'app/models/visit.rb', line 221 def landing_page_path landing_page_addressable&.path || landing_page end |
#location_summary ⇒ Object
209 210 211 |
# File 'app/models/visit.rb', line 209 def location_summary [city, region, postal_code, country].compact.join(' ') end |
#opportunities ⇒ ActiveRecord::Relation<Opportunity>
73 |
# File 'app/models/visit.rb', line 73 has_many :opportunities |
#remove_self_referrer ⇒ Object
269 270 271 272 273 274 |
# File 'app/models/visit.rb', line 269 def remove_self_referrer return unless /\A(www\.)?warmlyyours\.com/.match?(referring_domain) self.referrer = nil self.referring_domain = nil end |
#room_configurations ⇒ ActiveRecord::Relation<RoomConfiguration>
74 |
# File 'app/models/visit.rb', line 74 has_many :room_configurations |
#set_source ⇒ Object
276 277 278 |
# File 'app/models/visit.rb', line 276 def set_source self.source = find_source end |
#source ⇒ Source
66 |
# File 'app/models/visit.rb', line 66 belongs_to :source, inverse_of: :visits, optional: true |
#state_code ⇒ Object
193 194 195 196 197 |
# File 'app/models/visit.rb', line 193 def state_code return unless (c = country_iso3166) && region.present? c.find_subdivision_by_name(region)&.code end |
#update_associated_customer_source ⇒ Object
280 281 282 283 284 285 286 287 288 289 290 291 |
# File 'app/models/visit.rb', line 280 def update_associated_customer_source return unless source && user current = user.source # Allow update when: no source, unknown source, or narrowing within the # same source tree (e.g. "Google" → "Google Ads"). return unless current.nil? || current.unknown_source? || source.descendant_of?(current) user.update_attribute!(:source_id, source.id) end |
#user ⇒ Party
65 |
# File 'app/models/visit.rb', line 65 belongs_to :user, class_name: 'Party', inverse_of: :visits, optional: true |
#visit_events ⇒ ActiveRecord::Relation<VisitEvent>
70 |
# File 'app/models/visit.rb', line 70 has_many :visit_events, inverse_of: :visit |