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 Schedulable
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 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
80 |
# File 'app/models/visit.rb', line 80 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.
153 154 155 156 |
# File 'app/models/visit.rb', line 153 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
251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 |
# File 'app/models/visit.rb', line 251 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
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 |
# File 'app/models/visit.rb', line 162 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
158 159 160 |
# File 'app/models/visit.rb', line 158 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
293 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 |
# File 'app/models/visit.rb', line 293 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
134 135 136 137 138 |
# File 'app/models/visit.rb', line 134 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
77 |
# File 'app/models/visit.rb', line 77 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
78 |
# File 'app/models/visit.rb', line 78 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
129 130 131 |
# File 'app/models/visit.rb', line 129 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
141 142 143 |
# File 'app/models/visit.rb', line 141 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
79 |
# File 'app/models/visit.rb', line 79 scope :with_order_conversions, -> { joins(:orders).where("orders.state = 'invoiced'") } |
Instance Method Details
#country_iso3 ⇒ Object
204 205 206 |
# File 'app/models/visit.rb', line 204 def country_iso3 country_iso3166&.alpha3 end |
#country_iso3166 ⇒ Object
198 199 200 201 202 |
# File 'app/models/visit.rb', line 198 def country_iso3166 return if country.blank? ISO3166::Country.find_country_by_any_name(country) end |
#find_source ⇒ Object
Smart source lookup
235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 |
# File 'app/models/visit.rb', line 235 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
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 |
# File 'app/models/visit.rb', line 320 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
188 189 190 |
# File 'app/models/visit.rb', line 188 def keywords utm_term || search_keyword end |
#landing_page_addressable ⇒ Object
212 213 214 215 216 217 218 |
# File 'app/models/visit.rb', line 212 def landing_page_addressable @landing_page_addressable ||= begin Addressable::URI.parse(landing_page) rescue StandardError nil end end |
#landing_page_params ⇒ Object
224 225 226 227 228 229 230 231 232 |
# File 'app/models/visit.rb', line 224 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
220 221 222 |
# File 'app/models/visit.rb', line 220 def landing_page_path landing_page_addressable&.path || landing_page end |
#location_summary ⇒ Object
208 209 210 |
# File 'app/models/visit.rb', line 208 def location_summary [city, region, postal_code, country].compact.join(' ') end |
#opportunities ⇒ ActiveRecord::Relation<Opportunity>
74 |
# File 'app/models/visit.rb', line 74 has_many :opportunities |
#remove_self_referrer ⇒ Object
268 269 270 271 272 273 |
# File 'app/models/visit.rb', line 268 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>
75 |
# File 'app/models/visit.rb', line 75 has_many :room_configurations |
#set_source ⇒ Object
275 276 277 |
# File 'app/models/visit.rb', line 275 def set_source self.source = find_source end |
#source ⇒ Source
67 |
# File 'app/models/visit.rb', line 67 belongs_to :source, inverse_of: :visits, optional: true |
#state_code ⇒ Object
192 193 194 195 196 |
# File 'app/models/visit.rb', line 192 def state_code return unless (c = country_iso3166) && region.present? c.find_subdivision_by_name(region)&.code end |
#update_associated_customer_source ⇒ Object
279 280 281 282 283 284 285 286 287 288 289 290 |
# File 'app/models/visit.rb', line 279 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
66 |
# File 'app/models/visit.rb', line 66 belongs_to :user, class_name: 'Party', inverse_of: :visits, optional: true |
#visit_events ⇒ ActiveRecord::Relation<VisitEvent>
71 |
# File 'app/models/visit.rb', line 71 has_many :visit_events, inverse_of: :visit |