newrelic 13.9.1 → 13.10.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,14 +1,58 @@
1
- ### v13.9.1 (2026-01-12)
2
-
3
- #### Bug fixes
4
-
5
- * Updated `getHostnameSafe` to invalidate the cache is trying to assign the host based on the gcp cloud run id ([#3650](https://github.com/newrelic/node-newrelic/pull/3650)) ([d395c76](https://github.com/newrelic/node-newrelic/commit/d395c76d5612a7e562efa1a807c35e805a38ab14))
6
- * Updated logic to properly assign content and role in LangChain chat completion messages ([#3638](https://github.com/newrelic/node-newrelic/pull/3638)) ([407bcb6](https://github.com/newrelic/node-newrelic/commit/407bcb66b7e3be1b6f1d28bcf6e3b022a7022c96))
7
-
8
- #### Documentation
9
-
10
- * Updated compatibility report ([#3643](https://github.com/newrelic/node-newrelic/pull/3643)) ([8929ab3](https://github.com/newrelic/node-newrelic/commit/8929ab3ae6cc28edbef4bee41a2084154361d6e0))
11
-
1
+ ### v13.10.0 (2026-01-20)
2
+
3
+ #### Features
4
+
5
+ * Add `timestamp` to OpenAI input `LlmChatCompletionMessage`s ([#3657](https://github.com/newrelic/node-newrelic/pull/3657)) ([099e20e](https://github.com/newrelic/node-newrelic/commit/099e20eaa380615b7e8aae7eaa12e2754a931041))
6
+ * Added reduced tracing mode for span links ([#3649](https://github.com/newrelic/node-newrelic/pull/3649)) ([2414331](https://github.com/newrelic/node-newrelic/commit/24143313e2d894a5e11e42116f33ef4be18f2fb4))
7
+
8
+ #### Bug fixes
9
+
10
+ * Updated OpenAI instrumentation to properly parse headers when a call fails ([#3665](https://github.com/newrelic/node-newrelic/pull/3665)) ([641b7a8](https://github.com/newrelic/node-newrelic/commit/641b7a8c8015855d82574e8173cb6e9fd082b4a0))
11
+
12
+ #### Documentation
13
+
14
+ * Updated compatibility report ([#3669](https://github.com/newrelic/node-newrelic/pull/3669)) ([ec922ae](https://github.com/newrelic/node-newrelic/commit/ec922ae6b0cecfa4ecbce86a789c308d6876ecd7)) ([#3668](https://github.com/newrelic/node-newrelic/pull/3668)) ([85c601f](https://github.com/newrelic/node-newrelic/commit/85c601fe2eaaacd82c0abac81eba96ce6718f2f1)) ([#3663](https://github.com/newrelic/node-newrelic/pull/3663)) ([82b3ac8](https://github.com/newrelic/node-newrelic/commit/82b3ac8cf6f69d0609b7fbaaf8be2a35f6552485))
15
+
16
+ #### Miscellaneous chores
17
+
18
+ * Lazy load OTEL dependencies ([#3667](https://github.com/newrelic/node-newrelic/pull/3667)) ([0b0c764](https://github.com/newrelic/node-newrelic/commit/0b0c7647f3f0bc3093b9ffbf7596c1654ba6113d))
19
+ * Record subscriber usage metric ([#3626](https://github.com/newrelic/node-newrelic/pull/3626)) ([5796574](https://github.com/newrelic/node-newrelic/commit/57965740acd053e6c99a88d861a876bcf2601c07))
20
+ * Update `ai-support.json` ([#3666](https://github.com/newrelic/node-newrelic/pull/3666)) ([562b403](https://github.com/newrelic/node-newrelic/commit/562b403a3414430cdd0afab261d3d119d3d9b4db))
21
+ * Updated OpenAI instrumentation to skip creating a `LlmChatCompletionMessage` for an outgoing tool call response ([#3655](https://github.com/newrelic/node-newrelic/pull/3655)) ([572c3a1](https://github.com/newrelic/node-newrelic/commit/572c3a1741ebd13d77d857c262b050aeb0c801fc))
22
+
23
+ #### Tests
24
+
25
+ * Updated mock response to avoid retries on OpenAI LangChain vectorstore calls ([#3664](https://github.com/newrelic/node-newrelic/pull/3664)) ([bc1faf3](https://github.com/newrelic/node-newrelic/commit/bc1faf30654d920e31ad4ddc205560e56b6c796c))
26
+
27
+ ### v13.9.2 (2026-01-14)
28
+
29
+ #### Bug fixes
30
+
31
+ * Updated message consumer subscribers to properly time the consumption actions ([#3660](https://github.com/newrelic/node-newrelic/pull/3660)) ([ef1b611](https://github.com/newrelic/node-newrelic/commit/ef1b611a7c3b45f5fc8d483661859c1595bbe4ab))
32
+
33
+ #### Documentation
34
+
35
+ * Updated compatibility report ([#3654](https://github.com/newrelic/node-newrelic/pull/3654)) ([ca16dae](https://github.com/newrelic/node-newrelic/commit/ca16daefe4da1c17253e165c68d198d607c72d0b))
36
+
37
+ #### Miscellaneous chores
38
+
39
+ * Update OTEL metrics test ([#3661](https://github.com/newrelic/node-newrelic/pull/3661)) ([ae84b62](https://github.com/newrelic/node-newrelic/commit/ae84b62a3c7ef753d4ace8836f38405b4cbbfd88))
40
+
41
+ #### Tests
42
+
43
+ * Added cross agent tests for sampler configuration and sampling rate scenarios ([#3648](https://github.com/newrelic/node-newrelic/pull/3648)) ([fa0e2d1](https://github.com/newrelic/node-newrelic/commit/fa0e2d1bd78051c7f716ebc65bd7f3842698103e))
44
+
45
+ ### v13.9.1 (2026-01-12)
46
+
47
+ #### Bug fixes
48
+
49
+ * Updated `getHostnameSafe` to invalidate the cache is trying to assign the host based on the gcp cloud run id ([#3650](https://github.com/newrelic/node-newrelic/pull/3650)) ([d395c76](https://github.com/newrelic/node-newrelic/commit/d395c76d5612a7e562efa1a807c35e805a38ab14))
50
+ * Updated logic to properly assign content and role in LangChain chat completion messages ([#3638](https://github.com/newrelic/node-newrelic/pull/3638)) ([407bcb6](https://github.com/newrelic/node-newrelic/commit/407bcb66b7e3be1b6f1d28bcf6e3b022a7022c96))
51
+
52
+ #### Documentation
53
+
54
+ * Updated compatibility report ([#3643](https://github.com/newrelic/node-newrelic/pull/3643)) ([8929ab3](https://github.com/newrelic/node-newrelic/commit/8929ab3ae6cc28edbef4bee41a2084154361d6e0))
55
+
12
56
  ### v13.9.0 (2026-01-08)
13
57
 
14
58
  #### Features
@@ -8214,3 +8258,5 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
8214
8258
 
8215
8259
 
8216
8260
 
8261
+
8262
+
@@ -26,13 +26,18 @@ module.exports = class LlmChatCompletionMessage extends LlmEvent {
26
26
  // Check if the given message is from the response.
27
27
  // The response object differs based on the API called.
28
28
  // If it's `responses.create`, we check against `response.output`.
29
- // If it's `chat.completions.create` or langchain, we check against `response.choices`.
29
+ // If it's `chat.completions.create`, we check against `response.choices`.
30
30
  if (response?.object === 'response') {
31
31
  this.is_response = message.content === response?.output?.[0]?.content?.[0]?.text
32
32
  } else {
33
33
  this.is_response = message.content === response?.choices?.[0]?.message?.content
34
34
  }
35
35
 
36
+ // Spec 771: `timestamp` is only required for input/request messages.
37
+ if (this.is_response === false) {
38
+ this.timestamp = segment.timer.start
39
+ }
40
+
36
41
  // Assign content to the event.
37
42
  const content = message?.content
38
43
  if (agent.config.ai_monitoring.record_content.enabled === true) {
@@ -353,7 +353,8 @@ const FEATURES = {
353
353
  SOURCE_MAPS: `${SUPPORTABILITY.FEATURES}/EnableSourceMaps`,
354
354
  CERTIFICATES: SUPPORTABILITY.FEATURES + '/Certificates',
355
355
  INSTRUMENTATION: {
356
- ON_REQUIRE: SUPPORTABILITY.FEATURES + '/Instrumentation/OnRequire'
356
+ ON_REQUIRE: SUPPORTABILITY.FEATURES + '/Instrumentation/OnRequire',
357
+ SUBSCRIBER_USED: SUPPORTABILITY.FEATURES + '/Instrumentation/SubscriberUsed'
357
358
  }
358
359
  }
359
360
 
package/lib/otel/setup.js CHANGED
@@ -5,16 +5,11 @@
5
5
 
6
6
  'use strict'
7
7
 
8
- const opentelemetry = require('@opentelemetry/api')
9
-
10
- const SetupLogs = require('./logs/index.js')
11
- const SetupMetrics = require('./metrics/index.js')
12
- const SetupTraces = require('./traces/index.js')
13
- const ContextManager = require('./context-manager')
14
- const TracePropagator = require('./trace-propagator')
8
+ // The majority of dependencies in this module are lazy loaded in order to
9
+ // limit the impact on memory usage. In general, the OTEL dependencies are
10
+ // only loaded when they are needed, because they can have a significant
11
+ // impact once loaded.
15
12
  const defaultLogger = require('../logger').child({ component: 'opentelemetry-bridge' })
16
- const createOtelLogger = require('./logger')
17
- const interceptSpanKey = require('./span-key-interceptor')
18
13
 
19
14
  const signals = []
20
15
 
@@ -26,6 +21,12 @@ function setupOtel(agent, logger = defaultLogger) {
26
21
  return
27
22
  }
28
23
 
24
+ const opentelemetry = require('@opentelemetry/api')
25
+ const ContextManager = require('./context-manager')
26
+ const TracePropagator = require('./trace-propagator')
27
+ const createOtelLogger = require('./logger')
28
+ const interceptSpanKey = require('./span-key-interceptor')
29
+
29
30
  // When bridge mode is enabled, our context manager must utilize the OTEL
30
31
  // context manager. Otherwise, traces within, e.g. logs, will not be
31
32
  // captured correctly. The internal OTEL logger also must be configured
@@ -37,6 +38,7 @@ function setupOtel(agent, logger = defaultLogger) {
37
38
  opentelemetry.propagation.setGlobalPropagator(new TracePropagator(agent))
38
39
 
39
40
  if (agent.config.opentelemetry.traces.enabled === true) {
41
+ const SetupTraces = require('./traces/index.js')
40
42
  const signal = new SetupTraces({ agent })
41
43
  signals.push(signal)
42
44
  } else {
@@ -47,6 +49,7 @@ function setupOtel(agent, logger = defaultLogger) {
47
49
  }
48
50
 
49
51
  if (agent.config.opentelemetry.metrics.enabled === true) {
52
+ const SetupMetrics = require('./metrics/index.js')
50
53
  const signal = new SetupMetrics({ agent })
51
54
  signals.push(signal)
52
55
  } else {
@@ -57,6 +60,7 @@ function setupOtel(agent, logger = defaultLogger) {
57
60
  }
58
61
 
59
62
  if (agent.config.opentelemetry.logs.enabled === true) {
63
+ const SetupLogs = require('./logs/index.js')
60
64
  const signal = new SetupLogs({ agent })
61
65
  signals.push(signal)
62
66
  } else {
@@ -68,7 +72,7 @@ function setupOtel(agent, logger = defaultLogger) {
68
72
  .incrementCallCount()
69
73
  }
70
74
 
71
- function teardownOtel(agent) {
75
+ function teardownOtel(agent, { opentelemetry = require('@opentelemetry/api') } = {}) {
72
76
  if (agent?.config?.opentelemetry?.enabled !== true) {
73
77
  return
74
78
  }
package/lib/shimmer.js CHANGED
@@ -306,7 +306,7 @@ const shimmer = (module.exports = {
306
306
  registerHooks(agent) {
307
307
  // add the packages from the subscriber based instrumentation
308
308
  // this is only added to add tracking metrics
309
- pkgsToHook.push(...Object.keys(subscriptions), ...trackingPkgs)
309
+ Array.prototype.push.apply(pkgsToHook, trackingPkgs)
310
310
  this._ritm = new Hook(pkgsToHook, function onHook(exports, name, basedir) {
311
311
  return _postLoad(agent, exports, name, basedir)
312
312
  })
@@ -7,6 +7,8 @@
7
7
  // eslint-disable-next-line n/no-unsupported-features/node-builtins
8
8
  const { tracingChannel } = require('node:diagnostics_channel')
9
9
  const cat = require('#agentlib/util/cat.js')
10
+ const recordSupportabilityMetric = require('./record-supportability-metric.js')
11
+
10
12
  // Used for the `traceCallback` work.
11
13
  // This can be removed when we add true support into orchestrion
12
14
  const makeCall = (fn) => (...args) => fn.call(...args)
@@ -28,38 +30,45 @@ const ArrayPrototypeSplice = makeCall(Array.prototype.splice)
28
30
 
29
31
  /**
30
32
  * @property {object} agent A New Relic Node.js agent instance.
31
- * @property {object} logger An agent logger instance.
33
+ * @property {TracingChannel} channel The tracing channel instance this subscriber will be monitoring.
34
+ * @property {string} channelName A unique name for the diagnostics channel
35
+ * that will be registered.
32
36
  * @property {object} config The agent configuration object.
37
+ * @property {string} id A unique identifier for the subscriber, combining the prefix, package
38
+ * name, and channel name.
39
+ * @property {object} logger An agent logger instance.
33
40
  * @property {string} packageName The name of the module being instrumented.
34
41
  * This is the same string one would pass to the `require` function.
35
- * @property {string} channelName A unique name for the diagnostics channel
36
- * that will be registered.
42
+ * @property {AsyncLocalStorage} store The async local storage instance used for context management.
43
+ * @property {number} [callback=null] Position of callback if it needs to be wrapped for instrumentation.
44
+ * -1 means last argument.
37
45
  * @property {string[]} [events=[]] Set of tracing channel event names to
38
46
  * register handlers for. For any name in the set, a corresponding method
39
47
  * must exist on the subscriber instance. The method will be passed the
40
48
  * event object. Possible event names are `start`, `end`, `asyncStart`,
41
49
  * `asyncEnd`, and `error`.
42
- *
43
- * See {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel}
44
- * @property {boolean} [opaque=false] If true, any children segments will not be created.
50
+ * See {@link https://nodejs.org/api/diagnostics_channel.html#class-tracingchannel}
45
51
  * @property {boolean} [internal=false] If true, any children segments from the same library
46
52
  * will not be created.
53
+ * @property {boolean} [opaque=false] If true, any children segments will not be created.
47
54
  * @property {string} [prefix='orchestrion:'] String to prepend to diagnostics
48
55
  * channel event names. This provides a namespace for the events we are
49
56
  * injecting into a module.
50
- * @property {boolean} [requireActiveTx=true] If true, the subscriber will only handle events
51
- * when there is an active transaction.
52
57
  * @property {boolean} [propagateContext=false] If true, it will bind `asyncStart` to the store
53
58
  * and re-propagate the active context. It will also attach the `transaction` to the event in
54
59
  * `start.bindStore`. This is used for functions that queue async code and context is lost.
55
- * @property {string} id A unique identifier for the subscriber, combining the prefix, package
56
- * name, and channel name.
57
- * @property {TracingChannel} channel The tracing channel instance this subscriber will be monitoring.
58
- * @property {AsyncLocalStorage} store The async local storage instance used for context management.
59
- * @property {number} [callback=null] Position of callback if it needs to be wrapped for instrumentation.
60
- * -1 means last argument.
60
+ * @property {boolean} [requireActiveTx=true] If true, the subscriber will only handle events
61
+ * when there is an active transaction.
62
+ * @property {object} [targetModuleMeta] Defines the target module's name and
63
+ * version string, i.e. is an object `{ name, version }`. This is only necessary
64
+ * when target instrumentation can surface an unexpected name for the
65
+ * `packageName` property. For example, `express` uses multiple modules to
66
+ * compose its core functionality. We want to track things under the `express`
67
+ * name, but `packageName` will be set to `router` is most cases.
61
68
  */
62
69
  class Subscriber {
70
+ #usageMetricRecorded = false
71
+
63
72
  /**
64
73
  * @param {SubscriberParams} params the subscriber constructor params
65
74
  */
@@ -234,6 +243,15 @@ class Subscriber {
234
243
  * @returns {Context} The context after processing the event
235
244
  */
236
245
  const handler = (data) => {
246
+ if (this.#usageMetricRecorded === false) {
247
+ recordSupportabilityMetric({
248
+ agent: this.agent,
249
+ moduleName: this.packageName,
250
+ moduleVersion: data.moduleVersion
251
+ })
252
+ this.#usageMetricRecorded = true
253
+ }
254
+
237
255
  // only wrap the callback if a subscriber has a callback property defined
238
256
  if (this.callback !== null) {
239
257
  this.traceCallback(this.callback, data)
@@ -4,8 +4,11 @@
4
4
  */
5
5
 
6
6
  'use strict'
7
+
7
8
  // eslint-disable-next-line n/no-unsupported-features/node-builtins
8
9
  const dc = require('node:diagnostics_channel')
10
+ const recordSupportabilityMetric = require('./record-supportability-metric.js')
11
+ const resolvePackageVersion = require('./resolve-package-version.js')
9
12
 
10
13
  /**
11
14
  * The baseline parameters available to all subscribers.
@@ -15,6 +18,13 @@ const dc = require('node:diagnostics_channel')
15
18
  * @property {object} logger An agent logger instance.
16
19
  * @property {string} packageName The package name being instrumented.
17
20
  * This is what a developer would provide to the `require` function.
21
+ * @property {boolean} [skipUsageMetricRecording=false] When set to `true`, the
22
+ * instrumentation will not attempt to record the usage metric. This is useful
23
+ * when the module being instrumented is also being instrumented via the
24
+ * Orchestrion based subscriber system. It is much cheaper to record the metric
25
+ * via Orchestrion based subscribers than through this direct diagnostics
26
+ * channel method (Orchestrion provides the module version, whereas we have
27
+ * to perform expensive operations here to get the same information).
18
28
  */
19
29
 
20
30
  /**
@@ -34,19 +44,25 @@ const dc = require('node:diagnostics_channel')
34
44
  * This is the same string one would pass to the `require` function.
35
45
  */
36
46
  class Subscriber {
47
+ #usageMetricRecorded = false
48
+
37
49
  /**
38
50
  * @param {SubscriberParams} params to function
39
51
  */
40
- constructor({ agent, logger, packageName }) {
52
+ constructor({ agent, logger, packageName, skipUsageMetricRecording = false }) {
41
53
  this.agent = agent
42
54
  this.logger = logger.child({ component: `${packageName}-subscriber` })
43
55
  this.config = agent.config
44
56
  this.id = packageName
57
+
58
+ if (skipUsageMetricRecording === true) {
59
+ this.#usageMetricRecorded = true
60
+ }
45
61
  }
46
62
 
47
63
  set channels(channels) {
48
64
  if (!Array.isArray(channels)) {
49
- throw new Error('channels must be a collection of with propertiesof channel and hook')
65
+ throw new Error('channels must be a collection of objects with properties channel and hook')
50
66
  }
51
67
  this._channels = channels
52
68
  }
@@ -73,18 +89,43 @@ class Subscriber {
73
89
 
74
90
  subscribe() {
75
91
  for (let index = 0; index < this.channels.length; index++) {
92
+ const chan = this.channels[index]
76
93
  const { hook, channel } = this.channels[index]
77
94
  const boundHook = hook.bind(this)
78
- dc.subscribe(channel, boundHook)
79
- this.channels[index].boundHook = boundHook
95
+ chan.boundHook = boundHook
96
+ chan.eventHandler = (message, name) => {
97
+ this.#supportability()
98
+ boundHook(message, name)
99
+ }
100
+ dc.subscribe(channel, chan.eventHandler)
80
101
  }
81
102
  }
82
103
 
83
104
  unsubscribe() {
84
105
  for (let index = 0; index < this.channels.length; index++) {
85
- const { channel, boundHook } = this.channels[index]
86
- dc.unsubscribe(channel, boundHook)
106
+ const { channel, eventHandler } = this.channels[index]
107
+ dc.unsubscribe(channel, eventHandler)
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Since this class subscribes to diagnostics channels natively published by
113
+ * target modules, we do not get the package metadata that Orchestrion
114
+ * provides in its channel events. So we have to try and find the package
115
+ * manifest and get the version out of it in order to record our
116
+ * supportability metric.
117
+ */
118
+ #supportability() {
119
+ if (this.#usageMetricRecorded === true) {
120
+ return
87
121
  }
122
+ const version = resolvePackageVersion(this.id)
123
+ recordSupportabilityMetric({
124
+ agent: this.agent,
125
+ moduleName: this.id,
126
+ moduleVersion: version
127
+ })
128
+ this.#usageMetricRecorded = true
88
129
  }
89
130
  }
90
131
 
@@ -10,7 +10,7 @@ const MiddlewareWrapper = require('../middleware-wrapper')
10
10
 
11
11
  class FastifyInitialization extends DcBase {
12
12
  constructor({ agent, logger }) {
13
- super({ agent, logger, packageName: 'fastify' })
13
+ super({ agent, logger, packageName: 'fastify', skipUsageMetricRecording: true })
14
14
  this.channels = [
15
15
  { channel: initChannel, hook: this.handler }
16
16
  ]
@@ -24,7 +24,10 @@ class FastifyInitialization extends DcBase {
24
24
  return
25
25
  }
26
26
 
27
- routeOptions.handler = self.wrapper.wrap({ handler: routeOptions.handler, route: routeOptions.path })
27
+ routeOptions.handler = self.wrapper.wrap({
28
+ handler: routeOptions.handler,
29
+ route: routeOptions.path
30
+ })
28
31
  })
29
32
  }
30
33
  }
@@ -105,7 +105,7 @@ class MessageConsumerSubscriber extends Subscriber {
105
105
  */
106
106
  asyncStart() {
107
107
  const ctx = this.agent.tracer.getContext()
108
- const tx = ctx?.transaction
108
+ const tx = ctx.transaction
109
109
  tx.setPartialName(this.name)
110
110
  // Note: this is not using `Subscriber.createSegment`
111
111
  // this is because it enters the segment and returns a new ctx
@@ -117,6 +117,7 @@ class MessageConsumerSubscriber extends Subscriber {
117
117
  parent: tx.trace.root,
118
118
  transaction: tx
119
119
  })
120
+ tx.baseSegment.start()
120
121
 
121
122
  this.addConsumeParameters(tx)
122
123
 
@@ -37,13 +37,10 @@ class OpenAIChatCompletions extends OpenAISubscriber {
37
37
  return
38
38
  }
39
39
 
40
- const segment = this.agent.tracer.createSegment({
40
+ return this.createSegment({
41
41
  name: OPENAI.COMPLETION,
42
- parent: ctx.segment,
43
- transaction: ctx.transaction
42
+ ctx
44
43
  })
45
- const newCtx = ctx.enterSegment({ segment })
46
- return newCtx
47
44
  }
48
45
 
49
46
  asyncEnd(data) {
@@ -17,12 +17,18 @@ class OpenAIClientSubscriber extends OpenAISubscriber {
17
17
  }
18
18
 
19
19
  asyncEnd(data) {
20
- const { result } = data
20
+ const { result, error } = data
21
21
  const ctx = this.agent.tracer.getContext()
22
22
  if (ctx?.segment) {
23
- const headers = result?.response?.headers
24
- ? Object.fromEntries(result.response.headers)
25
- : { ...result?.headers }
23
+ const responseHeaders = result?.response?.headers
24
+ let headers = {}
25
+ if (responseHeaders) {
26
+ headers = Object.fromEntries(responseHeaders)
27
+ } else if (error?.headers) {
28
+ // In openai v5 they made error headers consistent with response headers
29
+ // they are now some custom class that can retrieve headers via `Object.fromEntries`
30
+ headers = error?.headers?.values ? Object.fromEntries(error.headers) : { ...error.headers }
31
+ }
26
32
  ctx.extras = { headers }
27
33
  }
28
34
  }
@@ -22,13 +22,10 @@ class OpenAIEmbeddings extends OpenAISubscriber {
22
22
  this.logger.debug('OpenAI instrumentation is disabled, not creating segment.')
23
23
  return
24
24
  }
25
- const segment = this.agent.tracer.createSegment({
25
+ return this.createSegment({
26
26
  name: OPENAI.EMBEDDING,
27
- parent: ctx.segment,
28
- transaction: ctx.transaction
27
+ ctx
29
28
  })
30
- const newCtx = ctx.enterSegment({ segment })
31
- return newCtx
32
29
  }
33
30
 
34
31
  asyncEnd(data) {
@@ -21,7 +21,8 @@ let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
21
21
  * Parses the response from OpenAI and extracts the message content and role.
22
22
  *
23
23
  * @param {object} response The OpenAI SDK response object
24
- * @returns {{ content: string, role: string }} the message object with fields `content` and `role`
24
+ * @returns {object[]} an array with the message object with fields `content`
25
+ * and `role` or [] if response invalid
25
26
  */
26
27
  function getMessageFromResponse(response) {
27
28
  let content
@@ -30,11 +31,17 @@ function getMessageFromResponse(response) {
30
31
  content = response?.output?.[0]?.content?.[0]?.text
31
32
  role = response?.output?.[0]?.role
32
33
  } else {
33
- content = response?.choices?.[0]?.message?.content
34
- role = response?.choices?.[0]?.message?.role
34
+ const choice = response?.choices?.[0]
35
+ if (choice?.finish_reason === 'tool_calls') {
36
+ // A false response. Don't create a LlmChatCompletionMessage
37
+ // for this -- the full conversation isn't done yet.
38
+ return []
39
+ }
40
+ content = choice?.message?.content
41
+ role = choice?.message?.role
35
42
  }
36
43
 
37
- return { content, role }
44
+ return [{ content, role }]
38
45
  }
39
46
 
40
47
  /**
@@ -73,7 +80,10 @@ function getMessagesFromRequest(request, logger) {
73
80
  }
74
81
 
75
82
  /**
76
- * Enqueues a LLM event to the custom event aggregator
83
+ * Enqueues a LLM event to the custom event aggregator.
84
+ *
85
+ * Will use `msg.timestamp` instead of the current time
86
+ * for `timestamp` if present.
77
87
  *
78
88
  * @param {object} params input params
79
89
  * @param {Agent} params.agent NR agent instance
@@ -82,9 +92,12 @@ function getMessagesFromRequest(request, logger) {
82
92
  */
83
93
  function recordEvent({ agent, type, msg }) {
84
94
  const llmContext = extractLlmContext(agent)
85
-
95
+ // Spec 771: The `timestamp` attribute MUST be used to override
96
+ // the timestamp value attached to the intrinsics of the custom
97
+ // LlmChatCompletionMessage event before it is recorded using
98
+ // record_custom_event().
86
99
  agent.customEventAggregator.add([
87
- { type, timestamp: Date.now() },
100
+ { type, timestamp: msg?.timestamp ?? Date.now() },
88
101
  Object.assign({}, msg, llmContext)
89
102
  ])
90
103
  }
@@ -159,7 +172,7 @@ function recordChatCompletionMessages({
159
172
  // the response message.
160
173
  const messages = [
161
174
  ...getMessagesFromRequest(request, logger),
162
- getMessageFromResponse(response)
175
+ ...getMessageFromResponse(response)
163
176
  ]
164
177
 
165
178
  for (let i = 0; i < messages.length; i++) {
@@ -0,0 +1,39 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ module.exports = recordSupportabilityMetric
9
+
10
+ const semver = require('semver')
11
+ const {
12
+ FEATURES: {
13
+ INSTRUMENTATION: { SUBSCRIBER_USED }
14
+ }
15
+ } = require('#agentlib/metrics/names.js')
16
+
17
+ function recordSupportabilityMetric({
18
+ agent,
19
+ moduleName,
20
+ moduleVersion = 'unknown'
21
+ } = {}) {
22
+ const major = moduleVersion === 'unknown'
23
+ ? semver.major(process.version)
24
+ : semver.major(moduleVersion)
25
+
26
+ let metric = agent.metrics.getOrCreateMetric(
27
+ `${SUBSCRIBER_USED}/${moduleName}/${major}`
28
+ )
29
+ if (metric.callCount === 0) {
30
+ metric.incrementCallCount()
31
+ }
32
+
33
+ metric = agent.metrics.getOrCreateMetric(
34
+ `${SUBSCRIBER_USED}/${moduleName}`
35
+ )
36
+ if (metric.callCount === 0) {
37
+ metric.incrementCallCount()
38
+ }
39
+ }
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const path = require('node:path')
9
+ const defaultLogger = require('#agentlib/logger.js').child({
10
+ component: 'resolve-module-version'
11
+ })
12
+
13
+ const dcFuncFrame = /^\s*at Channel\.publish/
14
+ const modPathReg = /at .+ \((.+):\d+:\d+\)/
15
+
16
+ module.exports = resolveModuleVersion
17
+
18
+ /**
19
+ * Given a module name, attempt to read the version string from its
20
+ * associated package manifest. If the module is a built-in, or one that has
21
+ * been bundled with Node.js (e.g. `undici`), a package manifest will not be
22
+ * available. In this case, the string "unknown" will be returned.
23
+ *
24
+ * This version resolver assumes that it will be invoked through our
25
+ * diagnostics channel subscriber instrumentations. That is, it expects the
26
+ * call tree to be similar to:
27
+ *
28
+ * 1. some-module.function()
29
+ * 2. diagnostics_channel.publish()
30
+ * 3. subscriber.handler()
31
+ *
32
+ * @param {string} moduleSpecifier What would be passed to `resolve()`.
33
+ * @param {object} [deps] Optional dependencies.
34
+ * @param {object} [deps.logger] Agent logger instance.
35
+ *
36
+ * @returns {string} The version string from the package manifest or "unknown".
37
+ */
38
+ function resolveModuleVersion(moduleSpecifier, { logger = defaultLogger } = {}) {
39
+ let pkgPath
40
+ // We'd prefer to use `require.resolve(moduleSpecifier)` here, but it gets
41
+ // a bit confused when there are non-standard module directories in play.
42
+ // Once we are able to refactor our "on require" metric recording to
43
+ // utilize `module.registerHooks`, we should be able to eliminate this
44
+ // 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)
50
+ const matches = modPathReg.exec(stack[1])
51
+ pkgPath = matches?.[1]
52
+
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'
59
+ }
60
+
61
+ const cwd = process.cwd()
62
+ let reachedCwd = false
63
+ let pkg
64
+ let base = path.dirname(pkgPath)
65
+ do {
66
+ try {
67
+ pkgPath = path.join(base, 'package.json')
68
+ pkg = require(pkgPath)
69
+ } catch {
70
+ base = path.resolve(path.join(base, '..'))
71
+ if (base === cwd) {
72
+ reachedCwd = true
73
+ } else if (reachedCwd === true) {
74
+ // We reached the supposed app root, attempted to load a manifest
75
+ // file in that location, and still couldn't find one. So we give up.
76
+ pkg = {}
77
+ }
78
+ }
79
+ } while (!pkg)
80
+
81
+ const version = pkg.version ?? 'unknown'
82
+ logger.trace({ moduleSpecifier, version }, 'Resolved package version.')
83
+ return version
84
+ }
@@ -19,15 +19,7 @@ const trackingPkgs = [
19
19
  'fancy-log',
20
20
  'knex',
21
21
  'loglevel',
22
- 'npmlog',
23
- // These packages are required for the tracking metrics
24
- // for @langchain/core to be created. Ideally, these
25
- // should not be here.
26
- // TODO: will be addressed in https://github.com/newrelic/node-newrelic/issues/3575
27
- '@langchain/core/prompts',
28
- '@langchain/core/tools',
29
- '@langchain/core/runnables',
30
- '@langchain/core/vectorstores',
22
+ 'npmlog'
31
23
  ]
32
24
 
33
25
  module.exports = trackingPkgs
@@ -65,6 +65,7 @@ class PartialTrace {
65
65
  addSpan({ span, isEntry }) {
66
66
  const id = span.id
67
67
  const parentId = span.parentId
68
+ const spanLinks = span.spanLinks
68
69
  span = span.applyPartialTraceRules({ isEntry, partialTrace: this })
69
70
  this.createMetrics(!!span)
70
71
  if (span) {
@@ -76,7 +77,32 @@ class PartialTrace {
76
77
  // unless compact where all parentIds are assigned to the entry span
77
78
  // in finalizeSpanEvents
78
79
  this.droppedSpans.set(id, parentId)
80
+
81
+ // span was dropped but we still need to move its span links to the last kept span
82
+ // spanLinks were captured before the span was dropped
83
+ if (spanLinks && spanLinks.length > 0) {
84
+ this.reparentSpanLinks(spanLinks)
85
+ }
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Iterates over the span links from a dropped span and reassigns them to the last kept span.
91
+ * The id intrinsic attribute will also be updated to the value of the last kept span id.
92
+ *
93
+ * @param {SpanLink[]} spanLinks an array of span links to reparent to last kept span
94
+ */
95
+ reparentSpanLinks(spanLinks) {
96
+ const lastSpan = this.spans.at(-1)
97
+
98
+ // The id intrinsics attribute needs to be updated to equal the id of the new span the
99
+ // span links are moving to.
100
+ for (const link of spanLinks) {
101
+ link.intrinsics.id = lastSpan.id
79
102
  }
103
+
104
+ // move the span links events to the last kept span
105
+ Array.prototype.push.apply(lastSpan.spanLinks, spanLinks)
80
106
  }
81
107
 
82
108
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "13.9.1",
3
+ "version": "13.10.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -198,6 +198,7 @@
198
198
  "imports": {
199
199
  "#agentlib/*.js": "./lib/*.js",
200
200
  "#testlib/*.js": "./test/lib/*.js",
201
+ "#testlib/*.json": "./test/lib/*.json",
201
202
  "#test/assert": "./test/lib/custom-assertions/index.js"
202
203
  },
203
204
  "dependencies": {