# This file is distributed under New Relic's license terms.
# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details.
# frozen_string_literal: true
require 'json'
require 'new_relic/agent/obfuscator'
module NewRelic
module Agent
class JavaScriptInstrumentor
include NewRelic::Coerce
RUM_KEY_LENGTH = 13
def initialize(event_listener)
event_listener.subscribe(:initial_configuration_complete, &method(:log_configuration))
end
def log_configuration
NewRelic::Agent.logger.debug("JS agent loader requested: #{NewRelic::Agent.config[:'browser_monitoring.loader']}",
"JS agent loader debug: #{NewRelic::Agent.config[:'browser_monitoring.debug']}",
"JS agent loader version: #{NewRelic::Agent.config[:'browser_monitoring.loader_version']}")
if !NewRelic::Agent.config[:'rum.enabled']
NewRelic::Agent.logger.debug('Real User Monitoring is disabled for this agent. Edit your configuration to change this.')
end
end
def enabled?
Agent.config[:'rum.enabled'] && !!Agent.config[:beacon]
end
def obfuscator
@obfuscator ||= NewRelic::Agent::Obfuscator.new(NewRelic::Agent.config[:license_key], RUM_KEY_LENGTH)
end
def js_enabled_and_ready?
if !enabled?
::NewRelic::Agent.logger.log_once(:debug, :js_agent_disabled,
'JS agent instrumentation is disabled.')
false
elsif missing_config?(:js_agent_loader)
::NewRelic::Agent.logger.log_once(:debug, :missing_js_agent_loader,
'Missing :js_agent_loader. Skipping browser instrumentation.')
false
elsif missing_config?(:beacon)
::NewRelic::Agent.logger.log_once(:debug, :missing_beacon,
'Beacon configuration not received (yet?). Skipping browser instrumentation.')
false
elsif missing_config?(:browser_key)
::NewRelic::Agent.logger.log_once(:debug, :missing_browser_key,
'Browser key is not set. Skipping browser instrumentation.')
false
else
true
end
rescue => e
::NewRelic::Agent.logger.debug("Failure during 'js_enabled_and_ready?'", e)
false
end
def insert_js?(state)
if !state.current_transaction
::NewRelic::Agent.logger.debug('Not in transaction. Skipping browser instrumentation.')
false
elsif !state.is_execution_traced?
::NewRelic::Agent.logger.debug('Execution is not traced. Skipping browser instrumentation.')
false
elsif state.current_transaction.ignore_enduser?
::NewRelic::Agent.logger.debug('Ignore end user for this transaction is set. Skipping browser instrumentation.')
false
else
true
end
rescue => e
::NewRelic::Agent.logger.debug('Failure during insert_js', e)
false
end
def missing_config?(key)
value = NewRelic::Agent.config[key]
value.nil? || value.empty?
end
def browser_timing_header(nonce = nil) # THREAD_LOCAL_ACCESS
return '' unless js_enabled_and_ready? # fast exit
state = NewRelic::Agent::Tracer.state
return '' unless insert_js?(state) # slower exit
bt_config = browser_timing_config(state, nonce)
return '' if bt_config.empty?
bt_config + browser_timing_loader(nonce)
rescue => e
::NewRelic::Agent.logger.debug('Failure during RUM browser_timing_header construction', e)
''
end
def browser_timing_loader(nonce = nil)
html_safe_if_needed("\n")
end
def browser_timing_config(state, nonce = nil)
txn = state.current_transaction
return '' if txn.nil?
txn.freeze_name_and_execute_if_not_ignored do
data = data_for_js_agent(txn)
json = ::JSON.dump(data)
return html_safe_if_needed("\n")
end
''
end
def create_nonce(nonce = nil)
return '' unless nonce
" nonce=\"#{nonce.to_s}\""
end
BEACON_KEY = 'beacon'.freeze
ERROR_BEACON_KEY = 'errorBeacon'.freeze
LICENSE_KEY_KEY = 'licenseKey'.freeze
APPLICATIONID_KEY = 'applicationID'.freeze
TRANSACTION_NAME_KEY = 'transactionName'.freeze
QUEUE_TIME_KEY = 'queueTime'.freeze
APPLICATION_TIME_KEY = 'applicationTime'.freeze
AGENT_KEY = 'agent'.freeze
SSL_FOR_HTTP_KEY = 'sslForHttp'.freeze
ATTS_KEY = 'atts'.freeze
ATTS_USER_SUBKEY = 'u'.freeze
ATTS_AGENT_SUBKEY = 'a'.freeze
# NOTE: Internal prototyping may override this, so leave name stable!
def data_for_js_agent(transaction)
queue_time_in_seconds = [transaction.queue_time, 0.0].max
start_time_in_seconds = [transaction.start_time, 0.0].max
app_time_in_seconds = Process.clock_gettime(Process::CLOCK_REALTIME) - start_time_in_seconds
queue_time_in_millis = (queue_time_in_seconds * 1000.0).round
app_time_in_millis = (app_time_in_seconds * 1000.0).round
transaction_name = transaction.best_name || ::NewRelic::Agent::UNKNOWN_METRIC
data = {
BEACON_KEY => NewRelic::Agent.config[:beacon],
ERROR_BEACON_KEY => NewRelic::Agent.config[:error_beacon],
LICENSE_KEY_KEY => NewRelic::Agent.config[:browser_key],
APPLICATIONID_KEY => NewRelic::Agent.config[:application_id],
TRANSACTION_NAME_KEY => obfuscator.obfuscate(transaction_name),
QUEUE_TIME_KEY => queue_time_in_millis,
APPLICATION_TIME_KEY => app_time_in_millis,
AGENT_KEY => NewRelic::Agent.config[:js_agent_file]
}
add_ssl_for_http(data)
add_attributes(data, transaction)
data
end
def add_ssl_for_http(data)
ssl_for_http = NewRelic::Agent.config[:'browser_monitoring.ssl_for_http']
data[SSL_FOR_HTTP_KEY] = ssl_for_http if ssl_for_http
end
def add_attributes(data, txn)
return unless txn
atts = {}
append_custom_attributes!(txn, atts)
append_agent_attributes!(txn, atts)
unless atts.empty?
json = ::JSON.dump(atts)
data[ATTS_KEY] = obfuscator.obfuscate(json)
end
end
def append_custom_attributes!(txn, atts)
custom_attributes = txn.attributes.custom_attributes_for(NewRelic::Agent::AttributeFilter::DST_BROWSER_MONITORING)
if custom_attributes.empty?
NewRelic::Agent.logger.debug("#{self.class}: No custom attributes found to append.")
return
end
NewRelic::Agent.logger.debug("#{self.class}: Appending the following custom attribute keys for browser " \
"monitoring: #{custom_attributes.keys}")
atts[ATTS_USER_SUBKEY] = custom_attributes
end
def append_agent_attributes!(txn, atts)
agent_attributes = txn.attributes.agent_attributes_for(NewRelic::Agent::AttributeFilter::DST_BROWSER_MONITORING)
unless agent_attributes.empty?
atts[ATTS_AGENT_SUBKEY] = agent_attributes
end
end
def html_safe_if_needed(string)
string = string.html_safe if string.respond_to?(:html_safe)
string
end
end
end
end