Class: Visit

Inherits:
ApplicationRecord show all
Defined in:
app/models/visit.rb

Overview

== Schema Information

Table name: visits
Database name: primary

id :bigint not null, primary key
browser :string
city :string
country :string
device_type :string
dnt :integer
gbraid :string
gclid :string
ip :string(39)
landing_page :text
latitude :decimal(10, 6)
locale :string(5)
longitude :decimal(10, 6)
os :string
postal_code :string
processed :boolean
referral_code :string
referrer :text
referring_domain :string
region :string
region_code :string(10)
screen_height :integer
screen_width :integer
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
wbraid :string
session_id :string
source_id :integer
user_id :integer
utm_id :string

Indexes

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 =
%w[orders quotes opportunities room_configurations].map { |t| "exists(select 1 from #{t} where #{t}.visit_id = visits.id)" }.join(' or ').freeze

Belongs to collapse

Has one collapse

Has many collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from ApplicationRecord

ransackable_associations, ransackable_attributes, ransortable_attributes, #to_relation

Methods included from Models::EventPublishable

#publish_event

Class Method Details

.by_source_id_with_descendantsActiveRecord::Relation<Visit>

A relation of Visits that are by source id with descendants. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Visit>)

See Also:



74
# File 'app/models/visit.rb', line 74

scope :by_source_id_with_descendants, ->(source_id) { where(source_id: Source.self_and_descendants_ids(source_id)) }

.populate_empty_sources(visits_query = nil) ⇒ Object



178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
# File 'app/models/visit.rb', line 178

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_idObject



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'app/models/visit.rb', line 90

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.to_f) * 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



86
87
88
# File 'app/models/visit.rb', line 86

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



220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
# File 'app/models/visit.rb', line 220

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_conversionsActiveRecord::Relation<Visit>

A relation of Visits that are with conversions. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Visit>)

See Also:



71
# File 'app/models/visit.rb', line 71

scope :with_conversions, -> { select("*,(#{SQL_CONVERSIONS}) as has_conversions") }

.with_events_countActiveRecord::Relation<Visit>

A relation of Visits that are with events count. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Visit>)

See Also:



72
# File 'app/models/visit.rb', line 72

scope :with_events_count, -> { select('visits.*, (SELECT COUNT(*) FROM visit_events WHERE visit_events.visit_id = visits.id) as events_count') }

.with_order_conversionsActiveRecord::Relation<Visit>

A relation of Visits that are with order conversions. Active Record Scope

Returns:

  • (ActiveRecord::Relation<Visit>)

See Also:



73
# File 'app/models/visit.rb', line 73

scope :with_order_conversions, -> { joins(:orders).where("orders.state = 'invoiced'") }

Instance Method Details

#country_iso3Object



132
133
134
# File 'app/models/visit.rb', line 132

def country_iso3
  country_iso3166&.alpha3
end

#country_iso3166Object



126
127
128
129
130
# File 'app/models/visit.rb', line 126

def country_iso3166
  return unless country.present?

  ISO3166::Country.find_country_by_any_name(country)
end

#find_sourceObject

Smart source lookup



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'app/models/visit.rb', line 163

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] ||= gclid if respond_to?(:gclid)
  params[:gbraid] ||= gbraid if respond_to?(:gbraid)
  params[:wbraid] ||= wbraid if respond_to?(:wbraid)
  Source.find_from_params(params, referrer: referrer)
end

#geocodeObject



247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
# File 'app/models/visit.rb', line 247

def geocode
  return if latitude && longitude

  location =
    begin
      Geocoder.search(ip).first
    rescue StandardError
      Rails.logger.error "Geocode error: #{e.class.name}: #{e.message}"
      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

#keywordsObject



116
117
118
# File 'app/models/visit.rb', line 116

def keywords
  utm_term || search_keyword
end

#landing_page_addressableObject



140
141
142
143
144
145
146
# File 'app/models/visit.rb', line 140

def landing_page_addressable
  @landing_page_parsed ||= begin
    Addressable::URI.parse(landing_page)
  rescue StandardError
    nil
  end
end

#landing_page_paramsObject



152
153
154
155
156
157
158
159
160
# File 'app/models/visit.rb', line 152

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_pathObject



148
149
150
# File 'app/models/visit.rb', line 148

def landing_page_path
  landing_page_addressable&.path || landing_page
end

#location_summaryObject



136
137
138
# File 'app/models/visit.rb', line 136

def location_summary
  [city, region, postal_code, country].compact.join(' ')
end

#opportunitiesActiveRecord::Relation<Opportunity>

Returns:

See Also:



68
# File 'app/models/visit.rb', line 68

has_many :opportunities

#ordersActiveRecord::Relation<Order>

Returns:

  • (ActiveRecord::Relation<Order>)

See Also:



66
# File 'app/models/visit.rb', line 66

has_many :orders

#partyParty

Returns:

See Also:



63
# File 'app/models/visit.rb', line 63

has_one :party, inverse_of: :visit

#quotesActiveRecord::Relation<Quote>

Returns:

  • (ActiveRecord::Relation<Quote>)

See Also:



67
# File 'app/models/visit.rb', line 67

has_many :quotes

#remove_self_referrerObject



195
196
197
198
199
200
# File 'app/models/visit.rb', line 195

def remove_self_referrer
  return unless /\A(www\.)?warmlyyours\.com/.match?(referring_domain)

  self.referrer = nil
  self.referring_domain = nil
end

#room_configurationsActiveRecord::Relation<RoomConfiguration>

Returns:

  • (ActiveRecord::Relation<RoomConfiguration>)

See Also:



69
# File 'app/models/visit.rb', line 69

has_many :room_configurations

#set_sourceObject



202
203
204
# File 'app/models/visit.rb', line 202

def set_source
  self.source = find_source
end

#sourceSource

Returns:

See Also:



61
# File 'app/models/visit.rb', line 61

belongs_to :source, inverse_of: :visits, optional: true

#state_codeObject



120
121
122
123
124
# File 'app/models/visit.rb', line 120

def state_code
  return unless (c = country_iso3166) && region.present?

  c.find_subdivision_by_name(region)&.code
end

#update_associated_customer_sourceObject



206
207
208
209
210
211
212
213
214
215
216
217
# File 'app/models/visit.rb', line 206

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

#userParty

Returns:

See Also:



60
# File 'app/models/visit.rb', line 60

belongs_to :user, class_name: 'Party', inverse_of: :visits, optional: true

#visit_eventsActiveRecord::Relation<VisitEvent>

Returns:

See Also:



65
# File 'app/models/visit.rb', line 65

has_many :visit_events, inverse_of: :visit