newrelic 13.10.0 → 13.12.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 (49) hide show
  1. package/NEWS.md +58 -0
  2. package/lib/instrumentation/aws-sdk/v3/bedrock.js +2 -1
  3. package/lib/instrumentation/when/index.js +0 -1
  4. package/lib/llm-events/aws-bedrock/chat-completion-message.js +5 -1
  5. package/lib/llm-events/aws-bedrock/chat-completion-summary.js +1 -0
  6. package/lib/llm-events/error-message.js +8 -2
  7. package/lib/llm-events/google-genai/chat-completion-message.js +5 -0
  8. package/lib/llm-events/google-genai/chat-completion-summary.js +1 -0
  9. package/lib/llm-events/langchain/chat-completion-message.js +16 -10
  10. package/lib/llm-events/langchain/chat-completion-summary.js +2 -1
  11. package/lib/llm-events/langgraph/agent.js +41 -0
  12. package/lib/llm-events/openai/chat-completion-summary.js +4 -2
  13. package/lib/metrics/names.js +10 -2
  14. package/lib/otel/traces/span-processor.js +9 -2
  15. package/lib/spans/span-event.js +10 -0
  16. package/lib/spans/span-link.js +5 -0
  17. package/lib/subscriber-configs.js +1 -0
  18. package/lib/subscribers/ai-monitoring/base.js +113 -0
  19. package/lib/subscribers/ai-monitoring/chat.js +117 -0
  20. package/lib/subscribers/ai-monitoring/embedding.js +72 -0
  21. package/lib/subscribers/ai-monitoring/index.js +14 -0
  22. package/lib/subscribers/google-genai/embed-content.js +18 -44
  23. package/lib/subscribers/google-genai/generate-content-stream.js +16 -39
  24. package/lib/subscribers/google-genai/generate-content.js +40 -29
  25. package/lib/subscribers/ioredis/index.js +2 -12
  26. package/lib/subscribers/langchain/runnable-stream.js +94 -99
  27. package/lib/subscribers/langchain/runnable.js +119 -33
  28. package/lib/subscribers/langchain/tool-callback-manager.js +4 -3
  29. package/lib/subscribers/langchain/tool.js +24 -22
  30. package/lib/subscribers/langchain/vectorstore.js +6 -26
  31. package/lib/subscribers/langgraph/config.js +39 -0
  32. package/lib/subscribers/langgraph/graph-stream.js +131 -0
  33. package/lib/subscribers/mcp-sdk/client-request.js +6 -12
  34. package/lib/subscribers/middleware-wrapper.js +5 -1
  35. package/lib/subscribers/openai/chat.js +204 -41
  36. package/lib/subscribers/openai/client.js +10 -3
  37. package/lib/subscribers/openai/embeddings.js +22 -31
  38. package/lib/subscribers/pg/connect.js +6 -1
  39. package/lib/subscribers/pg/native-connect.js +1 -1
  40. package/lib/subscribers/pg/native-query.js +1 -1
  41. package/lib/subscribers/pg/query.js +6 -1
  42. package/lib/subscribers/utils.js +38 -0
  43. package/lib/transaction/trace/partial-trace.js +49 -20
  44. package/lib/transaction/trace/segment.js +19 -1
  45. package/package.json +1 -1
  46. package/lib/subscribers/google-genai/base.js +0 -123
  47. package/lib/subscribers/langchain/base.js +0 -164
  48. package/lib/subscribers/openai/base.js +0 -46
  49. package/lib/subscribers/openai/utils.js +0 -371
package/NEWS.md CHANGED
@@ -1,3 +1,61 @@
1
+ ### v13.12.0 (2026-02-02)
2
+
3
+ #### Features
4
+
5
+ * Added instrumentation support for `@langchain/langgraph` ([#3645](https://github.com/newrelic/node-newrelic/pull/3645)) ([f339675](https://github.com/newrelic/node-newrelic/commit/f3396754fb182a3f0af09488a8abb1981b6f3e3f))
6
+ * Added `timestamp` to `LlmChatCompletionSummary` messages
7
+ * Added `timestamp` to AWS Bedrock `LlmChatCompletionSummary` ([#3702](https://github.com/newrelic/node-newrelic/pull/3702)) ([430d1dd](https://github.com/newrelic/node-newrelic/commit/430d1dd5ef847203045486a054142df3a02c7c1c))
8
+ * Added `timestamp` to Google Gen AI `LlmChatCompletionSummary` ([#3690](https://github.com/newrelic/node-newrelic/pull/3690)) ([7748e26](https://github.com/newrelic/node-newrelic/commit/7748e26aceb7b050b13dc686c852513026f043ac))
9
+ * Added `timestamp` to LangChain `LlmChatCompletionSummary` ([#3701](https://github.com/newrelic/node-newrelic/pull/3701)) ([7472118](https://github.com/newrelic/node-newrelic/commit/747211814396e4bf52dc63d249c0ec045b35457a))
10
+ * Added compact mode for span links ([#3681](https://github.com/newrelic/node-newrelic/pull/3681)) ([6ff6961](https://github.com/newrelic/node-newrelic/commit/6ff6961e6b46feb14638da97a95d0891a0d88645))
11
+
12
+ #### Bug fixes
13
+
14
+ * Updated langchain tool instrumentation to properly redefine the segment name on every call ([#3691](https://github.com/newrelic/node-newrelic/pull/3691)) ([4df6068](https://github.com/newrelic/node-newrelic/commit/4df6068d23b5da276486bd3d3f0c3af4b748c4a8))
15
+
16
+ #### Documentation
17
+
18
+ * Updated compatibility report ([#3699](https://github.com/newrelic/node-newrelic/pull/3699)) ([40b6b81](https://github.com/newrelic/node-newrelic/commit/40b6b813e7b6490c2e866a33cb1a7492b35d2a36))
19
+
20
+ ### v13.11.0 (2026-01-29)
21
+
22
+ #### Features
23
+
24
+ * Added `timestamp` to AIM `LlmChatCompletionMessage` messages
25
+ * Added `timestamp` to AWS Bedrock `LlmChatCompletionMessage` ([#3685](https://github.com/newrelic/node-newrelic/pull/3685)) ([ea297c8](https://github.com/newrelic/node-newrelic/commit/ea297c86772c1ae377a16e636db8bbcf53ef1547))
26
+ * Added `timestamp` to Google Gen AI `LlmChatCompletionMessage` ([#3686](https://github.com/newrelic/node-newrelic/pull/3686)) ([f4dd09e](https://github.com/newrelic/node-newrelic/commit/f4dd09ec332c47396277f6aaeff2f6f15daad97a))
27
+ * Added `timestamp` to LangChain input `LlmChatCompletionMessage` ([#3658](https://github.com/newrelic/node-newrelic/pull/3658)) ([0bce235](https://github.com/newrelic/node-newrelic/commit/0bce235ce02f772bc089bf947afadf312c735c1b))
28
+ * Added timestamp to OpenAI `LlmChatCompletionSummary` ([#3689](https://github.com/newrelic/node-newrelic/pull/3689)) ([55b0310](https://github.com/newrelic/node-newrelic/commit/55b03100323eefb3578816cff69f2b919e4b7bdd))
29
+ * Additional support for partial granularity traces (Not available for production use)
30
+ * Added essential tracing mode for span links ([#3670](https://github.com/newrelic/node-newrelic/pull/3670)) ([83dfb84](https://github.com/newrelic/node-newrelic/commit/83dfb84b842e0245392425ce61e92a2789fcaf06))
31
+ * Updated `Supportability/Nodejs/PartialGranularity/NrIds/Dropped` metric to only keep track of spans that got dropped when `nr.ids` execeeded 63 spans. ([#3684](https://github.com/newrelic/node-newrelic/pull/3684)) ([9797cf3](https://github.com/newrelic/node-newrelic/commit/9797cf38aa2808b29da9623158edb221b1819548))
32
+
33
+ #### Bug fixes
34
+
35
+ * Added defensive code in `lib/subscribers/middleware-wrapper.js` to prevent crash when attempting to associate an error on an incoming http request ([#3697](https://github.com/newrelic/node-newrelic/pull/3697)) ([658e78b](https://github.com/newrelic/node-newrelic/commit/658e78ba564eae996d790bb3334bda6847c7e2bd))
36
+ * Updated `pg` instrumentation to properly capture the TraceSegment duration for promise based queries ([#3695](https://github.com/newrelic/node-newrelic/pull/3695)) ([0051697](https://github.com/newrelic/node-newrelic/commit/005169776a3f3fe9d988174857b3c96e6131aea3))
37
+
38
+ #### Code refactoring
39
+
40
+ * Added AIM abstract subscribers and updated all AIM subscribers to use them ([#3676](https://github.com/newrelic/node-newrelic/pull/3676)) ([08cd38b](https://github.com/newrelic/node-newrelic/commit/08cd38b5b34b4f1a2636ccc12d359484af6b3e83))
41
+
42
+ #### Documentation
43
+
44
+ * Updated compatibility report ([#3675](https://github.com/newrelic/node-newrelic/pull/3675)) ([cc898f4](https://github.com/newrelic/node-newrelic/commit/cc898f4d8b0f520e120f330ff9e90fe7baaebedc))
45
+
46
+ #### Miscellaneous chores
47
+
48
+ * Added TimedEvent limit of 100 ([#3683](https://github.com/newrelic/node-newrelic/pull/3683)) ([0d595d8](https://github.com/newrelic/node-newrelic/commit/0d595d8579b5a58e49908bc24321ce7b526ddff2))
49
+ * Added supportability metrics `Supportability/Nodejs/SpanEvent/<Links|Events>/Dropped` when limit of 100 is exceeded ([#3688](https://github.com/newrelic/node-newrelic/pull/3688)) ([a247d31](https://github.com/newrelic/node-newrelic/commit/a247d31fc9c24752be50a73d4ddfc7a088a68ce6))
50
+ * Removed eslint disable next line for `sonarjs/no-dead-store` ([#3692](https://github.com/newrelic/node-newrelic/pull/3692)) ([593fa15](https://github.com/newrelic/node-newrelic/commit/593fa15a033437b95fe375cf6034b8453c7d2b36))
51
+
52
+ #### Continuous Integration
53
+ * Fixed CI issue when sec-agent present ([#3677](https://github.com/newrelic/node-newrelic/pull/3677)) ([23da654](https://github.com/newrelic/node-newrelic/commit/23da6547e72f49bf798046330e8cfa74a0b4d2af))
54
+
55
+ #### Tests
56
+
57
+ * Assert that span events drop in partial traces ([#3680](https://github.com/newrelic/node-newrelic/pull/3680)) ([25e3781](https://github.com/newrelic/node-newrelic/commit/25e3781098343b9ca9e4aed0cd5c9e0f94266926))
58
+
1
59
  ### v13.10.0 (2026-01-20)
2
60
 
3
61
  #### Features
@@ -75,9 +75,10 @@ function isStreamingEnabled({ commandName, config }) {
75
75
  function recordEvent({ agent, type, msg }) {
76
76
  msg.serialize()
77
77
  const llmContext = extractLlmContext(agent)
78
+ const timestamp = msg?.timestamp ?? Date.now()
78
79
 
79
80
  agent.customEventAggregator.add([
80
- { type, timestamp: Date.now() },
81
+ { type, timestamp },
81
82
  Object.assign({}, msg, llmContext)
82
83
  ])
83
84
  }
@@ -296,7 +296,6 @@ module.exports = function initialize(shim, when) {
296
296
  ret = agent.tracer.bindFunction(fn, context, true).apply(this, arguments)
297
297
  } finally {
298
298
  if (ret && typeof ret.then === 'function') {
299
- // eslint-disable-next-line sonarjs/no-dead-store
300
299
  ret = ctx.next[symbols.context].continue(ret)
301
300
  }
302
301
  }
@@ -38,7 +38,7 @@ class LlmChatCompletionMessage extends LlmEvent {
38
38
  params = Object.assign({}, defaultParams, params)
39
39
  super(params)
40
40
 
41
- const { agent, content, isResponse, index, completionId, role } = params
41
+ const { agent, content, isResponse, index, completionId, role, segment } = params
42
42
  const recordContent = agent.config?.ai_monitoring?.record_content?.enabled
43
43
 
44
44
  this.is_response = isResponse
@@ -46,6 +46,10 @@ class LlmChatCompletionMessage extends LlmEvent {
46
46
  this.sequence = index
47
47
  this.content = recordContent === true ? content : undefined
48
48
  this.role = role
49
+ if (this.is_response === false) {
50
+ // Only record for request/input messages
51
+ this.timestamp = segment.timer.start
52
+ }
49
53
 
50
54
  this.#setId(index)
51
55
  this.setTokenCount(agent)
@@ -37,6 +37,7 @@ class LlmChatCompletionSummary extends LlmEvent {
37
37
  this['request.temperature'] = cmd.temperature
38
38
  this['response.number_of_messages'] = (cmd.prompt.length ?? 0) + (res.completions.length ?? 0)
39
39
 
40
+ this.timestamp = segment.timer.start
40
41
  this.setTokens(agent)
41
42
  }
42
43
 
@@ -21,20 +21,26 @@ module.exports = class LlmErrorMessage {
21
21
  * @param {LlmVectorStoreSearch} [params.vectorsearch] Details about the vector
22
22
  * search if it was a vector search event.
23
23
  * @param {LlmTool} [params.tool] Details about the tool event if it was a tool event.
24
+ * @param {object} [params.aiAgent] Details about the AI agent event if it was an AI agent event.
24
25
  * @param {boolean} [params.useNameAsCode] defaults to false, only Bedrock sets it to true so far
25
26
  */
26
- constructor({ response, cause, summary = {}, embedding = {}, vectorsearch = {}, tool = {}, useNameAsCode = false } = {}) {
27
+ constructor({ response, cause, summary = {}, embedding = {}, vectorsearch = {}, tool = {}, aiAgent = {}, useNameAsCode = false } = {}) {
27
28
  this['http.statusCode'] = response?.statusCode ?? response?.status ?? cause?.status
28
29
  this['error.message'] = cause?.message
29
- this['error.code'] = response?.code ?? cause?.error?.code
30
+ this['error.code'] = response?.code ?? cause?.error?.code ?? cause?.code
30
31
  if (useNameAsCode) {
31
32
  this['error.code'] = cause?.name
32
33
  }
34
+ if (cause?.['lc_error_code']) {
35
+ // this is where langchain error codes live
36
+ this['error.code'] = cause['lc_error_code']
37
+ }
33
38
  this['error.param'] = response?.param ?? cause?.error?.param
34
39
  this.completion_id = summary?.id
35
40
  this.embedding_id = embedding?.id
36
41
  this.vector_store_id = vectorsearch?.id
37
42
  this.tool_id = tool?.id
43
+ this.agent_id = aiAgent?.id
38
44
 
39
45
  if (embedding?.vendor === 'gemini' || summary?.vendor === 'gemini') {
40
46
  this._handleGemini(cause)
@@ -33,6 +33,11 @@ module.exports = class LlmChatCompletionMessage extends LlmEvent {
33
33
  this.content = this.is_response ? message?.parts?.[0]?.text : message
34
34
  }
35
35
 
36
+ // only add timestamp for request/input messages
37
+ if (this.is_response === false) {
38
+ this.timestamp = segment.timer.start
39
+ }
40
+
36
41
  this.setTokenCount(agent, request, response)
37
42
  }
38
43
 
@@ -21,6 +21,7 @@ module.exports = class LlmChatCompletionSummary extends LlmEvent {
21
21
  this['request.max_tokens'] = request.config?.maxOutputTokens
22
22
  this['request.temperature'] = request.config?.temperature
23
23
 
24
+ this.timestamp = segment.timer.start
24
25
  this.setTokens(agent, request, response)
25
26
  }
26
27
 
@@ -38,19 +38,25 @@ class LangChainCompletionMessage extends LangChainEvent {
38
38
  constructor(params = defaultParams) {
39
39
  params = Object.assign({}, defaultParams, params)
40
40
  super(params)
41
- const { agent } = params
41
+ const { agent, runId, segment, sequence, role, content, completionId, isResponse } = params
42
42
 
43
- if (params.runId) {
44
- this.id = `${params.runId}-${params.sequence}`
43
+ if (runId) {
44
+ this.id = `${runId}-${sequence}`
45
45
  } else {
46
- this.id = `${this.id}-${params.sequence}`
46
+ this.id = `${this.id}-${sequence}`
47
47
  }
48
48
 
49
- this.sequence = params.sequence
50
- this.completion_id = params.completionId
51
- this.is_response = params.isResponse ?? false
52
- if (params.role) {
53
- this.role = params.role
49
+ this.sequence = sequence
50
+ this.completion_id = completionId
51
+
52
+ this.is_response = isResponse ?? false
53
+ if (this.is_response === false) {
54
+ // `timestamp` is only required for input/request messages.
55
+ this.timestamp = segment.timer.start
56
+ }
57
+
58
+ if (role) {
59
+ this.role = role
54
60
  } else {
55
61
  // As a backup, we can infer the role based on if it
56
62
  // is a response or not.
@@ -58,7 +64,7 @@ class LangChainCompletionMessage extends LangChainEvent {
58
64
  }
59
65
 
60
66
  if (agent.config.ai_monitoring.record_content.enabled === true) {
61
- this.content = params.content
67
+ this.content = content
62
68
  }
63
69
  }
64
70
  }
@@ -33,8 +33,9 @@ class LangChainCompletionSummary extends LangChainEvent {
33
33
  super(params)
34
34
  const { segment } = params
35
35
 
36
- this.duration = segment?.getDurationInMillis()
36
+ this.duration = segment.getDurationInMillis()
37
37
  this['response.number_of_messages'] = params.messages?.length
38
+ this.timestamp = segment.timer.start
38
39
  }
39
40
  }
40
41
 
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ const BaseLlmEvent = require('../event')
7
+ const { makeId } = require('../../util/hashes')
8
+
9
+ /**
10
+ * @typedef {object} LangGraphAgentEventParams
11
+ * @property {Agent} agent The New Relic agent instance.
12
+ * @property {string} name The name of the LangGraph agent, defaults to 'agent'.
13
+ * @property {object} segment The associated NR segment.
14
+ * @property {object} transaction The associated NR transaction.
15
+ * @property {boolean} error A boolean flag to indicate if an error occurred.
16
+ */
17
+
18
+ module.exports = class LangGraphAgentEvent extends BaseLlmEvent {
19
+ id = makeId(36)
20
+ span_id
21
+ trace_id
22
+ ingest_source = 'Node'
23
+ vendor = 'langgraph'
24
+
25
+ /**
26
+ * @param {LangGraphAgentEventParams} params should contain all necessary and optional LangGraph data
27
+ */
28
+ constructor(params) {
29
+ super(params)
30
+ const { agent, segment, transaction, error = false, name = 'agent' } = params
31
+
32
+ this.name = name
33
+ this.span_id = segment.id
34
+ this.trace_id = transaction.traceId
35
+ this.error = error
36
+
37
+ // Setting `metadata` as the NR agent instance will allow `BaseLlmEvent`
38
+ // to extract the relevant `llm.<user_defined_metadata>`.
39
+ this.metadata = agent
40
+ }
41
+ }
@@ -14,17 +14,19 @@ module.exports = class LlmChatCompletionSummary extends LlmEvent {
14
14
  this['request.temperature'] = request.temperature
15
15
 
16
16
  if (request?.input) {
17
- // `responses.create` logic
17
+ // `responses.create` api logic
18
18
  // `request.input` can be an array or a string.
19
19
  const requestLength = Array.isArray(request?.input) ? request.input.length : 1
20
20
  this['response.number_of_messages'] = requestLength + (response?.output?.length ?? 0)
21
21
  this['response.choices.finish_reason'] = response?.status
22
22
  } else {
23
- // `chat.completions.create` logic
23
+ // `chat.completions.create` api logic
24
24
  this['response.number_of_messages'] = request?.messages?.length + response?.choices?.length
25
25
  this['response.choices.finish_reason'] = response?.choices?.[0]?.finish_reason
26
26
  }
27
27
 
28
+ this.timestamp = segment.timer.start
29
+
28
30
  this.setTokens(agent, request, response)
29
31
  }
30
32
 
@@ -171,7 +171,8 @@ const AI = {
171
171
  COMPLETION: 'Llm/completion',
172
172
  TOOL: 'Llm/tool',
173
173
  CHAIN: 'Llm/chain',
174
- VECTORSTORE: 'Llm/vectorstore'
174
+ VECTORSTORE: 'Llm/vectorstore',
175
+ AGENT: 'Llm/agent'
175
176
  }
176
177
 
177
178
  AI.GEMINI = {
@@ -199,6 +200,11 @@ AI.LANGCHAIN = {
199
200
  VECTORSTORE: `${AI.VECTORSTORE}/LangChain`
200
201
  }
201
202
 
203
+ AI.LANGGRAPH = {
204
+ TRACKING_PREFIX: `${AI.TRACKING_PREFIX}/LangGraph`,
205
+ AGENT: `${AI.AGENT}/LangGraph`
206
+ }
207
+
202
208
  const MCP = {
203
209
  TRACKING_PREFIX: `${AI.TRACKING_PREFIX}/MCP`,
204
210
  TOOL: `${AI.TOOL}/MCP`,
@@ -325,7 +331,9 @@ const SPAN_EVENTS = {
325
331
  SEEN: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'TotalEventsSeen',
326
332
  SENT: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'TotalEventsSent',
327
333
  DROPPED: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'Discarded',
328
- LIMIT: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'Limit'
334
+ LIMIT: SUPPORTABILITY.PREFIX + SPAN_EVENT_PREFIX + 'Limit',
335
+ LINKS_DROPPED: SUPPORTABILITY.NODEJS + '/' + SPAN_EVENT_PREFIX + 'Links/Dropped',
336
+ EVENTS_DROPPED: SUPPORTABILITY.NODEJS + '/' + SPAN_EVENT_PREFIX + 'Events/Dropped'
329
337
  }
330
338
 
331
339
  const INFINITE_TRACING = {
@@ -23,6 +23,7 @@ const {
23
23
  transformTemplate
24
24
  } = require('./utils.js')
25
25
  const defaultLogger = require('#agentlib/logger.js').child({ component: 'span-processor' })
26
+ const { SPAN_EVENTS } = require('#agentlib/metrics/names.js')
26
27
 
27
28
  const {
28
29
  SPAN_STATUS_CODE
@@ -155,7 +156,10 @@ module.exports = class NrSpanProcessor {
155
156
  event: span.events[i],
156
157
  spanContext
157
158
  })
158
- segment.addTimedEvent(event)
159
+ const added = segment.addTimedEvent(event)
160
+ if (!added) {
161
+ this.agent.metrics.getOrCreateMetric(SPAN_EVENTS.EVENTS_DROPPED).incrementCallCount()
162
+ }
159
163
  }
160
164
  }
161
165
 
@@ -181,7 +185,10 @@ module.exports = class NrSpanProcessor {
181
185
  spanContext,
182
186
  timestamp
183
187
  })
184
- segment.addSpanLink(link)
188
+ const added = segment.addSpanLink(link)
189
+ if (!added) {
190
+ this.agent.metrics.getOrCreateMetric(SPAN_EVENTS.LINKS_DROPPED).incrementCallCount()
191
+ }
185
192
  }
186
193
  }
187
194
 
@@ -60,6 +60,8 @@ class SpanIntrinsics {
60
60
  this.duration = null
61
61
  this['nr.entryPoint'] = null
62
62
  this['nr.pg'] = null
63
+ this['nr.durations'] = null
64
+ this['nr.ids'] = null
63
65
  this['span.kind'] = null
64
66
  this.trustedParentId = null
65
67
  this.tracingVendors = null
@@ -316,6 +318,14 @@ class SpanEvent {
316
318
  * @returns {SpanEvent|null} the span after applying the rules, or null if dropped
317
319
  */
318
320
  applyPartialTraceRules({ isEntry, partialTrace }) {
321
+ // in essential and compact mode - we need to remove the non intrinsic attributes from span links early in this process because there can be an entry span,
322
+ // llm span exit span or span with no entity relationship attributes can have non intrinsics on it.
323
+ if (partialTrace.type !== PARTIAL_TYPES.REDUCED) {
324
+ for (const link of this.spanLinks) {
325
+ link.removeNonIntrinsicsAttrs()
326
+ }
327
+ }
328
+
319
329
  if (isEntry) {
320
330
  this.addIntrinsicAttribute('nr.pg', true)
321
331
  logger.trace('Span %s is an entry point, keeping span unchanged.', this.intrinsics.name)
@@ -86,6 +86,11 @@ class SpanLink {
86
86
  filterNulls(this.agentAttributes.get(DESTINATIONS.TRANS_SEGMENT))
87
87
  ]
88
88
  }
89
+
90
+ removeNonIntrinsicsAttrs() {
91
+ this.userAttributes = Object.create(null)
92
+ this.agentAttributes = Object.create(null)
93
+ }
89
94
  }
90
95
 
91
96
  function filterNulls(inputObj) {
@@ -18,6 +18,7 @@ const subscribers = {
18
18
  ...require('./subscribers/ioredis/config'),
19
19
  ...require('./subscribers/iovalkey/config'),
20
20
  ...require('./subscribers/langchain/config'),
21
+ ...require('./subscribers/langgraph/config'),
21
22
  ...require('./subscribers/mcp-sdk/config'),
22
23
  ...require('./subscribers/mysql/config'),
23
24
  ...require('./subscribers/mysql2/config'),
@@ -0,0 +1,113 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const Subscriber = require('../base')
8
+ const { DESTINATIONS } = require('#agentlib/config/attribute-filter.js')
9
+ const { extractLlmContext } = require('#agentlib/util/llm-utils.js')
10
+
11
+ class AiMonitoringSubscriber extends Subscriber {
12
+ /**
13
+ * @param {object} params constructor params object
14
+ * @param {object} params.agent A New Relic Node.js agent instance.
15
+ * @param {object} params.logger An agent logger instance.
16
+ * @param {string} params.packageName The package name being instrumented.
17
+ * This is what a developer would provide to the `require` function.
18
+ * @param {string} params.channelName A unique name for the diagnostics channel
19
+ * that will be created and monitored.
20
+ * @param {string} params.name name of segment for a given subscriber
21
+ * @param {string} params.trackingPrefix prefix for the tracking metric for a given subscriber
22
+ */
23
+ constructor({ agent, logger, packageName, channelName, name, trackingPrefix }) {
24
+ super({ agent, logger, packageName, channelName })
25
+ this.name = name
26
+ this.trackingPrefix = trackingPrefix
27
+ }
28
+
29
+ get enabled() {
30
+ return super.enabled && this.agent.config.ai_monitoring.enabled === true
31
+ }
32
+
33
+ set name(name) {
34
+ if (!name) {
35
+ throw new Error('subscriber must pass in `name`')
36
+ }
37
+ this._name = name
38
+ }
39
+
40
+ get name() {
41
+ return this._name
42
+ }
43
+
44
+ set trackingPrefix(prefix) {
45
+ if (!prefix) {
46
+ throw new Error('subscriber must pass in `trackingPrefix`')
47
+ }
48
+ this._prefix = prefix
49
+ }
50
+
51
+ get trackingPrefix() {
52
+ return this._prefix
53
+ }
54
+
55
+ /**
56
+ * Increments the tracking metric and sets the llm attribute on transactions
57
+ *
58
+ * @param {object} params input params
59
+ * @param {Transaction} params.ctx active context
60
+ * @param {string} params.version package version
61
+ */
62
+ addLlmMeta({ ctx, version }) {
63
+ if (!ctx?.transaction) {
64
+ return
65
+ }
66
+
67
+ const transaction = ctx.transaction
68
+ const trackingMetric = `${this.trackingPrefix}/${version}`
69
+ this.agent.metrics.getOrCreateMetric(trackingMetric).incrementCallCount()
70
+ transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
71
+ }
72
+
73
+ /**
74
+ * Enqueues a LLM event to the custom event aggregator
75
+ *
76
+ * @param {object} params input params
77
+ * @param {string} params.type LLM event type
78
+ * @param {object} params.msg LLM event
79
+ */
80
+ recordEvent({ type, msg }) {
81
+ const llmContext = extractLlmContext(this.agent)
82
+ // prefer timestamp on event if present, otherwise use `Date.now`
83
+ const timestamp = msg?.timestamp ?? Date.now()
84
+ this.agent.customEventAggregator.add([
85
+ { type, timestamp },
86
+ Object.assign({}, msg, llmContext)
87
+ ])
88
+ }
89
+
90
+ /**
91
+ * Defines the common handler for all AIM calls.
92
+ * All of these calls simply create a segment with a name
93
+ * The `this.name` must be defined in the inherited subscriber
94
+ * @param {object} data event passed to handler
95
+ * @param {Context} ctx context passed to handler
96
+ * @returns {Context} either new context or existing
97
+ */
98
+ handler(data, ctx) {
99
+ if (!this.enabled) {
100
+ this.logger.debug('`ai_monitoring.enabled` is set to false, not creating segment.')
101
+ return ctx
102
+ }
103
+
104
+ this.addLlmMeta({ ctx, version: data.moduleVersion })
105
+
106
+ return this.createSegment({
107
+ name: this.name,
108
+ ctx
109
+ })
110
+ }
111
+ }
112
+
113
+ module.exports = AiMonitoringSubscriber
@@ -0,0 +1,117 @@
1
+ /*
2
+ * Copyright 2026 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const AiMonitoringSubscriber = require('./base')
8
+ const LlmErrorMessage = require('#agentlib/llm-events/error-message.js')
9
+
10
+ class AiMonitoringChatSubscriber extends AiMonitoringSubscriber {
11
+ /**
12
+ * @param {object} params constructor params object
13
+ * @param {object} params.agent A New Relic Node.js agent instance.
14
+ * @param {object} params.logger An agent logger instance.
15
+ * @param {string} params.packageName The package name being instrumented.
16
+ * This is what a developer would provide to the `require` function.
17
+ * @param {string} params.channelName A unique name for the diagnostics channel
18
+ * that will be created and monitored.
19
+ * @param {string} params.name name of segment for a given subscriber
20
+ * @param {string} params.trackingPrefix prefix for the tracking metric for a given subscriber
21
+ */
22
+ constructor({ agent, logger, packageName, channelName, name, trackingPrefix }) {
23
+ super({ agent, logger, packageName, channelName, name, trackingPrefix })
24
+ }
25
+
26
+ get streamingEnabled() {
27
+ return this.agent.config.ai_monitoring.streaming.enabled === true
28
+ }
29
+
30
+ /**
31
+ * Function that must be implemented by inherited subscriber to create an llm completion message.
32
+ *
33
+ * @param {object} params to function
34
+ * @param {Context} params.ctx active context
35
+ * @param {object} params.request request made to method on a given llm library
36
+ * @param {object} params.response response from method on a given llm library
37
+ * @param {object|string} params.message the message object/string used to create llm completion message
38
+ * @param {string} params.completionId the id of the llm completion summary for a given conversation
39
+ * @param {number} params.index index of message for a given conversation
40
+ * returns {object} a llm completion message instance for the given LLM
41
+ */
42
+ createCompletionMessage({ ctx, request, response, message, completionId, index }) {
43
+ throw new Error('createCompletionMessage must be implemented by your subscriber')
44
+ }
45
+
46
+ /**
47
+ * Function that must be implemented by inherited subscriber to create the llm completion summary.
48
+ *
49
+ * @param {object} params to function
50
+ * @param {Context} params.ctx active context
51
+ * @param {object} params.request request made to method on a given llm library
52
+ * @param {object} params.response response from method on a given llm library
53
+ * @param {object} params.err error if present
54
+ * @param {object} params.metadata used only for langchain events at the moment
55
+ * @param {Array} params.tags used only for langchain events at the moment
56
+ * returns {object} a llm completion summary instance for the given LLM
57
+ */
58
+ createCompletionSummary({ ctx, request, response, err, metadata, tags }) {
59
+ throw new Error('createCompletionSummary must be implemented by your subscriber')
60
+ }
61
+
62
+ /**
63
+ * Function that must be implemented by inherited subscriber to retrieve the relevant
64
+ * messages for a given chat conversation.
65
+ *
66
+ * @param {object} params to function
67
+ * @param {object} params.request request to instrumented function
68
+ * @param {object} params.response response to instrumented function
69
+ * returns {Array} an array of relevant messages used to construct llm completion messages
70
+ */
71
+ getMessages({ request, response }) {
72
+ throw new Error('getMessages must be implemented by your subscriber')
73
+ }
74
+
75
+ /**
76
+ * Function to create both llm completion summary and messages
77
+ * as well as assign errors to transaction if present
78
+ * @param {object} params to function
79
+ * @param {Context} params.ctx active context
80
+ * @param {object} params.request request made to method on a given llm library
81
+ * @param {object} params.response response from method on a given llm library
82
+ * @param {object} params.err error if present
83
+ * @param {object} params.metadata used only for langchain events at the moment
84
+ * @param {Array} params.tags used only for langchain events at the moment
85
+ */
86
+ recordChatCompletionEvents({ ctx, request, response, err, metadata = {}, tags = [] }) {
87
+ if (!this.enabled) {
88
+ this.logger.debug('config.ai_monitoring.enabled is set to false, not creating chat completion events.')
89
+ return
90
+ }
91
+
92
+ if (!(ctx?.segment || ctx?.transaction)) {
93
+ this.logger.debug('Empty context, not creating completion events.')
94
+ return
95
+ }
96
+
97
+ // Explicitly end segment to provide consistent duration
98
+ // for both LLM events and the segment
99
+ ctx.segment.end()
100
+ const completionSummary = this.createCompletionSummary({ ctx, request, response, err, metadata, tags })
101
+ this.recordEvent({ type: 'LlmChatCompletionSummary', msg: completionSummary })
102
+
103
+ const messages = this.getMessages({ request, response })
104
+ for (let i = 0; i < messages.length; i++) {
105
+ const message = messages[i]
106
+ const completionMessage = this.createCompletionMessage({ ctx, completionId: completionSummary.id, index: i, message, request, response })
107
+ this.recordEvent({ type: 'LlmChatCompletionMessage', msg: completionMessage })
108
+ }
109
+
110
+ if (err) {
111
+ const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
112
+ this.agent.errors.add(ctx.transaction, err, llmError)
113
+ }
114
+ }
115
+ }
116
+
117
+ module.exports = AiMonitoringChatSubscriber