# encoding: utf-8 # This file is distributed under New Relic's license terms. # See https://github.com/newrelic/rpm/blob/master/LICENSE for complete details. require 'base64' require 'new_relic/agent/beacon_configuration' require 'new_relic/agent/transaction_timings' module NewRelic module Agent # This module contains support for Real User Monitoring - the # javascript generation and configuration # # @api public module BrowserMonitoring include NewRelic::Coerce class DummyTransaction attr_reader :custom_parameters attr_accessor :start_time def initialize @custom_parameters = {} end def queue_time 0.0 end def timings NewRelic::Agent::TransactionTimings.new(0.0, NewRelic::Agent::TransactionState.get) end def name ::NewRelic::Agent::UNKNOWN_METRIC end end @@dummy_txn = DummyTransaction.new # This method returns a string suitable for inclusion in a page # - known as 'manual instrumentation' for Real User # Monitoring. Can return either a script tag with associated # javascript, or in the case of disabled Real User Monitoring, # an empty string # # This is the header string - it should be placed as high in the # page as is reasonably possible - that is, before any style or # javascript inclusions, but after any header-related meta tags # # @api public # def browser_timing_header insert_js? ? header_js_string : "" end # This method returns a string suitable for inclusion in a page # - known as 'manual instrumentation' for Real User # Monitoring. Can return either a script tag with associated # javascript, or in the case of disabled Real User Monitoring, # an empty string # # This is the footer string - it should be placed as low in the # page as is reasonably possible. # # @api public # def browser_timing_footer if insert_js? NewRelic::Agent::Transaction.freeze_name generate_footer_js(NewRelic::Agent.instance.beacon_configuration) else "" end end module_function def obfuscate(config, text) obfuscated = "" if defined?(::Encoding::ASCII_8BIT) obfuscated.force_encoding(::Encoding::ASCII_8BIT) end key_bytes = config.license_bytes index = 0 text.each_byte{|byte| obfuscated.concat((byte ^ key_bytes[index % 13].to_i)) index+=1 } [obfuscated].pack("m0").gsub("\n", '') end def browser_monitoring_transaction_name current_timings.transaction_name || ::NewRelic::Agent::UNKNOWN_METRIC end def current_transaction NewRelic::Agent::TransactionState.get.transaction || @@dummy_txn end def current_timings NewRelic::Agent::TransactionState.get.timings end private # Check whether RUM header and footer should be generated. Log the # reason if they shouldn't. def insert_js? if NewRelic::Agent.instance.beacon_configuration.nil? ::NewRelic::Agent.logger.debug "Beacon configuration is nil. Skipping browser instrumentation." false elsif ! NewRelic::Agent.instance.beacon_configuration.enabled? ::NewRelic::Agent.logger.debug "Beacon configuration is disabled. Skipping browser instrumentation." ::NewRelic::Agent.logger.debug NewRelic::Agent.instance.beacon_configuration.inspect false elsif Agent.config[:browser_key].nil? || Agent.config[:browser_key].empty? ::NewRelic::Agent.logger.debug "Browser key is not set. Skipping browser instrumentation." false elsif ! NewRelic::Agent.is_transaction_traced? ::NewRelic::Agent.logger.debug "Transaction is not traced. Skipping browser instrumentation." false elsif ! NewRelic::Agent.is_execution_traced? ::NewRelic::Agent.logger.debug "Execution is not traced. Skipping browser instrumentation." false elsif NewRelic::Agent::TransactionState.get.request_ignore_enduser ::NewRelic::Agent.logger.debug "Ignore end user for this transaction is set. Skipping browser instrumentation." false else true end end def generate_footer_js(config) if current_transaction.start_time footer_js_string(config) else '' end end def tt_guid state = NewRelic::Agent::TransactionState.get return state.request_guid if include_guid?(state) "" end def include_guid?(state) state.request_token && state.timings.app_time_in_seconds > state.transaction.apdex_t end def tt_token return NewRelic::Agent::TransactionState.get.request_token end def js_agent_loader Agent.config[:js_agent_loader].to_s end def has_loader? !js_agent_loader.empty? end # NOTE: Internal prototyping often overrides this, so leave name stable! def header_js_string return "" unless has_loader? html_safe_if_needed("\n") end # NOTE: Internal prototyping often overrides this, so leave name stable! def footer_js_string(config) data = js_data(config) html_safe_if_needed("\n") 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 TT_GUID_KEY = "ttGuid".freeze AGENT_TOKEN_KEY = "agentToken".freeze AGENT_KEY = "agent".freeze EXTRA_KEY = "extra".freeze # NOTE: Internal prototyping may override this, so leave name stable! def js_data(config) { 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 => obfuscate(config, browser_monitoring_transaction_name), QUEUE_TIME_KEY => current_timings.queue_time_in_millis, APPLICATION_TIME_KEY => current_timings.app_time_in_millis, TT_GUID_KEY => tt_guid, AGENT_TOKEN_KEY => tt_token, AGENT_KEY => NewRelic::Agent.config[:js_agent_file], EXTRA_KEY => obfuscate(config, format_extra_data(extra_data)) } end ANALYTICS_ENABLED = :'analytics_events.enabled' ANALYTICS_TXN_ENABLED = :'analytics_events.transactions.enabled' ANALYTICS_TXN_IN_PAGE = :'capture_attributes.page_view_events' # NOTE: Internal prototyping may override this, so leave name stable! def extra_data return {} unless include_custom_parameters_in_extra? current_transaction.custom_parameters.dup end def include_custom_parameters_in_extra? NewRelic::Agent.config[ANALYTICS_ENABLED] && NewRelic::Agent.config[ANALYTICS_TXN_ENABLED] && NewRelic::Agent.config[ANALYTICS_TXN_IN_PAGE] end # Format the props using semicolon separated pairs separated by '=': # product=pro;user=bill@microsoft.com def format_extra_data(extra_props) extra_props = event_params(extra_props) extra_props.map do |k, v| key = escape_special_characters(k) value = format_value(v) "#{key}=#{value}" end.join(';') end def escape_special_characters(string) string.to_s.tr("\";=", "':-" ) end def format_value(v) # flag numeric values with `#' prefix if v.is_a? Numeric value = "##{v}" else value = escape_special_characters(v) end end def html_safe_if_needed(string) if string.respond_to?(:html_safe) string.html_safe else string end end end end end