newrelic 13.6.2 → 13.6.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.
Files changed (46) hide show
  1. package/NEWS.md +29 -11
  2. package/README.md +6 -11
  3. package/lib/config/default.js +46 -4
  4. package/lib/config/index.js +110 -1
  5. package/lib/instrumentation/@google/genai.js +15 -24
  6. package/lib/instrumentations.js +0 -1
  7. package/lib/llm-events/aws-bedrock/bedrock-response.js +25 -1
  8. package/lib/llm-events/aws-bedrock/chat-completion-message.js +2 -10
  9. package/lib/llm-events/aws-bedrock/chat-completion-summary.js +11 -15
  10. package/lib/llm-events/aws-bedrock/converse-stream-handler.js +11 -4
  11. package/lib/llm-events/aws-bedrock/embedding.js +3 -20
  12. package/lib/llm-events/aws-bedrock/event.js +3 -20
  13. package/lib/llm-events/aws-bedrock/stream-handler.js +21 -28
  14. package/lib/llm-events/aws-bedrock/utils.js +1 -59
  15. package/lib/llm-events/event.js +108 -0
  16. package/lib/llm-events/google-genai/chat-completion-message.js +3 -10
  17. package/lib/llm-events/google-genai/chat-completion-summary.js +4 -5
  18. package/lib/llm-events/google-genai/embedding.js +0 -34
  19. package/lib/llm-events/google-genai/event.js +7 -0
  20. package/lib/llm-events/openai/chat-completion-message.js +3 -9
  21. package/lib/llm-events/openai/chat-completion-summary.js +4 -5
  22. package/lib/llm-events/openai/embedding.js +4 -15
  23. package/lib/llm-events/openai/event.js +7 -0
  24. package/lib/shim/datastore-shim.js +5 -7
  25. package/lib/subscriber-configs.js +2 -0
  26. package/lib/subscribers/mysql/config.js +73 -0
  27. package/lib/subscribers/mysql/connection-query.js +56 -0
  28. package/lib/subscribers/mysql/helper.js +102 -0
  29. package/lib/subscribers/mysql/pool-get-connection.js +20 -0
  30. package/lib/subscribers/mysql/pool-namespace-query.js +19 -0
  31. package/lib/subscribers/mysql/pool-query.js +41 -0
  32. package/lib/subscribers/mysql2/config.js +127 -0
  33. package/lib/subscribers/mysql2/connection-execute.js +18 -0
  34. package/lib/subscribers/mysql2/connection-query.js +17 -0
  35. package/lib/subscribers/mysql2/pool-get-connection.js +20 -0
  36. package/lib/subscribers/mysql2/pool-namespace-query.js +19 -0
  37. package/lib/subscribers/mysql2/pool-query.js +19 -0
  38. package/lib/subscribers/openai/utils.js +11 -2
  39. package/lib/subscribers/pg/query.js +1 -1
  40. package/package.json +1 -1
  41. package/lib/db/utils.js +0 -19
  42. package/lib/instrumentation/mysql/mysql.js +0 -333
  43. package/lib/instrumentation/mysql/nr-hooks.js +0 -26
  44. package/lib/llm-events/google-genai/utils.js +0 -32
  45. package/lib/llm-events/openai/utils.js +0 -34
  46. package/lib/llm-events/utils.js +0 -110
package/NEWS.md CHANGED
@@ -1,14 +1,31 @@
1
- ### v13.6.2 (2025-10-29)
2
-
3
- #### Bug fixes
4
-
5
- * Updated express instrumentation to properly instrument an array of middleware defined on a route ([#3473](https://github.com/newrelic/node-newrelic/pull/3473)) ([e383c4e](https://github.com/newrelic/node-newrelic/commit/e383c4efc7171f02aef41d6c657455338b4b7258))
6
-
7
- #### Documentation
8
-
9
- * documented how to use the subscriber based instrumentation ([#3432](https://github.com/newrelic/node-newrelic/pull/3432)) ([3b3ab72](https://github.com/newrelic/node-newrelic/commit/3b3ab72eee3b4c1a12592d92eab59da2a48b1f46))
10
- * Updated compatibility report ([#3471](https://github.com/newrelic/node-newrelic/pull/3471)) ([964543b](https://github.com/newrelic/node-newrelic/commit/964543bae1a7c1fd5af32f02ba40d4bf68737d11))
11
-
1
+ ### v13.6.3 (2025-11-11)
2
+
3
+ #### Bug fixes
4
+
5
+ * Updated `@google/genai` to properly handle assigning token counts to LlmCompletionSummary message ([#3495](https://github.com/newrelic/node-newrelic/pull/3495)) ([f805d38](https://github.com/newrelic/node-newrelic/commit/f805d382ed0a898f784b053fb9855e9da97dea98))
6
+ * Updated AWS Bedrock instrumentation to properly assign token counts ([#3491](https://github.com/newrelic/node-newrelic/pull/3491)) ([b1687f6](https://github.com/newrelic/node-newrelic/commit/b1687f6dcb8782623a92dacee740a537ad791fdc))
7
+ * Updated openai instrumentation to properly handle streaming when `stream_options.include_usage` is set ([#3494](https://github.com/newrelic/node-newrelic/pull/3494)) ([37c43f5](https://github.com/newrelic/node-newrelic/commit/37c43f5b448f851b749b95b032c4fb52785f2ab0))
8
+
9
+ #### Code refactoring
10
+
11
+ * Update `mysql` and `mysql2` instrumentation to subscribe to events emitted ([#3423](https://github.com/newrelic/node-newrelic/pull/3423)) ([2e38914](https://github.com/newrelic/node-newrelic/commit/2e38914ffd8510dc610bd629a00b61e604dca2db))
12
+
13
+ #### Documentation
14
+
15
+ * Cleaned up README links and outdated verbiage ([#3484](https://github.com/newrelic/node-newrelic/pull/3484)) ([51e6d7a](https://github.com/newrelic/node-newrelic/commit/51e6d7ace2fb62b907113ec01f63a8d8e4be4e34))
16
+ * Updated compatibility report ([#3477](https://github.com/newrelic/node-newrelic/pull/3477)) ([859f27f](https://github.com/newrelic/node-newrelic/commit/859f27febe4d4afd46bf7d080a2db51ae9acc0cc))
17
+
18
+ ### v13.6.2 (2025-10-29)
19
+
20
+ #### Bug fixes
21
+
22
+ * Updated express instrumentation to properly instrument an array of middleware defined on a route ([#3473](https://github.com/newrelic/node-newrelic/pull/3473)) ([e383c4e](https://github.com/newrelic/node-newrelic/commit/e383c4efc7171f02aef41d6c657455338b4b7258))
23
+
24
+ #### Documentation
25
+
26
+ * documented how to use the subscriber based instrumentation ([#3432](https://github.com/newrelic/node-newrelic/pull/3432)) ([3b3ab72](https://github.com/newrelic/node-newrelic/commit/3b3ab72eee3b4c1a12592d92eab59da2a48b1f46))
27
+ * Updated compatibility report ([#3471](https://github.com/newrelic/node-newrelic/pull/3471)) ([964543b](https://github.com/newrelic/node-newrelic/commit/964543bae1a7c1fd5af32f02ba40d4bf68737d11))
28
+
12
29
  ### v13.6.1 (2025-10-28)
13
30
 
14
31
  #### Bug fixes
@@ -8022,3 +8039,4 @@ Special thanks to Ryan Copley (@RyanCopley) for the contribution.
8022
8039
 
8023
8040
 
8024
8041
 
8042
+
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  This package instruments your application for performance monitoring with [New Relic](https://newrelic.com).
7
7
 
8
- In order to take full advantage of this package, make sure you have a [New Relic account](https://newrelic.com) before starting. Available features, such as slow transaction traces, will vary [based on account level](https://newrelic.com/application-monitoring/features).
8
+ In order to take full advantage of this package, make sure you have a [New Relic account](https://newrelic.com) before starting.
9
9
 
10
10
  As with any instrumentation tool, please test before using in production.
11
11
 
@@ -15,7 +15,7 @@ To use New Relic's Node.js agent entails these three steps, which are described
15
15
 
16
16
  - Install [the `newrelic` package](https://www.npmjs.com/package/newrelic)
17
17
  - Create a base configuration file
18
- - Require the agent in your program
18
+ - Load the agent in your program
19
19
 
20
20
  1. To install the agent for performance monitoring, use your favorite npm-based package manager and install the `newrelic` package into your application:
21
21
 
@@ -95,7 +95,7 @@ node -r newrelic your-program.js
95
95
 
96
96
  If your application is written with `import` and `export` statements in javascript, you are using [ES Modules](https://nodejs.org/api/esm.html#modules-ecmascript-modules) and must bootstrap the agent in a different way.
97
97
 
98
- The New Relic Node.js agent includes ***_experimental_*** support for ES Modules. The agent is reliant on an experimental feature in Node.js in order to appropriately register instrumentation. Until the Node.js API for [ES Module Loaders](https://nodejs.org/api/esm.html#loaders) is stable, breaking changes may occur when updating Node.js. Lastly, the ESM loader does not follow the same [supported Node.js versions](https://docs.newrelic.com/docs/apm/agents/nodejs-agent/getting-started/compatibility-requirements-nodejs-agent#system) as the agent. The minimum supported version of Node.js is `v16.12.0`.
98
+ The New Relic Node.js agent includes support for ES Modules.
99
99
 
100
100
  ### Setup
101
101
 
@@ -237,10 +237,9 @@ If the issue has been confirmed as a bug or is a feature request, please file a
237
237
  **Support Channels**
238
238
 
239
239
  * [New Relic Documentation](https://docs.newrelic.com/docs/agents/nodejs-agent/getting-started/introduction-new-relic-nodejs): Comprehensive guidance for using our platform
240
- * [New Relic Community](https://forum.newrelic.com/): The best place to engage in troubleshooting questions
241
- * [New Relic Developer](https://developer.newrelic.com/): Resources for building a custom observability applications
240
+ * [New Relic Community](https://support.newrelic.com/): The best place to engage in troubleshooting questions
241
+ * [New Relic Technical Support Offerings](https://docs.newrelic.com/docs/licenses/license-information/general-usage-licenses/support-plan): Support offerings.
242
242
  * [New Relic University](https://learn.newrelic.com/): A range of online training for New Relic users of every level
243
- * [New Relic Technical Support](https://support.newrelic.com/) 24/7/365 ticketed support. Read more about our [Technical Support Offerings](https://docs.newrelic.com/docs/licenses/license-information/general-usage-licenses/support-plan).
244
243
 
245
244
 
246
245
  ## Privacy
@@ -250,10 +249,6 @@ We define “Personal Data” as any information relating to an identified or id
250
249
 
251
250
  Please review [New Relic’s General Data Privacy Notice](https://newrelic.com/termsandconditions/privacy) for more information.
252
251
 
253
- ## Roadmap
254
-
255
- See our [roadmap](./ROADMAP_Node.md), to learn more about our product vision, understand our plans, and provide us valuable feedback.
256
-
257
252
  ## Contribute
258
253
 
259
254
  We encourage your contributions to improve the Node.js agent! Keep in mind when you submit your pull request, you'll need to sign the CLA via the click-through using CLA-Assistant. You only have to sign the CLA one time per project.
@@ -268,7 +263,7 @@ If you believe you have found a security vulnerability in this project or any of
268
263
 
269
264
  If you would like to contribute to this project, review [these guidelines](./CONTRIBUTING.md).
270
265
 
271
- To [all contributors](https://github.com/newrelic/node-newrelic/graphs/contributors), we thank you! Without your contribution, this project would not be what it is today. We also host a community project page dedicated to [New Relic Node Agent](https://opensource.newrelic.com/projects/newrelic/node-newrelic).
266
+ To [all contributors](https://github.com/newrelic/node-newrelic/graphs/contributors), we thank you! Without your contribution, this project would not be what it is today.
272
267
 
273
268
  ## License
274
269
 
@@ -1166,6 +1166,32 @@ defaultConfig.definition = () => {
1166
1166
  formatter: int,
1167
1167
  default: 10
1168
1168
  },
1169
+ // The root sampling options are defined in an unusual way compared to the rest of our configuration but this is done to
1170
+ // allow compatibility with how OpenTelemetry sets their samplers and be backwards compatible as well. Root can either be
1171
+ // a string or an object.
1172
+ //
1173
+ // Example setting root sampler via config to a string value -
1174
+ // root: 'always_on'
1175
+ //
1176
+ // Example setting root sampler via config to trace id ratio based -
1177
+ // root: {
1178
+ // trace_id_ratio_based: {
1179
+ // ratio: 0.5
1180
+ // }
1181
+ // }
1182
+ //
1183
+ root: {
1184
+ formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1185
+ default: 'default',
1186
+ // if setting root to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT: 'trace_id_ratio_based'
1187
+ // and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1188
+ trace_id_ratio_based: {
1189
+ ratio: {
1190
+ formatter: float,
1191
+ env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_ROOT_TRACE_ID_RATIO_BASED_RATIO'
1192
+ }
1193
+ }
1194
+ },
1169
1195
 
1170
1196
  /**
1171
1197
  * When set to `always_on`, the sampled flag in the `traceparent` header
@@ -1178,8 +1204,16 @@ defaultConfig.definition = () => {
1178
1204
  * setting.
1179
1205
  */
1180
1206
  remote_parent_sampled: {
1181
- formatter: allowList.bind(null, ['always_on', 'always_off', 'default']),
1182
- default: 'default'
1207
+ formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1208
+ default: 'default',
1209
+ // if setting remote_parent_sampled to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED: 'trace_id_ratio_based'
1210
+ // and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1211
+ trace_id_ratio_based: {
1212
+ ratio: {
1213
+ formatter: float,
1214
+ env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO'
1215
+ }
1216
+ }
1183
1217
  },
1184
1218
 
1185
1219
  /**
@@ -1193,8 +1227,16 @@ defaultConfig.definition = () => {
1193
1227
  * is set to 0.
1194
1228
  */
1195
1229
  remote_parent_not_sampled: {
1196
- formatter: allowList.bind(null, ['always_on', 'always_off', 'default']),
1197
- default: 'default'
1230
+ formatter: allowList.bind(null, ['trace_id_ratio_based', 'adaptive', 'always_on', 'always_off', 'default']),
1231
+ default: 'default',
1232
+ // if setting remote_parent_not_sampled to trace_id_ratio_based via env vars, set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED: 'trace_id_ratio_based'
1233
+ // and then set NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO to the desired ratio
1234
+ trace_id_ratio_based: {
1235
+ ratio: {
1236
+ formatter: float,
1237
+ env: 'NEW_RELIC_DISTRIBUTED_TRACING_SAMPLER_REMOTE_PARENT_NOT_SAMPLED_TRACE_ID_RATIO_BASED_RATIO'
1238
+ }
1239
+ }
1198
1240
  }
1199
1241
  }
1200
1242
  },
@@ -1060,10 +1060,13 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
1060
1060
 
1061
1061
  for (const key of Object.keys(external)) {
1062
1062
  // if it's not in the defaults, it doesn't exist
1063
- if (!arbitrary && !(key in internal)) {
1063
+ if (!arbitrary && Object.hasOwn(internal, key) === false) {
1064
1064
  continue
1065
1065
  }
1066
1066
 
1067
+ // Handle trace_id_ratio_based for distributed tracing sampler configuration
1068
+ this._buildTraceIdRatioSamplers(key, internal, external[key])
1069
+
1067
1070
  if (key === 'ssl' && !isTruthular(external.ssl)) {
1068
1071
  logger.warn(SSL_WARNING)
1069
1072
  continue
@@ -1087,6 +1090,54 @@ Config.prototype._fromPassed = function _fromPassed(external, internal, arbitrar
1087
1090
  }
1088
1091
  }
1089
1092
 
1093
+ /**
1094
+ * Builds the trace_id_ratio_based for distributed tracing sampler configuration structure
1095
+ *
1096
+ * @param {string} key - The configuration key being processed
1097
+ * @param {object} internal - The internal configuration object being modified
1098
+ * @param {object} samplerConfig - Sampler configuration object from defaultsConfig
1099
+ */
1100
+ Config.prototype._buildTraceIdRatioSamplers = function _buildTraceIdRatioSamplers(key, internal, samplerConfig) {
1101
+ if (key !== 'sampler') {
1102
+ return
1103
+ }
1104
+
1105
+ const samplers = Object.keys(samplerConfig)
1106
+
1107
+ // user can set multiple samplers
1108
+ for (const sampler of samplers) {
1109
+ if (this._isTraceIdRatioBasedConfig(sampler, samplerConfig)) {
1110
+ const ratioValue = samplerConfig[sampler].trace_id_ratio_based.ratio
1111
+ if (ratioValue) {
1112
+ internal[key][sampler] = {
1113
+ trace_id_ratio_based: {
1114
+ ratio: ratioValue
1115
+ }
1116
+ }
1117
+ logger.trace('Setting trace id ratio based sampler on %s', samplers)
1118
+ } else {
1119
+ logger.error('Not setting trace id ratio based sampler on %s.', samplers)
1120
+ }
1121
+ }
1122
+ }
1123
+ }
1124
+
1125
+ /**
1126
+ * Checks if the external config contains a trace_id_ratio_based for distributed tracing
1127
+ * sampler configuration
1128
+ *
1129
+ * @param {string} selectedSampler - The selected sampler key
1130
+ * @param {object} samplerConfig - The sampler configuration object
1131
+ * @returns {boolean} true if it's a trace_id_ratio_based config
1132
+ */
1133
+ Config.prototype._isTraceIdRatioBasedConfig = function _isTraceIdRatioBasedConfig(selectedSampler, samplerConfig) {
1134
+ const samplers = ['root', 'remote_parent_sampled', 'remote_parent_not_sampled']
1135
+
1136
+ return samplers.includes(selectedSampler) &&
1137
+ typeof samplerConfig[selectedSampler] === 'object' &&
1138
+ 'trace_id_ratio_based' in samplerConfig[selectedSampler]
1139
+ }
1140
+
1090
1141
  /**
1091
1142
  * Some values should be picked up only if they're not otherwise set, like
1092
1143
  * the Windows / Azure application name. Don't set it if there's already
@@ -1163,6 +1214,53 @@ function setFromEnv({ config, key, envVar, formatter, paths }) {
1163
1214
  }
1164
1215
  }
1165
1216
 
1217
+ /**
1218
+ * Assigns the value of the distributed tracing sampler env var as an trace_id_ratio_based
1219
+ * object to the sampler in the config.
1220
+ *
1221
+ * @param {object} params object passed to fn
1222
+ * @param {string} params.key key of the sampler
1223
+ * Example: 'root' or 'remote_parent_sampled'
1224
+ * @param {object} params.value value of the sampler
1225
+ * Example: { default: 'default', formatter: someFunc, trace_id_ratio_based: {ratio} }
1226
+ * @param {object} params.config agent config
1227
+ * @param {Array} params.paths list of leaf nodes leading to the sampling configuration value
1228
+ * Example: ['distributed_tracing', 'sampler']
1229
+ */
1230
+ function setTraceIDRatioSampler({ key, value, config, paths }) {
1231
+ // trace id ratio based sampler is nested in leaf nodes under distributed_tracing > samplers > key
1232
+ // so don't continue if path is empty
1233
+ if (paths.length === 0) {
1234
+ return
1235
+ }
1236
+
1237
+ const samplers = ['root', 'remote_parent_sampled', 'remote_parent_not_sampled']
1238
+ if (samplers.includes(key) && paths[paths.length - 1] === 'sampler') {
1239
+ // config.distributed_tracing.sampler.[TYPE OF SAMPLER] needs to be set to trace_id_ratio_based
1240
+ // in order to continue
1241
+ if (config.distributed_tracing.sampler[key] !== 'trace_id_ratio_based') {
1242
+ return
1243
+ }
1244
+
1245
+ // set the trace id based ratio
1246
+ const envVar = value.trace_id_ratio_based.ratio.env
1247
+ const formatter = value.trace_id_ratio_based.ratio.formatter
1248
+ const setting = process.env[envVar]
1249
+ if (setting) {
1250
+ const formattedSetting = formatter(setting)
1251
+ config.distributed_tracing.sampler[key] = {
1252
+ trace_id_ratio_based: {
1253
+ ratio: formattedSetting
1254
+ }
1255
+ }
1256
+ logger.trace('Setting %s environment variable', envVar)
1257
+ } else {
1258
+ logger.error('Not setting %s environment variable. Setting sampler root to default.', envVar)
1259
+ config.distributed_tracing.sampler.root = 'default'
1260
+ }
1261
+ }
1262
+ }
1263
+
1166
1264
  /**
1167
1265
  * Recursively visit the nodes of the config definition and look for environment variable names, overriding any configuration values that are found.
1168
1266
  *
@@ -1201,6 +1299,17 @@ Config.prototype._fromEnvironment = function _fromEnvironment(
1201
1299
  if (Object.prototype.hasOwnProperty.call(value, 'default') === true) {
1202
1300
  const envVar = deriveEnvVar(key, paths)
1203
1301
  setFromEnv({ config, key, paths, envVar, formatter: value.formatter })
1302
+
1303
+ // Handle custom configuration of the sampler if it's set to trace_id_ratio_based
1304
+ // since sampler was set to a string but now has to be converted to an object.
1305
+ // This is called here because we need set the sampler to a value first in config before setting
1306
+ // trace id ratio sampler (if the user set that)
1307
+ setTraceIDRatioSampler({
1308
+ key,
1309
+ value,
1310
+ config,
1311
+ paths
1312
+ })
1204
1313
  continue
1205
1314
  }
1206
1315
 
@@ -121,23 +121,26 @@ function recordChatCompletionMessages({
121
121
 
122
122
  function instrumentStream ({ shim, request, response, segment, transaction }) {
123
123
  let err
124
- let content
125
- let modelVersion
126
- let finishReason
127
124
  let entireMessage = ''
125
+ let isDone = false
126
+ let cachedResult = {}
128
127
  shim.wrap(response, 'next', function wrapNext(shim, originalNext) {
129
128
  return async function wrappedNext(...nextArgs) {
130
129
  let result = {}
131
130
  try {
132
131
  result = await originalNext.apply(response, nextArgs)
132
+ // When the stream is done we get {value: undefined, done: true}
133
+ // we need to cache the composed value and add the entire message
134
+ // back in later
135
+ if (result.done === true) {
136
+ isDone = true
137
+ } else {
138
+ cachedResult = result.value
139
+ }
140
+
133
141
  if (result?.value?.text) {
134
- modelVersion = result.value.modelVersion
135
- content = result.value.candidates[0].content
136
142
  entireMessage += result.value.text // readonly variable that equates to result.value.candidates[0].content.parts[0].text
137
143
  }
138
- if (result?.value?.candidates?.[0]?.finishReason) {
139
- finishReason = result.value.candidates[0].finishReason
140
- }
141
144
  } catch (streamErr) {
142
145
  err = streamErr
143
146
  throw err
@@ -146,30 +149,18 @@ function instrumentStream ({ shim, request, response, segment, transaction }) {
146
149
  // time it took to handle the stream
147
150
  segment.touch()
148
151
 
149
- // result will be {value: undefined, done: true}
150
- // when the stream is done, so we need to create
151
- // a mock GenerateContentResponse object with
152
- // the entire message
153
- //
154
152
  // also need to enter this block if there was an
155
153
  // error, so we can record it
156
- if (result?.done || err) {
157
- if (content) {
158
- content.parts[0].text = entireMessage
159
- result.value = {
160
- candidates: [
161
- { content, finishReason }
162
- ],
163
- modelVersion
164
- }
154
+ if (isDone || err) {
155
+ if (cachedResult?.candidates?.[0]?.content?.parts) {
156
+ cachedResult.candidates[0].content.parts[0].text = entireMessage
165
157
  }
166
-
167
158
  recordChatCompletionMessages({
168
159
  agent: shim.agent,
169
160
  segment,
170
161
  transaction,
171
162
  request,
172
- response: result?.value,
163
+ response: cachedResult,
173
164
  err
174
165
  })
175
166
  }
@@ -30,7 +30,6 @@ module.exports = function instrumentations() {
30
30
  langchain: { module: './instrumentation/langchain' },
31
31
  memcached: { type: InstrumentationDescriptor.TYPE_DATASTORE },
32
32
  mongodb: { type: InstrumentationDescriptor.TYPE_DATASTORE },
33
- mysql: { module: './instrumentation/mysql' },
34
33
  next: { module: './instrumentation/nextjs' },
35
34
  q: { type: null },
36
35
  redis: { type: InstrumentationDescriptor.TYPE_DATASTORE },
@@ -73,6 +73,30 @@ class BedrockResponse {
73
73
  return this.#parsedBody
74
74
  }
75
75
 
76
+ get inputTokenCount() {
77
+ if (this.#isConverse) {
78
+ return parseInt(this.#originalResponse?.output?.usage?.inputTokens || 0, 10)
79
+ }
80
+
81
+ return parseInt(this?.headers?.['x-amzn-bedrock-input-token-count'] || 0, 10)
82
+ }
83
+
84
+ get outputTokenCount() {
85
+ if (this.#isConverse) {
86
+ return parseInt(this.#originalResponse?.output?.usage?.outputTokens || 0, 10)
87
+ }
88
+
89
+ return parseInt(this?.headers?.['x-amzn-bedrock-output-token-count'] || 0, 10)
90
+ }
91
+
92
+ get totalTokenCount() {
93
+ if (this.#isConverse) {
94
+ return parseInt(this.#originalResponse?.output?.usage?.totalTokens || 0, 10)
95
+ }
96
+
97
+ return this.inputTokenCount + this.outputTokenCount
98
+ }
99
+
76
100
  /**
77
101
  * The prompt responses returned by the model.
78
102
  *
@@ -164,7 +188,7 @@ class BedrockResponse {
164
188
  } else if (cmd.isClaude3() === true) {
165
189
  if (body?.type === 'message_stop') {
166
190
  // Streamed response
167
- this.#completions = body.completions
191
+ this.#completions.push(body.completions)
168
192
  } else {
169
193
  this.#completions = [stringifyClaudeChunkedMessage(body?.content)]
170
194
  }
@@ -6,9 +6,6 @@
6
6
  'use strict'
7
7
 
8
8
  const LlmEvent = require('./event')
9
- const { setTokenFromCallback } = require('../utils')
10
- const { tokenUsageAttributesExist, tokenUsageHeadersExist } = require('./utils')
11
-
12
9
  /**
13
10
  * @typedef {object} LlmChatCompletionParams
14
11
  * @augments LlmEventParams
@@ -71,9 +68,8 @@ class LlmChatCompletionMessage extends LlmEvent {
71
68
  const completionContent = this.bedrockResponse?.completions?.join(' ')
72
69
 
73
70
  if (promptContent && completionContent) {
74
- setTokenFromCallback(
71
+ this.setTokenFromCallback(
75
72
  {
76
- context: this,
77
73
  tokenCB,
78
74
  reqModel: this.bedrockCommand.modelId,
79
75
  resModel: this.bedrockCommand.modelId,
@@ -85,11 +81,7 @@ class LlmChatCompletionMessage extends LlmEvent {
85
81
  return
86
82
  }
87
83
 
88
- // If no token count callback is available, we need to check the response object
89
- // or response headers for usage information and set token_count to 0 if all usage attributes are present.
90
- if (tokenUsageAttributesExist(this.bedrockResponse) || tokenUsageHeadersExist(this.bedrockResponse)) {
91
- this.token_count = 0
92
- }
84
+ this.setTokenInCompletionMessage({ promptTokens: this.bedrockResponse.inputTokenCount, completionTokens: this.bedrockResponse.outputTokenCount })
93
85
  }
94
86
  }
95
87
 
@@ -7,8 +7,6 @@
7
7
 
8
8
  const LlmEvent = require('./event')
9
9
 
10
- const { setUsageTokens } = require('./utils')
11
- const { setTokenUsageFromCallback } = require('../utils')
12
10
  /**
13
11
  * @typedef {object} LlmChatCompletionSummaryParams
14
12
  * @augments LlmEventParams
@@ -50,21 +48,19 @@ class LlmChatCompletionSummary extends LlmEvent {
50
48
  const promptContent = this.bedrockCommand?.prompt?.map((msg) => msg.content).join(' ')
51
49
  const completionContent = this.bedrockResponse?.completions?.join(' ')
52
50
 
53
- if (promptContent && completionContent) {
54
- setTokenUsageFromCallback(
55
- {
56
- context: this,
57
- tokenCB,
58
- reqModel: this.bedrockCommand.modelId,
59
- resModel: this.bedrockCommand.modelId,
60
- promptContent,
61
- completionContent
62
- }
63
- )
64
- }
51
+ this.setTokenUsageFromCallback(
52
+ {
53
+ tokenCB,
54
+ reqModel: this.bedrockCommand.modelId,
55
+ resModel: this.bedrockCommand.modelId,
56
+ promptContent,
57
+ completionContent
58
+ }
59
+ )
65
60
  return
66
61
  }
67
- setUsageTokens(this.bedrockResponse, this)
62
+
63
+ this.setTokensInResponse({ promptTokens: this.bedrockResponse.inputTokenCount, completionTokens: this.bedrockResponse.outputTokenCount, totalTokens: this.bedrockResponse.totalTokenCount })
68
64
  }
69
65
  }
70
66
 
@@ -65,6 +65,11 @@ class ConverseStreamHandler {
65
65
  message: {
66
66
 
67
67
  }
68
+ },
69
+ usage: {
70
+ inputTokens: null,
71
+ outputTokens: null,
72
+ totalTokens: null
68
73
  }
69
74
  }
70
75
  }
@@ -119,9 +124,7 @@ async function * handleConverse() {
119
124
  for await (const event of this.stream) {
120
125
  yield event
121
126
  this.updateHeaders(event)
122
- if (event.messageStart?.role) {
123
- this.role = 'assistant'
124
- } else if (event.contentBlockStart?.start) {
127
+ if (event.contentBlockStart?.start) {
125
128
  // Handles a Content block start event. Tool use only.
126
129
  const blockStartData = event.contentBlockStart.start
127
130
  if (blockStartData.toolUse) {
@@ -133,6 +136,7 @@ async function * handleConverse() {
133
136
  activeChunk = null
134
137
  }
135
138
  } else if (event.contentBlockDelta?.delta) {
139
+ // There are also deltas for tool use (stringified inputs) but we don't currently record them so we just ignore for now
136
140
  if (event.contentBlockDelta.delta.text) {
137
141
  // It seems like the first streamed chunk does not always start with a contentBlockStart message
138
142
  // If the stream starts with a delta, assume the current chunk is text
@@ -141,9 +145,12 @@ async function * handleConverse() {
141
145
  }
142
146
  activeChunk.text += event.contentBlockDelta.delta.text
143
147
  }
144
- // There are also deltas for tool use (stringified inputs) but we don't currently record them so we just ignore for now
148
+ // used in `handleResponse` to determine why the model stopped
145
149
  } else if (event.messageStop) {
146
150
  this.stopReason = event.messageStop?.stopReason
151
+ // sends token usage info at the end of the stream
152
+ } else if (event.metadata?.usage) {
153
+ this.response.output.usage = event.metadata.usage
147
154
  }
148
155
  }
149
156
  } finally {
@@ -6,7 +6,6 @@
6
6
  'use strict'
7
7
 
8
8
  const LlmEvent = require('./event')
9
- const { validCallbackTokenCount, calculateCallbackTokens } = require('../utils')
10
9
 
11
10
  /**
12
11
  * @typedef {object} LlmEmbeddingParams
@@ -44,28 +43,12 @@ class LlmEmbedding extends LlmEvent {
44
43
  return
45
44
  }
46
45
 
47
- const totalTokenCount = calculateCallbackTokens(tokenCB, this['request.model'], content)
48
- const hasValidCallbackCounts = validCallbackTokenCount(totalTokenCount)
49
-
50
- if (hasValidCallbackCounts) {
51
- this['response.usage.total_tokens'] = Number(totalTokenCount)
52
- }
46
+ const totalTokens = this.calculateCallbackTokens(tokenCB, this['request.model'], content)
47
+ this.setTokensOnEmbeddingMessage(totalTokens)
53
48
  return
54
49
  }
55
50
 
56
- const totalTokens = this.getTotalTokens()
57
- if (totalTokens) {
58
- this['response.usage.total_tokens'] = Number(totalTokens)
59
- }
60
- }
61
-
62
- getTotalTokens() {
63
- // We record the input token count as total tokens
64
- const totalToken =
65
- this.bedrockResponse?.usage?.input_tokens || this.bedrockResponse?.usage?.inputTokens ||
66
- this.bedrockResponse?.headers['x-amzn-bedrock-input-token-count']
67
-
68
- return totalToken
51
+ this.setTokensOnEmbeddingMessage(this.bedrockResponse.inputTokenCount)
69
52
  }
70
53
  }
71
54
 
@@ -6,7 +6,7 @@
6
6
  'use strict'
7
7
 
8
8
  const { randomUUID } = require('crypto')
9
- const { DESTINATIONS } = require('../../config/attribute-filter')
9
+ const BaseEvent = require('../event')
10
10
 
11
11
  /**
12
12
  * @typedef {object} LlmEventParams
@@ -29,7 +29,7 @@ const defaultParams = {
29
29
  /**
30
30
  * Baseline object representing a LLM event.
31
31
  */
32
- class LlmEvent {
32
+ class LlmEvent extends BaseEvent {
33
33
  /**
34
34
  * All parameters that were passed in to the constructor after they have
35
35
  * been merged with the constructor's defaults.
@@ -43,6 +43,7 @@ class LlmEvent {
43
43
  * @param {LlmEventParams} params Construction parameters.
44
44
  */
45
45
  constructor(params = defaultParams) {
46
+ super()
46
47
  params = Object.assign({}, defaultParams, params)
47
48
  this.constructionParams = params
48
49
 
@@ -64,24 +65,6 @@ class LlmEvent {
64
65
  this['request.max_tokens'] = null
65
66
  }
66
67
 
67
- /**
68
- * Pull user set `llm.*` attributes from the current transaction and
69
- * add them to the event.
70
- *
71
- * @param {object} agent The New Relic agent that provides access to the
72
- * transaction.
73
- */
74
- set metadata(agent) { // eslint-disable-line accessor-pairs
75
- const tx = agent.tracer.getTransaction()
76
- const attrs = tx?.trace?.custom.get(DESTINATIONS.TRANS_SCOPE) || {}
77
- for (const [k, v] of Object.entries(attrs)) {
78
- if (k.startsWith('llm.') === false) {
79
- continue
80
- }
81
- this[k] = v
82
- }
83
- }
84
-
85
68
  /**
86
69
  * Removes the complex objects from the event
87
70
  * This will be called right before the event is enqueued to the custom event aggregator