newrelic 13.3.2 → 13.3.3

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,23 @@
1
+ ### v13.3.3 (2025-09-22)
2
+
3
+ #### Features
4
+
5
+ * Added support for tracing callback on subscribers ([#3371](https://github.com/newrelic/node-newrelic/pull/3371)) ([e6ad52e](https://github.com/newrelic/node-newrelic/commit/e6ad52e1f897ec757e90f9fc12f592e1c6eec2fe))
6
+
7
+ #### Bug fixes
8
+
9
+ * Updated `openai` instrumentation to properly return APIPromise to avoid crashing when using `completions.parse` or `responses.parse` ([#3382](https://github.com/newrelic/node-newrelic/pull/3382)) ([c7ccf26](https://github.com/newrelic/node-newrelic/commit/c7ccf260ffc9c788bde73b43231b716199724d09))
10
+
11
+ #### Documentation
12
+
13
+ * Updated compatibility report ([#3370](https://github.com/newrelic/node-newrelic/pull/3370)) ([754c2f0](https://github.com/newrelic/node-newrelic/commit/754c2f089339268fb73a3ff7765a630ea8982d0d))
14
+
15
+ #### Miscellaneous chores
16
+
17
+ * Removed unused `loader.mjs` ([#3376](https://github.com/newrelic/node-newrelic/pull/3376)) ([66a9fd5](https://github.com/newrelic/node-newrelic/commit/66a9fd543d1a7854b7405c7b5b884d8a71773b85))
18
+ * Updated metrics for OTel Bridge enablement/disablement ([#3381](https://github.com/newrelic/node-newrelic/pull/3381)) ([b976c37](https://github.com/newrelic/node-newrelic/commit/b976c376fe187bda5bde1b8cd4b86a7de86fe4f9))
19
+ * Updated supportability metric names for OTel Bridge enablement… ([#3383](https://github.com/newrelic/node-newrelic/pull/3383)) ([c7c1d6c](https://github.com/newrelic/node-newrelic/commit/c7c1d6c942c6b53fd944e2259b42d1e7e7be483a))
20
+
1
21
  ### v13.3.2 (2025-09-10)
2
22
 
3
23
  #### Bug fixes
@@ -7885,3 +7905,4 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
7885
7905
 
7886
7906
 
7887
7907
 
7908
+
@@ -42,7 +42,7 @@ class SetupMetrics extends SetupSignal {
42
42
  const getMeter = provider.getMeter
43
43
  provider.getMeter = function nrGetMeter(...args) {
44
44
  agent.metrics
45
- .getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Metrics/getMeter')
45
+ .getOrCreateMetric('Supportability/Metrics/Nodejs/OpenTelemetryBridge/getMeter')
46
46
  .incrementCallCount()
47
47
 
48
48
  const meter = getMeter.apply(provider, args)
@@ -61,7 +61,7 @@ class SetupMetrics extends SetupSignal {
61
61
  // + createObservableUpDownCounter
62
62
  meter[method] = function nrWrappedMethod(...args) {
63
63
  agent.metrics
64
- .getOrCreateMetric(`Supportability/Nodejs/OpenTelemetryBridge/Metrics/meter/${method}`)
64
+ .getOrCreateMetric(`Supportability/Metrics/Nodejs/OpenTelemetryBridge/meter/${method}`)
65
65
  .incrementCallCount()
66
66
  return originals[method].apply(meter, args)
67
67
  }
@@ -73,7 +73,7 @@ class SetupMetrics extends SetupSignal {
73
73
  this.coreApi.metrics.setGlobalMeterProvider(provider)
74
74
 
75
75
  agent.metrics
76
- .getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Metrics')
76
+ .getOrCreateMetric('Supportability/Metrics/Nodejs/OpenTelemetryBridge/enabled')
77
77
  .incrementCallCount()
78
78
 
79
79
  // We need access to `agent.config.entity_guid` in order to attach metrics
package/lib/otel/setup.js CHANGED
@@ -41,6 +41,9 @@ function setupOtel(agent, logger = defaultLogger) {
41
41
  signals.push(signal)
42
42
  } else {
43
43
  logger.debug('`opentelemetry_bridge.traces` is not enabled, skipping')
44
+ agent.metrics
45
+ .getOrCreateMetric('Supportability/Tracing/Nodejs/OpenTelemetryBridge/disabled')
46
+ .incrementCallCount()
44
47
  }
45
48
 
46
49
  if (agent.config.opentelemetry_bridge.metrics.enabled === true) {
@@ -48,6 +51,9 @@ function setupOtel(agent, logger = defaultLogger) {
48
51
  signals.push(signal)
49
52
  } else {
50
53
  logger.debug('`opentelemetry_bridge.metrics` is not enabled, skipping')
54
+ agent.metrics
55
+ .getOrCreateMetric('Supportability/Metrics/Nodejs/OpenTelemetryBridge/disabled')
56
+ .incrementCallCount()
51
57
  }
52
58
 
53
59
  if (agent.config.opentelemetry_bridge.logs.enabled === true) {
@@ -28,7 +28,7 @@ class SetupTraces extends SetupSignal {
28
28
  this.coreApi.trace.setGlobalTracerProvider(traceProvider)
29
29
 
30
30
  agent.metrics
31
- .getOrCreateMetric('Supportability/Nodejs/OpenTelemetryBridge/Traces')
31
+ .getOrCreateMetric('Supportability/Tracing/Nodejs/OpenTelemetryBridge/enabled')
32
32
  .incrementCallCount()
33
33
  }
34
34
 
@@ -8,6 +8,12 @@
8
8
  const { tracingChannel } = require('node:diagnostics_channel')
9
9
  const semver = require('semver')
10
10
  const NAMES = require('../metrics/names')
11
+ // Used for the `traceCallback` work.
12
+ // This can be removed when we add true support into orchestrion
13
+ const makeCall = (fn) => (...args) => fn.call(...args)
14
+ const ArrayPrototypeAt = makeCall(Array.prototype.at)
15
+ const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
16
+ // End temp work
11
17
 
12
18
  /**
13
19
  * The baseline parameters available to all subscribers.
@@ -15,9 +21,8 @@ const NAMES = require('../metrics/names')
15
21
  * @typedef {object} SubscriberParams
16
22
  * @property {object} agent A New Relic Node.js agent instance.
17
23
  * @property {object} logger An agent logger instance.
18
- * @property {string} packageName The npm installable name for the package
19
- * being instrumented. This is what a developer would provide to the `require`
20
- * function.
24
+ * @property {string} packageName The package name being instrumented.
25
+ * This is what a developer would provide to the `require` function.
21
26
  * @property {string} channelName A unique name for the diagnostics channel
22
27
  * that will be created and monitored.
23
28
  */
@@ -44,6 +49,7 @@ const NAMES = require('../metrics/names')
44
49
  * @property {string} id A unique identifier for the subscriber, combining the prefix, package name, and channel name.
45
50
  * @property {TracingChannel} channel The tracing channel instance this subscriber will be monitoring.
46
51
  * @property {AsyncLocalStorage} store The async local storage instance used for context management.
52
+ * @property {number} callback position of callback if it needs to be wrapped for instrumentation. -1 means last argument
47
53
  */
48
54
  class Subscriber {
49
55
  constructor({ agent, logger, packageName, channelName }) {
@@ -60,6 +66,7 @@ class Subscriber {
60
66
  this.id = `${this.prefix}${this.packageName}:${this.channelName}`
61
67
  this.channel = tracingChannel(this.id)
62
68
  this.store = agent.tracer._contextManager._asyncLocalStorage
69
+ this.callback = null
63
70
  }
64
71
 
65
72
  shouldCreateSegment(parent) {
@@ -68,6 +75,49 @@ class Subscriber {
68
75
  )
69
76
  }
70
77
 
78
+ /**
79
+ * Note: This is a temporary patch until we can get the correct implementation
80
+ * of `tracingChannel.traceCallback` into orchestrion-js.
81
+ *
82
+ * This will wrap a callback at a given position and reassign the callback argument to the wrapped one
83
+ *
84
+ * @param {number} position index of the callback, you can specify -1 to be the last
85
+ * @param {object} context the event passed to the tracing channel hooks
86
+ */
87
+ traceCallback(position, context) {
88
+ this.logger.trace('Wrapping the callback at position %s', position)
89
+ const { asyncStart, asyncEnd, error } = this.channel
90
+ function wrappedCallback(err, res) {
91
+ // assigning a boolean to the context so we know that the
92
+ // `error`, `asyncStart`, and `asyncEnd` are coming from the wrapped callback
93
+ context.callback = true
94
+ if (err) {
95
+ context.error = err
96
+ error.publish(context)
97
+ } else {
98
+ context.result = res
99
+ }
100
+
101
+ // Using runStores here enables manual context failure recovery
102
+ asyncStart.runStores(context, () => {
103
+ try {
104
+ if (callback) {
105
+ return Reflect.apply(callback, this, arguments)
106
+ }
107
+ } finally {
108
+ asyncEnd.publish(context)
109
+ }
110
+ })
111
+ }
112
+
113
+ const callback = ArrayPrototypeAt(context.arguments, position)
114
+ if (typeof callback !== 'function') {
115
+ this.logger.trace('Callback is not present, not wrapping')
116
+ } else {
117
+ ArrayPrototypeSplice(context.arguments, position, 1, wrappedCallback)
118
+ }
119
+ }
120
+
71
121
  /**
72
122
  * Creates the `Supportability/Features/Instrumentation/OnRequire/<packageName>`
73
123
  * and `Supportability/Features/Instrumentation/OnRequire/<packageName>/Version/<majorVersion>`
@@ -123,12 +173,12 @@ class Subscriber {
123
173
  segment.opaque = this.opaque
124
174
  segment.shimId = this.packageName
125
175
  segment.start()
126
- this.logger.trace('Created segment %s', name)
176
+ this.logger.trace('Created segment %s, returning new context', name)
127
177
  this.addAttributes(segment)
128
178
  const newCtx = ctx.enterSegment({ segment })
129
179
  return newCtx
130
180
  } else {
131
- this.logger.trace('Failed to create segment for %s', name)
181
+ this.logger.trace('Failed to create segment for %s, returning existing context', name)
132
182
  return ctx
133
183
  }
134
184
  }
@@ -144,6 +194,7 @@ class Subscriber {
144
194
 
145
195
  /**
146
196
  * Checks if the subscriber is enabled based on the agent's configuration.
197
+ * @returns boolean if subscriber is enabled
147
198
  */
148
199
  get enabled() {
149
200
  return this.config.instrumentation[this.packageName].enabled === true
@@ -156,14 +207,27 @@ class Subscriber {
156
207
  */
157
208
  enable() {
158
209
  this.channel.start.bindStore(this.store, (data) => {
210
+ // only wrap the callback if a subscriber has a callback property defined
211
+ if (this.callback !== null) {
212
+ this.traceCallback(this.callback, data)
213
+ }
159
214
  this.trackInstrumentationUsage(data?.moduleVersion)
160
215
  const ctx = this.agent.tracer.getContext()
161
216
  if (this.requireActiveTx && !ctx?.transaction?.isActive()) {
162
- this.logger.debug('Not recording event for %s, transaction is not active', this.package)
217
+ this.logger.trace('Not recording event for %s, transaction is not active', this.package)
163
218
  return ctx
164
219
  }
165
220
 
166
- return this.handler(data, ctx)
221
+ const result = this.handler(data, ctx)
222
+ // we cannot rely on the context manager to obtain the active segment
223
+ // in the `asyncStart` and `asyncEnd` events. This is because other instrumented
224
+ // functions are being executed at times. so we assign the active segment on the data
225
+ // so it can be used later to properly touch the segment in `asyncStart` and `asyncEnd`
226
+ if (this.callback !== null) {
227
+ data.segment = result?.segment
228
+ this.logger.trace('Adding segment %s to event context', data?.segment?.name)
229
+ }
230
+ return result
167
231
  })
168
232
  }
169
233
 
@@ -174,13 +238,41 @@ class Subscriber {
174
238
  this.channel.start.unbindStore(this.store)
175
239
  }
176
240
 
241
+ /**
242
+ * This should only be used for callback based functions to touch the segment for the function
243
+ * that implements a callback.
244
+ * @param {object} data event passed to asyncStart hook
245
+ */
246
+ asyncStart(data) {
247
+ const ctx = this.agent.tracer.getContext()
248
+ if (data.callback !== true || this.internal === true || (this.requireActiveTx && !ctx?.transaction?.isActive())) {
249
+ this.logger.trace('Not touching parent in asyncStart for %s, transaction is not active? %s, segment is internal? %s, or no callback to bind', this.id, ctx?.transaction?.isActive(), this.internal)
250
+ return
251
+ }
252
+
253
+ this.logger.trace('touching segment %s, in asyncStart', data?.segment?.name)
254
+ data?.segment?.touch()
255
+ }
256
+
177
257
  /**
178
258
  * Common handler for when async events end.
179
259
  * It gets the context and touches the segment if it exists.
260
+ * @param {object} data event passed to asyncEnd hook
180
261
  */
181
- asyncEnd() {
262
+ asyncEnd(data) {
182
263
  const ctx = this.agent.tracer.getContext()
183
- ctx?.segment?.touch()
264
+ if (this.internal === true) {
265
+ this.logger.trace('asyncEnd occurring for %s internal event, not touching segment', this.id)
266
+ return
267
+ }
268
+
269
+ if (data?.callback === true) {
270
+ this.logger.trace('touching callback segment %s, in asyncEnd', data?.segment?.name)
271
+ data?.segment?.touch()
272
+ } else {
273
+ this.logger.trace('touching segment %s, in asyncEnd', ctx?.segment?.name)
274
+ ctx?.segment?.touch()
275
+ }
184
276
  }
185
277
 
186
278
  /*
@@ -23,6 +23,7 @@ const MIN_STREAM_VERSION = '4.12.2'
23
23
  class OpenAIChatCompletions extends OpenAISubscriber {
24
24
  constructor({ agent, logger, channelName = 'nr_completionsCreate' }) {
25
25
  super({ agent, logger, channelName })
26
+ this.events = ['asyncEnd', 'end']
26
27
  }
27
28
 
28
29
  handler(data, ctx) {
@@ -42,6 +43,32 @@ class OpenAIChatCompletions extends OpenAISubscriber {
42
43
  return newCtx
43
44
  }
44
45
 
46
+ /**
47
+ * Temporary fix as `tracePromise` wraps the promise in a native one.
48
+ * We are now wrapping `openai.chat.completions.parse` in a traceSync call
49
+ * and then wrapping the promise here so it returns the custom promise.
50
+ * OpenAI has a [custom promise](https://github.com/openai/openai-node/blob/master/src/core/api-promise.ts) that crashes applications using `openai.chat.completions.parse`
51
+ * see: https://github.com/newrelic/node-newrelic/issues/3379
52
+ * see: https://github.com/nodejs/node/issues/59936
53
+ * @param data
54
+ */
55
+ end(data) {
56
+ const promise = data?.result
57
+ if (!promise.then) {
58
+ return promise
59
+ }
60
+
61
+ return promise.then((result) => {
62
+ data.result = result
63
+ this.channel.asyncEnd.publish(data)
64
+ return result
65
+ }).catch((err) => {
66
+ data.error = err
67
+ this.channel.asyncEnd.publish(data)
68
+ return err
69
+ })
70
+ }
71
+
45
72
  asyncEnd(data) {
46
73
  const ctx = this.agent.tracer.getContext()
47
74
  if (!ctx?.segment || !ctx?.transaction) {
@@ -16,7 +16,7 @@ module.exports = {
16
16
  functionQuery: {
17
17
  className: 'Completions',
18
18
  methodName: 'create',
19
- kind: 'Async'
19
+ kind: 'Sync'
20
20
  }
21
21
  },
22
22
  {
@@ -25,7 +25,7 @@ module.exports = {
25
25
  functionQuery: {
26
26
  className: 'Completions',
27
27
  methodName: 'create',
28
- kind: 'Async'
28
+ kind: 'Sync'
29
29
  }
30
30
  }
31
31
  ]
@@ -39,7 +39,7 @@ module.exports = {
39
39
  functionQuery: {
40
40
  className: 'Responses',
41
41
  methodName: 'create',
42
- kind: 'Async'
42
+ kind: 'Sync'
43
43
  }
44
44
  }
45
45
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "13.3.2",
3
+ "version": "13.3.3",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [