newrelic 11.12.0 → 11.14.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.
package/NEWS.md CHANGED
@@ -1,17 +1,60 @@
1
- ### v11.12.0 (2024-03-04)
2
-
3
- #### Features
4
-
5
- * Added instrumentation for `chain.stream` for langchain js. ([#2052](https://github.com/newrelic/node-newrelic/pull/2052)) ([03abfce](https://github.com/newrelic/node-newrelic/commit/03abfce666d3c48abd0994c44817bb5cbe8655a9))
6
-
7
- #### Miscellaneous chores
8
-
9
- * Added @azure/openai and @langchain/community/llms/bedrock as tracking packages so we can measure usage with angler ([#2053](https://github.com/newrelic/node-newrelic/pull/2053)) ([4830ea3](https://github.com/newrelic/node-newrelic/commit/4830ea3285726f8cc8fb60820c4cf2d3790e5250))
10
-
11
- #### Tests
12
-
13
- * Updated aws-sdk branch to main as the necessary code was merged. ([#2051](https://github.com/newrelic/node-newrelic/pull/2051)) ([1e52a5c](https://github.com/newrelic/node-newrelic/commit/1e52a5cd7a8acc8970dbdc6b1bf774c06aa6f5fb))
14
-
1
+ ### v11.14.0 (2024-03-20)
2
+
3
+ #### Features
4
+
5
+ * Added `guid` attribute to `TransactionError` events ([#2079](https://github.com/newrelic/node-newrelic/pull/2079)) ([3468fb1](https://github.com/newrelic/node-newrelic/commit/3468fb14635b10c30a9a814dc0d084aeacfd9e10))
6
+ * Added instrumentation for `initializeUnorderedBulkOp`, and `initializeOrderedBulkOp` in mongodb 4+ ([#2086](https://github.com/newrelic/node-newrelic/pull/2086)) ([666ae6e](https://github.com/newrelic/node-newrelic/commit/666ae6ea4be8167748e31fe9b1d086aee5d0135d))
7
+ * Added support for Mongo v5+ ([#2085](https://github.com/newrelic/node-newrelic/pull/2085)) ([00f6feb](https://github.com/newrelic/node-newrelic/commit/00f6febb4893ed2abe902644514af3ab629cdbc1))
8
+
9
+ #### Security improvements
10
+
11
+ * **deps:** bump follow-redirects from 1.15.5 to 1.15.6 ([#2078](https://github.com/newrelic/node-newrelic/pull/2078)) ([aa86482](https://github.com/newrelic/node-newrelic/commit/aa86482738ba6b17bf6c2c7362931a3ad57736dd))
12
+
13
+ #### Documentation
14
+
15
+ * Changed bug bounty URL ([#2081](https://github.com/newrelic/node-newrelic/pull/2081)) ([f4586d7](https://github.com/newrelic/node-newrelic/commit/f4586d71cbe9c886181fbfe4b414cf0f9d630873))
16
+
17
+ #### Miscellaneous chores
18
+
19
+ * Added instrumentation tracking class ([#2084](https://github.com/newrelic/node-newrelic/pull/2084)) ([07d7e7d](https://github.com/newrelic/node-newrelic/commit/07d7e7d50edf9cd8a1cd1b465b24bc589409c5dc))
20
+ * **deps:** pin @newrelic/security-agent to 1.0.1 ([#2089](https://github.com/newrelic/node-newrelic/pull/2089)) ([e133c76](https://github.com/newrelic/node-newrelic/commit/e133c76a34f7d2af16610f00958651a6cff8824d))
21
+
22
+ ### v11.13.0 (2024-03-13)
23
+
24
+ #### Features
25
+
26
+ * Added AI Monitoring(AIM) for OpenAI, Amazon Bedrock, and Langchain.
27
+ * To enable set `config.ai_monitoring.enabled` or `NEW_RELIC_AI_MONITORING_ENABLED` to `true`.
28
+ * It is recommended for a full experience to set `config.span_events.max_samples_stored` to `10000`, `config.custom_insights_events.max_samples_stored` to `100000`, and `config.distributed_tracing.enabled` should not be set to false.
29
+ * **Note**: Enabling [high-security mode](https://docs.newrelic.com/docs/apm/agents/manage-apm-agents/configuration/high-security-mode/) will disable the AIM instrumentation and events.
30
+ * Added instrumentation for VectorStore.similaritySearch for langchain.js ([#2049](https://github.com/newrelic/node-newrelic/pull/2049)) ([3c50606](https://github.com/newrelic/node-newrelic/commit/3c50606a57a8dc18c91fe36e3b180bafd15a4cbc))
31
+ * Added setLlmTokenCountCallback API endpoint to register a callback for calculating token count when none is provided ([#2065](https://github.com/newrelic/node-newrelic/pull/2065)) ([d2faf1a](https://github.com/newrelic/node-newrelic/commit/d2faf1a4942aacefd8b92e0bc2dc11bd8b687328))
32
+ * Added token_count to LlmEmbedding and LlmChatCompletionMessage for openai ([#2061](https://github.com/newrelic/node-newrelic/pull/2061)) ([47a925e](https://github.com/newrelic/node-newrelic/commit/47a925eccce8e5b5d682b46277bbef5191f8b553))
33
+ * Added tool_id to langchain error event ([#2064](https://github.com/newrelic/node-newrelic/pull/2064)) ([793abe8](https://github.com/newrelic/node-newrelic/commit/793abe8c9854b80fb1cf42f839a7db6a646cdc0b))
34
+
35
+ #### Miscellaneous chores
36
+
37
+ * Added flag for verbosity during docs publishing ([#2060](https://github.com/newrelic/node-newrelic/pull/2060)) ([fe0d41a](https://github.com/newrelic/node-newrelic/commit/fe0d41a86cba8da6173f5935e61c62b7dca75344))
38
+ * Added some delays to docs publishing ([#2062](https://github.com/newrelic/node-newrelic/pull/2062)) ([a602fd4](https://github.com/newrelic/node-newrelic/commit/a602fd48e929437e4bc933c4ce276989013c53f1))
39
+ * Changed casing of OpenAI in vendor property ([#2068](https://github.com/newrelic/node-newrelic/pull/2068)) ([64b4ca2](https://github.com/newrelic/node-newrelic/commit/64b4ca2750c9de8d9028fef515c15019e358a5d9))
40
+ * Changed token_count to only use tokenCountCallback ([#2070](https://github.com/newrelic/node-newrelic/pull/2070)) ([66f94b0](https://github.com/newrelic/node-newrelic/commit/66f94b05aa7018af6fa1db1d2d87de9013f23a5b))
41
+ * Removed langchain_instrumentation feature flag as the instrumentation is feature complete ([#2073](https://github.com/newrelic/node-newrelic/pull/2073)) ([925a463](https://github.com/newrelic/node-newrelic/commit/925a463c708b29c78567aecf78ac89ac226b7995))
42
+ * updated @newrelic/aws-sdk and @newrelic/koa to latest ([#2074](https://github.com/newrelic/node-newrelic/pull/2074)) ([0747ca4](https://github.com/newrelic/node-newrelic/commit/0747ca4f0f6615e1fbfcc734a36059960facf23e))
43
+
44
+ ### v11.12.0 (2024-03-04)
45
+
46
+ #### Features
47
+
48
+ * Added instrumentation for `chain.stream` for langchain js. ([#2052](https://github.com/newrelic/node-newrelic/pull/2052)) ([03abfce](https://github.com/newrelic/node-newrelic/commit/03abfce666d3c48abd0994c44817bb5cbe8655a9))
49
+
50
+ #### Miscellaneous chores
51
+
52
+ * Added @azure/openai and @langchain/community/llms/bedrock as tracking packages so we can measure usage with angler ([#2053](https://github.com/newrelic/node-newrelic/pull/2053)) ([4830ea3](https://github.com/newrelic/node-newrelic/commit/4830ea3285726f8cc8fb60820c4cf2d3790e5250))
53
+
54
+ #### Tests
55
+
56
+ * Updated aws-sdk branch to main as the necessary code was merged. ([#2051](https://github.com/newrelic/node-newrelic/pull/2051)) ([1e52a5c](https://github.com/newrelic/node-newrelic/commit/1e52a5cd7a8acc8970dbdc6b1bf774c06aa6f5fb))
57
+
15
58
  ### v11.11.0 (2024-02-27)
16
59
 
17
60
  #### Features
package/README.md CHANGED
@@ -234,7 +234,7 @@ If you have any questions, or to execute our corporate CLA, required if your con
234
234
 
235
235
  As noted in our [security policy](../../security/policy), New Relic is committed to the privacy and security of our customers and their data. We believe that providing coordinated disclosure by security researchers and engaging with the security community are important means to achieve our security goals.
236
236
 
237
- If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [HackerOne](https://hackerone.com/newrelic).
237
+ If you believe you have found a security vulnerability in this project or any of New Relic's products or websites, we welcome and greatly appreciate you reporting it to New Relic through [our bug bounty program](https://docs.newrelic.com/docs/security/security-privacy/information-security/report-security-vulnerabilities/).
238
238
 
239
239
  If you would like to contribute to this project, review [these guidelines](./CONTRIBUTING.md).
240
240
 
@@ -504,7 +504,7 @@ This product includes source derived from [@grpc/proto-loader](https://github.co
504
504
 
505
505
  ### @newrelic/aws-sdk
506
506
 
507
- This product includes source derived from [@newrelic/aws-sdk](https://github.com/newrelic/node-newrelic-aws-sdk) ([v7.1.0](https://github.com/newrelic/node-newrelic-aws-sdk/tree/v7.1.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-aws-sdk/blob/v7.1.0/LICENSE):
507
+ This product includes source derived from [@newrelic/aws-sdk](https://github.com/newrelic/node-newrelic-aws-sdk) ([v7.3.0](https://github.com/newrelic/node-newrelic-aws-sdk/tree/v7.3.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-aws-sdk/blob/v7.3.0/LICENSE):
508
508
 
509
509
  ```
510
510
  Apache License
@@ -712,7 +712,7 @@ This product includes source derived from [@newrelic/aws-sdk](https://github.com
712
712
 
713
713
  ### @newrelic/koa
714
714
 
715
- This product includes source derived from [@newrelic/koa](https://github.com/newrelic/node-newrelic-koa) ([v8.0.1](https://github.com/newrelic/node-newrelic-koa/tree/v8.0.1)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-koa/blob/v8.0.1/LICENSE):
715
+ This product includes source derived from [@newrelic/koa](https://github.com/newrelic/node-newrelic-koa) ([v9.0.0](https://github.com/newrelic/node-newrelic-koa/tree/v9.0.0)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-koa/blob/v9.0.0/LICENSE):
716
716
 
717
717
  ```
718
718
  Apache License
package/api.js CHANGED
@@ -1839,7 +1839,7 @@ API.prototype.setErrorGroupCallback = function setErrorGroupCallback(callback) {
1839
1839
  )
1840
1840
  metric.incrementCallCount()
1841
1841
 
1842
- if (!this.shim.isFunction(callback) || this.shim.isPromise(callback)) {
1842
+ if (!this.shim.isFunction(callback) || this.shim.isAsyncFunction(callback)) {
1843
1843
  logger.warn(
1844
1844
  'Error Group callback must be a synchronous function, Error Group attribute will not be added'
1845
1845
  )
@@ -1849,4 +1849,37 @@ API.prototype.setErrorGroupCallback = function setErrorGroupCallback(callback) {
1849
1849
  this.agent.errors.errorGroupCallback = callback
1850
1850
  }
1851
1851
 
1852
+ /**
1853
+ * Registers a callback which will be used for calculating token counts on Llm events when they are not
1854
+ * available. This function will typically only be used if `ai_monitoring.record_content.enabled` is false
1855
+ * and you want to still capture token counts for Llm events.
1856
+ *
1857
+ * Provided callbacks must return an integer value for the token count for a given piece of content.
1858
+ *
1859
+ * @param {Function} callback - synchronous function called to calculate token count for content.
1860
+ * @example
1861
+ * // @param {string} model - name of model (i.e. gpt-3.5-turbo)
1862
+ * // @param {string} content - prompt or completion response
1863
+ * function tokenCallback(model, content) {
1864
+ * // calculate tokens based on model and content
1865
+ * // return token count
1866
+ * return 40
1867
+ * }
1868
+ */
1869
+ API.prototype.setLlmTokenCountCallback = function setLlmTokenCountCallback(callback) {
1870
+ const metric = this.agent.metrics.getOrCreateMetric(
1871
+ NAMES.SUPPORTABILITY.API + '/setLlmTokenCountCallback'
1872
+ )
1873
+ metric.incrementCallCount()
1874
+
1875
+ if (!this.shim.isFunction(callback) || this.shim.isAsyncFunction(callback)) {
1876
+ logger.warn(
1877
+ 'Llm token count callback must be a synchronous function, callback will not be registered.'
1878
+ )
1879
+ return
1880
+ }
1881
+
1882
+ this.agent.llm.tokenCountCallback = callback
1883
+ }
1884
+
1852
1885
  module.exports = API
@@ -240,6 +240,7 @@ function _getErrorEventIntrinsicAttrs(transaction, errorClass, message, expected
240
240
  }
241
241
 
242
242
  attributes['nr.transactionGuid'] = transaction.id
243
+ attributes.guid = transaction.id
243
244
 
244
245
  if (transaction.port) {
245
246
  attributes.port = transaction.port
@@ -12,8 +12,7 @@ exports.prerelease = {
12
12
  reverse_naming_rules: false,
13
13
  undici_async_tracking: true,
14
14
  unresolved_promise_cleanup: true,
15
- legacy_context_manager: false,
16
- langchain_instrumentation: false
15
+ legacy_context_manager: false
17
16
  }
18
17
 
19
18
  // flags that are no longer used for released features
@@ -35,7 +34,8 @@ exports.released = [
35
34
  'certificate_bundle',
36
35
  'async_local_context',
37
36
  'undici_instrumentation',
38
- 'aws_bedrock_instrumentation'
37
+ 'aws_bedrock_instrumentation',
38
+ 'langchain_instrumentation'
39
39
  ]
40
40
 
41
41
  // flags that are no longer used for unreleased features
@@ -59,7 +59,5 @@ common.recordEvent = function recordEvent({ agent, type, msg, pkgVersion }) {
59
59
  * @returns {boolean} flag if we should skip instrumentation
60
60
  */
61
61
  common.shouldSkipInstrumentation = function shouldSkipInstrumentation(config) {
62
- return !(
63
- config.ai_monitoring.enabled === true && config.feature_flag.langchain_instrumentation === true
64
- )
62
+ return config.ai_monitoring.enabled !== true
65
63
  }
@@ -7,6 +7,7 @@
7
7
  const toolsInstrumentation = require('./tools')
8
8
  const cbManagerInstrumentation = require('./callback-manager')
9
9
  const runnableInstrumentation = require('./runnable')
10
+ const vectorstoreInstrumentation = require('./vectorstore')
10
11
 
11
12
  module.exports = [
12
13
  {
@@ -23,5 +24,10 @@ module.exports = [
23
24
  type: 'generic',
24
25
  moduleName: '@langchain/core/dist/runnables/base',
25
26
  onRequire: runnableInstrumentation
27
+ },
28
+ {
29
+ type: 'generic',
30
+ moduleName: '@langchain/core/vectorstores',
31
+ onRequire: vectorstoreInstrumentation
26
32
  }
27
33
  ]
@@ -57,7 +57,7 @@ module.exports = function initialize(shim, tools) {
57
57
  new LlmErrorMessage({
58
58
  response: {},
59
59
  cause: err,
60
- summary: {}
60
+ tool: toolEvent
61
61
  })
62
62
  )
63
63
  }
@@ -0,0 +1,108 @@
1
+ /*
2
+ * Copyright 2024 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+ const {
8
+ AI: { LANGCHAIN }
9
+ } = require('../../metrics/names')
10
+ const { LangChainVectorSearch, LangChainVectorSearchResult } = require('../../llm-events/langchain')
11
+ const { recordEvent, shouldSkipInstrumentation } = require('./common')
12
+ const { DESTINATIONS } = require('../../config/attribute-filter')
13
+ const { RecorderSpec } = require('../../shim/specs')
14
+ const LlmErrorMessage = require('../../llm-events/error-message')
15
+
16
+ /**
17
+ * Generates a LangChainVectorSearch for entire search request.
18
+ * Also iterates over documents in output and generates a
19
+ * LangChainVectorSearchResult for each document.
20
+ *
21
+ * @param {object} params input params
22
+ * @param {string} params.request vector search query
23
+ * @param {number} params.k vector search top k
24
+ * @param {object} params.output vector search documents
25
+ * @param {Agent} params.agent NR agent instance
26
+ * @param {TraceSegment} params.segment active segment from vector search
27
+ * @param {string} params.pkgVersion langchain version
28
+ * @param {err} params.err if it exists
29
+ */
30
+ function recordVectorSearch({ request, k, output, agent, segment, pkgVersion, err }) {
31
+ const vectorSearch = new LangChainVectorSearch({
32
+ agent,
33
+ segment,
34
+ query: request,
35
+ k,
36
+ documents: output,
37
+ error: err !== null
38
+ })
39
+
40
+ recordEvent({ agent, type: 'LlmVectorSearch', pkgVersion, msg: vectorSearch })
41
+
42
+ output.forEach((document, sequence) => {
43
+ const vectorSearchResult = new LangChainVectorSearchResult({
44
+ agent,
45
+ segment,
46
+ metadata: document.metadata,
47
+ pageContent: document.pageContent,
48
+ sequence,
49
+ search_id: vectorSearch.id
50
+ })
51
+
52
+ recordEvent({
53
+ agent,
54
+ type: 'LlmVectorSearchResult',
55
+ pkgVersion,
56
+ msg: vectorSearchResult
57
+ })
58
+ })
59
+
60
+ if (err) {
61
+ agent.errors.add(
62
+ segment.transaction,
63
+ err,
64
+ new LlmErrorMessage({
65
+ response: output,
66
+ cause: err,
67
+ vectorsearch: vectorSearch
68
+ })
69
+ )
70
+ }
71
+ }
72
+
73
+ module.exports = function initialize(shim, vectorstores) {
74
+ const { agent, pkgVersion } = shim
75
+
76
+ if (shouldSkipInstrumentation(agent.config)) {
77
+ shim.logger.debug(
78
+ 'langchain instrumentation is disabled. To enable set `config.ai_monitoring.enabled` to true'
79
+ )
80
+ return
81
+ }
82
+
83
+ shim.record(
84
+ vectorstores.VectorStore.prototype,
85
+ 'similaritySearch',
86
+ function wrapCall(shim, similaritySearch, fnName, args) {
87
+ const [request, k] = args
88
+
89
+ return new RecorderSpec({
90
+ name: `${LANGCHAIN.VECTORSTORE}/${fnName}`,
91
+ promise: true,
92
+ // eslint-disable-next-line max-params
93
+ after(_shim, _fn, _name, err, output, segment) {
94
+ if (!output) {
95
+ // If we get an error, it is possible that `output = null`.
96
+ // In that case, we define it to be an empty array.
97
+ output = []
98
+ }
99
+
100
+ segment.end()
101
+ recordVectorSearch({ request, k, output, agent, segment, pkgVersion, err })
102
+
103
+ segment.transaction.trace.attributes.addAttribute(DESTINATIONS.TRANS_EVENT, 'llm', true)
104
+ }
105
+ })
106
+ }
107
+ )
108
+ }
@@ -72,9 +72,13 @@ common.instrumentBulkOperation = function instrumentBulkOperation(shim, BulkOper
72
72
  common.instrumentDb = function instrumentDb(shim, Db) {
73
73
  if (Db && Db.prototype) {
74
74
  const proto = Db.prototype
75
- shim.recordOperation(proto, DB_OPS, new OperationSpec({ callback: shim.LAST, opaque: true }))
75
+ shim.recordOperation(
76
+ proto,
77
+ DB_OPS,
78
+ new OperationSpec({ callback: shim.LAST, opaque: true, promise: true })
79
+ )
76
80
  // link to client.connect(removed in v4.0)
77
- shim.recordOperation(Db, 'connect', new OperationSpec({ callback: shim.LAST }))
81
+ shim.recordOperation(Db, 'connect', new OperationSpec({ callback: shim.LAST, promise: true }))
78
82
  }
79
83
  }
80
84
 
@@ -133,7 +137,7 @@ common.makeBulkDescFunc = function makeBulkDescFunc(shim) {
133
137
  *
134
138
  * @param {Shim} shim instance of shim
135
139
  * @param {object} instrumenter instance of mongo APM class
136
- * @param {object} [options={}] provide command names to skip updating host/port as they are unrelated to the active query. This is only in v3 because after every command is runs `endSessions` which runs on the admin database
140
+ * @param {object} [options] provide command names to skip updating host/port as they are unrelated to the active query. This is only in v3 because after every command is runs `endSessions` which runs on the admin database
137
141
  */
138
142
  common.captureAttributesOnStarted = function captureAttributesOnStarted(
139
143
  shim,
@@ -214,7 +218,6 @@ function setHostPort(shim, connStr, db, client) {
214
218
  */
215
219
  function getInstanceAttributeParameters(shim, mongo) {
216
220
  let params
217
-
218
221
  if (mongo?.s?.topology) {
219
222
  shim.logger.trace('Adding datastore instance attributes from mongo.s.db + mongo.s.topology')
220
223
  const databaseName = mongo?.s?.db?.databaseName || mongo?.s?.namespace?.db || null
@@ -224,6 +227,10 @@ function getInstanceAttributeParameters(shim, mongo) {
224
227
  const databaseName = mongo?.s?.db?.databaseName || null
225
228
  const hosts = mongo.s.db.s.client.s.options.hosts
226
229
  params = getParametersFromHosts(hosts, databaseName)
230
+ } else if (mongo?.s?.db?.client?.topology) {
231
+ const databaseName = mongo?.s?.namespace?.db
232
+ const topology = mongo.s.db.client.topology
233
+ params = getParametersFromTopology(topology, databaseName)
227
234
  } else {
228
235
  shim.logger.trace('Could not find datastore instance attributes.')
229
236
  params = new DatastoreParameters()
@@ -272,6 +279,11 @@ function getParametersFromTopology(conf, database) {
272
279
  ;[{ host, port }] = conf.s.options.servers
273
280
  }
274
281
 
282
+ // hosts is an array but we will always pull the first for consistency
283
+ if (conf?.s?.options?.hosts?.length) {
284
+ ;[{ host, port }] = conf.s.options.hosts
285
+ }
286
+
275
287
  // host is a domain socket. set host as localhost and use the domain
276
288
  // socket host as the port
277
289
  if (host && host.endsWith('.sock')) {
@@ -280,7 +292,7 @@ function getParametersFromTopology(conf, database) {
280
292
  }
281
293
 
282
294
  return new DatastoreParameters({
283
- host: host,
295
+ host,
284
296
  port_path_or_id: port,
285
297
  database_name: database
286
298
  })
@@ -6,7 +6,13 @@
6
6
  'use strict'
7
7
 
8
8
  const { OperationSpec } = require('../../shim/specs')
9
- const { instrumentCollection, instrumentCursor, instrumentDb, parseAddress } = require('./common')
9
+ const {
10
+ instrumentBulkOperation,
11
+ instrumentCollection,
12
+ instrumentCursor,
13
+ instrumentDb,
14
+ parseAddress
15
+ } = require('./common')
10
16
 
11
17
  /**
12
18
  * parser used to grab the collection and operation
@@ -18,8 +24,11 @@ function queryParser(operation) {
18
24
  let collection = this.collectionName || 'unknown'
19
25
 
20
26
  // cursor methods have collection on namespace.collection
21
- if (this.namespace && this.namespace.collection) {
27
+ if (this?.namespace?.collection) {
22
28
  collection = this.namespace.collection
29
+ // (un)ordered bulk operations have collection on different key
30
+ } else if (this?.s?.collection?.collectionName) {
31
+ collection = this.s.collection.collectionName
23
32
  }
24
33
 
25
34
  return { operation, collection }
@@ -88,4 +97,5 @@ module.exports = function instrument(shim, mongodb) {
88
97
  instrumentCursor(shim, mongodb.AggregationCursor)
89
98
  instrumentCollection(shim, mongodb.Collection)
90
99
  instrumentDb(shim, mongodb.Db)
100
+ instrumentBulkOperation(shim, shim.require('./lib/bulk/common'))
91
101
  }
@@ -93,7 +93,7 @@ function wrapMiddleware(shim, middleware, _name, route) {
93
93
  })
94
94
 
95
95
  const wrappedMw = shim.recordMiddleware(middleware, spec)
96
- if (middleware.constructor.name === 'AsyncFunction') {
96
+ if (shim.isAsyncFunction(middleware)) {
97
97
  return async function asyncShim() {
98
98
  return wrappedMw.apply(this, arguments)
99
99
  }
@@ -0,0 +1,186 @@
1
+ /*
2
+ * Copyright 2024 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const IdGen = require('./util/idgen')
9
+ const idGen = new IdGen()
10
+
11
+ /**
12
+ * @typedef {function} InstrumentationOnRequire
13
+ * @param {Shim} shim The shim instance to use for the instrumentation.
14
+ * @param {object} resolvedNodule The module being instrumented as returned by
15
+ * Node's `require` function.
16
+ * @param {string} moduleName The simple name of the module, i.e. the value
17
+ * passed to the `require` function.
18
+ * @throws {Error|object}
19
+ */
20
+
21
+ /**
22
+ * @typedef {function} InstrumentationOnError
23
+ * @param {Error|object} error The error thrown by `onRequire` when there was
24
+ * an issue registering the instrumentation.
25
+ */
26
+
27
+ /* eslint-disable jsdoc/require-property-description */
28
+ /**
29
+ * @typedef {object} InstrumentationDescriptorParams
30
+ * @property {string} absolutePath
31
+ * @property {string} module
32
+ * @property {string} moduleName
33
+ * @property {InstrumentationOnError} onError
34
+ * @property {InstrumentationOnRequire} onRequire
35
+ * @property {string} resolvedName
36
+ * @property {string} type
37
+ */
38
+
39
+ /**
40
+ * Describes the configuration for an instrumentation. An instrumentation
41
+ * is what `newrelic` uses to wrap Node.js modules. In particular, a description
42
+ * details the name of the module, the path on disk to the module, and the
43
+ * hooks (`onRequire` and `onError`) to apply to the module.
44
+ */
45
+ class InstrumentationDescriptor {
46
+ /**
47
+ * Utility/generic module.
48
+ * @type {string}
49
+ */
50
+ static TYPE_GENERIC = 'generic'
51
+
52
+ /**
53
+ * @private
54
+ * @type {string}
55
+ */
56
+ static TYPE_CONGLOMERATE = 'conglomerate'
57
+
58
+ /**
59
+ * Database module, such as the MongoDB or MySQL drivers.
60
+ * @type {string}
61
+ */
62
+ static TYPE_DATASTORE = 'datastore'
63
+
64
+ /**
65
+ * Messaging module, such as AMQP.
66
+ * @type {string}
67
+ */
68
+ static TYPE_MESSAGE = 'message'
69
+
70
+ /**
71
+ * Promise module, such as Bluebird.
72
+ * @type {string}
73
+ */
74
+ static TYPE_PROMISE = 'promise'
75
+
76
+ /**
77
+ * @private
78
+ * @type {string}
79
+ */
80
+ static TYPE_TRANSACTION = 'transaction'
81
+
82
+ /**
83
+ * Web server framework module, such as Express or Fastify.
84
+ * @type {string}
85
+ */
86
+ static TYPE_WEB_FRAMEWORK = 'web-framework'
87
+
88
+ /**
89
+ * Used to load supportability metrics on installed versions of packages
90
+ * that the Node.js agent does not instrument (e.g. OTEL instrumentation or
91
+ * top logging libraries).
92
+ * @type {string}
93
+ */
94
+ static TYPE_TRACKING = 'tracking'
95
+
96
+ /**
97
+ * The type of the module being instrumented. See the static `TYPE_` fields.
98
+ * @type {string|null}
99
+ */
100
+ type
101
+
102
+ /**
103
+ * The name of the module being instrumented, i.e. the string used to require
104
+ * the module. This must map to a directory in `lib/instrumentations` which
105
+ * contains an `nr-hooks.js` file.
106
+ *
107
+ * This takes precedence over `moduleName`.
108
+ * @type {string}
109
+ */
110
+ module
111
+
112
+ /**
113
+ * The name of the module being instrumented, i.e. the string used to require
114
+ * the module. This must map to a JavaScript file of the same name in the
115
+ * `lib/instrumentations` directory.
116
+ * @type {string}
117
+ */
118
+ moduleName
119
+
120
+ /**
121
+ * The absolute path to the module to instrument. This should only be set
122
+ * when the module being instrumented does not reside in a `node_modules`
123
+ * directory; for example, when someone is instrumenting a module of their
124
+ * own through the public API.
125
+ *
126
+ * The `moduleName` property still needs to be set to the simple name, i.e.
127
+ * the string passed to `require`, for instrumentation tracking purposes.
128
+ *
129
+ * Note: this value takes precedence over `moduleName`.
130
+ */
131
+ absolutePath
132
+
133
+ /**
134
+ * The fully resolved path to the module, e.g. `/opt/app/node_modules/foo`.
135
+ * If the module is a core module, the special value `.` should be used.
136
+ * @type {string}
137
+ */
138
+ resolvedName
139
+
140
+ /**
141
+ * Hook to invoke when the module is required. This is the actual
142
+ * implementation of the instrumentation.
143
+ * @type {InstrumentationOnRequire}
144
+ */
145
+ onRequire
146
+
147
+ /**
148
+ * Hook to invoke when the `onRequire` hook throws an error.
149
+ * @type {InstrumentationOnError}
150
+ */
151
+ onError
152
+
153
+ /**
154
+ * @type {number}
155
+ */
156
+ #id
157
+
158
+ /* eslint-disable jsdoc/require-param-description */
159
+ /**
160
+ * @param {InstrumentationDescriptorParams} params
161
+ */
162
+ constructor(params) {
163
+ this.absolutePath = params.absolutePath
164
+ this.module = params.module
165
+ this.moduleName = params.moduleName
166
+ this.onError = params.onError
167
+ this.onRequire = params.onRequire
168
+ this.resolvedName = params.resolvedName
169
+ this.type = params.type
170
+
171
+ this.#id = idGen.idFor(this.moduleName)
172
+ }
173
+
174
+ /**
175
+ * Identifier for the instrumentation. Used by the internal instrumentation
176
+ * tracker to distinguish between different instrumentations targeting the
177
+ * same module.
178
+ *
179
+ * @returns {number} The identifier.
180
+ */
181
+ get instrumentationId() {
182
+ return this.#id
183
+ }
184
+ }
185
+
186
+ module.exports = InstrumentationDescriptor
@@ -0,0 +1,222 @@
1
+ /*
2
+ * Copyright 2024 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ const InstrumentationDescriptor = require('./instrumentation-descriptor')
9
+
10
+ /**
11
+ * @typedef {object} TrackedItemMeta
12
+ * @property {boolean} instrumented Indicates if the instrumentation
13
+ * has been successfully applied.
14
+ * @property {boolean|undefined} didError Indicates if the instrumentation
15
+ * application resulted in an error or not.
16
+ */
17
+
18
+ /**
19
+ * Represents tracked instrumentations in the {@link InstrumentationTracker}.
20
+ *
21
+ * @private
22
+ */
23
+ class TrackedItem {
24
+ /**
25
+ * @type {InstrumentationDescriptor}
26
+ */
27
+ instrumentation
28
+
29
+ /**
30
+ * @type {TrackedItemMeta}
31
+ */
32
+ meta
33
+
34
+ constructor(params) {
35
+ this.instrumentation = params.instrumentation
36
+ this.meta = params.meta
37
+ }
38
+ }
39
+
40
+ /**
41
+ * The instrumentation tracker is used to keep track of
42
+ * {@link InstrumentationDescriptor} instances in relation to modules that
43
+ * are being instrumented. The general process looks like:
44
+ *
45
+ * 1. We register an instrumentation with a simple name like `pino`. This
46
+ * "instrumentation" includes things like the `onRequire` and `onError`
47
+ * callbacks. In this context, "instrumentation" and "hook" are interchangeable
48
+ * terms.
49
+ *
50
+ * 2. Upon `require(<simple_name>)`, we hit `shimmer._postLoad` which will give
51
+ * us the fully resolved path to the module being loaded.
52
+ *
53
+ * 3. `_postLoad` will utilize the previously registered instrumentation
54
+ * information to determine if there are any callbacks for the module being
55
+ * loaded. If so, it will attempt to run the callbacks.
56
+ *
57
+ * 4. At this point we need to keep track of which simple name + fully resolved
58
+ * path has callbacks associated with it, and if the error callback was invoked.
59
+ * When a subsequent load of the same simple name + fully resolved path
60
+ * combination is encountered, we need to append that to the tracked hooks.
61
+ * Or, if the previous hook failed, provide a way for the loading algorithm
62
+ * to learn about that so that it can skip doing unnecessary work.
63
+ *
64
+ * The `InstrumentationTracker` object provides utility methods to facilitate
65
+ * that process.
66
+ *
67
+ * @private
68
+ */
69
+ class InstrumentationTracker {
70
+ #tracked = new Map()
71
+
72
+ get [Symbol.toStringTag]() {
73
+ return 'InstrumentationTracker'
74
+ }
75
+
76
+ /**
77
+ * Get all tracked instrumentations for the named module.
78
+ *
79
+ * @param {string} moduleName The simple name for the module, e.g. "pino".
80
+ *
81
+ * @returns {TrackedItem[]} All tracked items for the module.
82
+ */
83
+ getAllByName(moduleName) {
84
+ return this.#tracked.get(moduleName)
85
+ }
86
+
87
+ /**
88
+ * Get a specific tracked item for a module. This allows the
89
+ * {@link setHookSuccess} and {@link setHookFailure} methods to be used.
90
+ *
91
+ * @param {string} moduleName The simple name for the module, e.g. "pino".
92
+ * @param {InstrumentationDescriptor} instrumentation The instrumentation
93
+ * descriptor that is contained within the tracked item.
94
+ *
95
+ * @returns {TrackedItem|undefined} The full tracked item that includes the
96
+ * passed in descriptor along with the metadata about the instrumentation.
97
+ */
98
+ getTrackedItem(moduleName, instrumentation) {
99
+ const items = this.getAllByName(moduleName)
100
+ for (const item of items) {
101
+ /* istanbul ignore else */
102
+ if (item.instrumentation === instrumentation) {
103
+ return item
104
+ }
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The primary entrypoint to the tracker. It registers the basic information
110
+ * about an instrumentation prior to the to-be-instrumented module being
111
+ * loaded. If this method is not used first, other methods will throw because
112
+ * they will not be able to find any tracked items.
113
+ *
114
+ * @param {string} moduleName The simple name of the module being
115
+ * instrumented, e.g. "pino". That is, whatever is passed to the `require`
116
+ * function.
117
+ * @param {InstrumentationDescriptor} instrumentation The initial descriptor
118
+ * for the module being instrumented.
119
+ */
120
+ track(moduleName, instrumentation) {
121
+ const tracked = this.#tracked.get(moduleName)
122
+ if (tracked === undefined) {
123
+ this.#tracked.set(moduleName, [
124
+ new TrackedItem({ instrumentation, meta: { instrumented: false, didError: undefined } })
125
+ ])
126
+ return
127
+ }
128
+
129
+ let found
130
+ for (const t of tracked) {
131
+ if (t.instrumentation.instrumentationId === instrumentation.instrumentationId) {
132
+ return
133
+ }
134
+ }
135
+ /* istanbul ignore else */
136
+ if (found === undefined) {
137
+ tracked.push(
138
+ new TrackedItem({ instrumentation, meta: { instrumented: false, didError: undefined } })
139
+ )
140
+ }
141
+ }
142
+
143
+ /**
144
+ * Update the metadata for a tracked item to indicate that the hook failed.
145
+ *
146
+ * @param {TrackedItem} trackedItem The item to update.
147
+ */
148
+ setHookFailure(trackedItem) {
149
+ trackedItem.meta.instrumented = false
150
+ trackedItem.meta.didError = true
151
+ }
152
+
153
+ /**
154
+ * Update the metadata for a tracked item to indicate that the hook succeeded.
155
+ *
156
+ * @param {TrackedItem} trackedItem The item to update.
157
+ */
158
+ setHookSuccess(trackedItem) {
159
+ trackedItem.meta.instrumented = true
160
+ trackedItem.meta.didError = false
161
+ }
162
+
163
+ /**
164
+ * After a module has been loaded, via `require` or `import`, the tracked
165
+ * instrumentation for that module must have its `resolvedName` property
166
+ * updated. The `resolvedName` is used to uniquely identify instances of the
167
+ * module. Being able to uniquely identify instances is crucial to being able
168
+ * to instrument all loaded instances.
169
+ *
170
+ * @param {string} moduleName The simple name of the module, e.g. "pino".
171
+ * @param {string} resolvedName The fully resolved file system path to the
172
+ * module instance, e.g. "/opt/app/node_modules/pino".
173
+ *
174
+ * @throws {Error} If the provided `moduleName` is not present in the tracker.
175
+ */
176
+ setResolvedName(moduleName, resolvedName) {
177
+ const items = this.#tracked.get(moduleName)
178
+ if (items === undefined) {
179
+ throw Error(`module not tracked: ${moduleName}`)
180
+ }
181
+
182
+ const missingResolvedName = []
183
+ for (const item of items) {
184
+ if (item.instrumentation.resolvedName === undefined) {
185
+ missingResolvedName.push(item)
186
+ } else if (item.instrumentation.resolvedName === resolvedName) {
187
+ // We already have this specific instance of the module tracked.
188
+ // So we don't need to do anything.
189
+ return
190
+ }
191
+ }
192
+
193
+ // eslint-disable-next-line sonarjs/no-small-switch
194
+ switch (missingResolvedName.length) {
195
+ case 0: {
196
+ // We have encountered a new instance of the module. Therefore, we
197
+ // need to clone an existing instrumentation, but apply a different
198
+ // `resolvedName` to it.
199
+ //
200
+ // This happens when there is a dependency tree like:
201
+ // + `/opt/app/node_modules/foo`
202
+ // + `/opt/app/node_modules/transitive-dep/node_modules/foo`
203
+ const item = items[0]
204
+ this.track(
205
+ moduleName,
206
+ new InstrumentationDescriptor({ ...item.instrumentation, resolvedName })
207
+ )
208
+ break
209
+ }
210
+
211
+ default: {
212
+ // Add the same name to all found instrumentations. This definitely
213
+ // happens when the security agent is enabled.
214
+ missingResolvedName.forEach((item) => {
215
+ item.instrumentation.resolvedName = resolvedName
216
+ })
217
+ }
218
+ }
219
+ }
220
+ }
221
+
222
+ module.exports = InstrumentationTracker
@@ -18,14 +18,19 @@ module.exports = class LlmErrorMessage {
18
18
  * conversation if it was a chat completion conversation.
19
19
  * @param {LlmEmbedding} [params.embedding] Details about the conversation
20
20
  * if it was an embedding conversation.
21
+ * @param {LlmVectorStoreSearch} [params.vectorsearch] Details about the vector
22
+ * search if it was a vector search event.
23
+ * @param {LlmTool} [params.tool] Details about the tool event if it was a tool event.
21
24
  */
22
- constructor({ response, cause, summary, embedding } = {}) {
25
+ constructor({ response, cause, summary, embedding, vectorsearch, tool } = {}) {
23
26
  this['http.statusCode'] = response?.status ?? cause?.status
24
27
  this['error.message'] = cause?.message
25
28
  this['error.code'] = response?.code ?? cause?.error?.code
26
29
  this['error.param'] = response?.param ?? cause?.error?.param
27
30
  this.completion_id = summary?.id
28
31
  this.embedding_id = embedding?.id
32
+ this.vector_store_id = vectorsearch?.id
33
+ this.tool_id = tool?.id
29
34
  }
30
35
 
31
36
  get [Symbol.toStringTag]() {
@@ -6,13 +6,13 @@
6
6
  'use strict'
7
7
 
8
8
  const LangChainEvent = require('./event')
9
- const crypto = require('crypto')
10
9
 
11
10
  /**
12
11
  * @typedef {object} LangChainVectorSearchResultParams
13
12
  * @augments LangChainEventParams
14
13
  * @property {string} pageContent The stringified contents of the pageContent attribute on each returned search result document.
15
14
  * @property {number} [sequence=0] The index of the document in the search result documents list.
15
+ * @property {string} search_id The identifier from the LangChainVectorSearch event.
16
16
  */
17
17
  /**
18
18
  * @type {LangChainVectorSearchResultParams}
@@ -23,13 +23,12 @@ const defaultParams = {
23
23
  }
24
24
 
25
25
  class LangChainVectorSearchResult extends LangChainEvent {
26
- search_id = crypto.randomUUID()
27
-
28
26
  constructor(params) {
29
27
  params = Object.assign({}, defaultParams, params)
30
28
  super(params)
31
29
  const { agent } = params
32
30
 
31
+ this.search_id = params.search_id
33
32
  this.sequence = params.sequence
34
33
 
35
34
  if (agent.config.ai_monitoring.record_content.enabled === true) {
@@ -18,5 +18,16 @@ module.exports = class LlmChatCompletionMessage extends LlmEvent {
18
18
  if (agent.config.ai_monitoring.record_content.enabled === true) {
19
19
  this.content = message?.content
20
20
  }
21
+
22
+ const tokenCB = agent.llm?.tokenCountCallback
23
+ if (typeof tokenCB !== 'function') {
24
+ return
25
+ }
26
+
27
+ if (this.is_response) {
28
+ this.token_count = tokenCB(this['response.model'], message?.content)
29
+ } else {
30
+ this.token_count = tokenCB(request.model || request.engine, message?.content)
31
+ }
21
32
  }
22
33
  }
@@ -14,5 +14,9 @@ module.exports = class LlmEmbedding extends LlmEvent {
14
14
  if (agent.config.ai_monitoring.record_content.enabled === true) {
15
15
  this.input = request.input?.toString()
16
16
  }
17
+ this.token_count = agent.llm?.tokenCountCallback?.(
18
+ this['request.model'],
19
+ request.input?.toString()
20
+ )
17
21
  }
18
22
  }
@@ -19,7 +19,7 @@ module.exports = class LlmEvent extends BaseEvent {
19
19
  this.span_id = segment?.id
20
20
  this.transaction_id = segment?.transaction?.id
21
21
  this['response.model'] = response.model
22
- this.vendor = 'openAI'
22
+ this.vendor = 'openai'
23
23
  this.ingest_source = 'Node'
24
24
  this.metadata = agent
25
25
 
@@ -170,7 +170,8 @@ const AI = {
170
170
  EMBEDDING: 'Llm/embedding',
171
171
  COMPLETION: 'Llm/completion',
172
172
  TOOL: 'Llm/tool',
173
- CHAIN: 'Llm/chain'
173
+ CHAIN: 'Llm/chain',
174
+ VECTORSTORE: 'Llm/vectorstore'
174
175
  }
175
176
 
176
177
  AI.OPENAI = {
@@ -184,7 +185,8 @@ AI.LANGCHAIN = {
184
185
  EMBEDDING: `${AI.EMBEDDING}/Langchain`,
185
186
  COMPLETION: `${AI.COMPLETION}/Langchain`,
186
187
  TOOL: `${AI.TOOL}/Langchain`,
187
- CHAIN: `${AI.CHAIN}/Langchain`
188
+ CHAIN: `${AI.CHAIN}/Langchain`,
189
+ VECTORSTORE: `${AI.VECTORSTORE}/Langchain`
188
190
  }
189
191
 
190
192
  const RESTIFY = {
package/lib/shim/shim.js CHANGED
@@ -125,6 +125,7 @@ Shim.prototype.getName = getName
125
125
  Shim.prototype.isObject = isObject
126
126
  Shim.prototype.isFunction = isFunction
127
127
  Shim.prototype.isPromise = isPromise
128
+ Shim.prototype.isAsyncFunction = isAsyncFunction
128
129
  Shim.prototype.isString = isString
129
130
  Shim.prototype.isNumber = isNumber
130
131
  Shim.prototype.isBoolean = isBoolean
@@ -1345,6 +1346,20 @@ function isPromise(obj) {
1345
1346
  return obj && typeof obj.then === 'function'
1346
1347
  }
1347
1348
 
1349
+ /**
1350
+ * Determines if function is an async function.
1351
+ * Note it does not test if the return value of function is a
1352
+ * promise or async function
1353
+ *
1354
+ * @memberof Shim.prototype
1355
+ * @param fn
1356
+ * @param (function) function to test if async
1357
+ * @returns {boolean} True if the function is an async function
1358
+ */
1359
+ function isAsyncFunction(fn) {
1360
+ return fn.constructor.name === 'AsyncFunction'
1361
+ }
1362
+
1348
1363
  /**
1349
1364
  * Determines if the given value is null.
1350
1365
  *
@@ -0,0 +1,31 @@
1
+ /*
2
+ * Copyright 2024 New Relic Corporation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ 'use strict'
7
+
8
+ class IdGenerator {
9
+ #tracked = new Map()
10
+
11
+ /**
12
+ * Get the next available identifier for the provided `name`.
13
+ *
14
+ * @param {string} name The label to get an id for.
15
+ *
16
+ * @returns {number} The id.
17
+ */
18
+ idFor(name) {
19
+ const key = Symbol.for(name)
20
+ let val = this.#tracked.get(key)
21
+ if (val === undefined) {
22
+ val = 0
23
+ } else {
24
+ val += 1
25
+ }
26
+ this.#tracked.set(key, val)
27
+ return val
28
+ }
29
+ }
30
+
31
+ module.exports = IdGenerator
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "11.12.0",
3
+ "version": "11.14.0",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -193,10 +193,10 @@
193
193
  "dependencies": {
194
194
  "@grpc/grpc-js": "^1.9.4",
195
195
  "@grpc/proto-loader": "^0.7.5",
196
- "@newrelic/aws-sdk": "^7.1.0",
197
- "@newrelic/koa": "^8.0.1",
196
+ "@newrelic/aws-sdk": "^7.3.0",
197
+ "@newrelic/koa": "^9.0.0",
198
198
  "@newrelic/ritm": "^7.2.0",
199
- "@newrelic/security-agent": "^1.0.1",
199
+ "@newrelic/security-agent": "1.0.1",
200
200
  "@newrelic/superagent": "^7.0.1",
201
201
  "@tyriar/fibonacci-heap": "^2.0.7",
202
202
  "concat-stream": "^2.0.0",