class NewRelic::Rack::BrowserMonitoring
This middleware is used by the agent for the Real user monitoring (RUM) feature, and will usually be automatically injected in the middleware chain. If automatic injection is not working, you may manually use it in your middleware chain instead.
@api public
Constants
- ALREADY_INSTRUMENTED_KEY
- ATTACHMENT
- BODY_START
- CHARSET_RE
- CONTENT_DISPOSITION
- CONTENT_LENGTH
- CONTENT_TYPE
- GT
- HEAD_START
- SCAN_LIMIT
The maximum number of bytes of the response body that we will examine in order to look for a RUM insertion point.
- TEXT_HTML
- X_UA_COMPATIBLE_RE
Public Instance Methods
should_instrument?(env, status, headers)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 61 def should_instrument?(env, status, headers) NewRelic::Agent.config[:'browser_monitoring.auto_instrument'] && status == 200 && !env[ALREADY_INSTRUMENTED_KEY] && html?(headers) && !attachment?(headers) && !streaming?(env, headers) rescue StandardError => e NewRelic::Agent.logger.error('RUM instrumentation applicability check failed on exception:' \ "#{e.class} - #{e.message}") false end
traced_call(env)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 37 def traced_call(env) result = @app.call(env) (status, headers, response) = result js_to_inject = NewRelic::Agent.browser_timing_header if (js_to_inject != NewRelic::EMPTY_STR) && should_instrument?(env, status, headers) response_string = autoinstrument_source(response, js_to_inject) if headers.key?(CONTENT_LENGTH) content_length = response_string ? response_string.bytesize : 0 headers[CONTENT_LENGTH] = content_length.to_s end env[ALREADY_INSTRUMENTED_KEY] = true if response_string response = ::Rack::Response.new(response_string, status, headers) response.finish else result end else result end end
Private Instance Methods
attachment?(headers)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 110 def attachment?(headers) headers[CONTENT_DISPOSITION]&.match?(ATTACHMENT) end
autoinstrument_source(response, js_to_inject)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 76 def autoinstrument_source(response, js_to_inject) source = gather_source(response) close_old_response(response) return unless source modify_source(source, js_to_inject) rescue => e NewRelic::Agent.logger.debug("Skipping RUM instrumentation on exception: #{e.class} - #{e.message}") end
close_old_response(response)
click to toggle source
Per “The Response > The Body” section of Rack
spec, we should close if our response is able. github.com/rack/rack/blob/main/SPEC.rdoc
# File lib/new_relic/rack/browser_monitoring.rb, line 151 def close_old_response(response) response.close if response.respond_to?(:close) end
find_body_start(beginning_of_source)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 155 def find_body_start(beginning_of_source) beginning_of_source.index(BODY_START) end
find_charset_position(beginning_of_source)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 164 def find_charset_position(beginning_of_source) match = CHARSET_RE.match(beginning_of_source) match&.end(0) end
find_end_of_head_open(beginning_of_source)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 169 def find_end_of_head_open(beginning_of_source) head_open = beginning_of_source.index(HEAD_START) beginning_of_source.index(GT, head_open) + 1 if head_open end
find_insertion_index(tag_positions, source_beginning, body_start)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 128 def find_insertion_index(tag_positions, source_beginning, body_start) if !tag_positions.empty? tag_positions.max else find_end_of_head_open(source_beginning) || body_start end end
find_meta_tag_positions(source_beginning)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 136 def find_meta_tag_positions(source_beginning) [ find_x_ua_compatible_position(source_beginning), find_charset_position(source_beginning) ].compact end
find_x_ua_compatible_position(beginning_of_source)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 159 def find_x_ua_compatible_position(beginning_of_source) match = X_UA_COMPATIBLE_RE.match(beginning_of_source) match&.end(0) end
gather_source(response)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 143 def gather_source(response) source = nil response.each { |fragment| source ? (source << fragment.to_s) : (source = fragment.to_s) } source end
html?(headers)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 105 def html?(headers) # needs else branch coverage headers[CONTENT_TYPE]&.match?(TEXT_HTML) end
modify_source(source, js_to_inject)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 86 def modify_source(source, js_to_inject) # Only scan the first 50k (roughly) then give up. beginning_of_source = source[0..SCAN_LIMIT] meta_tag_positions = find_meta_tag_positions(beginning_of_source) if body_start = find_body_start(beginning_of_source) if insertion_index = find_insertion_index(meta_tag_positions, beginning_of_source, body_start) source = source_injection(source, insertion_index, js_to_inject) else NewRelic::Agent.logger.debug('Skipping RUM instrumentation. Could not properly determine location to ' \ 'inject script.') end else msg = "Skipping RUM instrumentation. Unable to find <body> tag in first #{SCAN_LIMIT} bytes of document." NewRelic::Agent.logger.log_once(:warn, :rum_insertion_failure, msg) NewRelic::Agent.logger.debug(msg) end source end
source_injection(source, insertion_index, js_to_inject)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 122 def source_injection(source, insertion_index, js_to_inject) source[0...insertion_index] << js_to_inject << source[insertion_index..-1] end
streaming?(env, headers)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 114 def streaming?(env, headers) # Chunked transfer encoding is a streaming data transfer mechanism available only in HTTP/1.1 return true if headers && headers['Transfer-Encoding'] == 'chunked' defined?(ActionController::Live) && env['action_controller.instance'].class.included_modules.include?(ActionController::Live) end