newrelic 11.6.0 → 11.6.1

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,3 +1,24 @@
1
+ ### v11.6.1 (2023-12-07)
2
+
3
+ #### Bug fixes
4
+
5
+ * **deps:** Updated @newrelic/aws-sdk to 7.0.3 ([#1902](https://github.com/newrelic/node-newrelic/pull/1902)) ([bf22ae5](https://github.com/newrelic/node-newrelic/commit/bf22ae502ee74d5071dc5578cc77c38039e68778))
6
+ * Updated shimmer to handle instrumenting named and default exports of CommonJS modules in ESM ([#1894](https://github.com/newrelic/node-newrelic/pull/1894)) ([9f7979c](https://github.com/newrelic/node-newrelic/commit/9f7979cd41dcb3735e553cbd4837ad455836e3ff))
7
+
8
+ #### Code refactoring
9
+
10
+ * Fixed cognitive complexity violations of openai instrumentation by moving functions outside of the parent function scope ([#1895](https://github.com/newrelic/node-newrelic/pull/1895)) ([31dc717](https://github.com/newrelic/node-newrelic/commit/31dc71797c4798793b1bba8ca15733482ba3e7d6))
11
+ * Updated span names of openai actions to allow AI O11y team to locate relevant spans ([#1896](https://github.com/newrelic/node-newrelic/pull/1896)) ([b7a644d](https://github.com/newrelic/node-newrelic/commit/b7a644d1ef56bf88171fb74868ae78d24745dd19))
12
+
13
+ #### Miscellaneous chores
14
+
15
+ * Improve OpenAI mock server streams ([#1890](https://github.com/newrelic/node-newrelic/pull/1890)) ([d12bfe4](https://github.com/newrelic/node-newrelic/commit/d12bfe45f2c8d01277a1f8186844f0dd3598cbd7))
16
+
17
+ #### Tests
18
+
19
+ * Fix winston-esm versioned tests ([#1898](https://github.com/newrelic/node-newrelic/pull/1898)) ([6e02e57](https://github.com/newrelic/node-newrelic/commit/6e02e57a2b4ed8351e39e5fb8d47e2702bcafde0))
20
+ * pin undici versioned to <6.0.0 on Node 16 ([#1900](https://github.com/newrelic/node-newrelic/pull/1900)) ([9973b24](https://github.com/newrelic/node-newrelic/commit/9973b24067211f5fcc06adae79e02df8f599d735))
21
+
1
22
  ### v11.6.0 (2023-11-29)
2
23
 
3
24
  #### Features
@@ -510,7 +510,7 @@ This product includes source derived from [@grpc/proto-loader](https://github.co
510
510
 
511
511
  ### @newrelic/aws-sdk
512
512
 
513
- This product includes source derived from [@newrelic/aws-sdk](https://github.com/newrelic/node-newrelic-aws-sdk) ([v7.0.2](https://github.com/newrelic/node-newrelic-aws-sdk/tree/v7.0.2)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-aws-sdk/blob/v7.0.2/LICENSE):
513
+ This product includes source derived from [@newrelic/aws-sdk](https://github.com/newrelic/node-newrelic-aws-sdk) ([v7.0.3](https://github.com/newrelic/node-newrelic-aws-sdk/tree/v7.0.3)), distributed under the [Apache-2.0 License](https://github.com/newrelic/node-newrelic-aws-sdk/blob/v7.0.3/LICENSE):
514
514
 
515
515
  ```
516
516
  Apache License
@@ -15,10 +15,12 @@ const LlmTrackedIds = require('../../lib/llm-events/tracked-ids')
15
15
 
16
16
  const MIN_VERSION = '4.0.0'
17
17
  const MIN_STREAM_VERSION = '4.12.2'
18
- const { AI } = require('../../lib/metrics/names')
18
+ const {
19
+ AI: { OPENAI }
20
+ } = require('../../lib/metrics/names')
19
21
  const semver = require('semver')
20
22
 
21
- let TRACKING_METRIC = AI.TRACKING_PREFIX
23
+ let TRACKING_METRIC = OPENAI.TRACKING_PREFIX
22
24
 
23
25
  /**
24
26
  * Checks if we should skip instrumentation.
@@ -39,7 +41,178 @@ function shouldSkipInstrumentation(config, shim) {
39
41
  return semver.lt(pkgVersion, MIN_VERSION)
40
42
  }
41
43
 
42
- // eslint-disable-next-line sonarjs/cognitive-complexity
44
+ /**
45
+ * Adds apiKey and response headers to the active segment
46
+ * on symbols
47
+ *
48
+ * @param {object} params input params
49
+ * @param {Shim} params.shim instance of shim
50
+ * @param {object} params.result from openai request
51
+ * @param {string} params.apiKey api key from openai client
52
+ */
53
+ function decorateSegment({ shim, result, apiKey }) {
54
+ const segment = shim.getActiveSegment()
55
+
56
+ if (segment) {
57
+ segment[openAiApiKey] = apiKey
58
+
59
+ // If the result is an error, which is an OpenAI client error, then
60
+ // the headers are provided via a proxy attached to `result.headers`.
61
+ // Otherwise, result is a typical response-like object.
62
+ const headers = result?.response?.headers
63
+ ? Object.fromEntries(result.response.headers)
64
+ : { ...result?.headers }
65
+ segment[openAiHeaders] = headers
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Enqueues a LLM event to the custom event aggregator
71
+ *
72
+ * @param {object} params input params
73
+ * @param {Agent} params.agent NR agent instance
74
+ * @param {string} params.type LLM event type
75
+ * @param {object} params.msg LLM event
76
+ */
77
+ function recordEvent({ agent, type, msg }) {
78
+ agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
79
+ msg = agent?.llm?.metadata ? { ...agent.llm.metadata, ...msg } : msg
80
+ agent.customEventAggregator.add([{ type, timestamp: Date.now() }, msg])
81
+ }
82
+
83
+ /**
84
+ * Assigns requestId, conversationId and messageIds for a given
85
+ * chat completion response on the active transaction.
86
+ * This is used for generating LlmFeedbackEvent via `api.recordLlmFeedbackEvent`
87
+ *
88
+ * @param {object} params input params
89
+ * @param {Transaction} params.tx active transaction
90
+ * @param {LlmChatCompletionMessage} params.completionMsg chat completion message
91
+ * @param {string} params.responseId id of response
92
+ */
93
+ function assignIdsToTx({ tx, completionMsg, responseId }) {
94
+ const tracker = tx.llm.responses
95
+ const trackedIds =
96
+ tracker.get(responseId) ??
97
+ new LlmTrackedIds({
98
+ requestId: completionMsg.request_id,
99
+ conversationId: completionMsg.conversation_id
100
+ })
101
+ trackedIds.message_ids.push(completionMsg.id)
102
+ tracker.set(responseId, trackedIds)
103
+ }
104
+
105
+ /**
106
+ * Generates LlmChatCompletionSummary for a chat completion creation.
107
+ * Also iterates over both input messages and the first response message
108
+ * and creates LlmChatCompletionMessage.
109
+ *
110
+ * Also assigns relevant ids by response id for LlmFeedbackEvent creation
111
+ *
112
+ * @param {object} params input params
113
+ * @param {Agent} params.agent NR agent instance
114
+ * @param {TraceSegment} params.segment active segment from chat completion
115
+ * @param {object} params.request chat completion params
116
+ * @param {object} params.response chat completion response
117
+ * @param {boolean} [params.err] err if it exists
118
+ */
119
+ function recordChatCompletionMessages({ agent, segment, request, response, err }) {
120
+ if (!response) {
121
+ // If we get an error, it is possible that `response = null`.
122
+ // In that case, we define it to be an empty object.
123
+ response = {}
124
+ }
125
+
126
+ response.headers = segment[openAiHeaders]
127
+ response.api_key = segment[openAiApiKey]
128
+ const tx = segment.transaction
129
+ // explicitly end segment to consistent duration
130
+ // for both LLM events and the segment
131
+ segment.end()
132
+ const completionSummary = new LlmChatCompletionSummary({
133
+ agent,
134
+ segment,
135
+ request,
136
+ response,
137
+ withError: err != null
138
+ })
139
+
140
+ // Only take the first response message and append to input messages
141
+ const messages = [...request.messages, response?.choices?.[0]?.message]
142
+ messages.forEach((message, index) => {
143
+ const completionMsg = new LlmChatCompletionMessage({
144
+ agent,
145
+ segment,
146
+ request,
147
+ response,
148
+ index,
149
+ completionId: completionSummary.id,
150
+ message
151
+ })
152
+
153
+ assignIdsToTx({ tx, completionMsg, responseId: response.id })
154
+ recordEvent({ agent, type: 'LlmChatCompletionMessage', msg: completionMsg })
155
+ })
156
+
157
+ recordEvent({ agent, type: 'LlmChatCompletionSummary', msg: completionSummary })
158
+
159
+ if (err) {
160
+ const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
161
+ agent.errors.add(segment.transaction, err, llmError)
162
+ }
163
+
164
+ delete response.headers
165
+ delete response.api_key
166
+ }
167
+
168
+ /*
169
+ * Chat completions create can return a stream once promise resolves
170
+ * This wraps the iterator which is a generator function
171
+ * We will call the original iterator, intercept chunks and yield
172
+ * to the original. On complete we will construct the new message object
173
+ * with what we have seen in the stream and create the chat completion
174
+ * messages
175
+ *
176
+ */
177
+ function instrumentStream({ shim, request, response, segment }) {
178
+ shim.wrap(response, 'iterator', function wrapIterator(shim, orig) {
179
+ return async function* wrappedIterator() {
180
+ let content = ''
181
+ let role = ''
182
+ let chunk
183
+ let err
184
+ try {
185
+ const iterator = orig.apply(this, arguments)
186
+
187
+ for await (chunk of iterator) {
188
+ if (chunk.choices?.[0]?.delta?.role) {
189
+ role = chunk.choices[0].delta.role
190
+ }
191
+
192
+ content += chunk.choices?.[0]?.delta?.content ?? ''
193
+ yield chunk
194
+ }
195
+ } catch (streamErr) {
196
+ err = streamErr
197
+ throw err
198
+ } finally {
199
+ chunk.choices[0].message = { role, content }
200
+ // update segment duration since we want to extend the time it took to
201
+ // handle the stream
202
+ segment.touch()
203
+
204
+ recordChatCompletionMessages({
205
+ agent: shim.agent,
206
+ segment,
207
+ request,
208
+ response: chunk,
209
+ err
210
+ })
211
+ }
212
+ }
213
+ })
214
+ }
215
+
43
216
  module.exports = function initialize(agent, openai, moduleName, shim) {
44
217
  if (shouldSkipInstrumentation(agent.config, shim)) {
45
218
  shim.logger.debug(
@@ -51,42 +224,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
51
224
  // Update the tracking metric name with the version of the library
52
225
  // being instrumented. We do not have access to the version when
53
226
  // initially declaring the variable.
54
- TRACKING_METRIC = `${TRACKING_METRIC}OpenAI/${shim.pkgVersion}`
55
-
56
- /**
57
- * Adds apiKey and response headers to the active segment
58
- * on symbols
59
- *
60
- * @param {object} result from openai request
61
- * @param {string} apiKey api key from openai client
62
- */
63
- function decorateSegment(result, apiKey) {
64
- const segment = shim.getActiveSegment()
65
-
66
- if (segment) {
67
- segment[openAiApiKey] = apiKey
68
-
69
- // If the result is an error, which is an OpenAI client error, then
70
- // the headers are provided via a proxy attached to `result.headers`.
71
- // Otherwise, result is a typical response-like object.
72
- const headers = result?.response?.headers
73
- ? Object.fromEntries(result.response.headers)
74
- : { ...result?.headers }
75
- segment[openAiHeaders] = headers
76
- }
77
- }
78
-
79
- /**
80
- * Enqueues a LLM event to the custom event aggregator
81
- *
82
- * @param {string} type of LLM event
83
- * @param {object} msg LLM event
84
- */
85
- function recordEvent(type, msg) {
86
- agent.metrics.getOrCreateMetric(TRACKING_METRIC).incrementCallCount()
87
- msg = agent?.llm?.metadata ? { ...agent.llm.metadata, ...msg } : msg
88
- agent.customEventAggregator.add([{ type, timestamp: Date.now() }, msg])
89
- }
227
+ TRACKING_METRIC = `${TRACKING_METRIC}/${shim.pkgVersion}`
90
228
 
91
229
  /**
92
230
  * Instrumentation is only done to get the response headers and attach
@@ -96,152 +234,21 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
96
234
  shim.wrap(openai.prototype, 'makeRequest', function wrapRequest(shim, makeRequest) {
97
235
  return function wrappedRequest() {
98
236
  const apiKey = this.apiKey
99
- const result = makeRequest.apply(this, arguments)
100
- result.then(
101
- (data) => {
237
+ const request = makeRequest.apply(this, arguments)
238
+ request.then(
239
+ (result) => {
102
240
  // add headers on resolve
103
- decorateSegment(data, apiKey)
241
+ decorateSegment({ shim, result, apiKey })
104
242
  },
105
- (data) => {
243
+ (result) => {
106
244
  // add headers on reject
107
- decorateSegment(data, apiKey)
245
+ decorateSegment({ shim, result, apiKey })
108
246
  }
109
247
  )
110
- return result
248
+ return request
111
249
  }
112
250
  })
113
251
 
114
- /**
115
- * Assigns requestId, conversationId and messageIds for a given
116
- * chat completion response on the active transaction.
117
- * This is used for generating LlmFeedbackEvent via `api.recordLlmFeedbackEvent`
118
- *
119
- * @param {object} params input params
120
- * @param {Transaction} params.tx active transaction
121
- * @param {LlmChatCompletionMessage} params.completionMsg chat completion message
122
- * @param {string} params.responseId id of response
123
- */
124
- function assignIdsToTx({ tx, completionMsg, responseId }) {
125
- const tracker = tx.llm.responses
126
- const trackedIds =
127
- tracker.get(responseId) ??
128
- new LlmTrackedIds({
129
- requestId: completionMsg.request_id,
130
- conversationId: completionMsg.conversation_id
131
- })
132
- trackedIds.message_ids.push(completionMsg.id)
133
- tracker.set(responseId, trackedIds)
134
- }
135
-
136
- /**
137
- * Generates LlmChatCompletionSummary for a chat completion creation.
138
- * Also iterates over both input messages and the first response message
139
- * and creates LlmChatCompletionMessage.
140
- *
141
- * Also assigns relevant ids by response id for LlmFeedbackEvent creation
142
- *
143
- * @param {object} params input params
144
- * @param {TraceSegment} params.segment active segment from chat completion
145
- * @param {object} params.request chat completion params
146
- * @param {object} params.response chat completion response
147
- * @param {boolean} [params.err] err if it exists
148
- */
149
- function recordChatCompletionMessages({ segment, request, response, err }) {
150
- if (!response) {
151
- // If we get an error, it is possible that `response = null`.
152
- // In that case, we define it to be an empty object.
153
- response = {}
154
- }
155
-
156
- response.headers = segment[openAiHeaders]
157
- response.api_key = segment[openAiApiKey]
158
- const tx = segment.transaction
159
- // explicitly end segment to consistent duration
160
- // for both LLM events and the segment
161
- segment.end()
162
- const completionSummary = new LlmChatCompletionSummary({
163
- agent,
164
- segment,
165
- request,
166
- response,
167
- withError: err != null
168
- })
169
-
170
- // Only take the first response message and append to input messages
171
- const messages = [...request.messages, response?.choices?.[0]?.message]
172
- messages.forEach((message, index) => {
173
- const completionMsg = new LlmChatCompletionMessage({
174
- agent,
175
- segment,
176
- request,
177
- response,
178
- index,
179
- completionId: completionSummary.id,
180
- message
181
- })
182
-
183
- assignIdsToTx({ tx, completionMsg, responseId: response.id })
184
- recordEvent('LlmChatCompletionMessage', completionMsg)
185
- })
186
-
187
- recordEvent('LlmChatCompletionSummary', completionSummary)
188
-
189
- if (err) {
190
- const llmError = new LlmErrorMessage({ cause: err, summary: completionSummary, response })
191
- shim.agent.errors.add(segment.transaction, err, llmError)
192
- }
193
-
194
- delete response.headers
195
- delete response.api_key
196
- }
197
-
198
- /*
199
- * Chat completions create can return a stream once promise resolves
200
- * This wraps the iterator which is a generator function
201
- * We will call the original iterator, intercept chunks and yield
202
- * to the original. On complete we will construct the new message object
203
- * with what we have seen in the stream and create the chat completion
204
- * messages
205
- *
206
- */
207
- function instrumentStream({ request, response, segment }) {
208
- shim.wrap(response, 'iterator', function wrapIterator(shim, orig) {
209
- return async function* wrappedIterator() {
210
- let content = ''
211
- let role = ''
212
- let chunk
213
- let err
214
- try {
215
- const iterator = orig.apply(this, arguments)
216
-
217
- for await (chunk of iterator) {
218
- if (chunk.choices?.[0]?.delta?.role) {
219
- role = chunk.choices[0].delta.role
220
- }
221
-
222
- content += chunk.choices?.[0]?.delta?.content ?? ''
223
- yield chunk
224
- }
225
- } catch (streamErr) {
226
- err = streamErr
227
- throw err
228
- } finally {
229
- chunk.choices[0].message = { role, content }
230
- // update segment duration since we want to extend the time it took to
231
- // handle the stream
232
- segment.touch()
233
-
234
- recordChatCompletionMessages({
235
- segment,
236
- request,
237
- response: chunk,
238
- err
239
- })
240
- }
241
- }
242
- })
243
- }
244
-
245
252
  /**
246
253
  * Instruments chat completion creation
247
254
  * and creates the LLM events
@@ -261,14 +268,15 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
261
268
  }
262
269
 
263
270
  return {
264
- name: `${AI.OPEN_AI}/Chat/Completions/Create`,
271
+ name: OPENAI.COMPLETION,
265
272
  promise: true,
266
273
  // eslint-disable-next-line max-params
267
274
  after(_shim, _fn, _name, err, response, segment) {
268
275
  if (request.stream) {
269
- instrumentStream({ request, response, segment })
276
+ instrumentStream({ shim, request, response, segment })
270
277
  } else {
271
278
  recordChatCompletionMessages({
279
+ agent,
272
280
  segment,
273
281
  request,
274
282
  response,
@@ -290,7 +298,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
290
298
  function wrapEmbeddingCreate(shim, embeddingCreate, name, args) {
291
299
  const [request] = args
292
300
  return {
293
- name: `${AI.OPEN_AI}/Embeddings/Create`,
301
+ name: OPENAI.EMBEDDING,
294
302
  promise: true,
295
303
  // eslint-disable-next-line max-params
296
304
  after(_shim, _fn, _name, err, response, segment) {
@@ -312,7 +320,7 @@ module.exports = function initialize(agent, openai, moduleName, shim) {
312
320
  withError: err != null
313
321
  })
314
322
 
315
- recordEvent('LlmEmbedding', embedding)
323
+ recordEvent({ agent, type: 'LlmEmbedding', msg: embedding })
316
324
 
317
325
  if (err) {
318
326
  const llmError = new LlmErrorMessage({ cause: err, embedding, response })
@@ -5,6 +5,8 @@
5
5
 
6
6
  'use strict'
7
7
 
8
+ const { nrEsmProxy } = require('../symbols')
9
+
8
10
  function getQuery(shim, original, name, args) {
9
11
  const config = args[0]
10
12
  let statement
@@ -78,27 +80,52 @@ module.exports = function initialize(agent, pgsql, moduleName, shim) {
78
80
  })
79
81
  }
80
82
 
81
- // The pg module defines "native" getter which sets up the native client lazily
82
- // (only when called). We replace the getter, so that we can instrument the native
83
- // client. The original getter replaces itself with the instance of the native
84
- // client, so only instrument if the getter exists (otherwise assume already
85
- // instrumented).
86
- const origGetter = pgsql.__lookupGetter__('native')
87
- if (origGetter) {
88
- delete pgsql.native
89
- pgsql.__defineGetter__('native', function getNative() {
90
- const temp = origGetter()
91
- if (temp != null) {
92
- instrumentPGNative(temp)
93
- }
94
- return temp
95
- })
96
- }
83
+ updateNative(pgsql, instrumentPGNative)
97
84
 
98
85
  // wrapping for JS
99
86
  shim.recordQuery(pgsql && pgsql.Client && pgsql.Client.prototype, 'query', wrapJSClientQuery)
100
87
  }
101
88
 
89
+ /**
90
+ * Determines if the `pg` module is a plain CJS module or a CJS module that
91
+ * has been imported via ESM's import. After making the determination, it will
92
+ * update the `native` export of the module to be instrumented.
93
+ *
94
+ * @param {object} pg The module to inspect.
95
+ * @param {Function} instrumentPGNative A function that will apply instrumentation
96
+ * to the `native` export.
97
+ */
98
+ function updateNative(pg, instrumentPGNative) {
99
+ if (pg[nrEsmProxy] === true) {
100
+ // When pg is imported via an ESM import statement, then our proxy will
101
+ // make our non-ESM native getter wrapper not work correctly. Basically,
102
+ // the getter will get evaluated by the proxy, and we never gain access to
103
+ // replace the getter with our own implementation. Luckily, we get to
104
+ // simplify in this scenario.
105
+ const native = pg.default.native
106
+ if (native !== null) {
107
+ instrumentPGNative(native)
108
+ }
109
+ } else {
110
+ // The pg module defines a "native" getter which sets up the native client lazily
111
+ // (only when called). We replace the getter, so that we can instrument the native
112
+ // client. The original getter replaces itself with the instance of the native
113
+ // client, so only instrument if the getter exists (otherwise assume already
114
+ // instrumented).
115
+ const origGetter = pg.__lookupGetter__('native')
116
+ if (origGetter) {
117
+ delete pg.native
118
+ pg.__defineGetter__('native', function getNative() {
119
+ const temp = origGetter()
120
+ if (temp != null) {
121
+ instrumentPGNative(temp)
122
+ }
123
+ return temp
124
+ })
125
+ }
126
+ }
127
+ }
128
+
102
129
  function getInstanceParameters(shim, client) {
103
130
  return {
104
131
  host: client.host || null,
@@ -101,7 +101,12 @@ function registerFormatter({ opts, agent, winston }) {
101
101
  if (opts.format) {
102
102
  opts.format = winston.format.combine(instrumentedFormatter(), opts.format)
103
103
  } else {
104
- opts.format = instrumentedFormatter()
104
+ // The default formatter for Winston is the JSON formatter. If the user
105
+ // has not provided a formatter through opts.format, we must emulate the
106
+ // default. Otherwise, the message symbol will not get attached to log
107
+ // messages and transports, e.g. the "Console" transport, will not be able
108
+ // to output logs correctly.
109
+ opts.format = winston.format.combine(instrumentedFormatter(), winston.format.json())
105
110
  }
106
111
  }
107
112
 
@@ -166,7 +166,14 @@ const EXPRESS = {
166
166
 
167
167
  const AI = {
168
168
  TRACKING_PREFIX: 'Nodejs/ML/',
169
- OPEN_AI: 'AI/OpenAI'
169
+ EMBEDDING: 'Llm/embedding',
170
+ COMPLETION: 'Llm/completion'
171
+ }
172
+
173
+ AI.OPENAI = {
174
+ TRACKING_PREFIX: `${AI.TRACKING_PREFIX}/OpenAI`,
175
+ EMBEDDING: `${AI.EMBEDDING}/OpenAI/create`,
176
+ COMPLETION: `${AI.COMPLETION}/OpenAI/create`
170
177
  }
171
178
 
172
179
  const RESTIFY = {
package/lib/shimmer.js CHANGED
@@ -13,6 +13,7 @@ const INSTRUMENTATIONS = require('./instrumentations')()
13
13
  const shims = require('./shim')
14
14
  const { Hook } = require('require-in-the-middle')
15
15
  const IitmHook = require('import-in-the-middle')
16
+ const { nrEsmProxy } = require('./symbols')
16
17
  let pkgsToHook = []
17
18
 
18
19
  const NAMES = require('./metrics/names')
@@ -537,10 +538,7 @@ function instrumentPostLoad(agent, nodule, moduleName, resolvedName, esmResolver
537
538
  return
538
539
  }
539
540
 
540
- // We only return the .default when we're a CJS module in the import-in-the-middle(ESM)
541
- // callback hook
542
- const resolvedNodule = instrumentation.isEsm || !esmResolver ? nodule : nodule.default
543
-
541
+ const resolvedNodule = resolveNodule({ nodule, instrumentation, esmResolver })
544
542
  const shim = shims.createShimFromType({
545
543
  type: instrumentation.type,
546
544
  agent,
@@ -579,6 +577,60 @@ function instrumentPostLoad(agent, nodule, moduleName, resolvedName, esmResolver
579
577
  return nodule
580
578
  }
581
579
 
580
+ /**
581
+ * If a module that is being shimmed was loaded via a typical `require` then
582
+ * we can use it as normal. If was loaded via an ESM import, then we need to
583
+ * handle it with some extra care. This function inspects the module,
584
+ * the conditions around its loading, and returns an appropriate object for
585
+ * subsequent shimming methods.
586
+ *
587
+ * @param {object} params Input parameters.
588
+ * @param {object} params.nodule The nodule being instrumented.
589
+ * @param {object} params.instrumentation The configuration for the nodule
590
+ * to be instrumented.
591
+ * @param {boolean|null} params.esmResolver Indicates if the nodule was loaded
592
+ * via an ESM import.
593
+ * @returns {object} The nodule or a Proxy.
594
+ */
595
+ function resolveNodule({ nodule, instrumentation, esmResolver }) {
596
+ if (instrumentation.isEsm === true || !esmResolver) {
597
+ return nodule
598
+ }
599
+
600
+ // We have a CJS module wrapped by import-in-the-middle having been
601
+ // imported through ESM syntax. Due to the way CJS modules are parsed by
602
+ // ESM's import, we can have the same "export" attached to the `default`
603
+ // export and as a top-level named export. In order to shim things such
604
+ // that our users don't need to know to access `something.default.foo`
605
+ // when they have done `import * as something from 'something'`, we need
606
+ // to proxy the proxy in order to set our wrappers on both instances.
607
+ const noduleDefault = nodule.default
608
+ const origNodule = nodule
609
+ return new Proxy(
610
+ { origNodule, noduleDefault },
611
+ {
612
+ get(target, name) {
613
+ if (name === nrEsmProxy) {
614
+ return true
615
+ }
616
+ if (target.noduleDefault[name]) {
617
+ return target.noduleDefault[name]
618
+ }
619
+ return target.origNodule[name]
620
+ },
621
+ set(target, name, value) {
622
+ if (target.origNodule[name]) {
623
+ target.origNodule[name] = value
624
+ }
625
+ if (target.noduleDefault[name]) {
626
+ target.noduleDefault[name] = value
627
+ }
628
+ return true
629
+ }
630
+ }
631
+ )
632
+ }
633
+
582
634
  /**
583
635
  * Attempts to execute an onRequire hook for a given module.
584
636
  * If it fails it will call an onError hook and log warnings accordingly
package/lib/symbols.js CHANGED
@@ -34,5 +34,10 @@ module.exports = {
34
34
  unwrapPool: Symbol('unwrapPool'),
35
35
  clusterOf: Symbol('clusterOf'),
36
36
  createPoolCluster: Symbol('createPoolCluster'),
37
- wrappedPoolConnection: Symbol('wrappedPoolConnection')
37
+ wrappedPoolConnection: Symbol('wrappedPoolConnection'),
38
+
39
+ // Used to "decorate" the proxy returned during ESM importing so that
40
+ // instrumentations can determine if the module looks like an ESM module
41
+ // or not.
42
+ nrEsmProxy: Symbol('nr-esm-proxy')
38
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "newrelic",
3
- "version": "11.6.0",
3
+ "version": "11.6.1",
4
4
  "author": "New Relic Node.js agent team <nodejs@newrelic.com>",
5
5
  "license": "Apache-2.0",
6
6
  "contributors": [
@@ -183,7 +183,7 @@
183
183
  "dependencies": {
184
184
  "@grpc/grpc-js": "^1.9.4",
185
185
  "@grpc/proto-loader": "^0.7.5",
186
- "@newrelic/aws-sdk": "^7.0.2",
186
+ "@newrelic/aws-sdk": "^7.0.3",
187
187
  "@newrelic/koa": "^8.0.1",
188
188
  "@newrelic/security-agent": "0.5.0",
189
189
  "@newrelic/superagent": "^7.0.1",