# frozen_string_literal: true # Estadísticas del sitio class StatsController < ApplicationController include Pundit include ActionView::Helpers::DateHelper before_action :authenticate_usuarie! before_action :authorize_stats breadcrumb -> { current_usuarie.email }, :edit_usuarie_registration_path breadcrumb 'sites.index', :sites_path, match: :exact breadcrumb -> { site.title }, -> { site_posts_path(site, locale: locale) }, match: :exact EXTRA_OPTIONS = { builds: {}, space_used: { bytes: true }, build_time: {} }.freeze # XXX: Permitir a Chart.js inyectar su propio CSS content_security_policy only: :index do |policy| policy.style_src :self, :unsafe_inline policy.script_src :self, :unsafe_inline end # Parámetros por defecto # # @return [Hash] def default_url_options { interval: 'day', period_start: Date.today.beginning_of_year, period_end: Date.today } end def index breadcrumb I18n.t('stats.index.title'), '' @chart_params = { interval: interval } hostnames last_stat chart_options normalized_urls end # Genera un gráfico de visitas por dominio asociado a este sitio def host return unless stale? [last_stat, hostnames, interval, period] stats = rollup_scope.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series| series.each do |serie| serie[:name] = serie.dig(:dimensions, 'host') serie[:data].transform_values! do |value| value * nodes end end end render json: stats end def resources return unless stale? [last_stat, interval, resource, period] options = { interval: interval, dimensions: { deploy_id: @site.deploys.where(type: 'DeployLocal').pluck(:id).first } } render json: rollup_scope.series(resource, **options) end def uris return unless stale? [last_stat, hostnames, interval, normalized_urls, period] options = { host: hostnames, uri: normalized_paths } stats = rollup_scope.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series| series.each do |serie| serie[:name] = serie[:dimensions].slice('host', 'uri').values.join.sub('/index.html', '/') serie[:data].transform_values! do |value| value * nodes end end end render json: stats end private def rollup_scope Rollup.where(time: period) end def last_stat @last_stat ||= Stat.last end def authorize_stats @site = find_site authorize SiteStat.new(@site) end # TODO: Eliminar cuando mergeemos referer-origin def hostnames @hostnames ||= site.hostnames end # Normalizar las URLs # # @return [Array] def normalized_urls @normalized_urls ||= begin urls = params.permit(:urls).try(:[], :urls)&.split("\n")&.map(&:strip)&.select(&:present?)&.select do |uri| uri.start_with? 'https://' end urls ||= [site.url] urls.map do |u| # XXX: Eliminar al deployear # @see {https://0xacab.org/sutty/containers/nginx/-/merge_requests/1} next u unless u.end_with? '/' "#{u}index.html" end.uniq end end def normalized_paths @normalized_paths ||= normalized_urls.map do |u| "/#{u.split('/', 4).last}" end.map do |u| URI.decode_www_form_component u end end # Opciones por defecto para los gráficos. # # La invitación a volver dentro de X tiempo es para dar un estimado de # cuándo habrá información disponible, porque Rollup genera intervalos # completos (¿aunque dice que no?) # # La diferencia se calcula sumando el intervalo a la hora de última # toma de estadísticas y restando el tiempo que pasó desde ese # momento. def chart_options time = (last_stat&.created_at || Time.now) + 1.try(interval) please_return_at = { please_return_at: distance_of_time_in_words(Time.now, time) } @chart_options ||= { locale: I18n.locale, empty: I18n.t('stats.index.empty', **please_return_at), loading: I18n.t('stats.index.loading'), html: %(
%{loading}
) } end # Obtiene y valida los intervalos # # @return [Symbol] def interval @interval ||= begin i = params[:interval]&.to_sym Stat::INTERVALS.include?(i) ? i : :day end end def resource @resource ||= begin r = params[:resource].to_sym Stat::RESOURCES.include?(r) ? r : :builds end end # Obtiene la cantidad de nodos de Sutty, para poder calcular la # cantidad de visitas. # # Como repartimos las visitas por nodo rotando las IPs en el # nameserver y los resolvedores de DNS eligen un nameserver # aleatoriamente, la cantidad de visitas se reparte # equitativamente. # # XXX: Remover cuando podamos centralizar los AccessLog # # @return [Integer] def nodes @nodes ||= ENV.fetch('NODES', 1).to_i end def period @period ||= begin p = params.permit(:period_start, :period_end) p[:period_start]..p[:period_end] end end end