# 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