api.js

/*
 * Copyright 2020 New Relic Corporation. All rights reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

'use strict'

const util = require('util')
const logger = require('./lib/logger').child({ component: 'api' })
const recordWeb = require('./lib/metrics/recorders/http')
const recordBackground = require('./lib/metrics/recorders/other')
const customRecorder = require('./lib/metrics/recorders/custom')
const hashes = require('./lib/util/hashes')
const properties = require('./lib/util/properties')
const stringify = require('json-stringify-safe')
const shimmer = require('./lib/shimmer')
const isValidType = require('./lib/util/attribute-types')
const TransactionShim = require('./lib/shim/transaction-shim')
const TransactionHandle = require('./lib/transaction/handle')
const AwsLambda = require('./lib/serverless/aws-lambda')
const applicationLogging = require('./lib/util/application-logging')
const {
  assignCLMSymbol,
  addCLMAttributes: maybeAddCLMAttributes
} = require('./lib/util/code-level-metrics')
const LlmFeedbackMessage = require('./lib/llm-events/feedback-message')

const ATTR_DEST = require('./lib/config/attribute-filter').DESTINATIONS
const MODULE_TYPE = require('./lib/instrumentation-descriptor').TYPES
const NAMES = require('./lib/metrics/names')
const obfuscate = require('./lib/util/sql/obfuscate')
const { DESTINATIONS } = require('./lib/config/attribute-filter')
const parse = require('module-details-from-path')
const { isSimpleObject } = require('./lib/util/objects')
const { AsyncLocalStorage } = require('async_hooks')

/*
 *
 * CONSTANTS
 *
 */
const RUM_STUB = 'window.NREUM||(NREUM={});NREUM.info = %s; %s'
const RUM_STUB_SHELL = `<script type='text/javascript'>${RUM_STUB}</script>`
const RUM_STUB_SHELL_WITH_NONCE_PARAM = `<script type='text/javascript' %s>${RUM_STUB}</script>`

// these messages are used in the _gracefail() method below in getBrowserTimingHeader
const RUM_ISSUES = [
  'NREUM: no browser monitoring headers generated; disabled',
  'NREUM: transaction ignored while generating browser monitoring headers',
  'NREUM: config.browser_monitoring missing, something is probably wrong',
  'NREUM: browser_monitoring headers need a transaction name',
  'NREUM: browser_monitoring requires valid application_id',
  'NREUM: browser_monitoring requires valid browser_key',
  'NREUM: browser_monitoring requires js_agent_loader script',
  'NREUM: browser_monitoring disabled by browser_monitoring.loader config'
]

// Can't overwrite internal parameters or all heck will break loose.
const CUSTOM_DENYLIST = new Set(['nr_flatten_leading'])

const CUSTOM_EVENT_TYPE_REGEX = /^[a-zA-Z0-9:_ ]+$/

/**
 * The exported New Relic API. This contains all of the functions meant to be
 * used by New Relic customers.
 *
 * You do not need to directly instantiate this class, as an instance of this is
 * the return from `require('newrelic')`.
 *
 * @param {object} agent Instantiation of lib/agent.js
 * @class
 */
function API(agent) {
  this.agent = agent
  this.shim = new TransactionShim(agent, 'NewRelicAPI')
  this.awsLambda = new AwsLambda(agent)
}

/**
 * Give the current transaction a custom name. Overrides any New Relic naming
 * rules set in configuration or from New Relic's servers.
 *
 * IMPORTANT: this function must be called when a transaction is active. New
 * Relic transactions are tied to web requests, so this method may be called
 * from within HTTP or HTTPS listener functions, Express routes, or other
 * contexts where a web request or response object are in scope.
 *
 * @param {string} name The name you want to give the web request in the New
 *                      Relic UI. Will be prefixed with 'Custom/' when sent.
 * @returns {void}
 */
API.prototype.setTransactionName = function setTransactionName(name) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/setTransactionName'
  )
  metric.incrementCallCount()

  const transaction = this.agent.tracer.getTransaction()
  if (!transaction) {
    return logger.warn("No transaction found when setting name to '%s'.", name)
  }

  if (!name) {
    if (transaction && transaction.url) {
      logger.error('Must include name in setTransactionName call for URL %s.', transaction.url)
    } else {
      logger.error('Must include name in setTransactionName call.')
    }

    return
  }

  logger.trace('Setting transaction %s name to %s', transaction.id, name)
  transaction.forceName = NAMES.CUSTOM + '/' + name
}

/**
 * This method returns an object with the following methods:
 * - end: end the transaction that was active when `API#getTransaction`
 *   was called.
 *
 * - ignore: set the transaction that was active when
 *   `API#getTransaction` was called to be ignored.
 *
 * @returns {TransactionHandle} The transaction object with the `end` and
 *  `ignore` methods on it.
 */
API.prototype.getTransaction = function getTransaction() {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/getTransaction')
  metric.incrementCallCount()

  const transaction = this.agent.tracer.getTransaction()
  if (!transaction) {
    logger.debug('No transaction found when calling API#getTransaction')
    return new TransactionHandle.Stub()
  }

  transaction.handledExternally = true

  return new TransactionHandle(transaction, this.agent.metrics)
}

/**
 * This method returns an object with the following keys/data:
 * - `trace.id`: The current trace ID
 * - `span.id`: The current span ID
 * - `entity.name`: The application name specified in the connect request as
 *   app_name. If multiple application names are specified this will only be
 *   the first name
 * - `entity.type`: The string "SERVICE"
 * - `entity.guid`: The entity ID returned in the connect reply as entity_guid
 * - `hostname`: The hostname as specified in the connect request as
 *   utilization.full_hostname. If utilization.full_hostname is null or empty,
 *   this will be the hostname specified in the connect request as host.
 *
 * @param {boolean} omitSupportability Whether or not to log the supportability metric, true means skip
 * @returns {object} The LinkingMetadata object with the data above
 */
API.prototype.getLinkingMetadata = function getLinkingMetadata(omitSupportability) {
  if (omitSupportability !== true) {
    const metric = this.agent.metrics.getOrCreateMetric(
      NAMES.SUPPORTABILITY.API + '/getLinkingMetadata'
    )
    metric.incrementCallCount()
  }

  return this.agent.getLinkingMetadata()
}

/**
 * Specify the `Dispatcher` and `Dispatcher Version` environment values.
 * A dispatcher is typically the service responsible for brokering
 * the request with the process responsible for responding to the
 * request.  For example Node's `http` module would be the dispatcher
 * for incoming HTTP requests.
 *
 * @param {string} name The string you would like to report to New Relic
 *                      as the dispatcher.
 * @param {string} [version] The dispatcher version you would like to
 *                           report to New Relic
 */
API.prototype.setDispatcher = function setDispatcher(name, version) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/setDispatcher')
  metric.incrementCallCount()

  if (!name || typeof name !== 'string') {
    logger.error('setDispatcher must be called with a name, and name must be a string.')
    return
  }

  // No objects allowed.
  if (version && typeof version !== 'object') {
    version = String(version)
  } else {
    logger.info('setDispatcher was called with an object as the version parameter')
    version = null
  }

  this.agent.environment.setDispatcher(name, version, true)
}

/**
 * Give the current transaction a name based on your own idea of what
 * constitutes a controller in your Node application. Also allows you to
 * optionally specify the action being invoked on the controller. If the action
 * is omitted, then the API will default to using the HTTP method used in the
 * request (e.g. GET, POST, DELETE). Overrides any New Relic naming rules set
 * in configuration or from New Relic's servers.
 *
 * IMPORTANT: this function must be called when a transaction is active. New
 * Relic transactions are tied to web requests, so this method may be called
 * from within HTTP or HTTPS listener functions, Express routes, or other
 * contexts where a web request or response object are in scope.
 *
 * @param {string} name   The name you want to give the controller in the New
 *                        Relic UI. Will be prefixed with 'Controller/' when
 *                        sent.
 * @param {string} action The action being invoked on the controller. Defaults
 *                        to the HTTP method used for the request.
 * @returns {void}
 */
API.prototype.setControllerName = function setControllerName(name, action) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/setControllerName'
  )
  metric.incrementCallCount()

  const transaction = this.agent.tracer.getTransaction()
  if (!transaction) {
    return logger.warn('No transaction found when setting controller to %s.', name)
  }

  if (!name) {
    if (transaction && transaction.url) {
      logger.error('Must include name in setControllerName call for URL %s.', transaction.url)
    } else {
      logger.error('Must include name in setControllerName call.')
    }

    return
  }

  action = action || transaction.verb || 'GET'
  transaction.forceName = NAMES.CONTROLLER + '/' + name + '/' + action
}

/**
 * Add a custom attribute to the current transaction and span. Some attributes are
 * reserved (see CUSTOM_DENYLIST for the current, very short list), and
 * as with most API methods, this must be called in the context of an
 * active transaction. Most recently set value wins.
 *
 * @param {string} key  The key you want displayed in the RPM UI.
 * @param {string} value The value you want displayed. Must be serializable.
 * @returns {false|undefined} Returns false when disabled/errored, otherwise undefined
 */
API.prototype.addCustomAttribute = function addCustomAttribute(key, value) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/addCustomAttribute'
  )
  metric.incrementCallCount()

  // If high security mode is on, custom attributes are disabled.
  if (this.agent.config.high_security) {
    logger.warnOnce('Custom attributes', 'Custom attributes are disabled by high security mode.')
    return false
  } else if (!this.agent.config.api.custom_attributes_enabled) {
    logger.debug('Config.api.custom_attributes_enabled set to false, not collecting value')
    return false
  }

  const transaction = this.agent.tracer.getTransaction()
  if (!transaction) {
    logger.warn('No transaction found for custom attributes.')
    return false
  }

  const trace = transaction.trace
  if (!trace.custom) {
    logger.warn('Could not add attribute %s to nonexistent custom attributes.', key)
    return false
  }

  if (CUSTOM_DENYLIST.has(key)) {
    logger.warn('Not overwriting value of NR-only attribute %s.', key)
    return false
  }

  trace.addCustomAttribute(key, value)

  const spanContext = this.agent.tracer.getSpanContext()
  if (!spanContext) {
    logger.debug('No span found for custom attributes.')
    // success/failure is ambiguous here. since at least 1 attempt tried, not returning false
    return
  }

  spanContext.addCustomAttribute(key, value, spanContext.ATTRIBUTE_PRIORITY.LOW)
}

/**
 * Adds all custom attributes in an object to the current transaction and span.
 *
 * See documentation for newrelic.addCustomAttribute for more information on
 * setting custom attributes.
 *
 * @example
 *    newrelic.addCustomAttributes({test: 'value', test2: 'value2'});
 *
 * @param {object} [atts] Attribute object
 * @param {string} [atts.KEY] The name you want displayed in the RPM UI.
 * @param {string} [atts.KEY.VALUE] The value you want displayed. Must be serializable.
 */
API.prototype.addCustomAttributes = function addCustomAttributes(atts) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/addCustomAttributes'
  )
  metric.incrementCallCount()

  for (const key in atts) {
    if (!properties.hasOwn(atts, key)) {
      continue
    }

    this.addCustomAttribute(key, atts[key])
  }
}

/**
 * Add custom span attributes in an object to the current segment/span.
 *
 * See documentation for newrelic.addCustomSpanAttribute for more information.
 *
 * @example
 *
 *    newrelic.addCustomSpanAttribute({test: 'value', test2: 'value2'})
 *
 * @param {object} [atts] Attribute object
 * @param {string} [atts.KEY] The name you want displayed in the RPM UI.API.
 * @param {string} [atts.KEY.VALUE] The value you want displayed.  Must be serializable.
 */
API.prototype.addCustomSpanAttributes = function addCustomSpanAttributes(atts) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/addCustomSpanAttributes'
  )
  metric.incrementCallCount()

  for (const key in atts) {
    if (properties.hasOwn(atts, key)) {
      this.addCustomSpanAttribute(key, atts[key])
    }
  }
}

/**
 * Add a custom span attribute to the current transaction. Some attributes
 * are reserved (see CUSTOM_DENYLIST for the current, very short list), and
 * as with most API methods, this must be called in the context of an
 * active segment/span. Most recently set value wins.
 *
 * @param {string} key  The key you want displayed in the RPM UI.
 * @param {string} value The value you want displayed. Must be serializable.
 * @returns {false|undefined} Returns false when disabled/errored, otherwise undefined
 */
API.prototype.addCustomSpanAttribute = function addCustomSpanAttribute(key, value) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/addCustomSpanAttribute'
  )
  metric.incrementCallCount()

  // If high security mode is on, custom attributes are disabled.
  if (this.agent.config.high_security) {
    logger.warnOnce(
      'Custom span attributes',
      'Custom span attributes are disabled by high security mode.'
    )
    return false
  } else if (!this.agent.config.api.custom_attributes_enabled) {
    logger.debug('Config.api.custom_attributes_enabled set to false, not collecting value')
    return false
  }

  const spanContext = this.agent.tracer.getSpanContext()

  if (!spanContext) {
    logger.debug('Could not add attribute %s. No available span.', key)
    return false
  }

  if (CUSTOM_DENYLIST.has(key)) {
    logger.warn('Not overwriting value of NR-only attribute %s.', key)
    return false
  }

  spanContext.addCustomAttribute(key, value)
}

/**
 * Send errors to New Relic that you've already handled yourself. Should be an
 * `Error` or one of its subtypes, but the API will handle strings and objects
 * that have an attached `.message` or `.stack` property.
 *
 * NOTE: Errors that are recorded using this method do _not_ obey the
 * `ignore_status_codes` configuration.
 *
 * @example
 * try {
 *  performSomeTask();
 * } catch (err) {
 *  newrelic.noticeError(
 *    err,
 *    {extraInformation: "error already handled in the application"},
 *    true
 *  );
 * }
 *
 * @param {Error} error
 *  The error to be traced.
 * @param {object} [customAttributes]
 *  Optional. Any custom attributes to be displayed in the New Relic UI.
 * @param {boolean} expected
 *  Optional. False by default. True if the error is expected, meaning it should be collected
 *  for error events and traces, but should not impact error rate.
 * @returns {false|undefined} Returns false when disabled/errored, otherwise undefined
 */
API.prototype.noticeError = function noticeError(error, customAttributes, expected = false) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/noticeError')
  metric.incrementCallCount()

  // let users skip the custom attributes if they want
  if (customAttributes && typeof customAttributes === 'boolean') {
    expected = customAttributes
    customAttributes = null
  }

  if (!this.agent.config.api.notice_error_enabled) {
    logger.debug('Config.api.notice_error_enabled set to false, not collecting error')
    return false
  }

  // If high security mode is on or custom attributes are disabled,
  // noticeError does not collect custom attributes.
  if (this.agent.config.high_security) {
    logger.debug('Passing custom attributes to notice error API is disabled in high security mode.')
  } else if (!this.agent.config.api.custom_attributes_enabled) {
    logger.debug(
      'Config.api.custom_attributes_enabled set to false, ' + 'ignoring custom error attributes.'
    )
  }

  if (typeof error === 'string') {
    error = new Error(error)
  }

  // Filter all object type valued attributes out
  let filteredAttributes = customAttributes
  if (customAttributes) {
    filteredAttributes = _filterAttributes(customAttributes, 'noticeError')
  }

  const transaction = this.agent.tracer.getTransaction()
  this.agent.errors.addUserError(transaction, error, filteredAttributes, expected)
}

/**
 * Sends an application log message to New Relic. The agent already
 * automatically does this for some instrumented logging libraries,
 * but in case you are using another logging method that is not
 * already instrumented by the agent, you can use this function
 * instead.
 *
 * If application log forwarding is disabled in the agent
 * configuration, this function does nothing.
 *
 * @example
 * newrelic.recordLogEvent({
 *  message: 'cannot find file',
 *  level: 'ERROR',
 *  error: new SystemError('missing.txt')
 * })
 *
 * @param {object} logEvent The log event object to send. Any
 *   attributes besides `message`, `level`, `timestamp`, and `error` are
 *   recorded unchanged. The `logEvent` object itself will be mutated by
 *   this function.
 * @param {string} logEvent.message The log message.
 * @param {string} logEvent.level The log level severity. If this key is
 *   missing, it will default to UNKNOWN
 * @param {number} logEvent.timestamp   ECMAScript epoch number denoting the
 *   time that this log message was produced. If this key is missing,
 *   it will default to the output of `Date.now()`.
 * @param {Error} logEvent.error Error associated to this log event. Ignored if missing.
 */
API.prototype.recordLogEvent = function recordLogEvent(logEvent = {}) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/recordLogEvent')
  metric.incrementCallCount()

  if (!applicationLogging.isLogForwardingEnabled(this.agent.config, this.agent)) {
    logger.warnOnce(
      'Record logs',
      'Application log forwarding disabled, method API#recordLogEvent will not record messages'
    )
    return
  }

  // If they don't pass a logEvent object, or it doesn't have the
  // required `message` key, bail out.
  if (typeof logEvent !== 'object' || logEvent.message === undefined) {
    logger.warn(
      'recordLogEvent requires an object with a `message` attribute for its single argument, got %s (%s)',
      stringify(logEvent),
      typeof logEvent
    )
    return
  }
  logEvent.message = applicationLogging.truncate(logEvent.message)

  if (!logEvent.level) {
    logger.debug('no log level set, setting it to UNKNOWN')
    logEvent.level = 'UNKNOWN'
  }

  if (typeof logEvent.timestamp !== 'number') {
    logger.debug('no timestamp set, setting it to `Date.now()`')
    logEvent.timestamp = Date.now()
  }

  if (logEvent.error) {
    logEvent['error.message'] = applicationLogging.truncate(logEvent.error.message)
    logEvent['error.stack'] = applicationLogging.truncate(logEvent.error.stack)
    logEvent['error.class'] =
      logEvent.error.name === 'Error' ? logEvent.error.constructor.name : logEvent.error.name
    delete logEvent.error
  }

  if (applicationLogging.isMetricsEnabled(this.agent.config)) {
    applicationLogging.incrementLoggingLinesMetrics(logEvent.level, this.agent.metrics)
  }

  const metadata = this.agent.getLinkingMetadata()
  this.agent.logs.add(Object.assign({}, logEvent, metadata))
}

/**
 * If the URL for a transaction matches the provided pattern, name the
 * transaction with the provided name. If there are capture groups in the
 * pattern (which is a standard JavaScript regular expression, and can be
 * passed as either a RegExp or a string), then the substring matches ($1, $2,
 * etc.) are replaced in the name string. BE CAREFUL WHEN USING SUBSTITUTION.
 * If the replacement substrings are highly variable (i.e. are identifiers,
 * GUIDs, or timestamps), the rule will generate too many metrics and
 * potentially get your application blocked by New Relic.
 *
 *
 * @example
 * // An example of a good rule with replacements:
 * newrelic.addNamingRule('^/storefront/(v[1-5])/(item|category|tag)',
 *                        'CommerceAPI/$1/$2')
 *
 * @example
 * // An example of a bad rule with replacements:
 * newrelic.addNamingRule('^/item/([0-9a-f]+)', 'Item/$1')
 *
 * // Keep in mind that the original URL and any query parameters will be sent
 * // along with the request, so slow transactions will still be identifiable.
 *
 * // Naming rules can not be removed once added. They can also be added via the
 * // agent's configuration. See configuration documentation for details.
 *
 * @param {RegExp} pattern The pattern to rename (with capture groups).
 * @param {string} name    The name to use for the transaction.
 * @returns {void}
 */
API.prototype.addNamingRule = function addNamingRule(pattern, name) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/addNamingRule')
  metric.incrementCallCount()

  if (!name) {
    return logger.error('Simple naming rules require a replacement name.')
  }

  this.agent.userNormalizer.addSimple(pattern, '/' + name)
}

/**
 * If the URL for a transaction matches the provided pattern, ignore the
 * transaction attached to that URL. Useful for filtering socket.io connections
 * and other long-polling requests out of your agents to keep them from
 * distorting an app's apdex or mean response time. Pattern may be a (standard
 * JavaScript) RegExp or a string.
 *
 * @example
 *   newrelic.addIgnoringRule('^/socket\\.io/')
 *
 * @param {RegExp} pattern The pattern to ignore.
 * @returns {void}
 */
API.prototype.addIgnoringRule = function addIgnoringRule(pattern) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/addIgnoringRule')
  metric.incrementCallCount()

  if (!pattern) {
    return logger.error('Must include a URL pattern to ignore.')
  }

  this.agent.userNormalizer.addSimple(pattern, null)
}

/**
 * Gracefully fail.
 *
 * Output an HTML comment and log a warning the comment is meant to be
 * innocuous to the end user.
 *
 * @private
 * @see RUM_ISSUES
 * @param {number} errorCode Error code from `RUM_ISSUES`.
 * @param {boolean} [quiet] Be quiet about this failure.
 * @returns {string} HTML comment for debugging purposes with specific error code
 */
function _gracefail(errorCode, quiet) {
  if (quiet) {
    logger.debug(RUM_ISSUES[errorCode])
  } else {
    logger.warn(RUM_ISSUES[errorCode])
  }
  return '<!-- NREUM: (' + errorCode + ') -->'
}

/**
 * Function for generating a fully formed RUM header based on configuration options
 *
 * @private
 * @param {object} options Configuration options for RUM
 * @param {string} [options.nonce] Nonce to inject into `<script>` header.
 * @param {boolean} [options.hasToRemoveScriptWrapper] Used to import agent script without `<script>` tag wrapper.
 * @param {string} metadata Stringified representation of rumHash metadata
 * @param {string} loader Agent Loader script
 * @returns {string} fully formed RUM header
 */
function _generateRUMHeader(options = {}, metadata, loader) {
  const formatArgs = []

  if (options.hasToRemoveScriptWrapper) {
    formatArgs.push(RUM_STUB)
  } else if (options.nonce) {
    formatArgs.push(RUM_STUB_SHELL_WITH_NONCE_PARAM, `nonce="${options.nonce}"`)
  } else {
    formatArgs.push(RUM_STUB_SHELL)
  }

  formatArgs.push(metadata, loader)

  return util.format(...formatArgs)
}

/**
 * Helper method for determining if we have the minimum required
 * information to generate our Browser Agent script tag
 *
 * @private
 * @param {object} config agent configuration settings
 * @param {Transaction} transaction the active transaction or null
 * @param {boolean} allowTransactionlessInjection whether or not to allow the Browser Agent to be injected when there is no active transaction
 * @returns {{ isValidConfig: boolean, failureIdx: number, quietMode: boolean }} object containing validation results
 */
function validateBrowserMonitoring(config, transaction, allowTransactionlessInjection) {
  /*
   * config.browser_monitoring should always exist, but we don't want the agent
   * to bail here if something goes wrong
   */
  if (!config.browser_monitoring) {
    return { isValidConfig: false, failureIdx: 2 }
  }

  /*
   * Can control header generation with configuration this setting is only
   * available in the newrelic.js config file, it is not ever set by the
   * server.
   */
  if (!config.browser_monitoring.enable) {
    // It has been disabled by the user; no need to warn them about their own
    // settings so fail quietly and gracefully.
    return { isValidConfig: false, failureIdx: 0, quietMode: true }
  }

  /*
   * This is only going to work if the agent has successfully handshaked with
   * the collector. If the networks is bad, or there is no license key set in
   * newrelic.js, there will be no application_id set.  We bail instead of
   * outputting null/undefined configuration values.
   */
  if (!config.application_id) {
    return { isValidConfig: false, failureIdx: 4 }
  }

  /*
   * If there is no browser_key, the server has likely decided to disable
   * browser monitoring.
   */
  if (!config.browser_monitoring.browser_key) {
    return { isValidConfig: false, failureIdx: 5 }
  }

  /*
   * If there is no agent_loader script, there is no point
   * in setting the rum data
   */
  if (!config.browser_monitoring.js_agent_loader) {
    return { isValidConfig: false, failureIdx: 6 }
  }

  /*
   * If rum is enabled, but then later disabled on the server,
   * this is the only parameter that gets updated.
   *
   * This condition should only be met if rum is disabled during
   * the lifetime of an application, and it should be picked up
   * on the next ForceRestart by the collector.
   */
  if (config.browser_monitoring.loader === 'none') {
    return { isValidConfig: false, failureIdx: 7 }
  }

  if (!allowTransactionlessInjection && !transaction) {
    return { isValidConfig: false, failureIdx: 1 }
  }

  return { isValidConfig: true }
}

/**
 * Get the script header necessary for Browser Monitoring
 * This script must be manually injected into your templates, as high as possible
 * in the header, but _after_ any X-UA-COMPATIBLE HTTP-EQUIV meta tags.
 * Otherwise you may hurt IE!
 *
 * By default this method will return a script wrapped by `<script>` tags, but with
 * option `hasToRemoveScriptWrapper` it can send back only the script content
 * without the `<script>` wrapper. Useful for React component based frontend.
 *
 * This method must be called every time you want to generate the headers.
 *
 * Do *not* reuse the headers between users, or even between requests.
 *
 * @param {object} options configuration options
 * @param {string} [options.nonce] - Nonce to inject into `<script>` header.
 * @param {boolean} [options.hasToRemoveScriptWrapper] - Used to import agent script without `<script>` tag wrapper.
 * @param {options} [options.allowTransactionlessInjection] Whether or not to allow the Browser Agent to be injected when there is no active transaction
 * @returns {string} The script content to be injected in `<head>` or put inside `<script>` tag (depending on options)
 */
API.prototype.getBrowserTimingHeader = function getBrowserTimingHeader(options = {}) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/getBrowserTimingHeader'
  )
  metric.incrementCallCount()

  const trans = this.agent.getTransaction()

  const { isValidConfig, failureIdx, quietMode } = validateBrowserMonitoring(
    this.agent.config,
    trans,
    options.allowTransactionlessInjection
  )

  if (!isValidConfig) {
    return _gracefail(failureIdx, quietMode)
  }

  const config = this.agent.config

  // This hash gets written directly into the browser.
  const rumHash = {
    agent: config.browser_monitoring.js_agent_file,
    beacon: config.browser_monitoring.beacon,
    errorBeacon: config.browser_monitoring.error_beacon,
    licenseKey: config.browser_monitoring.browser_key,
    applicationID: config.application_id,

    // we don't use these parameters yet
    agentToken: null
  }

  const hasActiveTransaction = trans !== null

  if (hasActiveTransaction) {
    // bail gracefully outside an ignored transaction
    if (trans.isIgnored()) {
      return _gracefail(1)
    }

    /* If we're in an unnamed transaction, add a friendly warning this is to
     * avoid people going crazy, trying to figure out why browser monitoring is
     * not working when they're missing a transaction name.
     */
    const name = trans.getFullName()
    if (!name) {
      return _gracefail(3)
    }

    const time = trans.timer.getDurationInMillis()
    rumHash.applicationTime = time

    /*
     * Only the first 13 chars of the license should be used for hashing with
     * the transaction name.
     */
    const key = config.license_key.substring(0, 13)
    rumHash.transactionName = hashes.obfuscateNameUsingKey(name, key)

    rumHash.queueTime = trans.queueTime
    rumHash.ttGuid = trans.id

    const attrs = Object.create(null)

    const customAttrs = trans.trace.custom.get(ATTR_DEST.BROWSER_EVENT)
    if (!properties.isEmpty(customAttrs)) {
      attrs.u = customAttrs
    }

    const agentAttrs = trans.trace.attributes.get(ATTR_DEST.BROWSER_EVENT)
    if (!properties.isEmpty(agentAttrs)) {
      attrs.a = agentAttrs
    }

    if (!properties.isEmpty(attrs)) {
      rumHash.atts = hashes.obfuscateNameUsingKey(JSON.stringify(attrs), key)
    }
  } else {
    logger.debug(
      'No transaction detected when generating RUM header, continuing without transaction info'
    )
  }

  // if debugging, do pretty format of JSON
  const tabs = config.browser_monitoring.debug ? 2 : 0
  const json = JSON.stringify(rumHash, null, tabs)

  // the complete header to be written to the browser
  const out = _generateRUMHeader(
    { nonce: options.nonce, hasToRemoveScriptWrapper: options.hasToRemoveScriptWrapper },
    json,
    config.browser_monitoring.js_agent_loader
  )

  logger.trace('generating RUM header', out)

  return out
}

/**
 * @callback startSegmentCallback
 * @param {Function} cb
 *   The function to time with the created segment.
 * @returns {Promise=} Returns a promise if cb returns a promise.
 */

/**
 * Wraps the given handler in a segment which may optionally be turned into a
 * metric.
 *
 * @example
 *  newrelic.startSegment('mySegment', false, function handler() {
 *    // The returned promise here will signify the end of the segment.
 *    return myAsyncTask().then(myNextTask)
 *  })
 * @param {string} name
 *  The name to give the new segment. This will also be the name of the metric.
 * @param {boolean} record
 *  Indicates if the segment should be recorded as a metric. Metrics will show
 *  up on the transaction breakdown table and server breakdown graph. Segments
 *  just show up in transaction traces.
 * @param {startSegmentCallback} handler
 *  The function to track as a segment.
 * @param {Function} [callback]
 *  An optional callback for the handler. This will indicate the end of the
 *  timing if provided.
 * @returns {*} Returns the result of calling `handler`.
 */
API.prototype.startSegment = function startSegment(name, record, handler, callback) {
  this.agent.metrics
    .getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/startSegment')
    .incrementCallCount()

  // Check that we have usable arguments.
  if (!name || typeof handler !== 'function') {
    logger.warn('Name and handler function are both required for startSegment')
    if (typeof handler === 'function') {
      return handler(callback)
    }
    return
  }
  if (callback && typeof callback !== 'function') {
    logger.warn('If using callback, it must be a function')
    return handler(callback)
  }

  // Are we inside a transaction?
  if (!this.shim.getActiveSegment()) {
    logger.debug('startSegment(%j) called outside of a transaction, not recording.', name)
    return handler(callback)
  }

  assignCLMSymbol(this.shim, handler)
  // Create the segment and call the handler.
  const wrappedHandler = this.shim.record(handler, function handlerNamer(shim) {
    return {
      name: name,
      recorder: record ? customRecorder : null,
      callback: callback ? shim.FIRST : null,
      promise: !callback
    }
  })

  return wrappedHandler(callback)
}

/**
 * Creates and starts a web transaction to record work done in
 * the handle supplied. This transaction will run until the handle
 * synchronously returns UNLESS:
 * 1. The handle function returns a promise, where the end of the
 *    transaction will be tied to the end of the promise returned.
 * 2. {@link API#getTransaction} is called in the handle, flagging the
 *    transaction as externally handled.  In this case the transaction
 *    will be ended when {@link TransactionHandle#end} is called in the user's code.
 *
 * @example
 * const newrelic = require('newrelic')
 * newrelic.startWebTransaction('/some/url/path', function() {
 *   const transaction = newrelic.getTransaction()
 *   setTimeout(function() {
 *     // do some work
 *     transaction.end()
 *   }, 100)
 * })
 * @param {string} url
 *  The URL of the transaction.  It is used to name and group related transactions in APM,
 *  so it should be a generic name and not include any variable parameters.
 * @param {Function}  handle
 *  Function that represents the transaction work.
 * @returns {null|*} Returns null if handle is not a function, otherwise the return value of handle
 */
API.prototype.startWebTransaction = function startWebTransaction(url, handle) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/startWebTransaction'
  )
  metric.incrementCallCount()

  if (typeof handle !== 'function') {
    logger.warn('startWebTransaction called with a handle arg that is not a function')
    return null
  }

  if (!url) {
    logger.warn('startWebTransaction called without a url, transaction not started')
    return handle()
  }

  logger.debug('starting web transaction %s (%s).', url, handle && handle.name)

  const shim = this.shim
  const tracer = this.agent.tracer
  const parent = tracer.getTransaction()

  assignCLMSymbol(shim, handle)
  return tracer.transactionNestProxy('web', function startWebSegment() {
    const tx = tracer.getTransaction()

    if (!tx) {
      return handle.apply(this, arguments)
    }

    if (tx === parent) {
      logger.debug('not creating nested transaction %s using transaction %s', url, tx.id)
      return tracer.addSegment(url, null, null, true, handle)
    }

    logger.debug(
      'creating web transaction %s (%s) with transaction id: %s',
      url,
      handle && handle.name,
      tx.id
    )
    tx.nameState.setName(NAMES.CUSTOM, null, NAMES.ACTION_DELIMITER, url)
    tx.url = url
    tx.applyUserNamingRules(tx.url)
    tx.baseSegment = tracer.createSegment(url, recordWeb)
    tx.baseSegment.start()

    const boundHandle = tracer.bindFunction(handle, tx.baseSegment)
    maybeAddCLMAttributes(handle, tx.baseSegment)
    let returnResult = boundHandle.call(this)
    if (returnResult && shim.isPromise(returnResult)) {
      returnResult = shim.interceptPromise(returnResult, tx.end.bind(tx))
    } else if (!tx.handledExternally) {
      logger.debug('Ending unhandled web transaction immediately.')
      tx.end()
    }
    return returnResult
  })()
}

API.prototype.startBackgroundTransaction = startBackgroundTransaction

/**
 * Creates and starts a background transaction to record work done in
 * the handle supplied. This transaction will run until the handle
 * synchronously returns UNLESS:
 * 1. The handle function returns a promise, where the end of the
 *    transaction will be tied to the end of the promise returned.
 * 2. {@link API#getTransaction} is called in the handle, flagging the
 *    transaction as externally handled.  In this case the transaction
 *    will be ended when {@link TransactionHandle#end} is called in the user's code.
 *
 * @example
 * const newrelic = require('newrelic')
 * newrelic.startBackgroundTransaction('Red October', 'Subs', function() {
 *   const transaction = newrelic.getTransaction()
 *   setTimeout(function() {
 *     // do some work
 *     transaction.end()
 *   }, 100)
 * })
 * @param {string} name
 *  The name of the transaction. It is used to name and group related
 *  transactions in APM, so it should be a generic name and not include any
 *  variable parameters.
 * @param {string} [group]
 *  Optional, used for grouping background transactions in APM. For more
 *  information see:
 *  https://docs.newrelic.com/docs/apm/applications-menu/monitoring/transactions-page#txn-type-dropdown
 * @param {Function} handle
 *  Function that represents the background work.
 * @memberof API#
 * @returns {null|*} Returns null if handle is not a function, otherwise the return value of handle
 */
function startBackgroundTransaction(name, group, handle) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/startBackgroundTransaction'
  )
  metric.incrementCallCount()

  if (handle === undefined && typeof group === 'function') {
    handle = group
    group = 'Nodejs'
  }

  if (typeof handle !== 'function') {
    logger.warn('startBackgroundTransaction called with a handle that is not a function')
    return null
  }

  if (!name) {
    logger.warn('startBackgroundTransaction called without a name')
    return handle()
  }

  logger.debug('starting background transaction %s:%s (%s)', name, group, handle && handle.name)

  const tracer = this.agent.tracer
  const shim = this.shim
  const txName = group + '/' + name
  const parent = tracer.getTransaction()

  assignCLMSymbol(shim, handle)
  return tracer.transactionNestProxy('bg', function startBackgroundSegment() {
    const tx = tracer.getTransaction()

    if (!tx) {
      return handle.apply(this, arguments)
    }

    if (tx === parent) {
      logger.debug('not creating nested transaction %s using transaction %s', txName, tx.id)
      return tracer.addSegment(txName, null, null, true, handle)
    }

    logger.debug(
      'creating background transaction %s:%s (%s) with transaction id: %s',
      name,
      group,
      handle && handle.name,
      tx.id
    )

    tx._partialName = txName
    tx.baseSegment = tracer.createSegment(name, recordBackground)
    tx.baseSegment.partialName = group
    tx.baseSegment.start()

    const boundHandle = tracer.bindFunction(handle, tx.baseSegment)
    maybeAddCLMAttributes(handle, tx.baseSegment)
    let returnResult = boundHandle.call(this)
    if (returnResult && shim.isPromise(returnResult)) {
      returnResult = shim.interceptPromise(returnResult, tx.end.bind(tx))
    } else if (!tx.handledExternally) {
      logger.debug('Ending unhandled background transaction immediately.')
      tx.end()
    }
    return returnResult
  })()
}

/**
 * End the current web or background custom transaction. This method requires being in
 * the correct transaction context when called.
 */
API.prototype.endTransaction = function endTransaction() {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/endTransaction')
  metric.incrementCallCount()

  const tracer = this.agent.tracer
  const tx = tracer.getTransaction()

  if (tx) {
    if (tx.baseSegment) {
      if (tx.type === 'web') {
        tx.finalizeNameFromUri(tx.url, 0)
      }
      tx.baseSegment.end()
    }
    tx.end()
    logger.debug('ended transaction with id: %s and name: %s', tx.id, tx.name)
  } else {
    logger.debug('endTransaction() called while not in a transaction.')
  }
}

/**
 * Record a custom metric, usually associated with a particular duration.
 * The `name` must be a string following standard metric naming rules. The `value` will
 * usually be a number, but it can also be an object.
 *   When `value` is a numeric value, it should represent the magnitude of a measurement
 *     associated with an event; for example, the duration for a particular method call.
 *   When `value` is an object, it must contain count, total, min, max, and sumOfSquares
 *     keys, all with number values. This form is useful to aggregate metrics on your own
 *     and report them periodically; for example, from a setInterval. These values will
 *     be aggregated with any previously collected values for the same metric. The names
 *     of these keys match the names of the keys used by the platform API.
 *
 * @param  {string} name  The name of the metric.
 * @param  {number|object} value The value of the metric to record
 */
API.prototype.recordMetric = function recordMetric(name, value) {
  const supportMetric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/recordMetric'
  )
  supportMetric.incrementCallCount()

  if (typeof name !== 'string') {
    logger.warn('Metric name must be a string')
    return
  }

  const metricName = NAMES.CUSTOM + NAMES.ACTION_DELIMITER + name
  const metric = this.agent.metrics.getOrCreateMetric(metricName)

  if (typeof value === 'number') {
    metric.recordValue(value)
    return
  }

  if (typeof value !== 'object') {
    logger.warn('Metric value must be either a number, or a metric object')
    return
  }

  const stats = Object.create(null)
  const required = ['count', 'total', 'min', 'max', 'sumOfSquares']
  const keyMap = { count: 'callCount' }

  for (let i = 0, l = required.length; i < l; ++i) {
    if (typeof value[required[i]] !== 'number') {
      logger.warn('Metric object must include %s as a number', required[i])
      return
    }

    const key = keyMap[required[i]] || required[i]
    stats[key] = value[required[i]]
  }

  if (typeof value.totalExclusive === 'number') {
    stats.totalExclusive = value.totalExclusive
  } else {
    stats.totalExclusive = value.total
  }

  metric.merge(stats)
}

/**
 * Create or update a custom metric that acts as a simple counter.
 * The count of the given metric will be incremented by the specified amount,
 * defaulting to 1.
 *
 * @param  {string} name  The name of the metric.
 * @param  {number} [value] The amount that the count of the metric should be incremented
 *                          by. Defaults to 1.
 */
API.prototype.incrementMetric = function incrementMetric(name, value) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/incrementMetric')
  metric.incrementCallCount()

  if (!value && value !== 0) {
    value = 1
  }

  if (typeof value !== 'number' || value % 1 !== 0) {
    logger.warn('Metric Increment value must be an integer')
    return
  }

  this.recordMetric(name, {
    count: value,
    total: 0,
    min: 0,
    max: 0,
    sumOfSquares: 0
  })
}

/**
 * Record custom event data which can be queried in New Relic Insights.
 *
 * @param  {string} eventType  The name of the event. It must be an alphanumeric string
 *                             less than 255 characters.
 * @param  {object} attributes Object of key and value pairs. The keys must be shorter
 *                             than 255 characters, and the values must be string, number,
 *                             or boolean.
 * @returns {false|undefined} Returns false explicitly if failed/disabled, otherwise undefined
 */
API.prototype.recordCustomEvent = function recordCustomEvent(eventType, attributes) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/recordCustomEvent'
  )
  metric.incrementCallCount()

  // If high security mode is on, custom events are disabled.
  if (this.agent.config.high_security) {
    logger.warnOnce('Custom Event', 'Custom events are disabled by high security mode.')
    return false
  } else if (!this.agent.config.api.custom_events_enabled) {
    logger.debug('Config.api.custom_events_enabled set to false, not collecting value')
    return false
  }

  if (!this.agent.config.custom_insights_events.enabled) {
    return
  }
  // Check all the arguments before bailing to give maximum information in a
  // single invocation.
  let fail = false

  if (!eventType || typeof eventType !== 'string') {
    logger.warn(
      'recordCustomEvent requires a string for its first argument, got %s (%s)',
      stringify(eventType),
      typeof eventType
    )
    fail = true
  } else if (!CUSTOM_EVENT_TYPE_REGEX.test(eventType)) {
    logger.warn(
      'recordCustomEvent eventType of %s is invalid, it must match /%s/',
      eventType,
      CUSTOM_EVENT_TYPE_REGEX.source
    )
    fail = true
  } else if (eventType.length > 255) {
    logger.warn(
      'recordCustomEvent eventType must have a length less than 256, got %s (%s)',
      eventType,
      eventType.length
    )
    fail = true
  }
  // If they don't pass an attributes object, or the attributes argument is not
  // an object, or if it is an object but is actually an array, log a
  // warning and set the fail bit.
  if (isSimpleObject(attributes) === false) {
    logger.warn(
      'recordCustomEvent requires an object for its second argument, got %s (%s)',
      stringify(attributes),
      typeof attributes
    )
    fail = true
  } else if (_checkKeyLength(attributes, 255)) {
    fail = true
  }

  if (fail) {
    return
  }

  // Filter all object type valued attributes out
  const filteredAttributes = _filterAttributes(attributes, `${eventType} custom event`)

  const intrinsics = {
    type: eventType,
    timestamp: Date.now()
  }

  const tx = this.agent.getTransaction()
  const priority = (tx && tx.priority) || Math.random()
  this.agent.customEventAggregator.add([intrinsics, filteredAttributes], priority)
}

/**
 * Registers an instrumentation function.
 *
 *  - `newrelic.instrument(moduleName, onRequire [,onError])`
 *  - `newrelic.instrument(options)`
 *
 * @param {string|object} moduleName The module name given to require to load the module, or the instrumentation specification
 * @param {string} moduleName.moduleName The module name given to require to load the module
 * @param {string} [moduleName.absolutePath] Must provide absolute path to module if it does not exist within node_modules. This is used to instrument a file within the same application.
 * @param {Function}  moduleName.onRequire The function to call when the module has been loaded
 * @param {Function} [moduleName.onError] If provided, should `onRequire` throw an error, the error will be passed to
 * @param {Function} onRequire The function to call when the module has been loaded
 * @param {Function} onError If provided, should `onRequire` throw an error, the error will be passed to this function.
 */
API.prototype.instrument = function instrument(moduleName, onRequire, onError) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/instrument')
  metric.incrementCallCount()

  let opts = moduleName
  if (typeof opts === 'string') {
    opts = {
      moduleName: moduleName,
      onRequire: onRequire,
      onError: onError
    }
  }

  opts.type = MODULE_TYPE.GENERIC
  shimmer.registerInstrumentation(opts)
}

/**
 * Registers an instrumentation function.
 *
 * - `newrelic.instrumentConglomerate(moduleName, onRequire [, onError])`
 * - `newrelic.instrumentConglomerate(options)`
 *
 * @param {string|object} moduleName The module name given to require to load the module, or the instrumentation specification
 * @param {string} moduleName.moduleName The module name given to require to load the module
 * @param {string} [moduleName.absolutePath] Must provide absolute path to module if it does not exist within node_modules. This is used to instrument a file within the same application.
 * @param {Function}  moduleName.onRequire The function to call when the module has been loaded
 * @param {Function} [moduleName.onError] If provided, should `onRequire` throw an error, the error will be passed to
 * @param {Function} onRequire The function to call when the module has been loaded
 * @param {Function} onError If provided, should `onRequire` throw an error, the error will be passed to this function.
 */
API.prototype.instrumentConglomerate = function instrumentConglomerate(
  moduleName,
  onRequire,
  onError
) {
  this.agent.metrics
    .getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/instrumentConglomerate')
    .incrementCallCount()

  let opts = moduleName
  if (typeof opts === 'string') {
    opts = { moduleName, onRequire, onError }
  }

  opts.type = MODULE_TYPE.CONGLOMERATE
  shimmer.registerInstrumentation(opts)
}

/**
 * Registers an instrumentation function.
 *
 *  - `newrelic.instrumentDatastore(moduleName, onRequire [,onError])`
 *  - `newrelic.instrumentDatastore(options)`
 *
 * @param {string|object} moduleName The module name given to require to load the module, or the instrumentation specification
 * @param {string} moduleName.moduleName The module name given to require to load the module
 * @param {string} [moduleName.absolutePath] Must provide absolute path to module if it does not exist within node_modules. This is used to instrument a file within the same application.
 * @param {Function}  moduleName.onRequire The function to call when the module has been loaded
 * @param {Function} [moduleName.onError] If provided, should `onRequire` throw an error, the error will be passed to
 * @param {Function} onRequire The function to call when the module has been loaded
 * @param {Function} onError If provided, should `onRequire` throw an error, the error will be passed to this function.
 */
API.prototype.instrumentDatastore = function instrumentDatastore(moduleName, onRequire, onError) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/instrumentDatastore'
  )
  metric.incrementCallCount()

  let opts = moduleName
  if (typeof opts === 'string') {
    opts = {
      moduleName: moduleName,
      onRequire: onRequire,
      onError: onError
    }
  }

  opts.type = MODULE_TYPE.DATASTORE
  shimmer.registerInstrumentation(opts)
}

/**
 * Registers an instrumentation function.
 *
 *  - `newrelic.instrumentWebframework(moduleName, onRequire [,onError])`
 *  - `newrelic.instrumentWebframework(options)`
 *
 * @param {string|object} moduleName The module name given to require to load the module, or the instrumentation specification
 * @param {string} moduleName.moduleName The module name given to require to load the module
 * @param {string} [moduleName.absolutePath] Must provide absolute path to module if it does not exist within node_modules. This is used to instrument a file within the same application.
 * @param {Function}  moduleName.onRequire The function to call when the module has been loaded
 * @param {Function} [moduleName.onError] If provided, should `onRequire` throw an error, the error will be passed to
 * @param {Function} onRequire The function to call when the module has been loaded
 * @param {Function} onError If provided, should `onRequire` throw an error, the error will be passed to this function.
 */
API.prototype.instrumentWebframework = function instrumentWebframework(
  moduleName,
  onRequire,
  onError
) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/instrumentWebframework'
  )
  metric.incrementCallCount()

  let opts = moduleName
  if (typeof opts === 'string') {
    opts = {
      moduleName: moduleName,
      onRequire: onRequire,
      onError: onError
    }
  }

  opts.type = MODULE_TYPE.WEB_FRAMEWORK
  shimmer.registerInstrumentation(opts)
}

/**
 * Registers an instrumentation function for instrumenting message brokers.
 *
 *  - `newrelic.instrumentMessages(moduleName, onRequire [,onError])`
 *  - `newrelic.instrumentMessages(options)`
 *
 * @param {string|object} moduleName The module name given to require to load the module, or the instrumentation specification
 * @param {string} moduleName.moduleName The module name given to require to load the module
 * @param {string} [moduleName.absolutePath] Must provide absolute path to module if it does not exist within node_modules. This is used to instrument a file within the same application.
 * @param {Function}  moduleName.onRequire The function to call when the module has been loaded
 * @param {Function} [moduleName.onError] If provided, should `onRequire` throw an error, the error will be passed to
 * @param {Function} onRequire The function to call when the module has been loaded
 * @param {Function} onError If provided, should `onRequire` throw an error, the error will be passed to this function.
 */
API.prototype.instrumentMessages = function instrumentMessages(moduleName, onRequire, onError) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/instrumentMessages'
  )
  metric.incrementCallCount()

  let opts = moduleName
  if (typeof opts === 'string') {
    opts = {
      moduleName: moduleName,
      onRequire: onRequire,
      onError: onError
    }
  }

  opts.type = MODULE_TYPE.MESSAGE
  shimmer.registerInstrumentation(opts)
}

/**
 * Applies an instrumentation to an already loaded CommonJs module.
 *
 * Note: This function will not work for ESM packages.
 * @example
 *
 * // oh no, express was loaded before newrelic
 * const express   = require('express')
 * const newrelic  = require('newrelic')
 *
 * // phew, we can use instrumentLoadedModule to make
 * // sure express is still instrumented
 * newrelic.instrumentLoadedModule('express', express)
 *
 * @param {string} moduleName
 *  The module's name/identifier.  Will be normalized
 *  into an instrumentation key.
 * @param {object} module
 *  The actual module object or function we're instrumenting
 * @returns {boolean} Whether or not the module was successfully instrumented
 */
API.prototype.instrumentLoadedModule = function instrumentLoadedModule(moduleName, module) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/instrumentLoadedModule'
  )
  metric.incrementCallCount()

  try {
    const resolvedName = require.resolve(moduleName)
    const parsed = parse(resolvedName)

    return shimmer.instrumentPostLoad(this.agent, module, moduleName, parsed.basedir)
  } catch (error) {
    logger.error('instrumentLoadedModule encountered an error, module not instrumented: %s', error)
  }

  return false
}

/**
 * Returns the current trace and span id.
 *
 * @returns {*} The object containing the current trace and span ids
 */
API.prototype.getTraceMetadata = function getTraceMetadata() {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/getTraceMetadata'
  )
  metric.incrementCallCount()

  const metadata = {}

  const segment = this.agent.tracer.getSegment()
  if (!segment) {
    logger.debug('No transaction found when calling API#getTraceMetadata')
  } else if (!this.agent.config.distributed_tracing.enabled) {
    logger.debug('Distributed tracing disabled when calling API#getTraceMetadata')
  } else {
    metadata.traceId = segment.transaction.traceId

    const spanId = segment.getSpanId()
    if (spanId) {
      metadata.spanId = spanId
    }
  }

  return metadata
}

/**
 * Record a LLM feedback event which can be viewed in New Relic API Monitoring.
 *
 * @param {object} params Input parameters.
 * @param {string} params.traceId Identifier for the feedback event.
 * Obtained from {@link getTraceMetadata}.
 * @param {string} params.category A tag for the event.
 * @param {string} params.rating A indicator of how useful the message was.
 * @param {string} [params.message] The message that triggered the event.
 * @param {object} [params.metadata] Additional key-value pairs to associate
 * with the recorded event.
 */
API.prototype.recordLlmFeedbackEvent = function recordLlmFeedbackEvent({
  traceId,
  category,
  rating,
  message = '',
  metadata = {}
} = {}) {
  this.agent.metrics
    .getOrCreateMetric(`${NAMES.SUPPORTABILITY.API}/recordLlmFeedbackEvent`)
    .incrementCallCount()

  if (!traceId) {
    logger.warn(
      'A feedback event will not be recorded.  recordLlmFeedbackEvent must be called with a traceId.'
    )
    return
  }

  if (this.agent.config?.ai_monitoring?.enabled !== true) {
    logger.warn('recordLlmFeedbackEvent invoked but ai_monitoring is disabled.')
    return
  }

  const tx = this.agent.tracer.getTransaction()
  if (!tx) {
    logger.warn(
      'A feedback events will not be recorded. recordLlmFeedbackEvent must be called within the scope of a transaction.'
    )
    return
  }

  const feedback = new LlmFeedbackMessage({
    traceId,
    category,
    rating,
    message
  })
  this.recordCustomEvent('LlmFeedbackMessage', { ...metadata, ...feedback })
}

/**
 * Shuts down the agent.
 *
 * @param {object} [options]
 *  Object with shut down options.
 * @param {boolean} [options.collectPendingData]
 *  If true, the agent will send any pending data to the collector before
 *  shutting down.
 * @param {number} [options.timeout]
 *  Time in milliseconds to wait before shutting down.
 * @param {boolean} [options.waitForIdle]
 *  If true, the agent will not shut down until there are no active transactions.
 * @param {Function} [cb]
 *  Callback function that runs when agent stops.
 */
API.prototype.shutdown = function shutdown(options, cb) {
  this.agent.metrics.getOrCreateMetric(`${NAMES.SUPPORTABILITY.API}/shutdown`).incrementCallCount()

  let callback = cb
  if (typeof options === 'function') {
    // shutdown(cb)
    callback = options
    options = {}
  } else if (typeof callback !== 'function') {
    // shutdown([options])
    callback = () => {}
  }
  if (!options) {
    // shutdown(null, cb)
    options = {}
  }

  _doShutdown(this, options, callback)
}

/**
 * Helper function for logging if an error occurs, and where
 *
 * @private
 * @param {Error} error If defined, the error that occurred
 * @param {string} phase Where in the process the error happened
 * @returns {void}
 */
function _logErrorCallback(error, phase) {
  if (error) {
    logger.error(error, `An error occurred during ${phase}`)
  }
}

/**
 * Function for handling the graceful shutdown process, including processing of data and handling errors
 *
 * @private
 * @param {object} api instantiation of this file
 * @param {object} options shutdown options object
 * @param {boolean} [options.collectPendingData]
 *  If true, the agent will send any pending data to the collector before
 *  shutting down.
 * @param {number} [options.timeout]
 *  Time in milliseconds to wait before shutting down.
 * @param {boolean} [options.waitForIdle]
 *  If true, the agent will not shut down until there are no active transactions.
 * @param {Function} callback callback function to execute after shutdown process is complete (successful or not)
 */
function _doShutdown(api, options, callback) {
  const agent = api.agent

  // If we need to wait for idle and there are currently active transactions,
  // listen for transactions ending and check if we're ready to go.
  if (options.waitForIdle && agent.activeTransactions) {
    options.waitForIdle = false // To prevent recursive waiting.
    agent.on('transactionFinished', function onTransactionFinished() {
      if (agent.activeTransactions === 0) {
        setImmediate(_doShutdown, api, options, callback)
      }
    })
    return
  }

  /**
   * Callback function for after harvest cycles happen as part of shutdown process
   *
   * @param {Error} error If defined, the error that occurred during harvest
   */
  function afterHarvest(error) {
    _logErrorCallback(error, 'last harvest before shutdown')
    agent.stop(callback)
  }

  if (options.collectPendingData && agent._state !== 'started') {
    if (typeof options.timeout === 'number') {
      setTimeout(function shutdownTimeout() {
        agent.stop(callback)
      }, options.timeout).unref()
    } else if (options.timeout) {
      logger.warn('options.timeout should be of type "number". Got %s', typeof options.timeout)
    }

    agent.on('started', function shutdownHarvest() {
      agent.forceHarvestAll(afterHarvest)
    })

    agent.on('errored', function logShutdownError(error) {
      _logErrorCallback(error, 'after shutdown')
      agent.stop(callback)
    })
  } else if (options.collectPendingData) {
    agent.forceHarvestAll(afterHarvest)
  } else {
    agent.stop(callback)
  }
}

/**
 * Validates that all keys in a given object have values that are less than or equal to a given length
 * Assumes all values have .length property (string/array)
 *
 * @private
 * @param {object} object The object to validate
 * @param {number} maxLength The max allowed length
 * @returns {boolean} Whether or not the object passes validation
 */
function _checkKeyLength(object, maxLength) {
  const keys = Object.keys(object)
  let badKey = false
  const len = keys.length
  let key = '' // init to string because gotta go fast
  for (let i = 0; i < len; i++) {
    key = keys[i]
    if (key.length > maxLength) {
      logger.warn(
        'recordCustomEvent requires keys to be less than 256 chars got %s (%s)',
        key,
        key.length
      )
      badKey = true
    }
  }
  return badKey
}

API.prototype.setLambdaHandler = function setLambdaHandler(handler) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/setLambdaHandler'
  )
  metric.incrementCallCount()

  return this.awsLambda.patchLambdaHandler(handler)
}

/**
 * Obfuscates SQL for a given database engine.
 *
 * @param {string} sql sql statement
 * @param {string} dialect engine of the sql (mysql, postgres, cassandra, oracle)
 * @returns {string} sql that obfuscates raw values
 */
API.prototype.obfuscateSql = function obfuscateSql(sql, dialect) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/obfuscateSql')
  metric.incrementCallCount()

  return obfuscate(sql, dialect)
}

/**
 * Assigns `enduser.id` attribute on transaction and trace events. It will also
 * assign the attribute to errors if they occur within a transaction.
 *
 * @param {string} id a unique identifier used to set the `enduser.id` attribute
 */
API.prototype.setUserID = function setUserID(id) {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/setUserID')
  metric.incrementCallCount()

  const transaction = this.agent.tracer.getTransaction()

  if (!(id && transaction)) {
    logger.warn(
      'User id is empty or not in a transaction, not assigning `enduser.id` attribute to transaction events, trace events, and/or errors.'
    )
    return
  }

  transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_COMMON, 'enduser.id', id)
}

/**
 * Function for removing invalid attribute key/value pairs from an object
 *
 * @private
 * @param {object} attributes The attribute object
 * @param {string} name Caller name, used for debugging/logging purposes only
 * @returns {object} Attribute object containing only valid key/value pairs
 */
function _filterAttributes(attributes, name) {
  const filteredAttributes = Object.create(null)
  Object.keys(attributes).forEach((attributeKey) => {
    if (!isValidType(attributes[attributeKey])) {
      logger.info(
        `Omitting attribute ${attributeKey} from ${name} call, type must ` +
          'be boolean, number, or string'
      )
      return
    }
    filteredAttributes[attributeKey] = attributes[attributeKey]
  })
  return filteredAttributes
}

/**
 * Function for adding a custom callback to generate Error Group names, which
 * will be used by the Errors Inbox to group similar errors together via the `error.group.name`
 * agent attribute.
 *
 * Provided functions must return a string, and receive an object as an argument,
 * which contains information related to the Error that occurred, and has the
 * following format:
 *
 * ```
 * {
 *   customAttributes: object,
 *   'request.uri': string,
 *   'http.statusCode': string,
 *   'http.method': string,
 *   error: Error,
 *   'error.expected': boolean
 * }
 * ```
 *
 * Calling this function multiple times will replace previously defined functions
 *
 * @param {Function} callback - callback function to generate `error.group.name` attribute
 * @example
 * function myCallback(metadata) {
 *   if (metadata['http.statusCode'] === '400') {
 *     return 'Bad User Input'
 *   }
 * }
 * newrelic.setErrorGroupCallback(myCallback)
 */
API.prototype.setErrorGroupCallback = function setErrorGroupCallback(callback) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/setErrorGroupCallback'
  )
  metric.incrementCallCount()

  if (!this.shim.isFunction(callback) || this.shim.isAsyncFunction(callback)) {
    logger.warn(
      'Error Group callback must be a synchronous function, Error Group attribute will not be added'
    )
    return
  }

  this.agent.errors.errorGroupCallback = callback
}

/**
 * Registers a callback which will be used for calculating token counts on Llm events when they are not
 * available. This function will typically only be used if `ai_monitoring.record_content.enabled` is false
 * and you want to still capture token counts for Llm events.
 *
 * Provided callbacks must return an integer value for the token count for a given piece of content.
 *
 * @param {Function} callback - synchronous function called to calculate token count for content.
 * @example
 * // @param {string} model - name of model (i.e. gpt-3.5-turbo)
 * // @param {string} content - prompt or completion response
 * function tokenCallback(model, content) {
 *  // calculate tokens based on model and content
 *  // return token count
 *  return 40
 * }
 */
API.prototype.setLlmTokenCountCallback = function setLlmTokenCountCallback(callback) {
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/setLlmTokenCountCallback'
  )
  metric.incrementCallCount()

  if (!this.shim.isFunction(callback) || this.shim.isAsyncFunction(callback)) {
    logger.warn(
      'Llm token count callback must be a synchronous function, callback will not be registered.'
    )
    return
  }

  this.agent.llm.tokenCountCallback = callback
}

/**
 * Ignores the current transaction when calculating your {@link https://docs.newrelic.com/docs/apm/new-relic-apm/apdex/apdex-measuring-user-satisfaction/|Apdex score}.
 * This is useful when you have either very short or very long transactions (such as file downloads) that can skew your Apdex score.
 */
API.prototype.ignoreApdex = function ignoreApdex() {
  const metric = this.agent.metrics.getOrCreateMetric(NAMES.SUPPORTABILITY.API + '/ignoreApdex')
  metric.incrementCallCount()

  const transaction = this.agent.tracer.getTransaction()

  if (!transaction) {
    logger.warn(
      'Apdex will not be ignored. ignoreApdex must be called within the scope of a transaction.'
    )
    return
  }

  transaction.ignoreApdex = true
}

/**
 * Run a function with the passed in LLM context as the active context and return its return value.
 *
 * @example
 * const OpenAI = require('openai')
 * const client = new OpenAI()
 * newrelic.withLlmCustomAttributes({'llm.someAttribute': 'someValue'}, async () => {
 *    const response = await client.chat.completions.create({ messages: [
 *      { role: 'user', content: 'Tell me about Node.js.'}
 *    ]})
 * })
 * @param {Object} context LLM custom attributes context
 * @param {Function} callback The function to execute in context.
 */
API.prototype.withLlmCustomAttributes = function withLlmCustomAttributes(context, callback) {
  context = context || {}
  const metric = this.agent.metrics.getOrCreateMetric(
    NAMES.SUPPORTABILITY.API + '/withLlmCustomAttributes'
  )
  metric.incrementCallCount()

  const transaction = this.agent.tracer.getTransaction()

  if (!callback || typeof callback !== 'function') {
    logger.warn('withLlmCustomAttributes must be used with a valid callback')
    return
  }

  if (!transaction) {
    logger.warn('withLlmCustomAttributes must be called within the scope of a transaction.')
    return callback()
  }

  for (const [key, value] of Object.entries(context)) {
    if (typeof value === 'object' || typeof value === 'function') {
      logger.warn(`Invalid attribute type for ${key}. Skipped.`)
      delete context[key]
    } else if (key.indexOf('llm.') !== 0) {
      logger.warn(`Invalid attribute name ${key}. Renamed to "llm.${key}".`)
      delete context[key]
      context[`llm.${key}`] = value
    }
  }

  transaction._llmContextManager = transaction._llmContextManager || new AsyncLocalStorage()
  const parentContext = transaction._llmContextManager.getStore() || {}

  const fullContext = Object.assign({}, parentContext, context)
  return transaction._llmContextManager.run(fullContext, callback)
}

module.exports = API