# 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 include RecursiveRollup # 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 # Recordar la última vez que se corrió la tarea stat = site.stats.create! name: STAT_NAME beginning = beginning_of_interval columns = %i[http_referer geoip2_data_country_name] # 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. site.hostnames.each do |host| break if stop? host_dimensions = { host: host } # 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. uris.each do |uri| break if stop? name = 'host|uri' dimensions = { host: host, uri: uri } rollup(name, beginning, **dimensions) reduce_rollup(name, beginning, **dimensions) columns.each do |column| # Obtener orígenes de visitas por host AccessLog.where(**host_dimensions).distinct(column).pluck(column).each do |value| column_name = "host|uri|#{column}" dimensions[column] = value rollup(column_name, beginning, **dimensions) reduce_rollup(column_name, beginning, **dimensions) end end end # Reducir todas las visitas a cantidad de visitas por host square_rollup(name: 'host|uri', new_name: 'host', interval: starting_interval, dimensions: host_dimensions, beginning: beginning) # Acumular por mes y año reduce_rollup('host', beginning, **host_dimensions) columns.each do |column| square_rollup(name: "host|uri|#{column}", new_name: "host|#{column}", interval: starting_interval, dimensions: host_dimensions, beginning: beginning) reduce_rollup("host|#{column}", beginning, **host_dimensions) end end stat.touch run_again! unless once end private # Generar un rollup de access logs # # @param :name [String] # @param :beginning [Time] # @param :dimensions [Hash] # @return [nil] def rollup(name, beginning, **dimensions) AccessLog.where(**dimensions) .where('created_at >= ?', beginning) .completed_requests .non_robots .group(*dimensions.keys) .rollup(name, interval: starting_interval, update: true) end # Reducir las estadísticas calculadas aplicando un rollup sobre el # intervalo más amplio. # # @param :name [String] # @param :beginning [Time] # @param :dimensions [Hash] # @return [nil] def reduce_rollup(name, beginning, **dimensions) Stat::INTERVALS.reduce do |previous, current| recursive_rollup(name: name, interval_previous: previous, interval: current, dimensions: dimensions, beginning: beginning) # Devolver el intervalo actual 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).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