# frozen_string_literal: true # Procesa los errores de JavaScript class BacktraceJob < ApplicationJob class BacktraceException < RuntimeError; end EMPTY_SOURCEMAP = { 'mappings' => '' }.freeze attr_reader :params def perform(site:, params:) @params = params unless sources.empty? params['errors'].each do |error| error['backtrace'].each do |backtrace| offset = SourceMap::Offset.new(backtrace['line'], backtrace['column']) mapping = sourcemap.bsearch(offset) next unless mapping data = data(backtrace['file']) backtrace['file'] = mapping.source backtrace['line'] = mapping.original.line backtrace['column'] = mapping.original.column # Encuentra el código fuente del error source = data.dig('sourcesContent', data['sources']&.index(backtrace['file']))&.split("\n") # XXX: Elimina la sangría aunque cambie las columnas porque # eso lo vamos a ver en el archivo fuente directo. backtrace['function'] = source[backtrace['line'] - 1].strip if source.present? end end end begin raise BacktraceException, "#{origin}: #{message}" rescue BacktraceException => e ExceptionNotifier.notify_exception(e, data: { site: site.name, params: params, javascript_backtrace: true }) end end private # Obtiene todos los archivos del backtrace solo si los puede descargar # desde fuentes seguras. # # XXX: Idealmente no trabajamos con recursos externos, pero en este # momento no podemos saber todas las URLs de un sitio y quizás nunca # lo sabremos :B def sources @sources ||= params['errors'].map do |x| x['backtrace'] end.flatten.map do |x| x['file'].split('@').last end.uniq.select do |x| %r{\Ahttps://} =~ x end end # Descarga y devuelve los datos de un archivo # # @param [String] La URL del map # @return [Hash] def data(map) return EMPTY_SOURCEMAP unless map.start_with? 'https://' map += '.map' unless map.end_with? '.map' @data ||= {} # TODO: Soportar ETags para la descarga, probablemente pasar a # Faraday con caché para esto. @data[map] ||= FastJsonparser.parse(Rails.cache.fetch(map, expires_in: 12.hours) do Down.open(map).read end, symbolize_keys: false) rescue Down::Error, FastJsonparser::Error EMPTY_SOURCEMAP end # Asume que todos los sourcemaps comparten la misma URL, lo # correcto sería buscarlo en sourceMappingURL al final de cada # archivo. # # Descarga los archivos y obtiene el backtrace original. # # @return [SourceMap::Map] def sourcemap @sourcemap ||= sources.map do |map| SourceMap::Map.from_hash data(map) end.reduce(&:+) end # @return [String] def origin URI.parse(params.dig('context', 'url')).host rescue URI::Error params.dig('context', 'url') end # @return [String,Nil] def message @message ||= params['errors']&.first&.dig('message') end end