dd-trace 5.86.0 → 5.88.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 (105) hide show
  1. package/LICENSE-3rdparty.csv +60 -32
  2. package/ext/exporters.d.ts +1 -0
  3. package/ext/exporters.js +1 -0
  4. package/index.d.ts +243 -7
  5. package/package.json +9 -6
  6. package/packages/datadog-instrumentations/src/ai.js +54 -90
  7. package/packages/datadog-instrumentations/src/cucumber.js +14 -0
  8. package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
  9. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  10. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
  11. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
  12. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
  13. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
  15. package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
  16. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
  17. package/packages/datadog-instrumentations/src/http/client.js +119 -1
  18. package/packages/datadog-instrumentations/src/jest.js +179 -15
  19. package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
  20. package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
  21. package/packages/datadog-instrumentations/src/mysql2.js +131 -64
  22. package/packages/datadog-instrumentations/src/playwright.js +9 -1
  23. package/packages/datadog-instrumentations/src/stripe.js +92 -0
  24. package/packages/datadog-instrumentations/src/vitest.js +11 -0
  25. package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
  26. package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
  27. package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
  28. package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
  29. package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
  30. package/packages/datadog-plugin-cucumber/src/index.js +9 -6
  31. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +33 -0
  32. package/packages/datadog-plugin-cypress/src/support.js +48 -8
  33. package/packages/datadog-plugin-jest/src/index.js +12 -2
  34. package/packages/datadog-plugin-jest/src/util.js +2 -1
  35. package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
  36. package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
  37. package/packages/datadog-plugin-mocha/src/index.js +9 -6
  38. package/packages/datadog-plugin-playwright/src/index.js +10 -6
  39. package/packages/datadog-plugin-vitest/src/index.js +13 -8
  40. package/packages/dd-trace/src/appsec/addresses.js +11 -0
  41. package/packages/dd-trace/src/appsec/channels.js +5 -1
  42. package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
  43. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
  44. package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
  45. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
  46. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
  47. package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
  48. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
  49. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
  50. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
  51. package/packages/dd-trace/src/appsec/index.js +103 -0
  52. package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
  53. package/packages/dd-trace/src/azure_metadata.js +0 -2
  54. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
  55. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  56. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
  57. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  58. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
  59. package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
  60. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  61. package/packages/dd-trace/src/config/defaults.js +148 -195
  62. package/packages/dd-trace/src/config/helper.js +43 -1
  63. package/packages/dd-trace/src/config/index.js +42 -14
  64. package/packages/dd-trace/src/config/supported-configurations.json +4115 -510
  65. package/packages/dd-trace/src/constants.js +0 -2
  66. package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
  67. package/packages/dd-trace/src/datastreams/pathway.js +22 -3
  68. package/packages/dd-trace/src/datastreams/processor.js +14 -1
  69. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
  70. package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
  71. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
  72. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
  73. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
  74. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
  75. package/packages/dd-trace/src/encode/agentless-json.js +141 -0
  76. package/packages/dd-trace/src/exporter.js +2 -0
  77. package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
  78. package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
  79. package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
  80. package/packages/dd-trace/src/exporters/common/agents.js +1 -1
  81. package/packages/dd-trace/src/exporters/common/request.js +4 -4
  82. package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
  83. package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
  84. package/packages/dd-trace/src/llmobs/sdk.js +34 -5
  85. package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
  86. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
  87. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
  88. package/packages/dd-trace/src/opentracing/span.js +6 -4
  89. package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
  90. package/packages/dd-trace/src/plugins/database.js +57 -45
  91. package/packages/dd-trace/src/plugins/outbound.js +27 -2
  92. package/packages/dd-trace/src/plugins/tracing.js +39 -4
  93. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
  94. package/packages/dd-trace/src/plugins/util/test.js +48 -0
  95. package/packages/dd-trace/src/plugins/util/web.js +8 -7
  96. package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
  97. package/packages/dd-trace/src/propagation-hash/index.js +145 -0
  98. package/packages/dd-trace/src/proxy.js +4 -0
  99. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
  100. package/packages/dd-trace/src/startup-log.js +3 -3
  101. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
  102. package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
  103. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
  104. package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
  105. package/packages/dd-trace/src/scope/noop/scope.js +0 -21
@@ -0,0 +1,108 @@
1
+ 'use strict'
2
+
3
+ module.exports = [
4
+ {
5
+ module: {
6
+ name: 'bullmq',
7
+ versionRange: '>=5.66.0',
8
+ filePath: 'dist/cjs/classes/queue.js',
9
+ },
10
+ functionQuery: {
11
+ methodName: 'add',
12
+ className: 'Queue',
13
+ kind: 'Async',
14
+ },
15
+ channelName: 'Queue_add',
16
+ },
17
+ {
18
+ module: {
19
+ name: 'bullmq',
20
+ versionRange: '>=5.66.0',
21
+ filePath: 'dist/cjs/classes/queue.js',
22
+ },
23
+ functionQuery: {
24
+ methodName: 'addBulk',
25
+ className: 'Queue',
26
+ kind: 'Async',
27
+ },
28
+ channelName: 'Queue_addBulk',
29
+ },
30
+ {
31
+ module: {
32
+ name: 'bullmq',
33
+ versionRange: '>=5.66.0',
34
+ filePath: 'dist/cjs/classes/worker.js',
35
+ },
36
+ functionQuery: {
37
+ methodName: 'callProcessJob',
38
+ className: 'Worker',
39
+ kind: 'Async',
40
+ },
41
+ channelName: 'Worker_callProcessJob',
42
+ },
43
+ {
44
+ module: {
45
+ name: 'bullmq',
46
+ versionRange: '>=5.66.0',
47
+ filePath: 'dist/cjs/classes/flow-producer.js',
48
+ },
49
+ functionQuery: {
50
+ methodName: 'add',
51
+ className: 'FlowProducer',
52
+ kind: 'Async',
53
+ },
54
+ channelName: 'FlowProducer_add',
55
+ },
56
+ {
57
+ module: {
58
+ name: 'bullmq',
59
+ versionRange: '>=5.66.0',
60
+ filePath: 'dist/esm/classes/queue.js',
61
+ },
62
+ functionQuery: {
63
+ methodName: 'add',
64
+ className: 'Queue',
65
+ kind: 'Async',
66
+ },
67
+ channelName: 'Queue_add',
68
+ },
69
+ {
70
+ module: {
71
+ name: 'bullmq',
72
+ versionRange: '>=5.66.0',
73
+ filePath: 'dist/esm/classes/queue.js',
74
+ },
75
+ functionQuery: {
76
+ methodName: 'addBulk',
77
+ className: 'Queue',
78
+ kind: 'Async',
79
+ },
80
+ channelName: 'Queue_addBulk',
81
+ },
82
+ {
83
+ module: {
84
+ name: 'bullmq',
85
+ versionRange: '>=5.66.0',
86
+ filePath: 'dist/esm/classes/worker.js',
87
+ },
88
+ functionQuery: {
89
+ methodName: 'callProcessJob',
90
+ className: 'Worker',
91
+ kind: 'Async',
92
+ },
93
+ channelName: 'Worker_callProcessJob',
94
+ },
95
+ {
96
+ module: {
97
+ name: 'bullmq',
98
+ versionRange: '>=5.66.0',
99
+ filePath: 'dist/esm/classes/flow-producer.js',
100
+ },
101
+ functionQuery: {
102
+ methodName: 'add',
103
+ className: 'FlowProducer',
104
+ kind: 'Async',
105
+ },
106
+ channelName: 'FlowProducer_add',
107
+ },
108
+ ]
@@ -1,6 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = [
4
- ...require('./langchain'),
4
+ ...require('./ai'),
5
5
  ...require('./bullmq'),
6
+ ...require('./langchain'),
6
7
  ]
@@ -0,0 +1,21 @@
1
+ 'use strict'
2
+
3
+ const transforms = require('./transforms')
4
+
5
+ function transform (state, ...args) {
6
+ const operator = state.operator = getOperator(state)
7
+
8
+ transforms[operator](state, ...args)
9
+ }
10
+
11
+ function getOperator ({ functionQuery: { kind } }) {
12
+ switch (kind) {
13
+ case 'Async': return 'tracePromise'
14
+ case 'AsyncIterator': return 'traceAsyncIterator'
15
+ case 'Callback': return 'traceCallback'
16
+ case 'Iterator': return 'traceIterator'
17
+ case 'Sync': return 'traceSync'
18
+ }
19
+ }
20
+
21
+ module.exports = { transform }
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { parse, query } = require('./compiler')
3
+ const { parse, query, traverse } = require('./compiler')
4
4
 
5
5
  const tracingChannelPredicate = (node) => (
6
6
  node.specifiers?.[0]?.local?.name === 'tr_ch_apm_tracingChannel' ||
@@ -8,15 +8,15 @@ const tracingChannelPredicate = (node) => (
8
8
  )
9
9
 
10
10
  const transforms = module.exports = {
11
- tracingChannelImport ({ format }, node) {
11
+ tracingChannelImport ({ sourceType }, node) {
12
12
  if (node.body.some(tracingChannelPredicate)) return
13
13
 
14
14
  const index = node.body.findIndex(child => child.directive === 'use strict')
15
- const code = format === 'module'
15
+ const code = sourceType === 'module'
16
16
  ? 'import { tracingChannel as tr_ch_apm_tracingChannel } from "diagnostics_channel"'
17
17
  : 'const {tracingChannel: tr_ch_apm_tracingChannel} = require("diagnostics_channel")'
18
18
 
19
- node.body.splice(index + 1, 0, parse(code, { module: format === 'module' }).body[0])
19
+ node.body.splice(index + 1, 0, parse(code, { sourceType }).body[0])
20
20
  },
21
21
 
22
22
  tracingChannelDeclaration (state, node) {
@@ -35,7 +35,9 @@ const transforms = module.exports = {
35
35
  node.body.splice(index + 1, 0, parse(code).body[0])
36
36
  },
37
37
 
38
+ traceAsyncIterator: traceAny,
38
39
  traceCallback: traceAny,
40
+ traceIterator: traceAny,
39
41
  tracePromise: traceAny,
40
42
  traceSync: traceAny,
41
43
  }
@@ -51,18 +53,25 @@ function traceAny (state, node, _parent, ancestry) {
51
53
  }
52
54
 
53
55
  function traceFunction (state, node, program) {
54
- const { operator } = state
55
-
56
56
  transforms.tracingChannelDeclaration(state, program)
57
57
 
58
58
  node.body = wrap(state, {
59
- type: 'ArrowFunctionExpression',
59
+ type: 'FunctionExpression',
60
60
  params: node.params,
61
61
  body: node.body,
62
- async: operator === 'tracePromise',
62
+ async: node.async,
63
63
  expression: false,
64
- generator: false,
65
- })
64
+ generator: node.generator,
65
+ }, program)
66
+
67
+ // The original function no longer contains any calls to `await` or `yield` as
68
+ // the function body is copied to the internal wrapped function, so we set
69
+ // these to false to avoid altering the return value of the wrapper. The old
70
+ // values are instead copied to the new AST node above.
71
+ node.generator = false
72
+ node.async = false
73
+
74
+ wrapSuper(state, node)
66
75
  }
67
76
 
68
77
  function traceInstanceMethod (state, node, program) {
@@ -100,15 +109,19 @@ function traceInstanceMethod (state, node, program) {
100
109
  const fn = ctorBody[1].expression.right
101
110
 
102
111
  fn.async = operator === 'tracePromise'
103
- fn.body = wrap(state, { type: 'Identifier', name: `__apm$${methodName}` })
112
+ fn.body = wrap(state, { type: 'Identifier', name: `__apm$${methodName}` }, program)
113
+
114
+ wrapSuper(state, fn)
104
115
 
105
116
  ctor.value.body.body.push(...ctorBody)
106
117
  }
107
118
 
108
- function wrap (state, node) {
119
+ function wrap (state, node, program) {
109
120
  const { channelName, operator } = state
110
121
 
122
+ if (operator === 'traceAsyncIterator') return wrapIterator(state, node, program)
111
123
  if (operator === 'traceCallback') return wrapCallback(state, node)
124
+ if (operator === 'traceIterator') return wrapIterator(state, node, program)
112
125
 
113
126
  const async = operator === 'tracePromise' ? 'async' : ''
114
127
  const channelVariable = 'tr_ch_apm$' + channelName.replaceAll(':', '_')
@@ -133,6 +146,55 @@ function wrap (state, node) {
133
146
  return wrapper
134
147
  }
135
148
 
149
+ function wrapSuper (_state, node) {
150
+ const members = new Set()
151
+
152
+ traverse(
153
+ node.body,
154
+ '[object.type=Super]',
155
+ (node, parent) => {
156
+ const { name } = node.property
157
+
158
+ let child
159
+
160
+ if (parent.callee) {
161
+ // This is needed because for generator functions we have to move the
162
+ // original function to a nested wrapped function, but we can't use an
163
+ // arrow function because arrow function cannot be generator functions,
164
+ // and `super` cannot be called from a nested function, so we have to
165
+ // rewrite any `super` call to not use the keyword.
166
+ const { expression } = parse(`__apm$super['${name}'].call(this)`).body[0]
167
+
168
+ parent.callee = child = expression.callee
169
+ parent.arguments.unshift(...expression.arguments)
170
+ } else {
171
+ parent.expression = child = parse(`__apm$super['${name}']`).body[0]
172
+ }
173
+
174
+ child.computed = parent.callee.computed
175
+ child.optional = parent.callee.optional
176
+
177
+ members.add(name)
178
+ }
179
+ )
180
+
181
+ for (const name of members) {
182
+ const member = parse(`
183
+ class Wrapper {
184
+ wrapper () {
185
+ __apm$super['${name}'] = super['${name}']
186
+ }
187
+ }
188
+ `).body[0].body.body[0].value.body.body[0]
189
+
190
+ node.body.body.unshift(member)
191
+ }
192
+
193
+ if (members.size > 0) {
194
+ node.body.body.unshift(parse('const __apm$super = {}').body[0])
195
+ }
196
+ }
197
+
136
198
  function wrapCallback (state, node) {
137
199
  const { channelName, functionQuery: { index = -1 } } = state
138
200
  const channelVariable = 'tr_ch_apm$' + channelName.replaceAll(':', '_')
@@ -194,3 +256,67 @@ function wrapCallback (state, node) {
194
256
 
195
257
  return wrapper
196
258
  }
259
+
260
+ function wrapIterator (state, node, program) {
261
+ const { channelName, operator } = state
262
+ const baseChannel = channelName.replaceAll(':', '_')
263
+ const channelVariable = 'tr_ch_apm$' + baseChannel
264
+ const nextChannel = baseChannel + '_next'
265
+ const traceMethod = operator === 'traceAsyncIterator' ? 'tracePromise' : 'traceSync'
266
+ const traceNext = `tr_ch_apm$${nextChannel}.${traceMethod}`
267
+
268
+ transforms.tracingChannelDeclaration({ ...state, channelName: nextChannel }, program)
269
+
270
+ const wrapper = parse(`
271
+ function wrapper () {
272
+ const __apm$traced = () => {
273
+ const __apm$wrapped = () => {};
274
+ return __apm$wrapped.apply(this, arguments);
275
+ };
276
+
277
+ if (!${channelVariable}.start.hasSubscribers) return __apm$traced();
278
+
279
+ {
280
+ const wrap = iter => {
281
+ const { next: iterNext, return: iterReturn, throw: iterThrow } = iter;
282
+
283
+ iter.next = (...args) => ${traceNext}(iterNext, ctx, iter, ...args);
284
+ iter.return = (...args) => ${traceNext}(iterReturn, ctx, iter, ...args);
285
+ iter.throw = (...args) => ${traceNext}(iterThrow, ctx, iter, ...args);
286
+
287
+ return iter;
288
+ };
289
+ const ctx = {
290
+ arguments,
291
+ self: this,
292
+ moduleVersion: "1.0.0"
293
+ };
294
+ const iter = ${channelVariable}.traceSync(__apm$traced, ctx);
295
+
296
+ if (typeof iter.then !== 'function') return wrap(iter);
297
+
298
+ return iter.then(result => {
299
+ ctx.result = result;
300
+
301
+ ${channelVariable}.asyncStart.publish(ctx);
302
+ ${channelVariable}.asyncEnd.publish(ctx);
303
+
304
+ return wrap(result);
305
+ }, err => {
306
+ ctx.error = err;
307
+
308
+ ${channelVariable}.error.publish(ctx);
309
+ ${channelVariable}.asyncStart.publish(ctx);
310
+ ${channelVariable}.asyncEnd.publish(ctx);
311
+
312
+ return Promise.reject(err);
313
+ });
314
+ };
315
+ }
316
+ `).body[0].body // Extract only block statement of function body.
317
+
318
+ // Replace the right-hand side assignment of `const __apm$wrapped = () => {}`.
319
+ query(wrapper, '[id.name=__apm$wrapped]')[0].init = node
320
+
321
+ return wrapper
322
+ }
@@ -14,6 +14,7 @@ const finishChannel = channel('apm:http:client:request:finish')
14
14
  const endChannel = channel('apm:http:client:request:end')
15
15
  const asyncStartChannel = channel('apm:http:client:request:asyncStart')
16
16
  const errorChannel = channel('apm:http:client:request:error')
17
+ const responseFinishChannel = channel('apm:http:client:response:finish')
17
18
 
18
19
  const names = ['http', 'https', 'node:http', 'node:https']
19
20
 
@@ -39,6 +40,112 @@ function normalizeCallback (inputOptions, callback, inputURL) {
39
40
  return typeof inputOptions === 'function' ? [inputOptions, inputURL || {}] : [callback, inputOptions]
40
41
  }
41
42
 
43
+ /**
44
+ * Wires the downstream response so we can observe when the customer consumes
45
+ * the body and when the stream finishes
46
+ *
47
+ * @param {object} ctx - Instrumentation context
48
+ * @param {import('http').IncomingMessage} res - The downstream response object.
49
+ * @returns {{ finalizeIfNeeded: () => void }|null} Cleanup helper used for drain.
50
+ */
51
+ function setupResponseInstrumentation (ctx, res) {
52
+ const shouldInstrumentFinish = responseFinishChannel.hasSubscribers
53
+
54
+ if (!shouldInstrumentFinish) {
55
+ return null
56
+ }
57
+
58
+ let bodyConsumed = false
59
+ let finishCalled = false
60
+ let originalRead = null
61
+ let dataListenerAdded = false
62
+ let dataReadStarted = false
63
+
64
+ const { shouldCollectBody } = ctx
65
+ const bodyChunks = shouldCollectBody ? [] : null
66
+
67
+ const collectChunk = chunk => {
68
+ if (!shouldCollectBody || !chunk) return
69
+
70
+ if (typeof chunk === 'string') {
71
+ bodyChunks.push(chunk)
72
+ } else if (Buffer.isBuffer(chunk)) {
73
+ bodyChunks.push(chunk)
74
+ } else {
75
+ // Handle Uint8Array or other array-like types
76
+ bodyChunks.push(Buffer.from(chunk))
77
+ }
78
+ }
79
+
80
+ // Listen for body consumption
81
+ const onNewListener = (eventName) => {
82
+ if (eventName === 'data' || eventName === 'readable') {
83
+ bodyConsumed = true
84
+
85
+ // For 'data' events, add our own listener to collect chunks
86
+ if (eventName === 'data' && !dataListenerAdded && !dataReadStarted) {
87
+ dataListenerAdded = true
88
+ res.on('data', collectChunk)
89
+ }
90
+
91
+ // For 'readable' events, wrap the read() method
92
+ if (eventName === 'readable' && !originalRead && !dataListenerAdded && typeof res.read === 'function') {
93
+ originalRead = res.read
94
+ res.read = function () {
95
+ const chunk = originalRead.apply(this, arguments)
96
+ if (!dataListenerAdded) {
97
+ dataReadStarted = true
98
+ collectChunk(chunk)
99
+ }
100
+ return chunk
101
+ }
102
+ }
103
+ }
104
+ }
105
+
106
+ res.on('newListener', onNewListener)
107
+
108
+ // Cleanup function to restore original behavior
109
+ const cleanup = () => {
110
+ res.off('newListener', onNewListener)
111
+ res.off('data', collectChunk)
112
+
113
+ if (originalRead) {
114
+ res.read = originalRead
115
+ originalRead = null
116
+ }
117
+ }
118
+
119
+ const notifyFinish = () => {
120
+ if (finishCalled) return
121
+ finishCalled = true
122
+
123
+ // Combine collected chunks into a single body
124
+ let body = null
125
+ if (bodyChunks?.length) {
126
+ const firstChunk = bodyChunks[0]
127
+ body = typeof firstChunk === 'string'
128
+ ? bodyChunks.join('')
129
+ : Buffer.concat(bodyChunks)
130
+ }
131
+
132
+ responseFinishChannel.publish({ ctx, res, body })
133
+ cleanup()
134
+ }
135
+
136
+ res.once('end', notifyFinish)
137
+ res.once('close', notifyFinish)
138
+
139
+ return {
140
+ finalizeIfNeeded () {
141
+ if (!bodyConsumed) {
142
+ // Body not consumed, resume to complete the response
143
+ notifyFinish()
144
+ }
145
+ },
146
+ }
147
+ }
148
+
42
149
  function patch (http, methodName) {
43
150
  shimmer.wrap(http, methodName, instrumentRequest)
44
151
 
@@ -103,7 +210,18 @@ function patch (http, methodName) {
103
210
  ctx.res = res
104
211
  res.once('end', finish)
105
212
  res.once(errorMonitor, finish)
106
- break
213
+
214
+ const instrumentation = setupResponseInstrumentation(ctx, res)
215
+
216
+ if (!instrumentation) {
217
+ break
218
+ }
219
+
220
+ const result = emit.apply(this, arguments)
221
+
222
+ instrumentation.finalizeIfNeeded()
223
+
224
+ return result
107
225
  }
108
226
  case 'connect':
109
227
  case 'upgrade':