# frozen_string_literal: true # Procesar una lista de URIs para una lista de dominios. Esto nos # permite procesar estadísticas a demanda. # # Hay varias cosas acá que van a convertirse en métodos propios, como la # detección de URIs de un sitio (aunque la versión actual detecta todas # las páginas y no solo las de posts como tenemos planeado, hay que # resolver eso). # # Los hostnames de un sitio van a poder obtenerse a partir de # Site#hostnames con la garantía de que son únicos. class UriCollectionJob < PeriodicJob # Ignoramos imágenes porque suelen ser demasiadas y no aportan a las # estadísticas. IMAGES = %w[.png .jpg .jpeg .gif .webp .jfif].freeze STAT_NAME = 'uri_collection_job' def perform(site_id:, once: true) @site = Site.find site_id # Obtener el principio del intervalo anterior beginning_of_interval # Recordar la última vez que se corrió la tarea stat = site.stats.create! name: STAT_NAME # Columnas a agrupar columns = Stat::COLUMNS.zip([nil]).to_h # Las URIs son la fuente de verdad de las visitas, porque son las # que indican las páginas y recursos descargables, el resto son # imágenes, CSS, JS y tipografías que no nos aportan números # significativos. uri_dimensions = { host: site.hostnames, uri: uris } host_dimensions = { host: site.hostnames } # Recorremos todos los hostnames y uris posibles y luego agrupamos # recursivamente para no tener que recalcular, asumiendo que es más # rápido buscar en los rollups indexados que en la tabla en bruto. # # Los referers solo se agrupan por host. columns.each_key do |column| columns[column] = AccessLog.where(**host_dimensions).distinct(column).pluck(column) end # Cantidad de visitas por host rollup(name: 'host', dimensions: host_dimensions, filter: uri_dimensions) reduce_rollup(name: 'host', dimensions: host_dimensions, filter: uri_dimensions) # Cantidad de visitas por página/recurso rollup(name: 'host|uri', dimensions: uri_dimensions) reduce_rollup(name: 'host|uri', dimensions: uri_dimensions) # Cantidad de visitas host y parámetro columns.each_pair do |column, values| column_name = "host|#{column}" column_dimensions = { host: site.hostnames } column_dimensions[column] = values rollup(name: column_name, dimensions: column_dimensions, filter: uri_dimensions) reduce_rollup(name: column_name, dimensions: column_dimensions) end stat.touch run_again! unless once end private # Generar un rollup de access logs # # @param :name [String] # @param :beginning [Time] # @param :dimensions [Hash] # @param :filter [Hash] # @return [nil] def rollup(name:, dimensions:, interval: starting_interval, filter: nil) AccessLog.where(**(filter || dimensions)) .where('created_at >= ?', beginning_of_interval) .completed_requests .non_robots .group(*dimensions.keys) .rollup(name, interval: interval, update: true) end # Generar rollups con el resto de la información # # @param :name [String] # @param :dimensions [Hash] # @param :filter [Hash] # @return [nil] def reduce_rollup(name:, dimensions:, filter: nil) Stat::INTERVALS.reduce do |_previous, current| rollup(name: name, dimensions: dimensions, filter: filter, interval: current) current end nil end def stat_name STAT_NAME end # Obtiene todas las ubicaciones de archivos # # @return [String] # # TODO: Cambiar al mergear origin-referer def destinations @destinations ||= site.deploys.map(&:destination).compact.select do |d| File.directory?(d) end.map do |d| File.realpath(d) end.uniq end # Recolecta todas las URIs menos imágenes # # TODO: Para los sitios con DeployLocalizedDomain estamos buscando # URIs de más. # # @return [Array] def uris @uris ||= destinations.map do |destination| locales.map do |locale| uri = "/#{locale}/".squeeze('/') dir = File.join(destination, locale) next unless File.directory? dir files(dir).map do |f| uri + f end end end.flatten(3).compact end # @return [Array] def locales @locales ||= ['', site.locales.map(&:to_s)].flatten(1) end # @param :dir [String] # @return [Array] def files(dir) Dir.chdir(dir) do pages = Dir.glob('**/*.html') files = Dir.glob('public/**/*') files = remove_directories files files = remove_images files [pages, files].flatten(1) end end # @param :files [Array] # @return [Array] def remove_directories(files) files.reject do |f| File.directory? f end end def remove_images(files) files.reject do |f| IMAGES.include? File.extname(f).downcase end end end