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
nonce(env)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 61 def nonce(env) return unless NewRelic::Agent.config[:'browser_monitoring.content_security_policy_nonce'] return unless NewRelic::Agent.config[:framework] == :rails_notifications return unless defined?(ActionDispatch::ContentSecurityPolicy::Request) env[ActionDispatch::ContentSecurityPolicy::Request::NONCE] end
should_instrument?(env, status, headers)
click to toggle source
# File lib/new_relic/rack/browser_monitoring.rb, line 69 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(nonce(env)) 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 118 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 84 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 163 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 167 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 176 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 181 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 140 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 148 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 171 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 155 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 113 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 94 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 134 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 122 def streaming?(env, headers) # Up until version 8.0, Rails would set 'Transfer-Encoding' to 'chunked' # to trigger the desired HTTP/1.1 based streaming functionality in Rack. # With version v8.0+, Rails assumes that the web server will be using # Rack v3+ or an equally modern alternative and simply leaves the # streaming behavior up to them. return true if headers && headers['Transfer-Encoding'] == 'chunked' defined?(ActionController::Live) && env['action_controller.instance'].class.included_modules.include?(ActionController::Live) end