# frozen_string_literal: true # Notifica excepciones a una instancia de Gitlab, como incidencias # nuevas o como comentarios a las incidencias pre-existentes. class GitlabNotifierJob < ApplicationJob include ExceptionNotifier::BacktraceCleaner # Variables que vamos a acceder luego attr_reader :exception, :options, :issue_data, :cached queue_as :low_priority # @param [Exception] la excepción lanzada # @param [Hash] opciones de ExceptionNotifier def perform(exception, **options) @exception = exception @options = options @issue_data = { count: 1 } # Necesitamos saber si el issue ya existía @cached = false # Traemos los datos desde la caché si existen, sino generamos un # issue nuevo e inicializamos la caché @issue_data = Rails.cache.fetch(cache_key) do issue = client.new_issue confidential: true, title: title, description: description, issue_type: 'incident' @cached = true { count: 1, issue: issue['iid'], user_agents: [user_agent].compact, params: [request&.filtered_parameters].compact, urls: [url].compact } end # No seguimos actualizando si acabamos de generar el issue return if cached # Incrementar la cuenta de veces que ocurrió issue_data[:count] += 1 # Guardar información útil issue_data[:urls] << url unless issue_data[:urls].include? url issue_data[:user_agents] << user_agent unless issue_data[:user_agents].include? user_agent # Editar el título para que incluya la cuenta de eventos client.edit_issue(iid: issue_data[:issue], title: title, state_event: 'reopen') # Agregar un comentario con la información posiblemente nueva client.new_note(iid: issue_data[:issue], body: body) # Guardar para después Rails.cache.write(cache_key, issue_data) # Si este trabajo genera una excepción va a entrar en un loop # TODO: Notificarnos por otros medios (mail) rescue Exception => e Rails.logger.info 'No entrar en loop' end private # La llave en la cache tiene en cuenta la excepción, el mensaje, la # ruta del backtrace y los errores de JS # # @return [String] def cache_key @cache_key ||= [ exception.class.name, Digest::SHA1.hexdigest(exception.message), Digest::SHA1.hexdigest(backtrace&.first.to_s), Digest::SHA1.hexdigest(options.dig(:data, :params, 'errors').to_s) ].join('/') end # Define si es una excepción de javascript o local # # @see BacktraceJob # @return [Boolean] def javascript? @javascript ||= options.dig(:data, :javascript_backtrace).present? end # Título # # @return [String] def title @title ||= ''.dup.tap do |t| t << "[#{exception.class}] " unless javascript? t << exception.message t << " [#{issue_data[:count]}]" end end # Descripción # # @return [String] def description @description ||= ''.dup.tap do |d| d << request_section d << javascript_section d << javascript_footer d << backtrace_section d << data_section end end # Comentario # # @return [String] def body @body ||= ''.dup.tap do |b| b << request_section b << javascript_footer b << data_section end end # Cadena de archivos donde se produjo el error # # @return [Array,Nil] def backtrace @backtrace ||= exception.backtrace ? clean_backtrace(exception) : nil end # Entorno del error # # @return [Hash] def env options[:env] end # Genera una petición a partir del entorno # # @return [ActionDispatch::Request] def request @request ||= ActionDispatch::Request.new(env) if env.present? end # Cliente de la API de Gitlab # # @return [GitlabApiClient] def client @client ||= GitlabApiClient.new end # Muestra información de la petición # # @return [String] def request_section return '' unless request <<~REQUEST # Request ``` #{request.request_method} #{url} #{pp request.filtered_parameters} ``` REQUEST end # Muestra información de JavaScript # # @return [String] def javascript_section return '' unless javascript? options.dig(:data, :params, 'errors')&.map do |error| # Algunos errores no son excepciones (?) error['type'] = 'undefined' if error['type'].blank? <<~JAVASCRIPT ## #{error['type']}: #{error['message']} ``` #{Terminal::Table.new headings: error['backtrace'].first.keys, rows: error['backtrace'].map(&:values)} ``` JAVASCRIPT end&.join end # Muestra información de la visita que generó el error en JS # # @return [String] def javascript_footer return '' unless javascript? <<~JAVASCRIPT #{user_agent} <#{url}> JAVASCRIPT end # Muestra el historial del error en Ruby # # @return [String] def backtrace_section return '' if javascript? return '' unless backtrace <<~BACKTRACE ## Backtrace ``` #{backtrace.join("\n")} ``` BACKTRACE end # Muestra datos extra de la visita # # @return [String] def data_section return '' unless options[:data] <<~DATA ## Data ``` #{pp options[:data]} ``` DATA end # Obtiene el UA de este error # # @return [String] def user_agent @user_agent ||= options.dig(:data, :params, 'context', 'userAgent') if javascript? @user_agent ||= request.headers['user-agent'] if request @user_agent end # Obtiene la URL actual # # @return [String] def url @url ||= request&.url || options.dig(:data, :params, 'context', 'url') end end