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