newrelic 12.11.0 → 12.11.2

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,43 @@
1
+ ### v12.11.2 (2025-01-23)
2
+
3
+ #### Features
4
+
5
+ * Added support for Claude 3+ Chat API in Bedrock ([#2870](https://github.com/newrelic/node-newrelic/pull/2870)) ([6a83abf](https://github.com/newrelic/node-newrelic/commit/6a83abf8b1a0cb0f993e9d9285566a09677d7d2e))
6
+
7
+ #### Bug fixes
8
+
9
+ * Added defensive code in shim to prevent crashing when checking parent segment ([#2898](https://github.com/newrelic/node-newrelic/pull/2898)) ([751eb96](https://github.com/newrelic/node-newrelic/commit/751eb9608d7401d1123a0e810eac9dc554127e4e))
10
+
11
+ #### Documentation
12
+
13
+ * Updated compatibility report ([#2888](https://github.com/newrelic/node-newrelic/pull/2888)) ([dce9deb](https://github.com/newrelic/node-newrelic/commit/dce9debeda6c5ed30c0ed6dbc00f73cf11c8f40f))
14
+
15
+ #### Miscellaneous chores
16
+
17
+ * Replaced backtracking regex with new algorithm ([#2887](https://github.com/newrelic/node-newrelic/pull/2887)) ([46462d0](https://github.com/newrelic/node-newrelic/commit/46462d00b68c4a4cedd60d46d531b2f31800df98))
18
+
19
+ #### Tests
20
+
21
+ * Improved test coverage of normalizing claude 3 messages ([#2893](https://github.com/newrelic/node-newrelic/pull/2893)) ([cc65763](https://github.com/newrelic/node-newrelic/commit/cc657633b81daf4b372e5715e696ed3c23ecb678))
22
+
23
+ ### v12.11.1 (2025-01-16)
24
+
25
+ #### Bug fixes
26
+
27
+ * Updated undici instrumentation to fix crash with trying to calculate exclusive duration on a segment that no longer exists ([#2884](https://github.com/newrelic/node-newrelic/pull/2884)) ([3b7e4bf](https://github.com/newrelic/node-newrelic/commit/3b7e4bf0a40b21f16b11e7761809cbaa83c02515))
28
+
29
+ #### Documentation
30
+
31
+ * Updated compatibility report ([#2878](https://github.com/newrelic/node-newrelic/pull/2878)) ([e784f84](https://github.com/newrelic/node-newrelic/commit/e784f8427bd49852f0ed7a15d6025ea8dfc73c72))
32
+
33
+ #### Miscellaneous chores
34
+
35
+ * Fixed dns integration tests ([#2883](https://github.com/newrelic/node-newrelic/pull/2883)) ([dd30ad7](https://github.com/newrelic/node-newrelic/commit/dd30ad71f07233682da39ca41f452a55c6798a15))
36
+
37
+ #### Tests
38
+
39
+ * Restored the branch to test apollo-server-plugin to main ([#2879](https://github.com/newrelic/node-newrelic/pull/2879)) ([978faab](https://github.com/newrelic/node-newrelic/commit/978faab59f5378e26cb8f5e584a3c9e684e7bbb1))
40
+
1
41
  ### v12.11.0 (2025-01-14)
2
42
 
3
43
  #### Features
@@ -13,7 +13,6 @@ exports.prerelease = {
13
13
 
14
14
  promise_segments: false,
15
15
  reverse_naming_rules: false,
16
- undici_async_tracking: true,
17
16
  unresolved_promise_cleanup: true,
18
17
  kafkajs_instrumentation: false
19
18
  }
@@ -36,6 +35,7 @@ exports.released = [
36
35
  'await_support',
37
36
  'certificate_bundle',
38
37
  'async_local_context',
38
+ 'undici_async_tracking',
39
39
  'undici_instrumentation',
40
40
  'aws_bedrock_instrumentation',
41
41
  'langchain_instrumentation'
@@ -95,10 +95,10 @@ function _headerToCamelCase(header) {
95
95
  const newHeader = header.charAt(0).toLowerCase() + header.slice(1)
96
96
 
97
97
  // Converts headers in the form 'header-name' to be in the form 'headerName'
98
- // eslint-disable-next-line sonarjs/slow-regex
99
- return newHeader.replace(/[\W_]+(\w)/g, function capitalize(m, $1) {
100
- return $1.toUpperCase()
101
- })
98
+ return newHeader.split(/[\W_]/).map((ele, i) => {
99
+ if (i === 0) return ele
100
+ return ele.slice(0, 1).toUpperCase() + ele.slice(1)
101
+ }).join('')
102
102
  }
103
103
 
104
104
  function _collectHeaders(headers, nameMap, prefix, transaction) {
@@ -118,18 +118,24 @@ function recordChatCompletionMessages({
118
118
  isError: err !== null
119
119
  })
120
120
 
121
- const msg = new LlmChatCompletionMessage({
122
- agent,
123
- segment,
124
- bedrockCommand,
125
- bedrockResponse,
126
- transaction,
127
- index: 0,
128
- completionId: summary.id
121
+ // Record context message(s)
122
+ const promptContextMessages = bedrockCommand.prompt
123
+ promptContextMessages.forEach((contextMessage, promptIndex) => {
124
+ const msg = new LlmChatCompletionMessage({
125
+ agent,
126
+ segment,
127
+ transaction,
128
+ bedrockCommand,
129
+ content: contextMessage.content,
130
+ role: contextMessage.role,
131
+ bedrockResponse,
132
+ index: promptIndex,
133
+ completionId: summary.id
134
+ })
135
+ recordEvent({ agent, type: 'LlmChatCompletionMessage', msg })
129
136
  })
130
- recordEvent({ agent, type: 'LlmChatCompletionMessage', msg })
131
137
 
132
- bedrockResponse.completions.forEach((content, index) => {
138
+ bedrockResponse.completions.forEach((content, completionIndex) => {
133
139
  const chatCompletionMessage = new LlmChatCompletionMessage({
134
140
  agent,
135
141
  segment,
@@ -137,8 +143,9 @@ function recordChatCompletionMessages({
137
143
  bedrockCommand,
138
144
  bedrockResponse,
139
145
  isResponse: true,
140
- index: index + 1,
146
+ index: promptContextMessages.length + completionIndex,
141
147
  content,
148
+ role: 'assistant',
142
149
  completionId: summary.id
143
150
  })
144
151
  recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: chatCompletionMessage })
@@ -179,18 +186,22 @@ function recordEmbeddingMessage({
179
186
  return
180
187
  }
181
188
 
182
- const embedding = new LlmEmbedding({
189
+ const embeddings = bedrockCommand.prompt.map(prompt => new LlmEmbedding({
183
190
  agent,
184
191
  segment,
185
192
  transaction,
186
193
  bedrockCommand,
194
+ input: prompt.content,
187
195
  bedrockResponse,
188
196
  isError: err !== null
197
+ }))
198
+
199
+ embeddings.forEach(embedding => {
200
+ recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
189
201
  })
190
202
 
191
- recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
192
203
  if (err) {
193
- const llmError = new LlmError({ bedrockResponse, err, embedding })
204
+ const llmError = new LlmError({ bedrockResponse, err, embedding: embeddings.length === 1 ? embeddings[0] : undefined })
194
205
  agent.errors.add(transaction, err, llmError)
195
206
  }
196
207
  }
@@ -11,8 +11,6 @@ const logger = require('../logger').child({ component: 'undici' })
11
11
  const NAMES = require('../metrics/names')
12
12
  const symbols = require('../symbols')
13
13
  // eslint-disable-next-line n/no-unsupported-features/node-builtins
14
- const { executionAsyncResource } = require('async_hooks')
15
- // eslint-disable-next-line n/no-unsupported-features/node-builtins
16
14
  const diagnosticsChannel = require('diagnostics_channel')
17
15
  const synthetics = require('../synthetics')
18
16
  const urltils = require('../util/urltils')
@@ -49,42 +47,6 @@ module.exports.unsubscribe = function unsubscribe() {
49
47
  })
50
48
  }
51
49
 
52
- /**
53
- * Retrieves the current segment(parent in our context) and transaction from executionAsyncResource
54
- * or from context manager then adds to the executionAsyncResource for future
55
- * undici requests within same async context.
56
- *
57
- * It was found that when running concurrent undici requests
58
- * within a transaction that the parent segment would get out of sync
59
- * depending on the async context of the transaction. By using
60
- * `async_hooks.executionResource` it is more reliable.
61
- *
62
- * Note: However, if you have concurrent undici requests in a transaction
63
- * and the request to the transaction is using a keep alive there is a chance the
64
- * executionAsyncResource may be incorrect because of shared connections. To revert to a more
65
- * naive tracking of parent set `config.feature_flag.undici_async_tracking: false` and
66
- * it will just get the segment and transaction from the context manager.
67
- *
68
- * @param {Shim} shim instance of shim
69
- * @returns {TraceSegment} parent segment
70
- */
71
- function getParentContext(shim) {
72
- const { config } = shim.agent
73
- if (config.feature_flag.undici_async_tracking) {
74
- const resource = executionAsyncResource()
75
-
76
- if (!resource[symbols.parentSegment]) {
77
- const parent = shim.getSegment()
78
- resource[symbols.parentSegment] = parent
79
- const tx = shim.tracer.getTransaction()
80
- resource[symbols.transaction] = tx
81
- }
82
- return { segment: resource[symbols.parentSegment], transaction: resource[symbols.transaction] }
83
- }
84
-
85
- return shim.tracer.getContext()
86
- }
87
-
88
50
  /**
89
51
  * Injects relevant DT headers for the external request
90
52
  *
@@ -160,7 +122,7 @@ function createExternalSegment({ shim, request, segment }) {
160
122
  */
161
123
  function requestCreateHook(shim, { request }) {
162
124
  const { config } = shim.agent
163
- const { transaction, segment } = getParentContext(shim)
125
+ const { transaction, segment } = shim.tracer.getContext()
164
126
  request[symbols.parentSegment] = segment
165
127
  request[symbols.transaction] = transaction
166
128
  if (!(segment || transaction) || segment?.opaque) {
@@ -221,9 +183,8 @@ function requestHeadersHook(shim, { request, response }) {
221
183
  }
222
184
 
223
185
  /**
224
- * Gets the active and parent from given ctx(request, client connector)
225
- * and ends active and restores parent to active. If an error exists
226
- * it will add the error to the transaction
186
+ * Gets the active segment, parent segment and transaction from given ctx(request, client connector)
187
+ * and ends segment and sets the previous parent segment as the active segment. If an error exists it will add the error to the transaction
227
188
  *
228
189
  * @param {Shim} shim instance of shim
229
190
  * @param {object} params object from undici hook
@@ -236,14 +197,14 @@ function endAndRestoreSegment(shim, { request, error }) {
236
197
  const tx = request[symbols.transaction]
237
198
  if (activeSegment) {
238
199
  activeSegment.end()
200
+ }
239
201
 
240
- if (error) {
241
- handleError(shim, tx, error)
242
- }
202
+ if (error && tx) {
203
+ handleError(shim, tx, error)
204
+ }
243
205
 
244
- if (parentSegment) {
245
- shim.setActiveSegment(parentSegment)
246
- }
206
+ if (parentSegment) {
207
+ shim.setActiveSegment(parentSegment)
247
208
  }
248
209
  }
249
210
 
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict'
7
7
 
8
+ const { stringifyClaudeChunkedMessage } = require('./utils')
9
+
8
10
  /**
9
11
  * Parses an AWS invoke command instance into a re-usable entity.
10
12
  */
@@ -68,37 +70,34 @@ class BedrockCommand {
68
70
  /**
69
71
  * The question posed to the LLM.
70
72
  *
71
- * @returns {string|string[]|undefined}
73
+ * @returns {object[]} The array of context messages passed to the LLM (or a single user prompt for legacy "non-chat" models)
72
74
  */
73
75
  get prompt() {
74
- let result
75
76
  if (this.isTitan() === true || this.isTitanEmbed() === true) {
76
- result = this.#body.inputText
77
+ return [
78
+ {
79
+ role: 'user',
80
+ content: this.#body.inputText
81
+ }
82
+ ]
77
83
  } else if (this.isCohereEmbed() === true) {
78
- result = this.#body.texts.join(' ')
84
+ return [
85
+ {
86
+ role: 'user',
87
+ content: this.#body.texts.join(' ')
88
+ }
89
+ ]
79
90
  } else if (
80
- this.isClaude() === true ||
91
+ this.isClaudeTextCompletionApi() === true ||
81
92
  this.isAi21() === true ||
82
93
  this.isCohere() === true ||
83
94
  this.isLlama() === true
84
95
  ) {
85
- result = this.#body.prompt
86
- } else if (this.isClaude3() === true) {
87
- const collected = []
88
- for (const message of this.#body?.messages) {
89
- if (message?.role === 'assistant') {
90
- continue
91
- }
92
- if (typeof message?.content === 'string') {
93
- collected.push(message?.content)
94
- continue
95
- }
96
- const mappedMsgObj = message?.content.map((msgContent) => msgContent.text)
97
- collected.push(mappedMsgObj)
98
- }
99
- result = collected.join(' ')
96
+ return [{ role: 'user', content: this.#body.prompt }]
97
+ } else if (this.isClaudeMessagesApi() === true) {
98
+ return normalizeClaude3Messages(this.#body?.messages)
100
99
  }
101
- return result
100
+ return []
102
101
  }
103
102
 
104
103
  /**
@@ -151,6 +150,41 @@ class BedrockCommand {
151
150
  isTitanEmbed() {
152
151
  return this.#modelId.startsWith('amazon.titan-embed')
153
152
  }
153
+
154
+ isClaudeMessagesApi() {
155
+ return (this.isClaude3() === true || this.isClaude() === true) && 'messages' in this.#body
156
+ }
157
+
158
+ isClaudeTextCompletionApi() {
159
+ return this.isClaude() === true && 'prompt' in this.#body
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Claude v3 requests in Bedrock can have two different "chat" flavors. This function normalizes them into a consistent
165
+ * format per the AIM agent spec
166
+ *
167
+ * @param messages - The raw array of messages passed to the invoke API
168
+ * @returns {number|undefined} - The normalized messages
169
+ */
170
+ function normalizeClaude3Messages(messages) {
171
+ const result = []
172
+ for (const message of messages ?? []) {
173
+ if (message == null) {
174
+ continue
175
+ }
176
+ if (typeof message.content === 'string') {
177
+ // Messages can be specified with plain string content
178
+ result.push({ role: message.role, content: message.content })
179
+ } else if (Array.isArray(message.content)) {
180
+ // Or in a "chunked" format for multi-modal support
181
+ result.push({
182
+ role: message.role,
183
+ content: stringifyClaudeChunkedMessage(message.content)
184
+ })
185
+ }
186
+ }
187
+ return result
154
188
  }
155
189
 
156
190
  module.exports = BedrockCommand
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict'
7
7
 
8
+ const { stringifyClaudeChunkedMessage } = require('./utils')
9
+
8
10
  /**
9
11
  * @typedef {object} AwsBedrockMiddlewareResponse
10
12
  * @property {object} response Has a `body` property that is an IncomingMessage,
@@ -63,7 +65,7 @@ class BedrockResponse {
63
65
  // Streamed response
64
66
  this.#completions = body.completions
65
67
  } else {
66
- this.#completions = body?.content?.map((c) => c.text)
68
+ this.#completions = [stringifyClaudeChunkedMessage(body?.content)]
67
69
  }
68
70
  this.#id = body.id
69
71
  } else if (cmd.isCohere() === true) {
@@ -39,7 +39,7 @@ class LlmChatCompletionMessage extends LlmEvent {
39
39
  params = Object.assign({}, defaultParams, params)
40
40
  super(params)
41
41
 
42
- const { agent, content, isResponse, index, completionId } = params
42
+ const { agent, content, isResponse, index, completionId, role } = params
43
43
  const recordContent = agent.config?.ai_monitoring?.record_content?.enabled
44
44
  const tokenCB = agent?.llm?.tokenCountCallback
45
45
 
@@ -47,20 +47,11 @@ class LlmChatCompletionMessage extends LlmEvent {
47
47
  this.completion_id = completionId
48
48
  this.sequence = index
49
49
  this.content = recordContent === true ? content : undefined
50
- this.role = ''
50
+ this.role = role
51
51
 
52
52
  this.#setId(index)
53
- if (this.is_response === true) {
54
- this.role = 'assistant'
55
- if (typeof tokenCB === 'function') {
56
- this.token_count = tokenCB(this.bedrockCommand.modelId, content)
57
- }
58
- } else {
59
- this.role = 'user'
60
- this.content = recordContent === true ? this.bedrockCommand.prompt : undefined
61
- if (typeof tokenCB === 'function') {
62
- this.token_count = tokenCB(this.bedrockCommand.modelId, this.bedrockCommand.prompt)
63
- }
53
+ if (typeof tokenCB === 'function') {
54
+ this.token_count = tokenCB(this.bedrockCommand.modelId, content)
64
55
  }
65
56
  }
66
57
 
@@ -36,7 +36,7 @@ class LlmChatCompletionSummary extends LlmEvent {
36
36
  const cmd = this.bedrockCommand
37
37
  this[cfr] = this.bedrockResponse.finishReason
38
38
  this[rt] = cmd.temperature
39
- this[nm] = 1 + this.bedrockResponse.completions.length
39
+ this[nm] = (this.bedrockCommand.prompt.length) + this.bedrockResponse.completions.length
40
40
  }
41
41
  }
42
42
 
@@ -10,7 +10,7 @@ const LlmEvent = require('./event')
10
10
  /**
11
11
  * @typedef {object} LlmEmbeddingParams
12
12
  * @augments LlmEventParams
13
- * @property
13
+ * @property {string} input - The input message for the embedding call
14
14
  */
15
15
  /**
16
16
  * @type {LlmEmbeddingParams}
@@ -20,16 +20,18 @@ const defaultParams = {}
20
20
  class LlmEmbedding extends LlmEvent {
21
21
  constructor(params = defaultParams) {
22
22
  super(params)
23
- const { agent } = params
23
+ const { agent, input } = params
24
24
  const tokenCb = agent?.llm?.tokenCountCallback
25
25
 
26
26
  this.input = agent.config?.ai_monitoring?.record_content?.enabled
27
- ? this.bedrockCommand.prompt
27
+ ? input
28
28
  : undefined
29
29
  this.error = params.isError
30
30
  this.duration = params.segment.getDurationInMillis()
31
+
32
+ // Even if not recording content, we should use the local token counting callback to record token usage
31
33
  if (typeof tokenCb === 'function') {
32
- this.token_count = tokenCb(this.bedrockCommand.modelId, this.bedrockCommand.prompt)
34
+ this.token_count = tokenCb(this.bedrockCommand.modelId, input)
33
35
  }
34
36
  }
35
37
  }
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Copyright 2024 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ /**
9
+ *
10
+ * @param {object[]} chunks - The "chunks" that make up a single conceptual message. In a multi-modal scenario, a single message
11
+ * might have a number of different-typed chunks interspersed
12
+ * @returns {string} - A stringified version of the message. We make a best-effort effort attempt to represent non-text chunks. In the future
13
+ * we may want to extend the agent to support these non-text chunks in a richer way. Placeholders are represented in an XML-like format but
14
+ * are NOT intended to be parsed as valid XML
15
+ */
16
+ function stringifyClaudeChunkedMessage(chunks) {
17
+ const stringifiedChunks = chunks.map((msgContent) => {
18
+ switch (msgContent.type) {
19
+ case 'text':
20
+ return msgContent.text
21
+ case 'image':
22
+ return '<image>'
23
+ case 'tool_use':
24
+ return `<tool_use>${msgContent.name}</tool_use>`
25
+ case 'tool_result':
26
+ return `<tool_result>${msgContent.content}</tool_result>`
27
+ default:
28
+ return '<unknown_chunk>'
29
+ }
30
+ })
31
+ return stringifiedChunks.join('\n\n')
32
+ }
33
+
34
+ module.exports = {
35
+ stringifyClaudeChunkedMessage
36
+ }
package/lib/shim/shim.js CHANGED
@@ -677,7 +677,7 @@ function recordWrapper({ shim, fn, name, recordNamer }) {
677
677
  return fnApply.call(fn, this, args)
678
678
  }
679
679
 
680
- // middleweare recorders pass in parent segment
680
+ // middleware recorders pass in parent segment
681
681
  // we need to destructure this as it is not needed past this function
682
682
  // and will overwhelm trace level loggers with logging the entire spec
683
683
  const { parent: specParent, ...segDesc } = spec
@@ -686,7 +686,7 @@ function recordWrapper({ shim, fn, name, recordNamer }) {
686
686
  const transaction = context.transaction
687
687
  const parent = transaction?.isActive() && specParent ? specParent : context.segment
688
688
 
689
- if (!transaction?.isActive()) {
689
+ if (!transaction?.isActive() || !parent) {
690
690
  shim.logger.debug('Not recording function %s, not in a transaction.', name)
691
691
  return fnApply.call(fn, this, arguments)
692
692
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "12.11.0",
3
+ "version": "12.11.2",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [