reportar el issue una sola vez y luego actualizarlo
This commit is contained in:
parent
fc7c2f31dd
commit
5468a11885
2 changed files with 130 additions and 22 deletions
|
@ -1,42 +1,98 @@
|
|||
# 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
|
||||
|
||||
attr_reader :exception, :options
|
||||
# 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
|
||||
|
||||
Rails.logger.info 'Enviando reporte a Gitlab'
|
||||
# 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
|
||||
|
||||
i = client.new_issue confidential: true, title: title, description: description
|
||||
{
|
||||
count: 1,
|
||||
issue: issue['iid'],
|
||||
user_agents: [user_agent].compact,
|
||||
params: [request&.filtered_parameters].compact,
|
||||
urls: [url].compact
|
||||
}
|
||||
end
|
||||
|
||||
Rails.logger.info "Enviado reporte a Gitlab: #{i['iid']}"
|
||||
# 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|
|
||||
|
@ -48,24 +104,48 @@ class GitlabNotifierJob < ApplicationJob
|
|||
end
|
||||
end
|
||||
|
||||
# @return [String,Nil]
|
||||
# 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
|
||||
|
||||
|
@ -74,7 +154,7 @@ class GitlabNotifierJob < ApplicationJob
|
|||
# Request
|
||||
|
||||
```
|
||||
#{request.request_method} #{request.url}#{' '}
|
||||
#{request.request_method} #{url}
|
||||
|
||||
#{pp request.filtered_parameters}
|
||||
```
|
||||
|
@ -82,14 +162,19 @@ class GitlabNotifierJob < ApplicationJob
|
|||
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'] || 'NoError'}: #{error['message']}
|
||||
|
||||
## #{error['type']}: #{error['message']}
|
||||
|
||||
```
|
||||
#{Terminal::Table.new headings: error['backtrace'].first.keys, rows: error['backtrace'].map(&:values)}
|
||||
|
@ -99,18 +184,24 @@ class GitlabNotifierJob < ApplicationJob
|
|||
end&.join
|
||||
end
|
||||
|
||||
# Muestra información de la visita que generó el error en JS
|
||||
#
|
||||
# @return [String]
|
||||
def javascript_footer
|
||||
return '' unless javascript?
|
||||
|
||||
<<~JAVASCRIPT
|
||||
|
||||
#{options.dig(:data, :params, 'context', 'userAgent')}
|
||||
#{user_agent}
|
||||
|
||||
<#{options.dig(:data, :params, 'context', 'url')}>
|
||||
<#{url}>
|
||||
|
||||
JAVASCRIPT
|
||||
end
|
||||
|
||||
# Muestra el historial del error en Ruby
|
||||
#
|
||||
# @return [String]
|
||||
def backtrace_section
|
||||
return '' if javascript?
|
||||
return '' unless backtrace
|
||||
|
@ -126,6 +217,9 @@ class GitlabNotifierJob < ApplicationJob
|
|||
BACKTRACE
|
||||
end
|
||||
|
||||
# Muestra datos extra de la visita
|
||||
#
|
||||
# @return [String]
|
||||
def data_section
|
||||
return '' unless options[:data]
|
||||
|
||||
|
@ -139,4 +233,20 @@ class GitlabNotifierJob < ApplicationJob
|
|||
|
||||
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
|
||||
|
|
|
@ -7,6 +7,9 @@ class GitlabApiClient
|
|||
|
||||
# TODO: Hacer configurable por sitio
|
||||
base_uri ENV.fetch('GITLAB_URI', 'https://0xacab.org')
|
||||
# No seguir redirecciones. Si nos olvidamos https:// en la dirección,
|
||||
# las redirecciones nos pueden llevar a cualquier lado y obtener
|
||||
# resultados diferentes.
|
||||
no_follow true
|
||||
|
||||
# Trae todos los proyectos. Como estamos usando un Project Token,
|
||||
|
@ -14,7 +17,7 @@ class GitlabApiClient
|
|||
#
|
||||
# @return [HTTParty::Response]
|
||||
def projects
|
||||
self.class.get('/api/v4/projects', { query: params(membership: true) })
|
||||
self.class.get('/api/v4/projects', { query: { membership: true }, headers: headers })
|
||||
end
|
||||
|
||||
# Obtiene el identificador del proyecto
|
||||
|
@ -29,15 +32,15 @@ class GitlabApiClient
|
|||
# @see https://docs.gitlab.com/ee/api/issues.html#new-issue
|
||||
# @return [HTTParty::Response]
|
||||
def new_issue(**args)
|
||||
self.class.post("/api/v4/projects/#{project_id}/issues", { query: params(**args) })
|
||||
self.class.post("/api/v4/projects/#{project_id}/issues", { body: args, headers: headers })
|
||||
end
|
||||
|
||||
# Modifica un issue
|
||||
#
|
||||
# @see https://docs.gitlab.com/ee/api/issues.html#edit-issue
|
||||
# @return [HTTParty::Response]
|
||||
def edit_issue(**args)
|
||||
self.class.put("/api/v4/projects/#{project_id}/issues", { query: params(**args) })
|
||||
def edit_issue(iid:, **args)
|
||||
self.class.put("/api/v4/projects/#{project_id}/issues/#{iid}", { body: args, headers: headers })
|
||||
end
|
||||
|
||||
# Crea un comentario
|
||||
|
@ -45,17 +48,12 @@ class GitlabApiClient
|
|||
# @see https://docs.gitlab.com/ee/api/notes.html#create-new-issue-note
|
||||
# @return [HTTParty::Response]
|
||||
def new_note(iid:, **args)
|
||||
self.class.post("/api/v4/projects/#{project_id}/issues/#{iid}/notes", { query: params(**args) })
|
||||
self.class.post("/api/v4/projects/#{project_id}/issues/#{iid}/notes", { body: args, headers: headers })
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def params(**args)
|
||||
default_params.merge(args)
|
||||
end
|
||||
|
||||
# TODO: Que cada sitio tenga su propio token y uri
|
||||
def default_params
|
||||
{ private_token: ENV['GITLAB_TOKEN'] }
|
||||
def headers(extra = {})
|
||||
{ 'Authorization' => "Bearer #{ENV['GITLAB_TOKEN']}" }.merge(extra)
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue