Files
MatchLiveTv/backend/app/services/analytics/aggregate.rb

108 lines
3.5 KiB
Ruby

# frozen_string_literal: true
module Analytics
class Aggregate
BATCH = 500
UPSERT_RETRIES = 3
def call
loop do
ids = AnalyticsEvent.order(:occurred_at).limit(BATCH).pluck(:id)
break if ids.empty?
events = AnalyticsEvent.where(id: ids).to_a
apply_points(events.select { |e| e.event_type == "click" }, :click_count)
apply_points(events.select { |e| e.event_type == "move" }, :move_count)
apply_stats(events.select { |e| %w[pageview scroll].include?(e.event_type) })
AnalyticsEvent.where(id: ids).delete_all
end
end
def purge_old!(retention: 30.days)
AnalyticsEvent.where("occurred_at < ?", retention.ago).delete_all
AnalyticsPageCell.where("day < ?", retention.ago.to_date).delete_all
AnalyticsPageStat.where("day < ?", retention.ago.to_date).delete_all
AnalyticsPageSnapshot.where("captured_at < ?", retention.ago).find_each do |snap|
snap.image.purge if snap.image.attached?
snap.destroy!
end
end
private
def apply_points(events, counter_attr)
return if events.empty?
grid = AnalyticsPageCell::GRID_SIZE
grouped = Hash.new(0)
events.each do |e|
next if e.x_pct.nil? || e.y_pct.nil?
cell_x = [[(e.x_pct.to_f / 100 * grid).floor, grid - 1].min, 0].max
cell_y = [[(e.y_pct.to_f / 100 * grid).floor, grid - 1].min, 0].max
key = [e.occurred_at.in_time_zone.to_date, e.page_path, e.device, cell_x, cell_y]
grouped[key] += e.weight.to_i.clamp(1, 500)
end
now = Time.current
grouped.each do |(day, page_path, device, cell_x, cell_y), count|
with_unique_retry do
cell = AnalyticsPageCell.find_or_initialize_by(
day: day, page_path: page_path, device: device, cell_x: cell_x, cell_y: cell_y
)
cell[counter_attr] = cell[counter_attr].to_i + count
cell.created_at ||= now
cell.updated_at = now
cell.save!
end
end
end
def apply_stats(events)
return if events.empty?
grouped = {}
events.each do |e|
day = e.occurred_at.in_time_zone.to_date
key = [day, e.page_path, e.device]
bucket = grouped[key] ||= { pageviews: 0, scroll_samples: 0, scroll_sum: 0, max_scroll: 0 }
case e.event_type
when "pageview"
bucket[:pageviews] += e.weight.to_i.clamp(1, 500)
when "scroll"
pct = e.scroll_pct.to_f.round
bucket[:scroll_samples] += 1
bucket[:scroll_sum] += pct
bucket[:max_scroll] = [bucket[:max_scroll], pct].max
end
end
now = Time.current
grouped.each do |(day, page_path, device), vals|
with_unique_retry do
stat = AnalyticsPageStat.find_or_initialize_by(day: day, page_path: page_path, device: device)
stat.pageview_count = stat.pageview_count.to_i + vals[:pageviews]
stat.scroll_samples = stat.scroll_samples.to_i + vals[:scroll_samples]
stat.scroll_sum_pct = stat.scroll_sum_pct.to_i + vals[:scroll_sum]
stat.max_scroll_pct = [stat.max_scroll_pct.to_i, vals[:max_scroll]].max
stat.created_at ||= now
stat.updated_at = now
stat.save!
end
end
end
def with_unique_retry
attempts = 0
begin
attempts += 1
yield
rescue ActiveRecord::RecordNotUnique
raise if attempts >= UPSERT_RETRIES
retry
end
end
end
end