newrelic 14.1.2 → 14.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,101 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const Wrapper = require('./wrapper.js')
9
+ const getHostDetails = require('./utils/get-host-details.js')
10
+
11
+ const {
12
+ DB: DB_METRIC_CONSTANTS,
13
+ MONGODB: MONGODB_METRIC_CONSTANTS
14
+ } = require('#agentlib/metrics/names.js')
15
+
16
+ const ADMIN_COMMANDS = require('./utils/admin-commands.js')
17
+
18
+ /**
19
+ * The "Db" class is the primary entrypoint into the mongodb client. Some
20
+ * methods are not present on newer versions of the class, e.g. `addUser`
21
+ * got removed in favor of a direct `command` invocation. This list is a list
22
+ * of all methods we are interested in instrumenting, regardless of client
23
+ * version. As we iterate through the list we will perform a presence check
24
+ * for each method.
25
+ *
26
+ * @type {string[]}
27
+ */
28
+ const DB_METHODS = [
29
+ '_executeInsertCommand',
30
+ '_executeQueryCommand',
31
+ 'addUser',
32
+ 'authenticate',
33
+ 'collection',
34
+ 'collectionNames',
35
+ 'collections',
36
+ 'command',
37
+ 'createCollection',
38
+ 'createIndex',
39
+ 'cursorInfo',
40
+ 'dereference',
41
+ 'dropCollection',
42
+ 'dropDatabase',
43
+ 'dropIndex',
44
+ 'ensureIndex',
45
+ 'eval',
46
+ 'executeDbAdminCommand',
47
+ 'indexInformation',
48
+ 'logout',
49
+ 'open',
50
+ 'reIndex',
51
+ 'removeUser',
52
+ 'renameCollection',
53
+ 'stats'
54
+ ]
55
+
56
+ module.exports = class DbClassSubscriber extends Wrapper {
57
+ constructor({ agent, logger }) {
58
+ super({
59
+ agent,
60
+ logger,
61
+ channelName: 'nr_db',
62
+ packageName: 'mongodb',
63
+ system: MONGODB_METRIC_CONSTANTS.PREFIX
64
+ })
65
+ }
66
+
67
+ end(data, ctx) {
68
+ const { self: dbClient, arguments: args } = data
69
+ const [dbInstance, dbName] = args
70
+ const details = getHostDetails(dbInstance)
71
+ this.parameters = {
72
+ host: details.host,
73
+ port_path_or_id: details.port_path_or_id,
74
+ database_name: dbName,
75
+ product: this.system
76
+ }
77
+
78
+ for (const method of DB_METHODS) {
79
+ if (typeof dbClient[method] !== 'function') {
80
+ continue
81
+ }
82
+
83
+ this.wrapDatabaseMethod(dbClient, method, {
84
+ getSegmentName: (method) => `${DB_METRIC_CONSTANTS.OPERATION}/${this.system}/${method}`,
85
+ getRecorderContext: (method) => {
86
+ return {
87
+ operation: method,
88
+ type: this.type
89
+ }
90
+ },
91
+ getSegmentAttributes: (method) => (
92
+ ADMIN_COMMANDS.includes(method)
93
+ ? { database_name: 'admin' }
94
+ : {}
95
+ )
96
+ })
97
+ }
98
+
99
+ return ctx
100
+ }
101
+ }
@@ -0,0 +1,19 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ /**
9
+ * Represents the list of methods/commmands that are available in the MongoDB
10
+ * client which target the "admin" collection instead of the actually selected
11
+ * collection.
12
+ *
13
+ * @see https://jira.mongodb.org/browse/NODE-827
14
+ * @type {string[]}
15
+ */
16
+ module.exports = [
17
+ 'rename',
18
+ 'renameCollection'
19
+ ]
@@ -0,0 +1,85 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const LOCALHOST_ALIASES = [
9
+ '127.0.0.1',
10
+ '::1',
11
+ '[::1]'
12
+ ]
13
+
14
+ /**
15
+ * @typedef {object} MongodbHostDetails
16
+ * @property {string} [database_name] The name of the database.
17
+ * @property {string} host The first host being targeted by the connection.
18
+ * @property {string|number} port_path_or_id The destination port associated
19
+ * with `host`.
20
+ */
21
+
22
+ /**
23
+ * Retrieves the connection details (host, port, collection_name) from the
24
+ * provided MongoDB object instance. Over versions of the driver, the location
25
+ * of these details get moved around. So we need to inspect various locations
26
+ * for availability and use the first one we can find.
27
+ *
28
+ * Important: in the case where the `mongoObject` is a direct instance of
29
+ * `MongoClient`, or other object that does not include database name details,
30
+ * the `database_name` field will be omitted. You must acquire this through
31
+ * other means.
32
+ *
33
+ * @param {object} mongoObject The MongoDB driver object to get the details
34
+ * from, e.g. an 'AbstractCursor' or 'BulkOperationBase' instance.
35
+ *
36
+ * @returns {MongodbHostDetails}
37
+ */
38
+ module.exports = function getHostDetails(mongoObject) {
39
+ // We prefer to get the details from the "client" object. This is the object
40
+ // the driver returns when you do `require('mongodb').MongoClient.connect()`.
41
+
42
+ let databaseName
43
+ let host
44
+ let port
45
+
46
+ if (mongoObject.constructor.name === 'MongoClient') {
47
+ host = mongoObject.options.hosts[0].host
48
+ port = mongoObject.options.hosts[0].port
49
+ } else if (mongoObject?.databaseName && mongoObject?.client) {
50
+ // Direct `Db` instance.
51
+ databaseName = mongoObject.databaseName
52
+ host = mongoObject.client.options.hosts[0].host
53
+ port = mongoObject.client.options.hosts[0].port
54
+ } else if (mongoObject.dbName && mongoObject.s?.db?.s?.client?.options) {
55
+ // Likely an instance of `Collection`.
56
+ databaseName = mongoObject.dbName
57
+ host = mongoObject.s.db.s.client.options.hosts[0].host
58
+ port = mongoObject.s.db.s.client.options.hosts[0].port
59
+ } else if (mongoObject.databaseName && mongoObject.s?.client?.options) {
60
+ // Some `mongodb@4` object.
61
+ databaseName = mongoObject.databaseName
62
+ host = mongoObject.s.client.options.hosts[0].host
63
+ port = mongoObject.s.client.options.hosts[0].port
64
+ } else if (mongoObject?.s?.db && mongoObject.s.db.client) {
65
+ // mongodb@5 bulk operation
66
+ databaseName = mongoObject.s.db.databaseName
67
+ host = mongoObject.s.db.client.options.hosts[0].host
68
+ port = mongoObject.s.db.client.options.hosts[0].port
69
+ } else if (mongoObject.constructor.name === 'Topology') {
70
+ // A "topology" object (from ancient times). Remove this when we drop
71
+ // support for v4.
72
+ host = mongoObject.s.options.hosts[0].host
73
+ port = mongoObject.s.options.hosts[0].port
74
+ }
75
+
76
+ if (LOCALHOST_ALIASES.includes(host) === true) {
77
+ host = 'localhost'
78
+ }
79
+
80
+ return {
81
+ database_name: databaseName,
82
+ port_path_or_id: port,
83
+ host
84
+ }
85
+ }
@@ -0,0 +1,109 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const DbSubscriber = require('../db.js')
9
+ const databaseRecorder = require('#agentlib/metrics/recorders/database.js')
10
+
11
+ /**
12
+ * This class provides a baseline for the MongoDB instrumentations that
13
+ * share a lot of common code when wrapping methods, but require some slight
14
+ * variations in the collected data. Anything extending this class is
15
+ * expected to leverage the {@link #wrapDatabaseMethod} method.
16
+ */
17
+ module.exports = class WrapperSubscriber extends DbSubscriber {
18
+ constructor(params) {
19
+ super(params)
20
+
21
+ this.events = ['end']
22
+ // We set `opaque` to true because the library uses its own methods
23
+ // internally, and we only care about the entry point. That is, if the
24
+ // code is `.findOne('something')`, the method will invoke `.next`
25
+ // internally. We don't care to track `.next` with its own reported segment.
26
+ // We only want to record the overall operation of `.findOne`.
27
+ //
28
+ // This is the case for all instrumentations that inherit from this class.
29
+ // The details change around the methods, but the desired outcome
30
+ // is the same.
31
+ this.opaque = true
32
+
33
+ // The database metrics recorder reads `this.type` to get the name of the
34
+ // database system.
35
+ this.type = this.system
36
+ }
37
+
38
+ /**
39
+ * Creates a standard database operation wrapper for a method.
40
+ * This method provides a common pattern for instrumenting database operations
41
+ * with transaction validation, segment creation, and database recording.
42
+ *
43
+ * @param {object} instance The instance with the method to wrap.
44
+ * @param {string} methodName The name of the method to wrap.
45
+ * @param {object} callbacks Functions used to retrieve context specific
46
+ * data.
47
+ * @param {Function} callbacks.getSegmentName Each subscriber names its
48
+ * segment slightly differently. This function should return the full
49
+ * string representation for the segment name.
50
+ * @param {Function} callbacks.getRecorderContext Each subscriber instruments
51
+ * multiple methods that can be invoked during the evaluation of one
52
+ * operation. As such, the instances cannot store details relevant to metrics
53
+ * generation on the instance itself. Thus, we need a scoped context object
54
+ * to provide to the metrics recorder function. This function is used to
55
+ * get a copy of that scoped context object.
56
+ * @param {Function} [callbacks.getSegmentAttributes] In most cases, the
57
+ * `addAttributes` method on the base class can be used to get a copy of
58
+ * attributes to add to the segment being recorded. But there are some cases
59
+ * where the attributes need to be calculated within the scope of the
60
+ * transaction and passed in to the `addAttributes` method. This function
61
+ * is used to get a copy of the attributes object to provide to the
62
+ * base method.
63
+ */
64
+ wrapDatabaseMethod(instance, methodName, callbacks) {
65
+ const self = this
66
+ const orig = instance[methodName]
67
+ const {
68
+ getSegmentName,
69
+ getRecorderContext,
70
+ getSegmentAttributes = () => { return {} }
71
+ } = callbacks
72
+
73
+ instance[methodName] = function nrWrappedMethod(...args) {
74
+ let ctx = self.agent.tracer.getContext()
75
+ if (ctx.transaction == null || ctx.transaction.isActive() === false) {
76
+ self.logger.debug(
77
+ 'Not recording function %s, not in a transaction',
78
+ methodName
79
+ )
80
+ return orig.apply(instance, args)
81
+ }
82
+
83
+ self.logger.debug('Recording function %s', methodName)
84
+
85
+ ctx = self.createSegment({
86
+ name: getSegmentName(methodName),
87
+ recorder: function dbRecorder(segment, scope, transaction) {
88
+ const recorderContext = getRecorderContext(methodName)
89
+ return databaseRecorder.call(
90
+ recorderContext,
91
+ segment,
92
+ scope,
93
+ transaction
94
+ )
95
+ },
96
+ attributes: getSegmentAttributes(methodName),
97
+ ctx
98
+ })
99
+
100
+ return self.agent.tracer.runInContext({
101
+ handler: orig,
102
+ context: ctx,
103
+ full: true,
104
+ thisArg: instance,
105
+ args
106
+ })
107
+ }
108
+ }
109
+ }
@@ -15,6 +15,22 @@ const modPathReg = /at .+ \((.+):\d+:\d+\)/
15
15
 
16
16
  module.exports = resolveModuleVersion
17
17
 
18
+ /**
19
+ * Captures the current call stack, dropping frames up to and including the
20
+ * `Channel.publish` frame so that the first returned frame belongs to the
21
+ * module that published the diagnostics channel event.
22
+ *
23
+ * @returns {string[]} The remaining stack frames.
24
+ */
25
+ function defaultGetStack() {
26
+ const err = Error()
27
+ const stack = err.stack.split('\n')
28
+ do {
29
+ stack.shift()
30
+ } while (dcFuncFrame.test(stack[0]) === false && stack.length > 0)
31
+ return stack
32
+ }
33
+
18
34
  /**
19
35
  * Given a module name, attempt to read the version string from its
20
36
  * associated package manifest. If the module is a built-in, or one that has
@@ -32,30 +48,44 @@ module.exports = resolveModuleVersion
32
48
  * @param {string} moduleSpecifier What would be passed to `resolve()`.
33
49
  * @param {object} [deps] Optional dependencies.
34
50
  * @param {object} [deps.logger] Agent logger instance.
51
+ * @param {Function} [deps.getStack] Returns the current call stack as an array
52
+ * of frame strings. Exposed for testing so a synthetic publisher frame can be
53
+ * supplied.
35
54
  *
36
55
  * @returns {string} The version string from the package manifest or "unknown".
37
56
  */
38
- function resolveModuleVersion(moduleSpecifier, { logger = defaultLogger } = {}) {
57
+ function resolveModuleVersion(
58
+ moduleSpecifier,
59
+ { logger = defaultLogger, getStack = defaultGetStack } = {}
60
+ ) {
39
61
  let pkgPath
40
62
  // We'd prefer to use `require.resolve(moduleSpecifier)` here, but it gets
41
63
  // a bit confused when there are non-standard module directories in play.
42
64
  // Once we are able to refactor our "on require" metric recording to
43
65
  // utilize `module.registerHooks`, we should be able to eliminate this
44
66
  // slow algorithm.
45
- const err = Error()
46
- const stack = err.stack.split('\n')
47
- do {
48
- stack.shift()
49
- } while (dcFuncFrame.test(stack[0]) === false && stack.length > 0)
67
+ const stack = getStack()
50
68
  const matches = modPathReg.exec(stack[1])
51
69
  pkgPath = matches?.[1]
52
70
 
53
- if (!pkgPath) {
54
- logger.warn(
55
- { moduleSpecifier },
56
- 'Could not resolve module path. Possibly a built-in or Node.js bundled module.'
57
- )
58
- return 'unknown'
71
+ // When the diagnostics channel event is published by a Node.js built-in or
72
+ // bundled module (e.g. `node:internal/deps/undici/undici`), `pkgPath` is not
73
+ // a real filesystem path, so the directory walk below cannot use it. This
74
+ // happens on modern Node.js versions where userland `undici` reuses Node's
75
+ // internally bundled undici channels. In that case, resolve the userland
76
+ // package manifest directly to report the version the application installed.
77
+ if (!pkgPath || pkgPath.startsWith('node:')) {
78
+ try {
79
+ const { version } = require(`${moduleSpecifier}/package.json`)
80
+ logger.trace({ moduleSpecifier, version }, 'Resolved package version.')
81
+ return version ?? 'unknown'
82
+ } catch {
83
+ logger.warn(
84
+ { moduleSpecifier },
85
+ 'Could not resolve module path. Possibly a built-in or Node.js bundled module.'
86
+ )
87
+ return 'unknown'
88
+ }
59
89
  }
60
90
 
61
91
  const cwd = process.cwd()
@@ -12,11 +12,18 @@ const NrTransport = require('./nr-winston-transport.js')
12
12
  * during initial construction and on any subsequent reconfiguration.
13
13
  *
14
14
  * Handles `NrTransport` injection (after `configure` runs, via `end`).
15
+ *
16
+ * @property {boolean} handlingExceptions Indicates if this instrumentation has
17
+ * already registered a global exception handler or not. We only need a single
18
+ * exception handler per process.
15
19
  */
16
20
  module.exports = class WinstonConfigure extends ApplicationLogsSubscriber {
17
21
  constructor({ agent, logger }) {
18
22
  super({ agent, logger, channelName: 'nr_configure', packageName: 'winston' })
19
23
  this.events = ['end']
24
+ // we only want to assign `handleExceptions` to the first NrTransport
25
+ // customers could be constructing multiple logger instances
26
+ this.handlingExceptions = false
20
27
  }
21
28
 
22
29
  handler(data, ctx) {
@@ -39,11 +46,12 @@ module.exports = class WinstonConfigure extends ApplicationLogsSubscriber {
39
46
  const hasNrTransport = logger.transports?.some((t) => t.name === 'newrelic')
40
47
  if (!hasNrTransport) {
41
48
  const nrTransport = new NrTransport({ agent: this.agent })
42
- // Only handle exceptions if the logger has user-configured transports.
43
- // This prevents the default logger (created during require('winston')
44
- // module init with no transports) from double-handling exceptions.
45
- if (logger.transports.length === 0) {
46
- nrTransport.handleExceptions = false
49
+ // assigning `transport.handleExceptions` adds a global `process.on('uncaughtException')`
50
+ // handler. Doing it for more than one NrTransport instance will cause duplicate log lines
51
+ if (this.handlingExceptions === false && logger.transports.length > 0) {
52
+ // See: https://github.com/winstonjs/winston#handling-uncaught-exceptions-with-winston
53
+ nrTransport.handleExceptions = true
54
+ this.handlingExceptions = true
47
55
  }
48
56
  logger.add(nrTransport)
49
57
  }
@@ -14,12 +14,15 @@ const { truncate } = require('../../util/application-logging')
14
14
  *
15
15
  * Note*: This copies the log line so no other transports will get the
16
16
  * mutated data.
17
+ *
18
+ * @property {string} name name of the transport
19
+ * @property {Agent} agent the instance of agent
20
+ * @property {object} config the agent configuration
21
+ * @property {boolean} handleExceptions set in `lib/subscribers/winston/configure.js` for the first NrTransport instance
22
+ * to ensure that only one is handling uncaught exceptions in winston transports
17
23
  */
18
24
  class NrTransport extends TransportStream {
19
25
  constructor(opts = {}) {
20
- // set this option to have winston handle uncaught exceptions
21
- // See: https://github.com/winstonjs/winston#handling-uncaught-exceptions-with-winston
22
- opts.handleExceptions = true
23
26
  super(opts)
24
27
  this.name = 'newrelic'
25
28
  this.agent = opts.agent
@@ -72,10 +72,9 @@ class TraceContext {
72
72
  /**
73
73
  * Creates a W3C TraceContext tracestate header payload.
74
74
  *
75
- * @param {object} spanContext if passed in, it'll use this to construct tracestate. only used in otel bridge mode.
76
75
  * @returns {string} tracestate, a hyphen-delimited string of trace information fields
77
76
  */
78
- createTracestate(spanContext) {
77
+ createTracestate() {
79
78
  const config = this.transaction.agent.config
80
79
  const trustedAccountKey = config.trusted_account_key
81
80
  const version = Tracestate.NR_TRACESTATE_VERSION
@@ -99,29 +98,17 @@ class TraceContext {
99
98
  return this._traceStateRaw || ''
100
99
  }
101
100
 
102
- // If no segment/span is in context, we do not send one as
103
- // we technically do not have a "span" on the agent side and
104
- // this trace data is newrelic specific.
105
- let spanId = ''
106
- if (config.span_events.enabled) {
107
- const segment = spanContext?.spanId ? { id: spanContext.spanId } : this.transaction.agent.tracer.getSegment()
108
- if (segment) {
109
- spanId = segment.id
110
- } else {
111
- logger.debug('No segment/span in context. Not sending spanId in tracestate.')
112
- }
113
- } else {
114
- logger.trace('Span events disabled. Not sending spanId in tracestate.')
115
- }
116
-
117
101
  const transactionId = config.transaction_events.enabled ? this.transaction.id : ''
118
102
  const sampled = this.transaction.sampled ? '1' : '0'
119
103
  const priority = this.transaction.priority ? this.transaction.priority.toFixed(6) : ''
120
104
  const timestamp = Date.now()
121
105
 
106
+ // As of 2026-06 we no longer emit spanId under any circumstance.
107
+ // So we omit it from the list here as this is the source
108
+ // when building the outgoing tracestate header.
122
109
  const nrTraceState =
123
110
  `${trustedAccountKey}@nr=${version}-${parentType}-${accountId}` +
124
- `-${appId}-${spanId}-${transactionId}-${sampled}-${priority}-${timestamp}`
111
+ `-${appId}--${transactionId}-${sampled}-${priority}-${timestamp}`
125
112
 
126
113
  if (this._traceStateRaw) {
127
114
  return `${nrTraceState},${this._traceStateRaw}`
@@ -151,7 +138,7 @@ class TraceContext {
151
138
 
152
139
  logger.trace('traceparent added with %s', traceParent)
153
140
 
154
- const tracestate = this.createTracestate(spanContext)
141
+ const tracestate = this.createTracestate()
155
142
  if (tracestate) {
156
143
  if (setter) {
157
144
  setter.set(headers, TRACE_CONTEXT_STATE_HEADER, tracestate)
@@ -306,6 +306,22 @@ function _maybeBindPromise({ result, full, segment }) {
306
306
  }
307
307
  }
308
308
 
309
+ /**
310
+ * Invokes a function within the scope of the given context. If the original
311
+ * function returns a promise, that promise will be bound to the correct
312
+ * segment.
313
+ *
314
+ * @param {object} params Function parameters.
315
+ * @param {Function} params.handler The original function to invoke.
316
+ * @param {AsyncContext} params.context The context for the invocation.
317
+ * @param {boolean} [params.full] When set to `true`, the provided segment
318
+ * will be started and ended automatically.
319
+ * @param {object} params.thisArg Entity to use as the `this` binding when
320
+ * invoking the original function.
321
+ * @param {*[]} params.args Array of arguments to provide to the function.
322
+ *
323
+ * @returns {*} Whatever the instrumented function returns.
324
+ */
309
325
  function runInContext({ handler, context, full, thisArg, args }) {
310
326
  const { segment } = context
311
327
  if (segment && full) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "14.1.2",
3
+ "version": "14.2.1",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -170,7 +170,7 @@
170
170
  "lint:fix": "eslint --fix .",
171
171
  "public-docs": "jsdoc -c ./jsdoc-conf.jsonc",
172
172
  "publish-docs": "./bin/publish-docs.sh",
173
- "services": "DOCKER_PLATFORM=linux/$(uname -m) docker compose up -d --wait",
173
+ "services": "./bin/start-services.sh",
174
174
  "services:start": "npm run services",
175
175
  "services:stop": "docker compose down",
176
176
  "smoke": "time borp --timeout 180000 --reporter ./test/lib/test-reporter.mjs 'test/smoke/**/*.test.js'",
@@ -223,9 +223,9 @@
223
223
  "winston-transport": "^4.5.0"
224
224
  },
225
225
  "optionalDependencies": {
226
- "@datadog/pprof": "^5.13.3",
227
- "@newrelic/fn-inspect": "^5.0.0",
228
- "@newrelic/native-metrics": "^13.0.0",
226
+ "@datadog/pprof": "^5.14.2",
227
+ "@newrelic/fn-inspect": "^5.1.0",
228
+ "@newrelic/native-metrics": "^13.1.0",
229
229
  "@prisma/prisma-fmt-wasm": "^4.17.0-16.27eb2449f178cd9fe1a4b892d732cc4795f75085"
230
230
  },
231
231
  "devDependencies": {