newrelic 13.7.0 → 13.8.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.
@@ -0,0 +1,123 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const {
7
+ LlmChatCompletionMessage,
8
+ LlmChatCompletionSummary,
9
+ LlmErrorMessage
10
+ } = require('../../../lib/llm-events/google-genai')
11
+ const { extractLlmContext } = require('../../util/llm-utils')
12
+ const { AI } = require('../../../lib/metrics/names')
13
+ const { GEMINI } = AI
14
+ const { DESTINATIONS } = require('../../config/attribute-filter')
15
+
16
+ const Subscriber = require('../base')
17
+
18
+ class GoogleGenAISubscriber extends Subscriber {
19
+ constructor({ agent, logger, channelName, packageName = '@google/genai' }) {
20
+ super({ agent, logger, channelName, packageName })
21
+ this.events = ['asyncEnd']
22
+ }
23
+
24
+ get enabled() {
25
+ return super.enabled && this.agent.config.ai_monitoring.enabled
26
+ }
27
+
28
+ /**
29
+ * Enqueues a LLM event to the custom event aggregator
30
+ *
31
+ * @param {object} params input params
32
+ * @param {string} params.type LLM event type
33
+ * @param {object} params.msg LLM event
34
+ */
35
+ recordEvent({ type, msg }) {
36
+ const llmContext = extractLlmContext(this.agent)
37
+
38
+ this.agent.customEventAggregator.add([
39
+ { type, timestamp: Date.now() },
40
+ Object.assign({}, msg, llmContext)
41
+ ])
42
+ }
43
+
44
+ /**
45
+ * Generates LlmChatCompletionSummary for a chat completion creation.
46
+ * Also iterates over both input messages and the first response message
47
+ * and creates LlmChatCompletionMessage.
48
+ *
49
+ * Also assigns relevant ids by response id for LlmFeedbackEvent creation
50
+ *
51
+ * @param {object} params input params
52
+ * @param {TraceSegment} params.segment active segment from chat completion
53
+ * @param {object} params.request chat completion params
54
+ * @param {object} [params.response] chat completion response
55
+ * @param {boolean} [params.err] err if it exists
56
+ * @param {Transaction} params.transaction active transaction
57
+ */
58
+ recordChatCompletionMessages({
59
+ segment,
60
+ request,
61
+ response = {},
62
+ err,
63
+ transaction
64
+ }) {
65
+ const agent = this.agent
66
+
67
+ // Explicitly end segment to provide consistent duration
68
+ // for both LLM events and the segment
69
+ segment.end()
70
+ const completionSummary = new LlmChatCompletionSummary({
71
+ agent,
72
+ segment,
73
+ transaction,
74
+ request,
75
+ response,
76
+ withError: err != null
77
+ })
78
+
79
+ // Only take the first response message and append to input messages
80
+ // request.contents can be a string or an array of strings
81
+ // response.candidates is an array of candidates (choices); we only take the first one
82
+ const inputMessages = Array.isArray(request.contents) ? request.contents : [request.contents]
83
+ const responseMessage = response?.candidates?.[0]?.content
84
+ const messages = responseMessage !== undefined ? [...inputMessages, responseMessage] : inputMessages
85
+ for (let i = 0; i < messages.length; i++) {
86
+ const message = messages[i]
87
+ const completionMsg = new LlmChatCompletionMessage({
88
+ agent,
89
+ segment,
90
+ transaction,
91
+ request,
92
+ response,
93
+ index: i,
94
+ completionId: completionSummary.id,
95
+ message
96
+ })
97
+
98
+ this.recordEvent({ type: 'LlmChatCompletionMessage', msg: completionMsg })
99
+ }
100
+
101
+ this.recordEvent({ type: 'LlmChatCompletionSummary', msg: completionSummary })
102
+
103
+ if (err) {
104
+ const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
105
+ agent.errors.add(transaction, err, llmError)
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Increments the tracking metric and sets the llm attribute on transactions
111
+ *
112
+ * @param {object} params input params
113
+ * @param {Transaction} params.transaction active transaction
114
+ * @param {string} params.version package version
115
+ */
116
+ addLlmMeta({ transaction, version }) {
117
+ const TRACKING_METRIC = `${GEMINI.TRACKING_PREFIX}/${version}`
118
+ this.agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
119
+ transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
120
+ }
121
+ }
122
+
123
+ module.exports = GoogleGenAISubscriber
@@ -0,0 +1,84 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const generateContentInternal = {
7
+ path: './google-genai/generate-content.js',
8
+ instrumentations: [
9
+ {
10
+ channelName: 'nr_generateContentInternal',
11
+ module: { name: '@google/genai', versionRange: '>=1.1.0', filePath: 'dist/node/index.cjs' },
12
+ functionQuery: {
13
+ className: 'Models',
14
+ methodName: 'generateContentInternal',
15
+ kind: 'Async'
16
+ }
17
+ },
18
+ {
19
+ channelName: 'nr_generateContentInternal',
20
+ module: { name: '@google/genai', versionRange: '>=1.1.0', filePath: 'dist/node/index.mjs' },
21
+ functionQuery: {
22
+ className: 'Models',
23
+ methodName: 'generateContentInternal',
24
+ kind: 'Async'
25
+ }
26
+ }
27
+ ]
28
+ }
29
+
30
+ const generateContentStreamInternal = {
31
+ path: './google-genai/generate-content-stream.js',
32
+ instrumentations: [
33
+ {
34
+ channelName: 'nr_generateContentStreamInternal',
35
+ module: { name: '@google/genai', versionRange: '>=1.1.0', filePath: 'dist/node/index.cjs' },
36
+ functionQuery: {
37
+ className: 'Models',
38
+ methodName: 'generateContentStreamInternal',
39
+ kind: 'Async'
40
+ }
41
+ },
42
+ {
43
+ channelName: 'nr_generateContentStreamInternal',
44
+ module: { name: '@google/genai', versionRange: '>=1.1.0', filePath: 'dist/node/index.mjs' },
45
+ functionQuery: {
46
+ className: 'Models',
47
+ methodName: 'generateContentStreamInternal',
48
+ kind: 'Async'
49
+ }
50
+ }
51
+ ]
52
+ }
53
+
54
+ const embedContent = {
55
+ path: './google-genai/embed-content.js',
56
+ instrumentations: [
57
+ {
58
+ channelName: 'nr_embedContent',
59
+ module: { name: '@google/genai', versionRange: '>=1.1.0', filePath: 'dist/node/index.cjs' },
60
+ functionQuery: {
61
+ className: 'Models',
62
+ methodName: 'embedContent',
63
+ kind: 'Async'
64
+ }
65
+ },
66
+ {
67
+ channelName: 'nr_embedContent',
68
+ module: { name: '@google/genai', versionRange: '>=1.1.0', filePath: 'dist/node/index.mjs' },
69
+ functionQuery: {
70
+ className: 'Models',
71
+ methodName: 'embedContent',
72
+ kind: 'Async'
73
+ }
74
+ }
75
+ ]
76
+ }
77
+
78
+ module.exports = {
79
+ '@google/genai': [
80
+ generateContentInternal,
81
+ generateContentStreamInternal,
82
+ embedContent
83
+ ]
84
+ }
@@ -0,0 +1,70 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const GoogleGenAISubscriber = require('./base')
7
+ const { AI } = require('../../../lib/metrics/names')
8
+ const { GEMINI } = AI
9
+ const {
10
+ LlmErrorMessage,
11
+ LlmEmbedding
12
+ } = require('../../../lib/llm-events/google-genai')
13
+
14
+ class GoogleGenAIEmbedContentSubscriber extends GoogleGenAISubscriber {
15
+ constructor ({ agent, logger }) {
16
+ super({ agent, logger, channelName: 'nr_embedContent' })
17
+ }
18
+
19
+ handler(data, ctx) {
20
+ if (!this.enabled) {
21
+ this.logger.debug('`ai_monitoring.enabled` is set to false, not creating segment.')
22
+ return ctx
23
+ }
24
+ const segment = this.agent.tracer.createSegment({
25
+ name: GEMINI.EMBEDDING,
26
+ parent: ctx.segment,
27
+ transaction: ctx.transaction
28
+ })
29
+ return ctx.enterSegment({ segment })
30
+ }
31
+
32
+ asyncEnd(data) {
33
+ const agent = this.agent
34
+ if (!this.enabled) {
35
+ this.logger.debug('`ai_monitoring.enabled` is set to false, not recording Llm events.')
36
+ return
37
+ }
38
+ const ctx = agent.tracer.getContext()
39
+ if (!ctx?.segment || !ctx?.transaction) {
40
+ return
41
+ }
42
+ this.addLlmMeta({ transaction: ctx.transaction, version: data.moduleVersion })
43
+ // If we get an error, it is possible that `response = null`.
44
+ // In that case, we define it to be an empty object.
45
+ const { result: response = {}, arguments: args, error: err } = data
46
+ const [request] = args
47
+
48
+ // Explicitly end segment to get consistent duration
49
+ // for both LLM events and the segment
50
+ ctx.segment.end()
51
+
52
+ const embedding = new LlmEmbedding({
53
+ agent,
54
+ segment: ctx.segment,
55
+ transaction: ctx.transaction,
56
+ request,
57
+ response,
58
+ withError: err != null
59
+ })
60
+
61
+ this.recordEvent({ type: 'LlmEmbedding', msg: embedding })
62
+
63
+ if (err) {
64
+ const llmError = new LlmErrorMessage({ cause: err, embedding, response })
65
+ agent.errors.add(ctx.transaction, err, llmError)
66
+ }
67
+ }
68
+ }
69
+
70
+ module.exports = GoogleGenAIEmbedContentSubscriber
@@ -0,0 +1,125 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const GoogleGenAIGenerateContentSubscriber = require('./generate-content')
7
+ const { AI } = require('../../../lib/metrics/names')
8
+
9
+ class GoogleGenAIGenerateContentStreamSubscriber extends GoogleGenAIGenerateContentSubscriber {
10
+ constructor({ agent, logger }) {
11
+ super({ agent, logger, channelName: 'nr_generateContentStreamInternal' })
12
+ }
13
+
14
+ get enabled() {
15
+ return super.enabled && this.streamingEnabled
16
+ }
17
+
18
+ get streamingEnabled() {
19
+ return this.agent.config.ai_monitoring.streaming.enabled
20
+ }
21
+
22
+ handler(data, ctx) {
23
+ if (!this.enabled) {
24
+ this.logger.debug('`ai_monitoring.enabled` or `ai_monitoring.streaming.enabled` is set to false, not creating segment.')
25
+ return ctx
26
+ }
27
+
28
+ return super.handler(data, ctx)
29
+ }
30
+
31
+ /**
32
+ * Instruments the streaming response by wrapping the next function.
33
+ * @param {object} params function params
34
+ * @param {object} params.request the original request object
35
+ * @param {object} params.response the original response object
36
+ * @param {TraceSegment} params.segment the active trace segment
37
+ * @param {Transaction} params.transaction the active transaction
38
+ */
39
+ instrumentStream({ request, response, segment, transaction }) {
40
+ const self = this
41
+ const originalNext = response.next
42
+ let isDone = false
43
+ let cachedResult = {}
44
+ let err
45
+ let entireMessage = ''
46
+ response.next = async function wrappedNext(...nextArgs) {
47
+ let result = {}
48
+ try {
49
+ result = await originalNext.apply(response, nextArgs)
50
+ // When the stream is done we get {value: undefined, done: true}
51
+ // we need to cache the composed value and add the entire message
52
+ // back in later
53
+ if (result.done === true) {
54
+ isDone = true
55
+ } else {
56
+ cachedResult = result.value
57
+ }
58
+
59
+ if (result?.value?.text) {
60
+ entireMessage += result.value.text // readonly variable that equates to result.value.candidates[0].content.parts[0].text
61
+ }
62
+ } catch (streamErr) {
63
+ err = streamErr
64
+ throw err
65
+ } finally {
66
+ // Update segment duration since we want to extend the
67
+ // time it took to handle the stream
68
+ segment.touch()
69
+
70
+ // also need to enter this block if there was an
71
+ // error, so we can record it
72
+ if (isDone || err) {
73
+ if (cachedResult?.candidates?.[0]?.content?.parts) {
74
+ cachedResult.candidates[0].content.parts[0].text = entireMessage
75
+ }
76
+ self.recordChatCompletionMessages({
77
+ segment,
78
+ transaction,
79
+ request,
80
+ response: cachedResult,
81
+ err
82
+ })
83
+ }
84
+ }
85
+ return result
86
+ }
87
+ }
88
+
89
+ asyncEnd(data) {
90
+ // Check config is ai_monitoring is still enabled
91
+ if (!this.streamingEnabled) {
92
+ this.logger.warn(
93
+ '`ai_monitoring.streaming.enabled` is set to `false`, stream will not be instrumented.'
94
+ )
95
+ this.agent.metrics.getOrCreateMetric(AI.STREAMING_DISABLED).incrementCallCount()
96
+ return
97
+ }
98
+ if (!this.enabled) {
99
+ this.logger.debug('`ai_monitoring.enabled` is set to false, not recording Llm events.')
100
+ return
101
+ }
102
+
103
+ // Instrument the stream
104
+ const ctx = this.agent.tracer.getContext()
105
+ if (!ctx?.segment || !ctx?.transaction) {
106
+ return
107
+ }
108
+ const { result: response, arguments: args } = data
109
+ const [request] = args
110
+
111
+ this.instrumentStream({
112
+ request,
113
+ response,
114
+ segment: ctx.segment,
115
+ transaction: ctx.transaction,
116
+ })
117
+
118
+ this.addLlmMeta({
119
+ transaction: ctx.transaction,
120
+ version: data.moduleVersion,
121
+ })
122
+ }
123
+ }
124
+
125
+ module.exports = GoogleGenAIGenerateContentStreamSubscriber
@@ -0,0 +1,55 @@
1
+ /*
2
+ * Copyright 2025 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const GoogleGenAISubscriber = require('./base')
7
+ const { AI } = require('../../metrics/names')
8
+ const { GEMINI } = AI
9
+
10
+ class GoogleGenAIGenerateContentSubscriber extends GoogleGenAISubscriber {
11
+ constructor({ agent, logger, channelName = 'nr_generateContentInternal' }) {
12
+ super({ agent, logger, channelName })
13
+ }
14
+
15
+ handler(data, ctx) {
16
+ if (!this.enabled) {
17
+ this.logger.debug('`ai_monitoring.enabled` is set to false, not creating segment.')
18
+ return ctx
19
+ }
20
+ const segment = this.agent.tracer.createSegment({
21
+ name: GEMINI.COMPLETION,
22
+ parent: ctx.segment,
23
+ transaction: ctx.transaction
24
+ })
25
+ return ctx.enterSegment({ segment })
26
+ }
27
+
28
+ asyncEnd(data) {
29
+ if (!this.enabled) {
30
+ this.logger.debug('`ai_monitoring.enabled` is set to false, not recording Llm events.')
31
+ return
32
+ }
33
+ const ctx = this.agent.tracer.getContext()
34
+ if (!ctx?.segment || !ctx?.transaction) {
35
+ return
36
+ }
37
+ const { result: response, arguments: args, error: err } = data
38
+ const [request] = args
39
+
40
+ this.recordChatCompletionMessages({
41
+ segment: ctx.segment,
42
+ transaction: ctx.transaction,
43
+ request,
44
+ response,
45
+ err
46
+ })
47
+
48
+ this.addLlmMeta({
49
+ transaction: ctx.transaction,
50
+ version: data.moduleVersion
51
+ })
52
+ }
53
+ }
54
+
55
+ module.exports = GoogleGenAIGenerateContentSubscriber
@@ -67,6 +67,12 @@ const MULTIPLE_INSERT_MESSAGE =
67
67
  'insertDistributedTraceHeaders called on headers object that already contains ' +
68
68
  "distributed trace data. These may be overwritten. traceparent? '%s', newrelic? '%s'."
69
69
 
70
+ const PARTIAL_TYPES = {
71
+ REDUCED: 'reduced',
72
+ ESSENTIAL: 'essential',
73
+ COMPACT: 'compact'
74
+ }
75
+
70
76
  /**
71
77
  * Bundle together the metrics and the trace segment for a single agent
72
78
  * transaction.
@@ -166,6 +172,7 @@ Transaction.TYPES_SET = TYPES_SET
166
172
  Transaction.TRANSPORT_TYPES = TRANSPORT_TYPES
167
173
  Transaction.TRANSPORT_TYPES_SET = TRANSPORT_TYPES_SET
168
174
  Transaction.TRACE_CONTEXT_PARENT_HEADER = TRACE_CONTEXT_PARENT_HEADER
175
+ Transaction.PARTIAL_TYPES = PARTIAL_TYPES
169
176
 
170
177
  /**
171
178
  * Add a clear API method for determining whether a transaction is web or
@@ -63,6 +63,8 @@ function Trace(transaction) {
63
63
  this.displayName = displayName
64
64
  }
65
65
  this.domain = null
66
+ this.spans = []
67
+ this.droppedSpans = new Map()
66
68
  }
67
69
 
68
70
  /**
@@ -79,11 +81,86 @@ Trace.prototype.end = function end(node = this.segments.root) {
79
81
  }
80
82
  }
81
83
 
84
+ /**
85
+ * Adds a span to the trace. If the span is null, it indicates that the span was dropped,
86
+ * and we must keep track of its id and parentId for potential reparenting.
87
+ *
88
+ * @param {object} params to function
89
+ * @param {Span} [params.span] span to add, if present
90
+ * @param {string} params.id id of dropped span
91
+ * @param {string} params.parentId parentId of dropped span
92
+ */
93
+ Trace.prototype.addSpan = function addSpan({ span, id, parentId }) {
94
+ // span was not dropped, add to trace until all spans have been processed
95
+ if (span) {
96
+ this.spans.push(span)
97
+ // span was dropped so keep track of its id and parent as any spans
98
+ // whose parent id was dropped needs to update to this new id
99
+ } else {
100
+ this.droppedSpans.set(id, parentId)
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Iterates over dropped spans and reparents span if its current parent was dropped.
106
+ * It'll traverse until it finds a parent that wasn't dropped or there are no more parents to check.
107
+ *
108
+ * @param {Span} span the span to potentially reparent
109
+ */
110
+ Trace.prototype.maybeReparentSpan = function maybeReparentSpan(span) {
111
+ let result = this.droppedSpans.get(span.parentId)
112
+ while (this.droppedSpans.has(result)) {
113
+ result = this.droppedSpans.get(result)
114
+ }
115
+
116
+ if (result) {
117
+ logger.debug(`Reparenting span ${span.id} from parent ${span.parentId} to ${result}`)
118
+ span.addIntrinsicAttribute('parentId', result)
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Finalizes span events for partial traces by reparenting spans)
124
+ * if their parent was dropped, associates `nr.ids` and `nr.duratiosn` intrinsics for
125
+ * partial traces of type `compact`. Lastly, adds the span events to the span event
126
+ * aggregator.
127
+ *
128
+ * Note: This is a no-op for full traces and traces using infinite tracing.
129
+ */
130
+ Trace.prototype.finalizeSpanEvents = function finalizeSpanEvents() {
131
+ if (!this.transaction.partialType) {
132
+ logger.debug('Trace is full granularity, not finalizing span events.')
133
+ return
134
+ }
135
+
136
+ if (this.transaction.agent.spanEventAggregator.toString() === 'StreamingSpanEventAggregator') {
137
+ logger.debug('Infinite tracing is enabled, not finalizing span events.')
138
+ return
139
+ }
140
+
141
+ for (const span of this.spans) {
142
+ this.maybeReparentSpan(span)
143
+ this.transaction.agent.spanEventAggregator.add(span, this.transaction.priority)
144
+ }
145
+
146
+ this.spans.length = 0
147
+ this.droppedSpans.clear()
148
+ }
149
+
150
+ /**
151
+ * Creates span events for all segments in the trace.
152
+ * Finalizes spans for partial traces by reparenting spans if their parent was dropped,
153
+ * and applying `compact` logic to assign`nr.ids` and `nr.durations` intrinsics.
154
+ */
155
+ Trace.prototype.generateSpanEvents = function generateSpanEvents() {
156
+ this.generateSpans(this.segments.root)
157
+ this.finalizeSpanEvents()
158
+ }
82
159
  /**
83
160
  * Iterates over the trace tree and generates a span event for each segment.
84
161
  * @param {Node} [node] the node to process the segment and its children
85
162
  */
86
- Trace.prototype.generateSpanEvents = function generateSpanEvents(node = this.segments.root) {
163
+ Trace.prototype.generateSpans = function generateSpans(node = this.segments.root) {
87
164
  const config = this.transaction.agent.config
88
165
 
89
166
  if (!shouldGenerateSpanEvents(config, this.transaction)) {
@@ -127,7 +204,7 @@ Trace.prototype.generateSpanEvents = function generateSpanEvents(node = this.seg
127
204
  }
128
205
 
129
206
  for (let i = 0; i < children.length; ++i) {
130
- this.generateSpanEvents(children[i])
207
+ this.generateSpans(children[i])
131
208
  }
132
209
  }
133
210
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "13.7.0",
3
+ "version": "13.8.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [