newrelic 12.20.0 → 12.21.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.
Files changed (40) hide show
  1. package/NEWS.md +21 -0
  2. package/lib/collector/facts.js +1 -5
  3. package/lib/config/default.js +36 -1
  4. package/lib/config/index.js +13 -4
  5. package/lib/feature_flags.js +10 -10
  6. package/lib/instrumentation/@google/genai.js +289 -0
  7. package/lib/instrumentation/openai.js +27 -25
  8. package/lib/instrumentations.js +1 -0
  9. package/lib/llm-events/error-message.js +17 -2
  10. package/lib/llm-events/google-genai/chat-completion-message.js +46 -0
  11. package/lib/llm-events/google-genai/chat-completion-summary.js +24 -0
  12. package/lib/llm-events/google-genai/embedding.js +25 -0
  13. package/lib/llm-events/google-genai/event.js +29 -0
  14. package/lib/llm-events/google-genai/index.js +18 -0
  15. package/lib/metrics/names.js +6 -0
  16. package/lib/otel/attr-reconciler.js +7 -9
  17. package/lib/otel/exception-mapping.js +25 -0
  18. package/lib/otel/fake-span.js +1 -1
  19. package/lib/otel/rules.js +55 -26
  20. package/lib/otel/segment-synthesis.js +6 -6
  21. package/lib/otel/segments/consumer.js +7 -20
  22. package/lib/otel/segments/database.js +40 -75
  23. package/lib/otel/segments/http-external.js +25 -25
  24. package/lib/otel/segments/internal.js +2 -2
  25. package/lib/otel/segments/producer.js +5 -15
  26. package/lib/otel/segments/server.js +6 -49
  27. package/lib/otel/setup.js +22 -6
  28. package/lib/otel/span-processor.js +144 -116
  29. package/lib/otel/transformation-rules.json +1228 -0
  30. package/lib/otel/utils.js +162 -0
  31. package/lib/shim/datastore-shim.js +1 -1
  32. package/lib/transaction/tracer/index.js +1 -1
  33. package/package.json +1 -1
  34. package/lib/otel/attr-mapping/db.js +0 -83
  35. package/lib/otel/attr-mapping/exceptions.js +0 -23
  36. package/lib/otel/attr-mapping/faas.js +0 -25
  37. package/lib/otel/attr-mapping/http.js +0 -193
  38. package/lib/otel/attr-mapping/messaging.js +0 -105
  39. package/lib/otel/attr-mapping/utils.js +0 -75
  40. package/lib/otel/rules.json +0 -500
package/NEWS.md CHANGED
@@ -1,3 +1,24 @@
1
+ ### v12.21.0 (2025-06-04)
2
+
3
+ #### Features
4
+
5
+ * Added `@google/genai` Gemini AI LLM instrumentation ([#3119](https://github.com/newrelic/node-newrelic/pull/3119)) ([7f28a29](https://github.com/newrelic/node-newrelic/commit/7f28a293fbd623464ed1f3f809de59eaea0ff367))
6
+ * Updated opentelemetry bridge to rely on configuration based transformation rules ([#3125](https://github.com/newrelic/node-newrelic/pull/3125)) ([ad116d2](https://github.com/newrelic/node-newrelic/commit/ad116d2aa49818c2824bca606a22c48abecf9580))
7
+
8
+ #### Bug fixes
9
+
10
+ * Refactored getHostnameSafe to accommodate GCP ([#3132](https://github.com/newrelic/node-newrelic/pull/3132)) ([f1eea8d](https://github.com/newrelic/node-newrelic/commit/f1eea8d076586cd0b6c4d671c261deefe5e0f027))
11
+ * Updated openai instrumentation to work with 5.0.0+ ([#3129](https://github.com/newrelic/node-newrelic/pull/3129)) ([09146a9](https://github.com/newrelic/node-newrelic/commit/09146a9d550c0cdbb23ec54325be6f8343db8b78))
12
+ * Updated rules to work with 1.17.0 and 1.24.0 for consumer and producer span kind ([#3128](https://github.com/newrelic/node-newrelic/pull/3128)) ([686c747](https://github.com/newrelic/node-newrelic/commit/686c747cedc2582061608c20402f11912092d5b5))
13
+
14
+ #### Documentation
15
+
16
+ * Updated compatibility report ([#3124](https://github.com/newrelic/node-newrelic/pull/3124)) ([c51d45f](https://github.com/newrelic/node-newrelic/commit/c51d45f4fc3c4fd56bdaa80c1028a83c7f4aadd2)) ([#3117](https://github.com/newrelic/node-newrelic/pull/3117)) ([084e599](https://github.com/newrelic/node-newrelic/commit/084e599428ac9c21e6991e27b5c154db2830ce17))
17
+
18
+ #### Miscellaneous chores
19
+
20
+ * Added preliminary otel configuration ([#3121](https://github.com/newrelic/node-newrelic/pull/3121)) ([664ee4a](https://github.com/newrelic/node-newrelic/commit/664ee4ae956d9cc6ebc6b68ad79a47948510422f))
21
+
1
22
  ### v12.20.0 (2025-05-27)
2
23
 
3
24
  #### Features
@@ -31,11 +31,7 @@ async function facts(agent, callback, { logger = defaultLogger } = {}) {
31
31
  systemInfo = systemInfo || Object.create(null)
32
32
  environment = environment || []
33
33
 
34
- let hostname = agent.config.getHostnameSafe()
35
- if (agent.config.gcp_cloud_run.use_instance_as_host && process.env.K_SERVICE) {
36
- // K_SERVICE is the name of the service in GCP Cloud Run
37
- hostname = systemInfo.vendors?.gcp?.id
38
- }
34
+ const hostname = agent.config.getHostnameSafe(systemInfo?.vendors?.gcp?.id)
39
35
  const results = {
40
36
  utilization: {
41
37
  metadata_version: 5,
@@ -1673,7 +1673,42 @@ defaultConfig.definition = () => ({
1673
1673
  * **Note**: Disabling a given library may affect the instrumentation of libraries used after
1674
1674
  * the disabled library. Use at your own risk.
1675
1675
  */
1676
- instrumentation: pkgInstrumentation
1676
+ instrumentation: pkgInstrumentation,
1677
+
1678
+ /**
1679
+ * Governs the various OpenTelemetry based features provided by the
1680
+ * agent.
1681
+ *
1682
+ * NOTICE: this configuration is subject to change while the OTEL
1683
+ * feature set is in development.
1684
+ */
1685
+ opentelemetry_bridge: {
1686
+ /**
1687
+ * Global switch for the whole OpenTelemetry feature. If it is set to
1688
+ * `false`, any other sub-feature, e.g. `traces`, will not be enabled
1689
+ * regardless of that specific sub-feature setting.
1690
+ */
1691
+ enabled: { default: false, formatter: boolean },
1692
+
1693
+ /**
1694
+ * `traces` are instrumentations, e.g. `@fastify/otel`. Enabling `traces`
1695
+ * enables bridging OpenTelemetry instrumentations into the New Relic
1696
+ * agent.
1697
+ */
1698
+ traces: {
1699
+ enabled: { default: false, formatter: boolean }
1700
+ },
1701
+
1702
+ /**
1703
+ * `metrics` governs automatic configuration of the OpenTelemetry
1704
+ * metrics API. When `true`, the agent will automatically configure the
1705
+ * metrics API to send metrics to New Relic and attach them to the
1706
+ * application entity that is instrumented by the New Relic agent.
1707
+ */
1708
+ metrics: {
1709
+ enabled: { default: false, formatter: boolean }
1710
+ }
1711
+ }
1677
1712
  })
1678
1713
 
1679
1714
  /**
@@ -972,16 +972,25 @@ Config.prototype.getIPAddresses = function getIPAddresses() {
972
972
  return addresses
973
973
  }
974
974
 
975
- function getHostnameSafe() {
975
+ /**
976
+ * Gets the system's host name. If that fails, it just returns ipv4/6
977
+ * based on the user's process_host.ipv_preference setting.
978
+ * @param {string} gcpId GCP metadata ID
979
+ * @returns {string} host name
980
+ */
981
+ function getHostnameSafe(gcpId = null) {
976
982
  let _hostname
977
983
  const config = this
978
984
  this.getHostnameSafe = function getCachedHostname() {
979
985
  return _hostname
980
986
  }
981
987
  try {
982
- if (config.heroku.use_dyno_names) {
983
- const dynoName = process.env.DYNO
984
- _hostname = dynoName || os.hostname()
988
+ // Check for any special hostname scenarios.
989
+ // If not applicable, use the os.hostname()
990
+ if (config.heroku.use_dyno_names && process.env.DYNO) {
991
+ _hostname = process.env.DYNO || os.hostname()
992
+ } else if (config.gcp_cloud_run.use_instance_as_host && process.env.K_SERVICE && gcpId) {
993
+ _hostname = gcpId
985
994
  } else {
986
995
  _hostname = os.hostname()
987
996
  }
@@ -10,7 +10,6 @@ exports.prerelease = {
10
10
  // internal_test_only is used for testing our feature flag implementation.
11
11
  // It is not used to gate any features.
12
12
  internal_test_only: false,
13
- opentelemetry_bridge: false,
14
13
  promise_segments: false,
15
14
  reverse_naming_rules: false,
16
15
  unresolved_promise_cleanup: true,
@@ -20,26 +19,27 @@ exports.prerelease = {
20
19
 
21
20
  // flags that are no longer used for released features
22
21
  exports.released = [
23
- 'released',
22
+ 'async_local_context',
23
+ 'await_support',
24
+ 'aws_bedrock_instrumentation',
24
25
  'cat',
26
+ 'certificate_bundle',
25
27
  'custom_instrumentation',
26
28
  'custom_metrics',
29
+ 'dt_format_w3c',
27
30
  'express_segments',
31
+ 'fastify_instrumentation',
32
+ 'langchain_instrumentation',
28
33
  'native_metrics',
29
34
  'new_promise_tracking',
35
+ 'opentelemetry_bridge',
30
36
  'protocol_17',
31
- 'serverless_mode',
37
+ 'released',
32
38
  'send_request_uri_attribute',
39
+ 'serverless_mode',
33
40
  'synthetics',
34
- 'dt_format_w3c',
35
- 'fastify_instrumentation',
36
- 'await_support',
37
- 'certificate_bundle',
38
- 'async_local_context',
39
41
  'undici_async_tracking',
40
42
  'undici_instrumentation',
41
- 'aws_bedrock_instrumentation',
42
- 'langchain_instrumentation'
43
43
  ]
44
44
 
45
45
  // flags that are no longer used for unreleased features
@@ -0,0 +1,289 @@
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 {
9
+ LlmChatCompletionMessage,
10
+ LlmChatCompletionSummary,
11
+ LlmEmbedding,
12
+ LlmErrorMessage
13
+ } = require('../../../lib/llm-events/google-genai')
14
+ const { RecorderSpec } = require('../../../lib/shim/specs')
15
+ const { extractLlmContext } = require('../../util/llm-utils')
16
+
17
+ const { AI } = require('../../../lib/metrics/names')
18
+ const { GEMINI } = AI
19
+ const { DESTINATIONS } = require('../../config/attribute-filter')
20
+ let TRACKING_METRIC = GEMINI.TRACKING_PREFIX
21
+
22
+ /**
23
+ * Enqueues a LLM event to the custom event aggregator
24
+ *
25
+ * @param {object} params input params
26
+ * @param {Agent} params.agent NR agent instance
27
+ * @param {string} params.type LLM event type
28
+ * @param {object} params.msg LLM event
29
+ */
30
+ function recordEvent({ agent, type, msg }) {
31
+ const llmContext = extractLlmContext(agent)
32
+
33
+ agent.customEventAggregator.add([
34
+ { type, timestamp: Date.now() },
35
+ Object.assign({}, msg, llmContext)
36
+ ])
37
+ }
38
+
39
+ /**
40
+ * Increments the tracking metric and sets the llm attribute on transactions
41
+ *
42
+ * @param {object} params input params
43
+ * @param {Agent} params.agent NR agent instance
44
+ * @param {Transaction} params.transaction active transaction
45
+ */
46
+ function addLlmMeta({ agent, transaction }) {
47
+ agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
48
+ transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
49
+ }
50
+
51
+ /**
52
+ * Generates LlmChatCompletionSummary for a chat completion creation.
53
+ * Also iterates over both input messages and the first response message
54
+ * and creates LlmChatCompletionMessage.
55
+ *
56
+ * Also assigns relevant ids by response id for LlmFeedbackEvent creation
57
+ *
58
+ * @param {object} params input params
59
+ * @param {Agent} params.agent NR agent instance
60
+ * @param {Shim} params.shim the current shim instance
61
+ * @param {TraceSegment} params.segment active segment from chat completion
62
+ * @param {object} params.request chat completion params
63
+ * @param {object} params.response chat completion response
64
+ * @param {boolean} [params.err] err if it exists
65
+ * @param {Transaction} params.transaction active transaction
66
+ */
67
+ function recordChatCompletionMessages({
68
+ agent,
69
+ shim,
70
+ segment,
71
+ request,
72
+ response,
73
+ err,
74
+ transaction
75
+ }) {
76
+ if (!response) {
77
+ // If we get an error, it is possible that `response = null`.
78
+ // In that case, we define it to be an empty object.
79
+ response = {}
80
+ }
81
+
82
+ // Explicitly end segment to provide consistent duration
83
+ // for both LLM events and the segment
84
+ segment.end()
85
+ const completionSummary = new LlmChatCompletionSummary({
86
+ agent,
87
+ segment,
88
+ transaction,
89
+ request,
90
+ response,
91
+ withError: err != null
92
+ })
93
+
94
+ // Only take the first response message and append to input messages
95
+ // request.contents can be a string or an array of strings
96
+ // response.candidates is an array of candidates (choices); we only take the first one
97
+ const inputMessages = Array.isArray(request.contents) ? request.contents : [request.contents]
98
+ const responseMessage = response?.candidates?.[0]?.content
99
+ const messages = responseMessage !== undefined ? [...inputMessages, responseMessage] : inputMessages
100
+ messages.forEach((message, index) => {
101
+ const completionMsg = new LlmChatCompletionMessage({
102
+ agent,
103
+ segment,
104
+ transaction,
105
+ request,
106
+ response,
107
+ index,
108
+ completionId: completionSummary.id,
109
+ message
110
+ })
111
+
112
+ recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
113
+ })
114
+
115
+ recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
116
+
117
+ if (err) {
118
+ const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
119
+ agent.errors.add(transaction, err, llmError)
120
+ }
121
+ }
122
+
123
+ function instrumentStream ({ agent, shim, request, response, segment, transaction }) {
124
+ let err
125
+ let content
126
+ let modelVersion
127
+ let finishReason
128
+ let entireMessage = ''
129
+ shim.wrap(response, 'next', function wrapNext(shim, originalNext) {
130
+ return async function wrappedNext(...nextArgs) {
131
+ let result = {}
132
+ try {
133
+ result = await originalNext.apply(response, nextArgs)
134
+ if (result?.value?.text) {
135
+ modelVersion = result.value.modelVersion
136
+ content = result.value.candidates[0].content
137
+ entireMessage += result.value.text // readonly variable that equates to result.value.candidates[0].content.parts[0].text
138
+ }
139
+ if (result?.value?.candidates?.[0]?.finishReason) {
140
+ finishReason = result.value.candidates[0].finishReason
141
+ }
142
+ } catch (streamErr) {
143
+ err = streamErr
144
+ throw err
145
+ } finally {
146
+ // Update segment duration since we want to extend the
147
+ // time it took to handle the stream
148
+ segment.touch()
149
+
150
+ // result will be {value: undefined, done: true}
151
+ // when the stream is done, so we need to create
152
+ // a mock GenerateContentResponse object with
153
+ // the entire message
154
+ //
155
+ // also need to enter this block if there was an
156
+ // error, so we can record it
157
+ if (result?.done || err) {
158
+ if (content) {
159
+ content.parts[0].text = entireMessage
160
+ result.value = {
161
+ candidates: [
162
+ { content, finishReason }
163
+ ],
164
+ modelVersion
165
+ }
166
+ }
167
+
168
+ recordChatCompletionMessages({
169
+ agent: shim.agent,
170
+ shim,
171
+ segment,
172
+ transaction,
173
+ request,
174
+ response: result?.value,
175
+ err
176
+ })
177
+ }
178
+ }
179
+ return result
180
+ }
181
+ })
182
+ }
183
+
184
+ module.exports = function initialize(agent, googleGenAi, moduleName, shim) {
185
+ if (agent?.config?.ai_monitoring?.enabled !== true) {
186
+ shim.logger.debug('config.ai_monitoring.enabled is set to false.')
187
+ return
188
+ }
189
+
190
+ // Update the tracking metric name with the version of the library
191
+ // being instrumented. We do not have access to the version when
192
+ // initially declaring the variable.
193
+ TRACKING_METRIC = `${TRACKING_METRIC}/${shim.pkgVersion}`
194
+
195
+ const models = googleGenAi.Models
196
+
197
+ // Instruments chat completion creation
198
+ // and creates the LLM events
199
+ shim.record(models.prototype, 'generateContentInternal',
200
+ function wrapGenerateContent(shim, func, name, args) {
201
+ const [request] = args
202
+
203
+ return new RecorderSpec({
204
+ name: GEMINI.COMPLETION,
205
+ promise: true,
206
+ after({ error: err, result: response, segment, transaction }) {
207
+ recordChatCompletionMessages({
208
+ agent,
209
+ shim,
210
+ segment,
211
+ transaction,
212
+ request,
213
+ response,
214
+ err
215
+ })
216
+
217
+ addLlmMeta({ agent, transaction })
218
+ }
219
+ })
220
+ }
221
+ )
222
+
223
+ // Instruments chat completion streaming
224
+ // and creates the LLM events
225
+ shim.record(models.prototype, 'generateContentStreamInternal',
226
+ function wrapGenerateContentStream(shim, func, name, args) {
227
+ if (!agent.config.ai_monitoring.streaming.enabled) {
228
+ shim.logger.warn(
229
+ '`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
230
+ )
231
+ agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
232
+ return
233
+ }
234
+ const [request] = args
235
+
236
+ return new RecorderSpec({
237
+ name: GEMINI.COMPLETION,
238
+ promise: true,
239
+ after({ result: response, segment, transaction }) {
240
+ instrumentStream({ agent, shim, request, response, segment, transaction })
241
+ addLlmMeta({ agent, transaction })
242
+ }
243
+ })
244
+ })
245
+
246
+ // Instruments embedding creation
247
+ // and creates LlmEmbedding event
248
+ shim.record(
249
+ models.prototype,
250
+ 'embedContent',
251
+ function wrapEmbedContent(shim, func, name, args) {
252
+ const [request] = args
253
+
254
+ return new RecorderSpec({
255
+ name: GEMINI.EMBEDDING,
256
+ promise: true,
257
+ after({ error: err, result: response, segment, transaction }) {
258
+ addLlmMeta({ agent, transaction })
259
+
260
+ if (!response) {
261
+ // If we get an error, it is possible that `response = null`.
262
+ // In that case, we define it to be an empty object.
263
+ response = {}
264
+ }
265
+
266
+ // Explicitly end segment to get consistent duration
267
+ // for both LLM events and the segment
268
+ segment.end()
269
+
270
+ const embedding = new LlmEmbedding({
271
+ agent,
272
+ segment,
273
+ transaction,
274
+ request,
275
+ response,
276
+ withError: err != null
277
+ })
278
+
279
+ recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
280
+
281
+ if (err) {
282
+ const llmError = new LlmErrorMessage({ cause: err, embedding, response })
283
+ shim.agent.errors.add(transaction, err, llmError)
284
+ }
285
+ }
286
+ })
287
+ }
288
+ )
289
+ }
@@ -25,7 +25,7 @@ let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
25
25
 
26
26
  /**
27
27
  * Checks if we should skip instrumentation.
28
- * Currently it checks if `ai_monitoring.enabled` is true
28
+ * Currently, it checks if `ai_monitoring.enabled` is true
29
29
  * and the package version >= 4.0.0
30
30
  *
31
31
  * @param {object} config agent config
@@ -172,14 +172,20 @@ function recordChatCompletionMessages({
172
172
  delete response.headers
173
173
  }
174
174
 
175
- /*
176
- * Chat completions create can return a stream once promise resolves
177
- * This wraps the iterator which is a generator function
175
+ /**
176
+ * `chat.completions.create` can return a stream once promise resolves.
177
+ * This wraps the iterator which is a generator function.
178
178
  * We will call the original iterator, intercept chunks and yield
179
- * to the original. On complete we will construct the new message object
179
+ * to the original. On complete, we will construct the new message object
180
180
  * with what we have seen in the stream and create the chat completion
181
- * messages
182
- *
181
+ * messages.
182
+ * @param {object} params input params
183
+ * @param {Agent} params.agent NR agent instance
184
+ * @param {Shim} params.shim the current shim instance
185
+ * @param {object} params.request chat completion params
186
+ * @param {object} params.response chat completion response
187
+ * @param {TraceSegment} params.segment active segment from chat completion
188
+ * @param {Transaction} params.transaction active transaction
183
189
  */
184
190
  function instrumentStream({ agent, shim, request, response, segment, transaction }) {
185
191
  if (!agent.config.ai_monitoring.streaming.enabled) {
@@ -243,12 +249,13 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
243
249
  // initially declaring the variable.
244
250
  TRACKING_METRIC = `${TRACKING_METRIC}/${shim.pkgVersion}`
245
251
 
246
- /**
247
- * Instrumentation is only done to get the response headers and attach
248
- * to the active segment as openai hides the headers from the functions we are
249
- * trying to instrument
250
- */
251
- shim.wrap(openai.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
252
+ // openai@^5.0.0 uses a different prototype structure
253
+ const openaiClient = semver.gte(shim.pkgVersion, '5.0.0') ? openai.OpenAI : openai
254
+
255
+ // Instrumentation is only done to get the response headers and attach
256
+ // to the active segment as openai hides the headers from the functions we are
257
+ // trying to instrument
258
+ shim.wrap(openaiClient.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
252
259
  return function wrappedRequest() {
253
260
  const apiKey = this.apiKey
254
261
  const request = makeRequest.apply(this, arguments)
@@ -266,14 +273,11 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
266
273
  }
267
274
  })
268
275
 
269
- /**
270
- * Instruments chat completion creation
271
- * and creates the LLM events
272
- *
273
- * **Note**: Currently only for promises. streams will come later
274
- */
276
+ // Instruments chat completion creation
277
+ // and creates the LLM events
278
+ // **Note**: Currently only for promises. streams will come later
275
279
  shim.record(
276
- openai.Chat.Completions.prototype,
280
+ openaiClient.Chat.Completions.prototype,
277
281
  'create',
278
282
  function wrapCreate(shim, create, name, args) {
279
283
  const [request] = args
@@ -308,12 +312,10 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
308
312
  }
309
313
  )
310
314
 
311
- /**
312
- * Instruments embedding creation
313
- * and creates LlmEmbedding event
314
- */
315
+ // Instruments embedding creation
316
+ // and creates LlmEmbedding event
315
317
  shim.record(
316
- openai.Embeddings.prototype,
318
+ openaiClient.Embeddings.prototype,
317
319
  'create',
318
320
  function wrapEmbeddingCreate(shim, embeddingCreate, name, args) {
319
321
  const [request] = args
@@ -13,6 +13,7 @@ module.exports = function instrumentations() {
13
13
  '@azure/functions': { type: InstrumentationDescriptor.TYPE_GENERIC },
14
14
  '@elastic/elasticsearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
15
15
  '@opensearch-project/opensearch': { type: InstrumentationDescriptor.TYPE_DATASTORE },
16
+ '@google/genai': { type: InstrumentationDescriptor.TYPE_GENERIC },
16
17
  '@grpc/grpc-js': { module: './instrumentation/grpc-js' },
17
18
  '@hapi/hapi': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
18
19
  '@hapi/vision': { type: InstrumentationDescriptor.TYPE_WEB_FRAMEWORK },
@@ -23,9 +23,24 @@ module.exports = class LlmErrorMessage {
23
23
  * @param {LlmTool} [params.tool] Details about the tool event if it was a tool event.
24
24
  */
25
25
  constructor({ response, cause, summary, embedding, vectorsearch, tool } = {}) {
26
- this['http.statusCode'] = response?.status ?? cause?.status
26
+ // For @google/genai only, cause does not have the `error` or `status` fields,
27
+ // but it does have `message` with the info we need. So, we need to parse
28
+ // the relevant fields from cause.message to get `status` and `error`.
29
+ let parsedError
30
+ const isGeminiVendor = embedding?.vendor === 'gemini' || summary?.vendor === 'gemini'
31
+ if (isGeminiVendor && cause?.message) {
32
+ try {
33
+ // Extract the JSON portion of the cause.message
34
+ const jsonStartIndex = cause.message.indexOf('{')
35
+ const jsonString = cause.message.substring(jsonStartIndex)
36
+ parsedError = JSON.parse(jsonString)?.error
37
+ } catch {
38
+ parsedError = undefined
39
+ }
40
+ }
41
+ this['http.statusCode'] = response?.status ?? cause?.status ?? parsedError?.code
27
42
  this['error.message'] = cause?.message
28
- this['error.code'] = response?.code ?? cause?.error?.code
43
+ this['error.code'] = response?.code ?? cause?.error?.code ?? parsedError?.code
29
44
  this['error.param'] = response?.param ?? cause?.error?.param
30
45
  this.completion_id = summary?.id
31
46
  this.embedding_id = embedding?.id
@@ -0,0 +1,46 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const LlmEvent = require('./event')
8
+ const { makeId } = require('../../util/hashes')
9
+
10
+ module.exports = class LlmChatCompletionMessage extends LlmEvent {
11
+ constructor({
12
+ agent,
13
+ segment,
14
+ request = {},
15
+ response = {},
16
+ index = 0,
17
+ message,
18
+ completionId,
19
+ transaction
20
+ }) {
21
+ super({ agent, segment, request, response, transaction })
22
+ this.id = makeId(36)
23
+ // message?.role is only defined if the message is
24
+ // a response and it is always 'model'.
25
+ // request messages do not have a role
26
+ this.role = message?.role ?? 'user'
27
+ this.sequence = index
28
+ this.completion_id = completionId
29
+ const responseText = response?.text ?? response?.candidates?.[0]?.content?.parts?.[0]?.text
30
+ this.is_response = responseText === message?.parts?.[0]?.text
31
+
32
+ if (agent.config.ai_monitoring.record_content.enabled === true) {
33
+ this.content = this.is_response ? message?.parts?.[0]?.text : message
34
+ }
35
+
36
+ const tokenCB = agent.llm?.tokenCountCallback
37
+
38
+ if (typeof tokenCB === 'function') {
39
+ if (this.is_response) {
40
+ this.token_count = tokenCB(this['response.model'], this.content)
41
+ } else {
42
+ this.token_count = tokenCB(request.model, this.content)
43
+ }
44
+ }
45
+ }
46
+ }
@@ -0,0 +1,24 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const LlmEvent = require('./event')
8
+
9
+ module.exports = class LlmChatCompletionSummary extends LlmEvent {
10
+ constructor({ agent, segment, request = {}, response = {}, withError = false, transaction }) {
11
+ super({ agent, segment, request, response, responseAttrs: true, transaction })
12
+ this.error = withError
13
+ let requestMessagesLength = 0
14
+ if (Array.isArray(request?.contents)) {
15
+ requestMessagesLength = request.contents.length
16
+ } else if (typeof request?.contents === 'string') {
17
+ requestMessagesLength = 1
18
+ }
19
+ this['response.number_of_messages'] = requestMessagesLength + (response?.candidates?.length || 0)
20
+ this['response.choices.finish_reason'] = response?.candidates?.[0]?.finishReason
21
+ this['request.max_tokens'] = request.config?.maxOutputTokens
22
+ this['request.temperature'] = request.config?.temperature
23
+ }
24
+ }
@@ -0,0 +1,25 @@
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 LlmEvent = require('./event')
9
+
10
+ class LlmEmbedding extends LlmEvent {
11
+ constructor({ agent, segment, request = {}, response = {}, withError = false, transaction }) {
12
+ super({ agent, segment, request, response, responseAttrs: true, transaction })
13
+ this.error = withError
14
+
15
+ if (agent.config.ai_monitoring.record_content.enabled === true) {
16
+ this.input = request.contents?.toString()
17
+ }
18
+ this.token_count = agent.llm?.tokenCountCallback?.(
19
+ this['request.model'],
20
+ request.contents?.toString()
21
+ )
22
+ }
23
+ }
24
+
25
+ module.exports = LlmEmbedding