# This could definitely use some refactoring love, but I have no time ATM # The code is pretty self-contained; we're just listening to notifications # and taking action. module Graphiti class Debugger class << self attr_accessor :enabled, :chunks, :debug_models, :preserve, :pry end self.chunks = [] class << self def on_data(name, start, stop, id, payload) return [] unless enabled took = ((stop - start) * 1000.0).round(2) params = scrub_params(payload[:params]) if payload[:exception] on_data_exception(payload, params) elsif payload[:sideload] if payload[:results] on_sideload_data(payload, params, took) end else on_primary_data(payload, params, took) end end private def on_data_exception(payload, params) unless payload[:exception_object].instance_variable_get(:@__graphiti_debug) add_chunk do |logs, json| logs << ["\n=== Graphiti Debug ERROR", :red, true] if (sideload = payload[:sideload]) logs << ["#{sideload.parent_resource.class}: Sideload \"#{sideload.name}\"", :red, true] json[:parent_resource] = sideload.parent_resource.class.name json[:sideload] = sideload.name end if params query = "#{payload[:resource].class.name}.#{payload[:action]}(#{JSON.pretty_generate(params)}).data" logs << [query, :cyan, true] logs << ["The error occurred when running the above query. Copy/paste it into a rake task or Rails console session to reproduce. Keep in mind you may have to set context.", :yellow, true] else query = "This sideload is done manually via .scope - no debug information available." logs << [query, :cyan, true] end json[:query] = query logs << "\n\n" payload[:exception_object]&.instance_variable_set(:@__graphiti_debug, json) end end end private def results(raw_results) raw_results.map { |r| "[#{r.class.name}, #{r.id.inspect}]" }.join(", ") end private def on_sideload_data(payload, params, took) sideload = payload[:sideload] results = results(payload[:results]) add_chunk(payload[:resource], payload[:parent]) do |logs, json| logs << [" \\_ #{sideload.name}", :yellow, true] json[:name] = sideload.name query = if sideload.class.scope_proc "#{payload[:resource].class.name}: Manual sideload via .scope" else "#{payload[:resource].class.name}.#{payload[:action]}(#{params.inspect})" end logs << [" #{query}", :cyan, true] json[:query] = query logs << [" Returned Models: #{results}"] if debug_models logs << [" Took: #{took}ms", :magenta, true] json[:took] = took end end private def on_primary_data(payload, params, took) results = results(payload[:results]) add_chunk(payload[:resource], payload[:parent]) do |logs, json| logs << [""] logs << ["=== Graphiti Debug", :green, true] title = "Top Level Data Retrieval (+ sideloads):" logs << [title, :green, true] json[:title] = title query = "#{payload[:resource].class.name}.#{payload[:action]}(#{params.inspect})" logs << [query, :cyan, true] json[:query] = query logs << ["Returned Models: #{results}"] if debug_models logs << ["Took: #{took}ms", :magenta, true] json[:took] = took end end def on_render(name, start, stop, id, payload) return [] unless enabled add_chunk do |logs| took = ((stop - start) * 1000.0).round(2) logs << [""] logs << ["=== Graphiti Debug", :green, true] if payload[:proxy]&.cached? && Graphiti.config.cache_rendering? logs << ["Rendering (cached):", :green, true] Graphiti::Util::CacheDebug.new(payload[:proxy]).analyze do |cache_debug| logs << ["Cache key for #{cache_debug.name}", :blue, true] logs << if cache_debug.volatile? [" \\_ volatile | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :red, true] else [" \\_ stable | Request count: #{cache_debug.request_count} | Hit count: #{cache_debug.hit_count}", :blue, true] end if cache_debug.changed_key? logs << [" [x] cache key changed #{cache_debug.last_version[:etag]} -> #{cache_debug.current_version[:etag]}", :red] logs << [" removed: #{cache_debug.removed_segments}", :red] logs << [" added: #{cache_debug.added_segments}", :red] elsif cache_debug.new_key? logs << [" [+] cache key added #{cache_debug.current_version[:etag]}", :red, true] else logs << [" [✓] #{cache_debug.current_version[:etag]}", :green, true] end end else logs << ["Rendering:", :green, true] end logs << ["Took: #{took}ms", :magenta, true] end end def debug if enabled begin self.chunks = [] yield ensure flush self.chunks = [] unless preserve end else yield end end def to_a debugs = [] graph_statements.each do |chunk| debugs << chunk_to_hash(chunk) end debugs end def flush Graphiti.broadcast(:flush_debug, {}) do |payload| payload[:chunks] = chunks graph_statements.each do |chunk| flush_chunk(chunk) end end end private def scrub_params(params) params ||= {} params = params.to_unsafe_h if params.respond_to?(:to_unsafe_h) params.reject! { |k, v| [:controller, :action, :format, :debug].include?(k.to_sym) } params.deep_symbolize_keys end def add_chunk(resource = nil, parent = nil) logs, json = [], {} yield(logs, json) chunks << { resource: resource, parent: parent, logs: logs, json: json, children: [] } end def graph_statements @chunks.each do |chunk| if (parent = chunk[:parent]) relevant = chunks.find { |c| c[:resource] == parent } relevant[:children].unshift(chunk) if relevant end end @chunks.reject! { |c| !!c[:parent] } @chunks end def chunk_to_hash(chunk) hash = {} hash.merge!(chunk[:json]) sideloads = [] chunk[:children].each do |child_chunk| sideloads << chunk_to_hash(child_chunk) end hash[:sideloads] = sideloads hash end def flush_chunk(chunk, depth = 0) chunk[:logs].each do |args| indent = " " * depth args[0] = "#{indent}#{args[0]}" Graphiti.log(*args) end chunk[:children].each do |child_chunk| flush_chunk(child_chunk, depth + 1) end end end ActiveSupport::Notifications.subscribe \ "resolve.graphiti", method(:on_data) ActiveSupport::Notifications.subscribe \ "render.graphiti", method(:on_render) end end