5
0
Fork 0
mirror of https://0xacab.org/sutty/sutty synced 2024-11-16 15:31:43 +00:00
panel/app/jobs/gitlab_notifier_job.rb

290 lines
6.5 KiB
Ruby
Raw Normal View History

# 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
class GitlabNotifierError < StandardError; end
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
@issue = {}
# 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
if @issue['iid'].blank? && issue_data[:issue].blank?
Rails.cache.delete(cache_key)
raise GitlabNotifierError, @issue.dig('message', 'title')&.join(', ')
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, así que
# la notificamos por correo
rescue Exception => e
email_notification.call(e, data: @issue)
fix: enviar más información con el error closes #10064 closes #10065 closes #10066 closes #10067 closes #10068 closes #10069 closes #10070 closes #10071 closes #10072 closes #10073 closes #10074 closes #10075 closes #10076 closes #10077 closes #10078 closes #10079 closes #10080 closes #10081 closes #10082 closes #10083 closes #10084 closes #10085 closes #10086 closes #10087 closes #10088 closes #10089 closes #10090 closes #10091 closes #10092 closes #10093 closes #10094 closes #10095 closes #10096 closes #10097 closes #10098 closes #10099 closes #10100 closes #10101 closes #10102 closes #10103 closes #10104 closes #10105 closes #10106 closes #10107 closes #10108 closes #10109 closes #10110 closes #10111 closes #10112 closes #10113 closes #10114 closes #10115 closes #10116 closes #10117 closes #10118 closes #10119 closes #10120 closes #10121 closes #10122 closes #10123 closes #10124 closes #10125 closes #10126 closes #10127 closes #10128 closes #10129 closes #10130 closes #10131 closes #10132 closes #10133 closes #10134 closes #10135 closes #10136 closes #10137 closes #10138 closes #10139 closes #10140 closes #10141 closes #10142 closes #10143 closes #10144 closes #10145 closes #10146 closes #10147 closes #10148 closes #10149 closes #10150 closes #10151 closes #10152 closes #10153 closes #10154 closes #10155 closes #10156 closes #10157 closes #10158 closes #10159 closes #10160 closes #10161 closes #10162 closes #10163 closes #10164 closes #10165 closes #10166 closes #10167 closes #10168 closes #10169 closes #10170 closes #10171 closes #10172 closes #10173 closes #10174 closes #10175 closes #10176 closes #10177 closes #10178 closes #10179 closes #10180 closes #10181 closes #10182 closes #10183 closes #10184 closes #10185 closes #10186 closes #10187 closes #10188 closes #10189 closes #10190 closes #10191 closes #10192 closes #10193 closes #10194 closes #10195 closes #10196 closes #10197 closes #10198 closes #10199 closes #10200 closes #10201 closes #10202 closes #10203 closes #10204 closes #10205 closes #10206 closes #10207 closes #10208 closes #10209 closes #10210 closes #10211 closes #10212 closes #10213 closes #10214 closes #10215 closes #10216 closes #10217 closes #10218 closes #10219 closes #10220 closes #10221 closes #10222 closes #10223 closes #10224 closes #10225 closes #10226 closes #10227 closes #10228 closes #10229 closes #10230 closes #10231 closes #10232 closes #10234 closes #10235 closes #10236 closes #10237 closes #10238 closes #10239 closes #10240 closes #10241 closes #10242 closes #10243 closes #10244
2023-03-14 19:10:59 +00:00
email_notification.call(exception, data: options)
end
private
# 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
# 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)
].join('/')
end
2023-03-14 19:16:46 +00:00
def errors
options.dig(:data, :params, 'errors') if options.dig(:data, :params).is_a? Hash
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|
2023-03-13 23:47:54 +00:00
d << log_section
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 << log_section
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
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
```
#{options[:data].delete(:log)}
2023-03-13 23:47:54 +00:00
```
LOG
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
2023-03-13 23:31:16 +00:00
```yaml
#{options[:data].to_yaml}
```
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