2021-05-29 20:42:45 +00:00
|
|
|
# frozen_string_literal: true
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Notifica excepciones a una instancia de Gitlab, como incidencias
|
|
|
|
# nuevas o como comentarios a las incidencias pre-existentes.
|
2021-05-29 20:42:45 +00:00
|
|
|
class GitlabNotifierJob < ApplicationJob
|
2023-03-13 23:04:18 +00:00
|
|
|
class GitlabNotifierError < StandardError; end
|
|
|
|
|
2021-05-29 20:42:45 +00:00
|
|
|
include ExceptionNotifier::BacktraceCleaner
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Variables que vamos a acceder luego
|
|
|
|
attr_reader :exception, :options, :issue_data, :cached
|
2021-05-29 20:42:45 +00:00
|
|
|
|
|
|
|
queue_as :low_priority
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# @param [Exception] la excepción lanzada
|
|
|
|
# @param [Hash] opciones de ExceptionNotifier
|
2021-05-29 20:42:45 +00:00
|
|
|
def perform(exception, **options)
|
|
|
|
@exception = exception
|
2023-03-20 18:33:28 +00:00
|
|
|
@options = fix_options options
|
2021-05-31 15:08:19 +00:00
|
|
|
@issue_data = { count: 1 }
|
|
|
|
# Necesitamos saber si el issue ya existía
|
|
|
|
@cached = false
|
2023-03-13 23:04:18 +00:00
|
|
|
@issue = {}
|
2021-05-31 15:08:19 +00:00
|
|
|
|
|
|
|
# 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
|
2023-03-13 23:04:18 +00:00
|
|
|
@issue = client.new_issue confidential: true, title: title, description: description, issue_type: 'incident'
|
2021-05-31 15:08:19 +00:00
|
|
|
@cached = true
|
|
|
|
|
|
|
|
{
|
|
|
|
count: 1,
|
2023-03-13 23:04:18 +00:00
|
|
|
issue: @issue['iid'],
|
2021-05-31 15:08:19 +00:00
|
|
|
user_agents: [user_agent].compact,
|
|
|
|
params: [request&.filtered_parameters].compact,
|
|
|
|
urls: [url].compact
|
|
|
|
}
|
|
|
|
end
|
|
|
|
|
2023-03-13 23:42:40 +00:00
|
|
|
if @issue['iid'].blank? && issue_data[:issue].blank?
|
2023-03-13 23:04:18 +00:00
|
|
|
Rails.cache.delete(cache_key)
|
|
|
|
raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ')
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# 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
|
2021-05-29 20:42:45 +00:00
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Editar el título para que incluya la cuenta de eventos
|
|
|
|
client.edit_issue(iid: issue_data[:issue], title: title, state_event: 'reopen')
|
2021-05-29 20:42:45 +00:00
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Agregar un comentario con la información posiblemente nueva
|
|
|
|
client.new_note(iid: issue_data[:issue], body: body)
|
2021-05-29 20:42:45 +00:00
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Guardar para después
|
|
|
|
Rails.cache.write(cache_key, issue_data)
|
2021-05-31 15:18:23 +00:00
|
|
|
# Si este trabajo genera una excepción va a entrar en un loop, así que
|
|
|
|
# la notificamos por correo
|
2023-03-20 14:46:21 +00:00
|
|
|
rescue StandardError => e
|
2023-03-13 23:42:40 +00:00
|
|
|
email_notification.call(e, data: @issue)
|
2023-03-14 19:10:59 +00:00
|
|
|
email_notification.call(exception, data: options)
|
2021-05-29 20:42:45 +00:00
|
|
|
end
|
|
|
|
|
|
|
|
private
|
|
|
|
|
2021-05-31 15:18:23 +00:00
|
|
|
# Notificar por correo
|
|
|
|
#
|
|
|
|
# @return [ExceptionNotifier::EmailNotifier]
|
|
|
|
def email_notification
|
|
|
|
@email_notification ||= ExceptionNotifier::EmailNotifier.new(email_prefix: '[ERROR] ', sender_address: ENV['DEFAULT_FROM'], exception_recipients: ENV['EXCEPTION_TO'])
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# 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),
|
2023-03-14 19:16:46 +00:00
|
|
|
Digest::SHA1.hexdigest(errors.to_s)
|
2021-05-31 15:08:19 +00:00
|
|
|
].join('/')
|
|
|
|
end
|
|
|
|
|
2023-03-20 18:33:28 +00:00
|
|
|
# @return [Array]
|
2023-03-14 19:16:46 +00:00
|
|
|
def errors
|
2023-03-20 18:33:28 +00:00
|
|
|
options.dig(:data, :params, 'errors') || []
|
2023-03-14 19:16:46 +00:00
|
|
|
end
|
|
|
|
|
2021-05-29 20:42:45 +00:00
|
|
|
# Define si es una excepción de javascript o local
|
|
|
|
#
|
|
|
|
# @see BacktraceJob
|
2021-05-31 15:08:19 +00:00
|
|
|
# @return [Boolean]
|
2021-05-29 20:42:45 +00:00
|
|
|
def javascript?
|
|
|
|
@javascript ||= options.dig(:data, :javascript_backtrace).present?
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Título
|
|
|
|
#
|
2021-05-29 20:42:45 +00:00
|
|
|
# @return [String]
|
|
|
|
def title
|
|
|
|
@title ||= ''.dup.tap do |t|
|
|
|
|
t << "[#{exception.class}] " unless javascript?
|
|
|
|
t << exception.message
|
2021-05-31 15:08:19 +00:00
|
|
|
t << " [#{issue_data[:count]}]"
|
2021-05-29 20:42:45 +00:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Descripción
|
|
|
|
#
|
2021-05-29 20:42:45 +00:00
|
|
|
# @return [String]
|
|
|
|
def description
|
|
|
|
@description ||= ''.dup.tap do |d|
|
2023-03-13 23:47:54 +00:00
|
|
|
d << log_section
|
2021-05-29 20:42:45 +00:00
|
|
|
d << request_section
|
|
|
|
d << javascript_section
|
|
|
|
d << javascript_footer
|
|
|
|
d << backtrace_section
|
|
|
|
d << data_section
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Comentario
|
|
|
|
#
|
|
|
|
# @return [String]
|
|
|
|
def body
|
|
|
|
@body ||= ''.dup.tap do |b|
|
2023-03-13 23:51:28 +00:00
|
|
|
b << log_section
|
2021-05-31 15:08:19 +00:00
|
|
|
b << request_section
|
|
|
|
b << javascript_footer
|
|
|
|
b << data_section
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# Cadena de archivos donde se produjo el error
|
|
|
|
#
|
|
|
|
# @return [Array,Nil]
|
2021-05-29 20:42:45 +00:00
|
|
|
def backtrace
|
|
|
|
@backtrace ||= exception.backtrace ? clean_backtrace(exception) : nil
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Entorno del error
|
|
|
|
#
|
|
|
|
# @return [Hash]
|
2021-05-29 20:42:45 +00:00
|
|
|
def env
|
|
|
|
options[:env]
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Genera una petición a partir del entorno
|
|
|
|
#
|
|
|
|
# @return [ActionDispatch::Request]
|
2021-05-29 20:42:45 +00:00
|
|
|
def request
|
|
|
|
@request ||= ActionDispatch::Request.new(env) if env.present?
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Cliente de la API de Gitlab
|
|
|
|
#
|
2021-05-29 20:42:45 +00:00
|
|
|
# @return [GitlabApiClient]
|
|
|
|
def client
|
|
|
|
@client ||= GitlabApiClient.new
|
|
|
|
end
|
|
|
|
|
2023-03-13 23:47:54 +00:00
|
|
|
# @return [String]
|
|
|
|
def log_section
|
2023-03-13 23:49:00 +00:00
|
|
|
return '' unless options.dig(:data, :log)
|
2023-03-13 23:47:54 +00:00
|
|
|
|
|
|
|
<<~LOG
|
|
|
|
|
|
|
|
# Build log
|
|
|
|
|
|
|
|
```
|
2023-03-13 23:52:30 +00:00
|
|
|
#{options[:data].delete(:log)}
|
2023-03-13 23:47:54 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
LOG
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Muestra información de la petición
|
|
|
|
#
|
|
|
|
# @return [String]
|
2021-05-29 20:42:45 +00:00
|
|
|
def request_section
|
|
|
|
return '' unless request
|
|
|
|
|
|
|
|
<<~REQUEST
|
|
|
|
|
|
|
|
# Request
|
|
|
|
|
|
|
|
```
|
2021-05-31 15:08:19 +00:00
|
|
|
#{request.request_method} #{url}
|
2021-05-29 20:42:45 +00:00
|
|
|
|
|
|
|
#{pp request.filtered_parameters}
|
|
|
|
```
|
|
|
|
|
|
|
|
REQUEST
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Muestra información de JavaScript
|
|
|
|
#
|
|
|
|
# @return [String]
|
2021-05-29 20:42:45 +00:00
|
|
|
def javascript_section
|
|
|
|
return '' unless javascript?
|
|
|
|
|
|
|
|
options.dig(:data, :params, 'errors')&.map do |error|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Algunos errores no son excepciones (?)
|
|
|
|
error['type'] = 'undefined' if error['type'].blank?
|
2021-05-29 20:42:45 +00:00
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
<<~JAVASCRIPT
|
2021-05-29 20:42:45 +00:00
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
## #{error['type']}: #{error['message']}
|
2021-05-29 20:42:45 +00:00
|
|
|
|
|
|
|
```
|
|
|
|
#{Terminal::Table.new headings: error['backtrace'].first.keys, rows: error['backtrace'].map(&:values)}
|
|
|
|
```
|
|
|
|
|
|
|
|
JAVASCRIPT
|
|
|
|
end&.join
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Muestra información de la visita que generó el error en JS
|
|
|
|
#
|
|
|
|
# @return [String]
|
2021-05-29 20:42:45 +00:00
|
|
|
def javascript_footer
|
|
|
|
return '' unless javascript?
|
|
|
|
|
|
|
|
<<~JAVASCRIPT
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
#{user_agent}
|
2021-05-29 20:42:45 +00:00
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
<#{url}>
|
2021-05-29 20:42:45 +00:00
|
|
|
|
|
|
|
JAVASCRIPT
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Muestra el historial del error en Ruby
|
|
|
|
#
|
|
|
|
# @return [String]
|
2021-05-29 20:42:45 +00:00
|
|
|
def backtrace_section
|
|
|
|
return '' if javascript?
|
|
|
|
return '' unless backtrace
|
|
|
|
|
|
|
|
<<~BACKTRACE
|
|
|
|
|
|
|
|
## Backtrace
|
|
|
|
|
|
|
|
```
|
|
|
|
#{backtrace.join("\n")}
|
|
|
|
```
|
|
|
|
|
|
|
|
BACKTRACE
|
|
|
|
end
|
|
|
|
|
2021-05-31 15:08:19 +00:00
|
|
|
# Muestra datos extra de la visita
|
|
|
|
#
|
|
|
|
# @return [String]
|
2021-05-29 20:42:45 +00:00
|
|
|
def data_section
|
|
|
|
return '' unless options[:data]
|
|
|
|
|
|
|
|
<<~DATA
|
|
|
|
|
|
|
|
## Data
|
|
|
|
|
2023-03-13 23:31:16 +00:00
|
|
|
```yaml
|
|
|
|
#{options[:data].to_yaml}
|
2021-05-29 20:42:45 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
DATA
|
|
|
|
end
|
2021-05-31 15:08:19 +00:00
|
|
|
|
|
|
|
# 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
|
2023-03-20 18:33:28 +00:00
|
|
|
|
|
|
|
# Define llaves necesarias
|
|
|
|
#
|
|
|
|
# @param :options [Hash]
|
|
|
|
# @return [Hash]
|
|
|
|
def fix_options(options)
|
|
|
|
options[:data] ||= {}
|
|
|
|
options[:data][:params] ||= {}
|
|
|
|
|
|
|
|
options
|
|
|
|
end
|
2021-05-29 20:42:45 +00:00
|
|
|
end
|