# frozen_string_literal: true module Admin class AnalyticsController < Admin::BaseController def index @filters = { from: parse_date(params[:from]) || 7.days.ago.to_date, to: parse_date(params[:to]) || Time.zone.today, device: params[:device].presence } @analytics_preview_active = analytics_preview_active? scope = AnalyticsPageStat.where(day: @filters[:from]..@filters[:to]) scope = scope.where(device: @filters[:device]) if @filters[:device].present? && AnalyticsEvent::DEVICES.include?(@filters[:device]) cell_scope = AnalyticsPageCell.where(day: @filters[:from]..@filters[:to]) cell_scope = cell_scope.where(device: @filters[:device]) if @filters[:device].present? && AnalyticsEvent::DEVICES.include?(@filters[:device]) clicks_by_path = cell_scope.group(:page_path).sum(:click_count) moves_by_path = cell_scope.group(:page_path).sum(:move_count) pageviews_by_path = scope.group(:page_path).sum(:pageview_count) scroll_samples_by_path = scope.group(:page_path).sum(:scroll_samples) scroll_sum_by_path = scope.group(:page_path).sum(:scroll_sum_pct) max_scroll_by_path = scope.group(:page_path).maximum(:max_scroll_pct) paths = (pageviews_by_path.keys + clicks_by_path.keys + moves_by_path.keys).uniq @pages = paths.map do |path| samples = scroll_samples_by_path[path].to_i { page_path: path, pageviews: pageviews_by_path[path].to_i, clicks: clicks_by_path[path].to_i, moves: moves_by_path[path].to_i, scroll_samples: samples, scroll_sum: scroll_sum_by_path[path].to_i, max_scroll: max_scroll_by_path[path].to_i } end.sort_by { |r| [-r[:pageviews], -r[:moves], -r[:clicks], r[:page_path]] } pageviews_by_day = scope.group(:day).sum(:pageview_count) clicks_by_day = cell_scope.group(:day).sum(:click_count) moves_by_day = cell_scope.group(:day).sum(:move_count) @trend = (@filters[:from]..@filters[:to]).map do |day| { day: day.iso8601, pageviews: pageviews_by_day[day].to_i, clicks: clicks_by_day[day].to_i, moves: moves_by_day[day].to_i } end @trend_totals = { pageviews: @trend.sum { |r| r[:pageviews] }, clicks: @trend.sum { |r| r[:clicks] }, moves: @trend.sum { |r| r[:moves] } } end def show @page_path = params[:page_path].to_s redirect_to admin_analytics_path, alert: t("admin.analytics.missing_path") and return if @page_path.blank? from = parse_date(params[:from]) || 7.days.ago.to_date to = parse_date(params[:to]) || Time.zone.today @device_tab_stats = device_tab_stats(@page_path, from, to) device = resolve_heatmap_device(@device_tab_stats, params[:device].presence) @filters = { from: from, to: to, device: device, layer: params[:layer].to_s } cells = AnalyticsPageCell.where( page_path: @page_path, day: @filters[:from]..@filters[:to], device: @filters[:device] ) @click_total = cells.sum(:click_count) @move_total = cells.sum(:move_count) @filters[:layer] = if %w[move click].include?(@filters[:layer]) @filters[:layer] elsif @move_total.positive? "move" else "click" end counter = @filters[:layer] == "click" ? :click_count : :move_count @cells = cells.group(:cell_x, :cell_y).sum(counter) @max_weight = @cells.values.max.to_i @total_points = @cells.values.sum stats = AnalyticsPageStat.where( page_path: @page_path, day: @filters[:from]..@filters[:to], device: @filters[:device] ) @pageviews = stats.sum(:pageview_count) @scroll_samples = stats.sum(:scroll_samples) @scroll_sum = stats.sum(:scroll_sum_pct) @max_scroll = stats.maximum(:max_scroll_pct).to_i @avg_scroll = @scroll_samples.positive? ? (@scroll_sum.to_f / @scroll_samples).round : 0 @grid = AnalyticsPageCell::GRID_SIZE @snapshot = find_snapshot(@page_path, @filters[:device]) end def preview_enable Analytics::Suppress.enable!(cookies) redirect_to preview_return_to(params[:return_to]), notice: t("admin.analytics.preview.enabled") end def preview_disable Analytics::Suppress.disable!(cookies) redirect_to admin_analytics_path, notice: t("admin.analytics.preview.disabled") end private def analytics_preview_active? Analytics::Suppress.active?(cookies[Analytics::Suppress::COOKIE_NAME]) end def preview_return_to(value) path = value.to_s.strip return root_path if path.blank? return path if path.start_with?("/") && !path.start_with?("//") admin_analytics_path end def parse_date(value) return nil if value.blank? Date.parse(value.to_s) rescue ArgumentError, TypeError nil end def device_tab_stats(page_path, from, to) cell_totals = AnalyticsPageCell.where(page_path: page_path, day: from..to) .group(:device) .pluck( :device, Arel.sql("SUM(click_count)"), Arel.sql("SUM(move_count)") ) cell_by_device = cell_totals.to_h { |device, clicks, moves| [device, { clicks: clicks.to_i, moves: moves.to_i }] } pageview_totals = AnalyticsPageStat.where(page_path: page_path, day: from..to) .group(:device) .sum(:pageview_count) AnalyticsEvent::DEVICES.index_with do |device| cells = cell_by_device[device] || { clicks: 0, moves: 0 } cells.merge(pageviews: pageview_totals[device].to_i) end end def resolve_heatmap_device(tab_stats, requested) if requested.present? && AnalyticsEvent::DEVICES.include?(requested) return requested end AnalyticsEvent::DEVICES.max_by do |device| stats = tab_stats[device] stats[:clicks] + stats[:moves] + stats[:pageviews] end end def find_snapshot(page_path, device) return nil unless AnalyticsEvent::DEVICES.include?(device) AnalyticsPageSnapshot.where(page_path: page_path, device: device) .order(captured_at: :desc) .detect { |snapshot| snapshot.image.attached? } end end end