newrelic 13.15.0 → 13.16.0

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.
package/NEWS.md CHANGED
@@ -1,3 +1,22 @@
1
+ ### v13.16.0 (2026-03-11)
2
+
3
+ #### Bug fixes
4
+
5
+ * Updated `@apm-js-collab/tracing-hooks` to `0.5.0`, to allow instrumentation on windows environments. ([#3819](https://github.com/newrelic/node-newrelic/pull/3819)) ([d8f6d0f](https://github.com/newrelic/node-newrelic/commit/d8f6d0f6ac7f3ff17a667391269c41e6e57dc2fb))
6
+ * This version of `@apm-js-collab/tracing-hooks` includes the windows path fix for `@apm-js-collab/code-transformer`, [see](https://github.com/apm-js-collab/tracing-hooks/issues/19).
7
+
8
+ #### Code refactoring
9
+
10
+ * Updated `redis` v3 instrumentation to subscribe to events emitted ([#3802](https://github.com/newrelic/node-newrelic/pull/3802)) ([4d18302](https://github.com/newrelic/node-newrelic/commit/4d1830237c91d3e60c6b4e898f2c82ff131ebd57))
11
+
12
+ #### Documentation
13
+
14
+ * Updated compatibility report ([#3813](https://github.com/newrelic/node-newrelic/pull/3813)) ([27cb0a5](https://github.com/newrelic/node-newrelic/commit/27cb0a5e7a4c6047cce54483bd18d1337023efd3))
15
+
16
+ #### Tests
17
+
18
+ * Add `redis-client` and `node-redis-client` segment duration timing tests ([#3810](https://github.com/newrelic/node-newrelic/pull/3810)) ([f07bcfe](https://github.com/newrelic/node-newrelic/commit/f07bcfecf06453c846e12f030fbf7d8c055da139))
19
+
1
20
  ### v13.15.0 (2026-03-09)
2
21
 
3
22
  #### Features
@@ -8431,6 +8450,8 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
8431
8450
 
8432
8451
 
8433
8452
 
8453
+
8454
+
8434
8455
 
8435
8456
 
8436
8457
 
@@ -25,7 +25,6 @@ module.exports = function instrumentations() {
25
25
  mongodb: { type: InstrumentationDescriptor.TYPE_DATASTORE },
26
26
  next: { module: './instrumentation/nextjs' },
27
27
  q: { type: null },
28
- redis: { type: InstrumentationDescriptor.TYPE_DATASTORE },
29
28
  restify: { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
30
29
  when: { module: './instrumentation/when' },
31
30
  winston: { type: InstrumentationDescriptor.TYPE_GENERIC }
@@ -27,6 +27,7 @@ const subscribers = {
27
27
  ...require('./subscribers/openai/config'),
28
28
  ...require('./subscribers/pino/config'),
29
29
  ...require('./subscribers/pg/config'),
30
+ ...require('./subscribers/redis/config'),
30
31
  ...require('./subscribers/redis-client/config'),
31
32
  ...require('./subscribers/undici/config')
32
33
  }
@@ -42,6 +42,9 @@ const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
42
42
  * @property {AsyncLocalStorage} store The async local storage instance used for context management.
43
43
  * @property {number} [callback=null] Position of callback if it needs to be wrapped for instrumentation.
44
44
  * -1 means last argument.
45
+ * @property {string} [callbackKey=null] Property name on the argument at `callback` position
46
+ * that holds the actual callback function. Used when the callback is not a direct argument but a
47
+ * property on an object argument (e.g. `args[0].callback` when callback=0 and callbackKey='callback').
45
48
  * @property {string[]} [events=[]] Set of tracing channel event names to
46
49
  * register handlers for. For any name in the set, a corresponding method
47
50
  * must exist on the subscriber instance. The method will be passed the
@@ -88,6 +91,7 @@ class Subscriber {
88
91
  this.channel = tracingChannel(this.id)
89
92
  this.store = agent.tracer._contextManager._asyncLocalStorage
90
93
  this.callback = null
94
+ this.callbackKey = null
91
95
  }
92
96
 
93
97
  shouldCreateSegment(parent) {
@@ -103,11 +107,23 @@ class Subscriber {
103
107
  * This will wrap a callback at a given position and reassign the callback argument to the wrapped one
104
108
  *
105
109
  * @param {number} position index of the callback, you can specify -1 to be the last
110
+ * @param {string} [key] property name to look up on the argument at `position` when the
111
+ * callback is not a direct argument but a property on an object argument
112
+ * (e.g. `args[0].callback` when position=0 and key='callback')
106
113
  * @param {object} context the event passed to the tracing channel hooks
107
114
  */
108
- traceCallback(position, context) {
115
+ traceCallback(position, key, context) {
109
116
  this.logger.trace('Wrapping the callback at position %s', position)
110
117
  const { asyncStart, asyncEnd, error } = this.channel
118
+
119
+ const holder = ArrayPrototypeAt(context.arguments, position)
120
+ const callback = key ? holder?.[key] : holder
121
+
122
+ if (typeof callback !== 'function') {
123
+ this.logger.trace('Callback is not present, not wrapping')
124
+ return
125
+ }
126
+
111
127
  function wrappedCallback(err, res) {
112
128
  // assigning a boolean to the context so we know that the
113
129
  // `error`, `asyncStart`, and `asyncEnd` are coming from the wrapped callback
@@ -133,9 +149,8 @@ class Subscriber {
133
149
  })
134
150
  }
135
151
 
136
- const callback = ArrayPrototypeAt(context.arguments, position)
137
- if (typeof callback !== 'function') {
138
- this.logger.trace('Callback is not present, not wrapping')
152
+ if (key) {
153
+ context.arguments[position][key] = wrappedCallback
139
154
  } else {
140
155
  ArrayPrototypeSplice(context.arguments, position, 1, wrappedCallback)
141
156
  }
@@ -254,7 +269,7 @@ class Subscriber {
254
269
 
255
270
  // only wrap the callback if a subscriber has a callback property defined
256
271
  if (this.callback !== null) {
257
- this.traceCallback(this.callback, data)
272
+ this.traceCallback(this.callback, this.callbackKey, data)
258
273
  }
259
274
  const ctx = this.agent.tracer.getContext()
260
275
  if (this.requireActiveTx && !ctx?.transaction?.isActive()) {
@@ -0,0 +1,23 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const internalSendCommand = {
7
+ path: './redis/internal-send-command',
8
+ instrumentations: [{
9
+ channelName: 'nr_internalSendCommand',
10
+ module: { name: 'redis', versionRange: '>=2.6.0 <4', filePath: 'index.js' },
11
+ functionQuery: {
12
+ // RedisClient.prototype.internal_send_command
13
+ expressionName: 'internal_send_command',
14
+ kind: 'Sync'
15
+ }
16
+ }]
17
+ }
18
+
19
+ module.exports = {
20
+ redis: [
21
+ internalSendCommand
22
+ ]
23
+ }
@@ -0,0 +1,60 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const DbOperationSubscriber = require('../db-operation')
7
+ const stringify = require('json-stringify-safe')
8
+
9
+ module.exports = class RedisInternalSendCommandSubscriber extends DbOperationSubscriber {
10
+ constructor({ agent, logger }) {
11
+ super({ agent, logger, packageName: 'redis', channelName: 'nr_internalSendCommand', system: 'Redis' })
12
+ this.events = ['asyncEnd', 'end']
13
+ this.propagateContext = true
14
+ this.callback = 0
15
+ this.callbackKey = 'callback'
16
+ }
17
+
18
+ handler(data, ctx) {
19
+ const { arguments: args, self: client } = data
20
+ const commandObject = args[0]
21
+ const keys = commandObject.args
22
+ this.parameters = this.#getInstanceParameters(client, keys)
23
+ this.operation = commandObject.command || 'other'
24
+
25
+ return super.handler(data, ctx)
26
+ }
27
+
28
+ /**
29
+ * Captures the necessary datastore parameters from redis v3 client
30
+ *
31
+ * @param {object} client instance of redis v3 client
32
+ * @param {string[]} keys list of keys
33
+ * @returns {object} datastore parameters: host, port_path_or_id, database_name, product, key
34
+ */
35
+ #getInstanceParameters(client, keys) {
36
+ const opts = client?.connection_options
37
+
38
+ return {
39
+ host: opts.host || 'localhost',
40
+ port_path_or_id: opts.path || opts.port || '6379',
41
+ database_name: client.selected_db || opts.db || 0,
42
+ product: this.system,
43
+ key: this.#stringifyKeys(keys)
44
+ }
45
+ }
46
+
47
+ #stringifyKeys(keys) {
48
+ let key = null
49
+ if (keys && keys.length) {
50
+ try {
51
+ key = stringify(keys[0])
52
+ } catch (err) {
53
+ this.logger.debug(err, 'Failed to stringify redis key for send command')
54
+ key = '<unknown>'
55
+ }
56
+ }
57
+
58
+ return key
59
+ }
60
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "13.15.0",
3
+ "version": "13.16.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -202,7 +202,7 @@
202
202
  "#test/assert": "./test/lib/custom-assertions/index.js"
203
203
  },
204
204
  "dependencies": {
205
- "@apm-js-collab/tracing-hooks": "^0.4.0",
205
+ "@apm-js-collab/tracing-hooks": "^0.5.0",
206
206
  "@grpc/grpc-js": "^1.13.2",
207
207
  "@grpc/proto-loader": "^0.7.5",
208
208
  "@newrelic/security-agent": "^3.0.0",
@@ -1,104 +0,0 @@
1
- /*
2
- * Copyright 2020 New Relic Corporation. All rights reserved.
3
- * SPDX-License-Identifier: Apache-2.0
4
- */
5
-
6
- 'use strict'
7
-
8
- const stringify = require('json-stringify-safe')
9
- const {
10
- OperationSpec,
11
- params: { DatastoreParameters }
12
- } = require('../shim/specs')
13
-
14
- module.exports = function initialize(_agent, redis, _moduleName, shim) {
15
- const proto = redis?.RedisClient?.prototype
16
- if (!proto) {
17
- // This will happen when redis >= 4.0.0
18
- shim.logger.warn('Skipping redis instrumentation due to unrecognized module shape')
19
- return false
20
- }
21
-
22
- shim.setDatastore(shim.REDIS)
23
-
24
- if (!proto.internal_send_command) {
25
- shim.logger.warn(
26
- 'New Relic Node.js agent no longer supports redis < 2.6.0, current version %s. Please downgrade to v11 for support, if needed',
27
- shim.pkgVersion
28
- )
29
- return
30
- }
31
-
32
- registerInternalSendCommand(shim, proto)
33
- }
34
-
35
- /**
36
- * Instrumentation used in versions of redis >= 2.6.0 < 4 to record all redis commands
37
- *
38
- * @param {Shim} shim instance of shim
39
- * @param {object} proto RedisClient prototype
40
- */
41
- function registerInternalSendCommand(shim, proto) {
42
- shim.recordOperation(
43
- proto,
44
- 'internal_send_command',
45
- function wrapInternalSendCommand(shim, _, __, args) {
46
- const commandObject = args[0]
47
- const keys = commandObject.args
48
- const parameters = getInstanceParameters(this)
49
-
50
- parameters.key = stringifyKeys(shim, keys)
51
-
52
- return new OperationSpec({
53
- name: commandObject.command || 'other',
54
- parameters,
55
- callback: function bindCallback(shim, _f, _n, segment) {
56
- if (shim.isFunction(commandObject.callback)) {
57
- shim.bindCallbackSegment(null, commandObject, 'callback', segment)
58
- } else {
59
- const self = this
60
- commandObject.callback = shim.bindSegment(
61
- function NRCallback(err) {
62
- if (err && self.emit instanceof Function) {
63
- self.emit('error', err)
64
- }
65
- },
66
- segment,
67
- true
68
- )
69
- }
70
- }
71
- })
72
- }
73
- )
74
- }
75
-
76
- function stringifyKeys(shim, keys) {
77
- let key = null
78
- if (keys && keys.length && !shim.isFunction(keys)) {
79
- try {
80
- key = stringify(keys[0])
81
- } catch (err) {
82
- shim.logger.debug(err, 'Failed to stringify redis key for send command')
83
- key = '<unknown>'
84
- }
85
- }
86
-
87
- return key
88
- }
89
-
90
- /**
91
- * Captures the necessary datastore parameters from redis client
92
- *
93
- * @param {object} client instance of redis client
94
- * @returns {object} datastore parameters
95
- */
96
- function getInstanceParameters(client = {}) {
97
- const opts = client?.connection_options
98
-
99
- return new DatastoreParameters({
100
- host: opts.host || 'localhost',
101
- port_path_or_id: opts.path || opts.port || '6379',
102
- database_name: client.selected_db || opts.db || 0
103
- })
104
- }