5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-22 23:26:21 +00:00
panel/app/controllers/stats_controller.rb

226 lines
6.1 KiB
Ruby
Raw Normal View History

2019-08-02 00:20:42 +00:00
# frozen_string_literal: true
# Estadísticas del sitio
class StatsController < ApplicationController
include Pundit
2021-10-08 21:35:40 +00:00
include ActionView::Helpers::DateHelper
2019-08-02 00:20:42 +00:00
before_action :authenticate_usuarie!
before_action :authorize_stats
2019-08-02 00:20:42 +00:00
2022-04-26 19:48:25 +00:00
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
2021-10-08 21:40:16 +00:00
EXTRA_OPTIONS = {
builds: {},
space_used: { bytes: true },
build_time: {}
}.freeze
2021-10-08 19:31:02 +00:00
# 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
2019-08-02 00:20:42 +00:00
def index
2022-04-26 19:48:25 +00:00
breadcrumb I18n.t('stats.index.title'), ''
params.with_defaults! default_url_options
@chart_params = {
interval: interval,
period_start: params[:period_start],
period_end: params[:period_end]
}
2021-10-08 19:31:02 +00:00
hostnames
last_stat
2021-10-08 21:35:40 +00:00
chart_options
normalized_urls
expires_in = Time.now.try(:"end_of_#{Stat.default_interval}") - Time.now
@columns = {}
Stat::COLUMNS.each do |column|
@columns[column] =
Rails.cache.fetch("stats/#{column}/#{site.id}", expires_in: expires_in) do
2022-04-30 23:41:38 +00:00
rollup_scope.where(interval: interval, name: "host|#{column}")
.where_dimensions(host: hostnames)
.group("dimensions->>'#{column}'")
2022-05-02 16:41:37 +00:00
.order('sum(value) desc')
2022-04-30 23:41:38 +00:00
.sum(:value)
.transform_values(&:to_i)
.transform_values do |v|
v * nodes
end
end
end
2021-10-08 19:31:02 +00:00
end
# Genera un gráfico de visitas por dominio asociado a este sitio
def host
return unless stale? [last_stat, hostnames, interval, period]
2021-10-09 18:52:18 +00:00
stats = rollup_scope.where_dimensions(host: hostnames).multi_series('host', interval: interval).tap do |series|
2021-10-09 18:52:18 +00:00
series.each do |serie|
serie[:name] = serie.dig(:dimensions, 'host')
serie[:data].transform_values! do |value|
value * nodes
2021-10-08 19:31:02 +00:00
end
end
end
2021-10-09 18:52:18 +00:00
render json: stats
end
2021-10-08 19:31:02 +00:00
def resources
return unless stale? [last_stat, interval, resource, period]
2021-10-09 18:52:18 +00:00
2022-04-30 19:38:16 +00:00
options = { interval: interval, dimensions: { site_id: site.id } }
render json: rollup_scope.series(resource, **options)
2021-10-08 19:31:02 +00:00
end
def uris
return unless stale? [last_stat, hostnames, interval, normalized_urls, period]
options = { host: hostnames, uri: normalized_paths }
2022-04-30 19:38:32 +00:00
# XXX: where_dimensions es más corto pero no aprovecha los índices
# de Rollup
stats = rollup_scope.where_dimensions(**options).multi_series('host|uri', interval: interval).tap do |series|
series.each do |serie|
2021-10-09 21:29:53 +00:00
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
2021-10-08 19:31:02 +00:00
private
def rollup_scope
Rollup.where(time: period)
end
def last_stat
@last_stat ||= site.stats.last
end
def authorize_stats
2022-04-26 19:55:54 +00:00
authorize SiteStat.new(site)
end
2021-10-08 19:31:02 +00:00
# TODO: Eliminar cuando mergeemos referer-origin
def hostnames
2022-04-26 19:54:04 +00:00
@hostnames ||= site.hostnames
2021-10-08 19:31:02 +00:00
end
# Normalizar las URLs
#
# @return [Array]
def normalized_urls
2022-04-26 19:54:04 +00:00
@normalized_urls ||=
begin
urls = params[:urls].is_a?(Array) ? params[:urls] : params[:urls]&.split("\n")
urls = urls&.map(&:strip)&.select(&:present?)&.select do |uri|
2022-04-26 19:54:04 +00:00
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
2021-10-08 21:35:40 +00:00
# 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
2021-10-20 20:36:30 +00:00
time = (last_stat&.created_at || Time.now) + 1.try(interval)
2021-10-08 21:35:40 +00:00
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: %(<div id="%{id}" class="d-flex align-items-center justify-content-center" style="height: %{height}; width: %{width};">%{loading}</div>)
}
end
2021-10-08 19:31:02 +00:00
# Obtiene y valida los intervalos
#
# @return [Symbol]
def interval
@interval ||= begin
2021-10-09 20:28:26 +00:00
i = params[:interval]&.to_sym
2022-04-26 19:56:45 +00:00
Stat::INTERVALS.include?(i) ? i : Stat::INTERVALS.first
2021-10-08 19:31:02 +00:00
end
end
2022-04-26 19:56:45 +00:00
# @return [Symbol]
def resource
@resource ||= begin
r = params[:resource].to_sym
2022-04-26 19:56:45 +00:00
Stat::RESOURCES.include?(r) ? r : Stat::RESOURCES.first
end
end
2021-10-08 19:31:02 +00:00
# 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
2019-08-02 00:20:42 +00:00
end
def period
@period ||= begin
p = params.permit(:period_start, :period_end)
p[:period_start]..p[:period_end]
end
end
2022-04-26 19:55:54 +00:00
def site
@site ||= find_site
end
2019-08-02 00:20:42 +00:00
end