newrelic 12.11.1 → 12.11.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,44 @@
1
+ ### v12.11.3 (2025-01-29)
2
+
3
+ #### Bug fixes
4
+
5
+ * Fixed issue with `bluebird` and `when` instrumentation where checking active context crashed when transaction prematurely ends ([#2909](https://github.com/newrelic/node-newrelic/pull/2909)) ([4a30d5c](https://github.com/newrelic/node-newrelic/commit/4a30d5c5082e963cca3664f2ed152017f6360d21))
6
+ * Fixed transaction-shim to properly create new transactions when the existing transaction is not active ([#2912](https://github.com/newrelic/node-newrelic/pull/2912)) ([3ad8c59](https://github.com/newrelic/node-newrelic/commit/3ad8c59e15e037a366ddf6803729b61ecfa701f3))
7
+
8
+ #### Documentation
9
+
10
+ * Updated compatibility report ([#2902](https://github.com/newrelic/node-newrelic/pull/2902)) ([cb16516](https://github.com/newrelic/node-newrelic/commit/cb16516e90a3dc2cefb98e6131a7243412aefbfc))
11
+
12
+ #### Miscellaneous chores
13
+
14
+ * Updated lint rule suppression comment ([#2895](https://github.com/newrelic/node-newrelic/pull/2895)) ([559dc98](https://github.com/newrelic/node-newrelic/commit/559dc98e18c8ba8280b73779780f3efc1c946ed7))
15
+
16
+ #### Continuous integration
17
+
18
+ * Move init container release from lambda to GHA ([#2848](https://github.com/newrelic/node-newrelic/pull/2848)) ([8d8608d](https://github.com/newrelic/node-newrelic/commit/8d8608d1089cafaeb8c17354034c96fe1b49597a))
19
+
20
+ ### v12.11.2 (2025-01-23)
21
+
22
+ #### Features
23
+
24
+ * 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))
25
+
26
+ #### Bug fixes
27
+
28
+ * 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))
29
+
30
+ #### Documentation
31
+
32
+ * Updated compatibility report ([#2888](https://github.com/newrelic/node-newrelic/pull/2888)) ([dce9deb](https://github.com/newrelic/node-newrelic/commit/dce9debeda6c5ed30c0ed6dbc00f73cf11c8f40f))
33
+
34
+ #### Miscellaneous chores
35
+
36
+ * Replaced backtracking regex with new algorithm ([#2887](https://github.com/newrelic/node-newrelic/pull/2887)) ([46462d0](https://github.com/newrelic/node-newrelic/commit/46462d00b68c4a4cedd60d46d531b2f31800df98))
37
+
38
+ #### Tests
39
+
40
+ * 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))
41
+
1
42
  ### v12.11.1 (2025-01-16)
2
43
 
3
44
  #### Bug fixes
package/lib/db/utils.js CHANGED
@@ -11,9 +11,9 @@ function extractDatabaseChangeFromUse(sql) {
11
11
  // The character ranges for this were pulled from
12
12
  // http://dev.mysql.com/doc/refman/5.7/en/identifiers.html
13
13
 
14
- // Suppressing a warning on this regex because it is not obvious what this
15
- // regex does, and we don't want to break anything.
16
- // eslint-disable-next-line sonarjs/slow-regex, sonarjs/duplicates-in-character-class
17
- const match = /^\s*use[^\w`]+([\w$_\u0080-\uFFFF]+|`[^`]+`)[\s;]*$/i.exec(sql)
14
+ // The lint rule being suppressed here has been evaluated, and it has been
15
+ // determined that the regular expression is sufficient for our use case.
16
+ // eslint-disable-next-line sonarjs/slow-regex
17
+ const match = /^\s*use[^\w`]+([\w$\u0080-\uFFFF]+|`[^`]+`)[\s;]*$/i.exec(sql)
18
18
  return (match && match[1]) || null
19
19
  }
@@ -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
  }
@@ -116,7 +116,8 @@ Contextualizer.prototype = Object.create(null)
116
116
  Contextualizer.prototype.isActive = function isActive() {
117
117
  const segments = this.context.segments
118
118
  const segment = segments[this.idx] || segments[this.parentIdx] || segments[0]
119
- return segment && this.context.transaction.isActive()
119
+ const transaction = this.getTransaction()
120
+ return segment && transaction?.isActive()
120
121
  }
121
122
 
122
123
  /**
@@ -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
+ }
@@ -121,7 +121,7 @@ class PromiseShim extends Shim {
121
121
  const transaction = shim.tracer.getTransaction()
122
122
  // This extra property is added by `_wrapExecutorContext` in the pre step.
123
123
  const executor = args[0]
124
- const context = executor && executor[symbols.executorContext]
124
+ const context = executor?.[symbols.executorContext]
125
125
  if (!context || !shim.isFunction(context.executor)) {
126
126
  return
127
127
  }
@@ -387,9 +387,7 @@ function _wrapExecutorContext(shim, args) {
387
387
  function _wrapResolver(context, fn) {
388
388
  return function wrappedResolveReject(val) {
389
389
  const promise = context.promise
390
- if (promise && promise[symbols.context]) {
391
- promise[symbols.context].getSegment().touch()
392
- }
390
+ promise?.[symbols.context]?.getSegment()?.touch()
393
391
  fn(val)
394
392
  }
395
393
  }
@@ -416,7 +414,7 @@ function wrapHandler({ handler, index, argsLength, useAllParams, ctx, shim }) {
416
414
  }
417
415
 
418
416
  return function __NR_wrappedThenHandler() {
419
- if (!ctx.handler || !ctx.handler[symbols.context]) {
417
+ if (!ctx?.handler?.[symbols.context]) {
420
418
  return handler.apply(this, arguments)
421
419
  }
422
420
 
@@ -591,7 +589,8 @@ class Contextualizer {
591
589
  isActive() {
592
590
  const segments = this.context.segments
593
591
  const segment = segments[this.idx] || segments[this.parentIdx] || segments[0]
594
- return segment && this.context.transaction.isActive()
592
+ const transaction = this.getTransaction()
593
+ return segment && transaction?.isActive()
595
594
  }
596
595
 
597
596
  getTransaction() {
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
  }
@@ -372,12 +372,13 @@ function _makeNestedTransWrapper(shim, func, name, spec) {
372
372
  }
373
373
 
374
374
  let context = shim.tracer.getContext()
375
+ let transaction = shim.tracer.getTransaction()
375
376
 
376
377
  // Only create a new transaction if we either do not have a current
377
378
  // transaction _or_ the current transaction is not of the type we want.
378
- if (!context?.transaction || spec.type !== context?.transaction?.type) {
379
+ if (!transaction || spec.type !== transaction?.type) {
379
380
  shim.logger.trace('Creating new nested %s transaction for %s', spec.type, name)
380
- const transaction = new Transaction(shim.agent)
381
+ transaction = new Transaction(shim.agent)
381
382
  transaction.type = spec.type
382
383
  context = context.enterTransaction(transaction)
383
384
  }
@@ -405,10 +406,11 @@ function _makeNestedTransWrapper(shim, func, name, spec) {
405
406
  */
406
407
  function _makeTransWrapper(shim, func, name, spec) {
407
408
  return function transactionWrapper() {
408
- // Don't nest transactions, reuse existing ones!
409
409
  let context = shim.tracer.getContext()
410
- const existingTransaction = context.transaction
410
+ // Don't nest transactions, reuse existing ones!
411
+ const existingTransaction = shim.tracer.getTransaction()
411
412
  if (!shim.agent.canCollectData() || existingTransaction) {
413
+ shim.logger.trace('Transaction %s exists, not creating new transaction %s for %s', existingTransaction?.id, spec.type, name)
412
414
  return func.apply(this, arguments)
413
415
  }
414
416
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "12.11.1",
3
+ "version": "12.11.3",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [