dd-trace 5.102.0 → 5.103.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 (133) hide show
  1. package/ext/exporters.js +1 -0
  2. package/package.json +12 -11
  3. package/packages/datadog-esbuild/src/utils.js +2 -2
  4. package/packages/datadog-instrumentations/src/ai.js +1 -1
  5. package/packages/datadog-instrumentations/src/confluentinc-kafka-javascript.js +32 -15
  6. package/packages/datadog-instrumentations/src/couchbase.js +69 -220
  7. package/packages/datadog-instrumentations/src/cucumber.js +1 -1
  8. package/packages/datadog-instrumentations/src/electron/preload.js +42 -0
  9. package/packages/datadog-instrumentations/src/electron.js +240 -0
  10. package/packages/datadog-instrumentations/src/fetch.js +5 -5
  11. package/packages/datadog-instrumentations/src/graphql.js +13 -12
  12. package/packages/datadog-instrumentations/src/helpers/callback-instrumentor.js +1 -1
  13. package/packages/datadog-instrumentations/src/helpers/hook.js +4 -1
  14. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  15. package/packages/datadog-instrumentations/src/helpers/instrument.js +2 -2
  16. package/packages/datadog-instrumentations/src/helpers/kafka.js +41 -0
  17. package/packages/datadog-instrumentations/src/ioredis.js +16 -12
  18. package/packages/datadog-instrumentations/src/jest.js +351 -50
  19. package/packages/datadog-instrumentations/src/kafkajs.js +164 -173
  20. package/packages/datadog-instrumentations/src/mocha/main.js +73 -1
  21. package/packages/datadog-instrumentations/src/mongodb-core.js +33 -8
  22. package/packages/datadog-instrumentations/src/pg.js +24 -10
  23. package/packages/datadog-instrumentations/src/playwright.js +427 -55
  24. package/packages/datadog-instrumentations/src/redis.js +19 -10
  25. package/packages/datadog-plugin-apollo/src/gateway/request.js +1 -21
  26. package/packages/datadog-plugin-aws-sdk/src/base.js +18 -24
  27. package/packages/datadog-plugin-aws-sdk/src/services/cloudwatchlogs.js +1 -1
  28. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -1
  29. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -1
  30. package/packages/datadog-plugin-aws-sdk/src/services/lambda.js +1 -1
  31. package/packages/datadog-plugin-aws-sdk/src/services/redshift.js +1 -1
  32. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +1 -1
  33. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +2 -2
  34. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -1
  35. package/packages/datadog-plugin-aws-sdk/src/services/stepfunctions.js +1 -1
  36. package/packages/datadog-plugin-couchbase/src/index.js +58 -52
  37. package/packages/datadog-plugin-cucumber/src/index.js +1 -0
  38. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +214 -22
  39. package/packages/datadog-plugin-cypress/src/support.js +13 -1
  40. package/packages/datadog-plugin-electron/src/index.js +17 -0
  41. package/packages/datadog-plugin-electron/src/ipc.js +143 -0
  42. package/packages/datadog-plugin-electron/src/net.js +82 -0
  43. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +27 -18
  44. package/packages/datadog-plugin-graphql/src/execute.js +6 -28
  45. package/packages/datadog-plugin-graphql/src/resolve.js +30 -35
  46. package/packages/datadog-plugin-graphql/src/tools/signature.js +32 -7
  47. package/packages/datadog-plugin-graphql/src/tools/transforms.js +118 -100
  48. package/packages/datadog-plugin-graphql/src/utils.js +29 -0
  49. package/packages/datadog-plugin-grpc/src/client.js +6 -7
  50. package/packages/datadog-plugin-grpc/src/util.js +57 -22
  51. package/packages/datadog-plugin-http/src/client.js +2 -2
  52. package/packages/datadog-plugin-jest/src/index.js +92 -50
  53. package/packages/datadog-plugin-mocha/src/index.js +1 -0
  54. package/packages/datadog-plugin-mongodb-core/src/index.js +36 -70
  55. package/packages/datadog-plugin-mysql/src/index.js +1 -1
  56. package/packages/datadog-plugin-openai/src/services.js +2 -1
  57. package/packages/datadog-plugin-pg/src/index.js +3 -3
  58. package/packages/datadog-plugin-playwright/src/index.js +4 -0
  59. package/packages/datadog-plugin-redis/src/index.js +18 -23
  60. package/packages/dd-trace/src/aiguard/index.js +3 -1
  61. package/packages/dd-trace/src/aiguard/sdk.js +36 -30
  62. package/packages/dd-trace/src/aiguard/tags.js +20 -11
  63. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +2 -2
  64. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +1 -1
  65. package/packages/dd-trace/src/azure_metadata.js +17 -6
  66. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +4 -4
  67. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
  68. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +6 -4
  69. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +1 -1
  70. package/packages/dd-trace/src/config/defaults.js +3 -14
  71. package/packages/dd-trace/src/config/generated-config-types.d.ts +3 -1
  72. package/packages/dd-trace/src/config/helper.js +4 -0
  73. package/packages/dd-trace/src/config/index.js +2 -2
  74. package/packages/dd-trace/src/config/major-overrides.js +98 -0
  75. package/packages/dd-trace/src/config/parsers.js +7 -1
  76. package/packages/dd-trace/src/config/supported-configurations.json +51 -38
  77. package/packages/dd-trace/src/datastreams/checkpointer.js +2 -2
  78. package/packages/dd-trace/src/datastreams/manager.js +1 -1
  79. package/packages/dd-trace/src/datastreams/processor.js +2 -2
  80. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +2 -2
  81. package/packages/dd-trace/src/debugger/devtools_client/source-maps.js +1 -1
  82. package/packages/dd-trace/src/debugger/devtools_client/state.js +2 -1
  83. package/packages/dd-trace/src/debugger/index.js +7 -7
  84. package/packages/dd-trace/src/dogstatsd.js +2 -2
  85. package/packages/dd-trace/src/encode/0.4.js +45 -54
  86. package/packages/dd-trace/src/encode/0.5.js +34 -3
  87. package/packages/dd-trace/src/encode/agentless-json.js +1 -1
  88. package/packages/dd-trace/src/exporter.js +2 -0
  89. package/packages/dd-trace/src/exporters/agent/index.js +2 -1
  90. package/packages/dd-trace/src/exporters/agentless/index.js +3 -2
  91. package/packages/dd-trace/src/exporters/agentless/writer.js +2 -2
  92. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -1
  93. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  94. package/packages/dd-trace/src/exporters/electron/index.js +49 -0
  95. package/packages/dd-trace/src/external-logger/src/index.js +2 -1
  96. package/packages/dd-trace/src/git_metadata.js +10 -8
  97. package/packages/dd-trace/src/lambda/handler-paths.js +52 -0
  98. package/packages/dd-trace/src/lambda/index.js +62 -14
  99. package/packages/dd-trace/src/lambda/runtime/patch.js +21 -46
  100. package/packages/dd-trace/src/llmobs/index.js +13 -2
  101. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +45 -15
  102. package/packages/dd-trace/src/llmobs/writers/base.js +2 -1
  103. package/packages/dd-trace/src/openfeature/writers/base.js +2 -1
  104. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +2 -1
  105. package/packages/dd-trace/src/opentracing/propagation/text_map.js +20 -9
  106. package/packages/dd-trace/src/payload-tagging/config/index.js +2 -2
  107. package/packages/dd-trace/src/plugins/ci_plugin.js +49 -4
  108. package/packages/dd-trace/src/plugins/database.js +54 -12
  109. package/packages/dd-trace/src/plugins/index.js +1 -0
  110. package/packages/dd-trace/src/plugins/plugin.js +2 -4
  111. package/packages/dd-trace/src/plugins/util/ci.js +8 -8
  112. package/packages/dd-trace/src/plugins/util/git-cache.js +20 -18
  113. package/packages/dd-trace/src/plugins/util/stacktrace.js +2 -2
  114. package/packages/dd-trace/src/plugins/util/test.js +37 -5
  115. package/packages/dd-trace/src/plugins/util/user-provided-git.js +17 -15
  116. package/packages/dd-trace/src/priority_sampler.js +1 -1
  117. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  118. package/packages/dd-trace/src/profiling/profilers/wall.js +1 -1
  119. package/packages/dd-trace/src/profiling/ssi-heuristics.js +1 -1
  120. package/packages/dd-trace/src/rate_limiter.js +1 -1
  121. package/packages/dd-trace/src/remote_config/scheduler.js +1 -1
  122. package/packages/dd-trace/src/ritm.js +2 -1
  123. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +5 -8
  124. package/packages/dd-trace/src/serverless.js +5 -2
  125. package/packages/dd-trace/src/service-naming/schemas/v0/messaging.js +20 -0
  126. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
  127. package/packages/dd-trace/src/service-naming/schemas/v1/messaging.js +20 -0
  128. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
  129. package/packages/dd-trace/src/span_stats.js +1 -1
  130. package/packages/dd-trace/src/telemetry/dependencies.js +1 -1
  131. package/packages/dd-trace/src/telemetry/endpoints.js +1 -1
  132. package/packages/dd-trace/src/telemetry/telemetry.js +2 -2
  133. package/packages/dd-trace/src/lambda/runtime/ritm.js +0 -133
@@ -7,6 +7,10 @@ const {
7
7
  channel,
8
8
  addHook,
9
9
  } = require('./helpers/instrument')
10
+ const {
11
+ clientToCluster,
12
+ cloneMessages,
13
+ } = require('./helpers/kafka')
10
14
 
11
15
  const producerStartCh = channel('apm:kafkajs:produce:start')
12
16
  const producerCommitCh = channel('apm:kafkajs:produce:commit')
@@ -24,6 +28,26 @@ const batchConsumerErrorCh = channel('apm:kafkajs:consume-batch:error')
24
28
 
25
29
  const disabledHeaderWeakSet = new WeakSet()
26
30
 
31
+ addHook({ name: 'kafkajs', file: 'src/producer/index.js', versions: ['>=1.4'] }, (createProducer) =>
32
+ shimmer.wrapFunction(createProducer, original => function wrappedCreateProducer (params) {
33
+ const producer = original(params)
34
+ if (params?.cluster) {
35
+ clientToCluster.set(producer, params.cluster)
36
+ }
37
+ return producer
38
+ })
39
+ )
40
+
41
+ addHook({ name: 'kafkajs', file: 'src/consumer/index.js', versions: ['>=1.4'] }, (createConsumer) =>
42
+ shimmer.wrapFunction(createConsumer, original => function wrappedCreateConsumer (params) {
43
+ const consumer = original(params)
44
+ if (params?.cluster) {
45
+ clientToCluster.set(consumer, params.cluster)
46
+ }
47
+ return consumer
48
+ })
49
+ )
50
+
27
51
  addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKafka) => {
28
52
  class Kafka extends BaseKafka {
29
53
  constructor (options) {
@@ -34,79 +58,95 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
34
58
  }
35
59
  }
36
60
 
37
- shimmer.wrap(Kafka.prototype, 'producer', createProducer => function (...args) {
38
- const producer = createProducer.apply(this, args)
39
- const send = producer.send
61
+ shimmer.wrap(Kafka.prototype, 'producer', createProducer => function () {
62
+ const producer = createProducer.apply(this, arguments)
63
+ const originalSend = producer.send
40
64
  const bootstrapServers = this._brokers
41
-
42
- const kafkaClusterIdPromise = getKafkaClusterId(this)
65
+ const cluster = clientToCluster.get(producer)
43
66
 
44
67
  producer.send = function (...args) {
45
- const wrappedSend = (clusterId) => {
46
- const { topic, messages = [] } = args[0]
47
-
48
- const ctx = {
49
- bootstrapServers,
50
- clusterId,
51
- disableHeaderInjection: disabledHeaderWeakSet.has(producer),
52
- messages,
53
- topic,
54
- }
68
+ if (!producerStartCh.hasSubscribers) {
69
+ return originalSend.apply(this, args)
70
+ }
55
71
 
56
- for (const message of messages) {
57
- if (message !== null && typeof message === 'object' && !ctx.disableHeaderInjection) {
58
- message.headers = message.headers || {}
59
- }
60
- }
72
+ // Fast path: kafkajs has fetched metadata, so clusterId is already on
73
+ // the broker pool.
74
+ const metadata = cluster?.brokerPool?.metadata
75
+ if (metadata) {
76
+ return runSend.call(this, args, metadata.clusterId)
77
+ }
61
78
 
62
- return producerStartCh.runStores(ctx, () => {
63
- try {
64
- const result = send.apply(this, args)
65
- result.then(
66
- (res) => {
67
- ctx.result = res
68
- producerFinishCh.publish(ctx)
69
- producerCommitCh.publish(ctx)
70
- },
71
- (err) => {
72
- ctx.error = err
73
- if (err) {
74
- // Fixes bug where we would inject message headers for kafka brokers that don't support headers
75
- // (version <0.11). On the error, we disable header injection.
76
- // Unfortunately the error name / type is not more specific.
77
- // This approach is implemented by other tracers as well.
78
- if (err.name === 'KafkaJSProtocolError' && err.type === 'UNKNOWN') {
79
- disabledHeaderWeakSet.add(producer)
80
- log.error(
81
- // eslint-disable-next-line @stylistic/max-len
82
- 'Kafka Broker responded with UNKNOWN_SERVER_ERROR (-1). Please look at broker logs for more information. Tracer message header injection for Kafka is disabled.'
83
- )
84
- }
85
- producerErrorCh.publish(err)
86
- }
87
- producerFinishCh.publish(ctx)
88
- })
79
+ // Slow path, taken at most once per producer connect cycle. Prime the
80
+ // metadata fetch kafkajs's send would do internally a few stack frames
81
+ // later. `sharedPromiseTo` collapses our call and kafkajs's call into a
82
+ // single round trip, so total latency is unchanged.
83
+ if (typeof cluster?.refreshMetadataIfNecessary !== 'function') {
84
+ return runSend.call(this, args)
85
+ }
86
+ return cluster.refreshMetadataIfNecessary().then(
87
+ () => runSend.call(this, args, cluster.brokerPool?.metadata?.clusterId),
88
+ () => runSend.call(this, args)
89
+ )
90
+ }
89
91
 
90
- return result
91
- } catch (e) {
92
- ctx.error = e
93
- producerErrorCh.publish(ctx)
94
- producerFinishCh.publish(ctx)
95
- throw e
96
- }
97
- })
92
+ function runSend (args, clusterId) {
93
+ const arg0 = args[0]
94
+ const topic = arg0?.topic
95
+ const inputMessages = Array.isArray(arg0?.messages) ? arg0.messages : []
96
+ const disableHeaderInjection = disabledHeaderWeakSet.has(producer)
97
+
98
+ // Hand kafkajs and the plugin a shallow clone so injection writes to
99
+ // tracer-owned objects instead of the caller's. With injection
100
+ // disabled the clone must not seed `headers: {}` either: brokers that
101
+ // reject any header field cannot recover otherwise.
102
+ let messages = inputMessages
103
+ if (inputMessages.length > 0) {
104
+ messages = cloneMessages(inputMessages, !disableHeaderInjection)
105
+ args[0] = { ...arg0, messages }
98
106
  }
99
107
 
100
- if (isPromise(kafkaClusterIdPromise)) {
101
- // promise is not resolved
102
- return kafkaClusterIdPromise.then((clusterId) => {
103
- return wrappedSend(clusterId)
104
- })
108
+ const ctx = {
109
+ bootstrapServers,
110
+ clusterId,
111
+ disableHeaderInjection,
112
+ messages,
113
+ topic,
105
114
  }
106
115
 
107
- // promise is already resolved
108
- return wrappedSend(kafkaClusterIdPromise)
116
+ return producerStartCh.runStores(ctx, () => {
117
+ try {
118
+ const result = originalSend.apply(this, args)
119
+ result.then(
120
+ (res) => {
121
+ ctx.result = res
122
+ producerFinishCh.publish(ctx)
123
+ producerCommitCh.publish(ctx)
124
+ },
125
+ (error) => {
126
+ ctx.error = error
127
+ if (error) {
128
+ if (error.name === 'KafkaJSProtocolError' && error.type === 'UNKNOWN') {
129
+ disabledHeaderWeakSet.add(producer)
130
+ log.error(
131
+ // eslint-disable-next-line @stylistic/max-len
132
+ 'Kafka Broker responded with UNKNOWN_SERVER_ERROR (-1). Please look at broker logs for more information. Tracer message header injection for Kafka is disabled.'
133
+ )
134
+ }
135
+ producerErrorCh.publish(error)
136
+ }
137
+ producerFinishCh.publish(ctx)
138
+ }
139
+ )
140
+ return result
141
+ } catch (error) {
142
+ ctx.error = error
143
+ producerErrorCh.publish(ctx)
144
+ producerFinishCh.publish(ctx)
145
+ throw error
146
+ }
147
+ })
109
148
  }
149
+
110
150
  return producer
111
151
  })
112
152
 
@@ -115,24 +155,26 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
115
155
  return createConsumer.apply(this, args)
116
156
  }
117
157
 
118
- const kafkaClusterIdPromise = getKafkaClusterId(this)
119
- let resolvedClusterId = null
158
+ const consumer = createConsumer.apply(this, arguments)
159
+ const cluster = clientToCluster.get(consumer)
160
+ const groupId = arguments[0].groupId
161
+
162
+ const readClusterId = () => cluster?.brokerPool?.metadata?.clusterId
120
163
 
121
- const eachMessageExtractor = (args, clusterId) => {
164
+ const eachMessageExtractor = (args) => {
122
165
  const { topic, partition, message } = args[0]
123
- return { topic, partition, message, groupId, clusterId }
166
+ return { topic, partition, message, groupId, clusterId: readClusterId() }
124
167
  }
125
168
 
126
- const eachBatchExtractor = (args, clusterId) => {
169
+ const eachBatchExtractor = (args) => {
127
170
  const { batch } = args[0]
128
171
  const { topic, partition, messages } = batch
129
- return { topic, partition, messages, groupId, clusterId }
172
+ return { topic, partition, messages, groupId, clusterId: readClusterId() }
130
173
  }
131
174
 
132
- const consumer = createConsumer.apply(this, args)
133
-
134
175
  consumer.on(consumer.events.COMMIT_OFFSETS, (event) => {
135
176
  const { payload: { groupId: commitGroupId, topics } } = event
177
+ const clusterId = readClusterId()
136
178
  const commitList = []
137
179
  for (const { topic, partitions } of topics) {
138
180
  for (const { partition, offset } of partitions) {
@@ -141,7 +183,7 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
141
183
  partition,
142
184
  offset,
143
185
  topic,
144
- clusterId: resolvedClusterId,
186
+ clusterId,
145
187
  })
146
188
  }
147
189
  }
@@ -149,118 +191,67 @@ addHook({ name: 'kafkajs', file: 'src/index.js', versions: ['>=1.4'] }, (BaseKaf
149
191
  })
150
192
 
151
193
  const run = consumer.run
152
- const groupId = args[0].groupId
153
194
 
154
195
  consumer.run = function ({ eachMessage, eachBatch, ...runArgs }) {
155
- const wrapConsume = (clusterId) => {
156
- // In kafkajs COMMIT_OFFSETS always happens in the context of one synchronous run
157
- // So this will always reference a correct cluster id
158
- resolvedClusterId = clusterId
159
- return run({
160
- eachMessage: wrappedCallback(
161
- eachMessage,
162
- consumerStartCh,
163
- consumerFinishCh,
164
- consumerErrorCh,
165
- eachMessageExtractor,
166
- clusterId
167
- ),
168
- eachBatch: wrappedCallback(
169
- eachBatch,
170
- batchConsumerStartCh,
171
- batchConsumerFinishCh,
172
- batchConsumerErrorCh,
173
- eachBatchExtractor,
174
- clusterId
175
- ),
176
- ...runArgs,
177
- })
178
- }
179
-
180
- if (isPromise(kafkaClusterIdPromise)) {
181
- // promise is not resolved
182
- return kafkaClusterIdPromise.then((clusterId) => {
183
- return wrapConsume(clusterId)
184
- })
185
- }
186
-
187
- // promise is already resolved
188
- return wrapConsume(kafkaClusterIdPromise)
196
+ return run({
197
+ eachMessage: wrappedCallback(
198
+ eachMessage,
199
+ consumerStartCh,
200
+ consumerFinishCh,
201
+ consumerErrorCh,
202
+ eachMessageExtractor
203
+ ),
204
+ eachBatch: wrappedCallback(
205
+ eachBatch,
206
+ batchConsumerStartCh,
207
+ batchConsumerFinishCh,
208
+ batchConsumerErrorCh,
209
+ eachBatchExtractor
210
+ ),
211
+ ...runArgs,
212
+ })
189
213
  }
214
+
190
215
  return consumer
191
216
  })
217
+
192
218
  return Kafka
193
219
  })
194
220
 
195
- const wrappedCallback = (fn, startCh, finishCh, errorCh, extractArgs, clusterId) => {
196
- return typeof fn === 'function'
197
- ? function (...args) {
198
- const extractedArgs = extractArgs(args, clusterId)
199
- const ctx = {
200
- extractedArgs,
201
- }
221
+ const wrappedCallback = (fn, startCh, finishCh, errorCh, extractArgs) => {
222
+ if (typeof fn !== 'function') return fn
223
+ return function (...args) {
224
+ const ctx = {
225
+ extractedArgs: extractArgs(args),
226
+ }
202
227
 
203
- return startCh.runStores(ctx, () => {
204
- try {
205
- const result = fn.apply(this, args)
206
- if (result && typeof result.then === 'function') {
207
- result.then(
208
- (res) => {
209
- ctx.result = res
210
- finishCh.publish(ctx)
211
- },
212
- (err) => {
213
- ctx.error = err
214
- if (err) {
215
- errorCh.publish(ctx)
216
- }
217
- finishCh.publish(ctx)
218
- })
219
- } else {
220
- finishCh.publish(ctx)
221
- }
222
- return result
223
- } catch (e) {
224
- ctx.error = e
225
- errorCh.publish(ctx)
228
+ return startCh.runStores(ctx, () => {
229
+ try {
230
+ const result = fn.apply(this, args)
231
+ if (result && typeof result.then === 'function') {
232
+ result.then(
233
+ (res) => {
234
+ ctx.result = res
235
+ finishCh.publish(ctx)
236
+ },
237
+ (error) => {
238
+ ctx.error = error
239
+ if (error) {
240
+ errorCh.publish(ctx)
241
+ }
242
+ finishCh.publish(ctx)
243
+ }
244
+ )
245
+ } else {
226
246
  finishCh.publish(ctx)
227
- throw e
228
247
  }
229
- })
230
- }
231
- : fn
232
- }
233
-
234
- const getKafkaClusterId = (kafka) => {
235
- if (kafka._ddKafkaClusterId) {
236
- return kafka._ddKafkaClusterId
237
- }
238
-
239
- if (!kafka.admin) {
240
- return null
241
- }
242
-
243
- const admin = kafka.admin()
244
-
245
- if (!admin.describeCluster) {
246
- return null
247
- }
248
-
249
- return admin.connect()
250
- .then(() => {
251
- return admin.describeCluster()
252
- })
253
- .then((clusterInfo) => {
254
- const clusterId = clusterInfo?.clusterId
255
- kafka._ddKafkaClusterId = clusterId
256
- admin.disconnect()
257
- return clusterId
258
- })
259
- .catch((error) => {
260
- throw error
248
+ return result
249
+ } catch (error) {
250
+ ctx.error = error
251
+ errorCh.publish(ctx)
252
+ finishCh.publish(ctx)
253
+ throw error
254
+ }
261
255
  })
262
- }
263
-
264
- function isPromise (obj) {
265
- return !!obj && (typeof obj === 'object' || typeof obj === 'function') && typeof obj.then === 'function'
256
+ }
266
257
  }
@@ -469,6 +469,10 @@ addHook({
469
469
  }
470
470
 
471
471
  const { suitesByTestFile, numSuitesByTestFile } = getSuitesByTestFile(this.suite)
472
+ // Root-level tests (direct children of root, no describe wrapper) keyed by file.
473
+ // Populated during the root 'suite' event so the normal finish path can include them
474
+ // in mixed-file status calculation.
475
+ const rootTestsByFile = new Map()
472
476
 
473
477
  this.once('start', getOnStartHandler(frameworkVersion))
474
478
 
@@ -489,6 +493,30 @@ addHook({
489
493
 
490
494
  this.on('suite', function (suite) {
491
495
  if (suite.root || !suite.tests.length) {
496
+ // This branch can be triggered when we have top level it(...) inside test files.
497
+ // In that case, they all (even if they are from different files) are going to be
498
+ // children of the root suite.
499
+ // Note: We could have suites that contain top level it(...) and also it(...) nested
500
+ // inside describe(...) ("mixed case"). Duplication is avoided by the context guard
501
+ // below. Since 'suite' fires for root first, in the mixed case the ctx is created
502
+ // here and the describe-based handler finds it already set.
503
+ if (suite.root && suite.tests.length > 0) {
504
+ const files = new Set(suite.tests.map(test => test.file).filter(Boolean))
505
+ for (const file of files) {
506
+ rootTestsByFile.set(file, suite.tests.filter(t => t.file === file))
507
+ if (testFileToSuiteCtx.get(file)) continue
508
+ const isUnskippable = unskippableSuites.includes(file)
509
+ isForcedToRun = isUnskippable && suitesToSkip.includes(getTestSuitePath(file, process.cwd()))
510
+ const ctx = {
511
+ testSuiteAbsolutePath: file,
512
+ isUnskippable,
513
+ isForcedToRun,
514
+ itrCorrelationId,
515
+ }
516
+ testFileToSuiteCtx.set(file, ctx)
517
+ testSuiteStartCh.runStores(ctx, () => {})
518
+ }
519
+ }
492
520
  return
493
521
  }
494
522
  let ctx = testFileToSuiteCtx.get(suite.file)
@@ -508,6 +536,44 @@ addHook({
508
536
 
509
537
  this.on('suite end', function (suite) {
510
538
  if (suite.root) {
539
+ // Symmetric to the suite start fix
540
+ const fileToTests = new Map()
541
+ for (const test of suite.tests) {
542
+ if (!test.file) continue
543
+ if (!fileToTests.has(test.file)) fileToTests.set(test.file, [])
544
+ fileToTests.get(test.file).push(test)
545
+ }
546
+ for (const [file, tests] of fileToTests) {
547
+ // Mixed case: if a file appears in suitesByTestFile (pre-populated before the run),
548
+ // its numSuitesByTestFile counter hits zero when its last describe-based suite ends
549
+ // and the normal path below fires testSuiteFinishCh. Since root is last when
550
+ // 'suite end' fires, any such file has already been handled — skipping it here
551
+ // avoids duplication.
552
+ if (suitesByTestFile[file]) continue
553
+ let status = 'pass'
554
+ if (tests.every(test => test.isPending())) {
555
+ status = 'skip'
556
+ } else {
557
+ for (const test of tests) {
558
+ if (test.state === 'failed' || test.timedOut) {
559
+ status = 'fail'
560
+ break
561
+ }
562
+ }
563
+ }
564
+ if (global.__coverage__) {
565
+ const coverageFiles = getCoveredFilenamesFromCoverage(global.__coverage__)
566
+ testSuiteCodeCoverageCh.publish({ coverageFiles, suiteFile: file })
567
+ mergeCoverage(global.__coverage__, originalCoverageMap)
568
+ resetCoverage(global.__coverage__)
569
+ }
570
+ const ctx = testFileToSuiteCtx.get(file)
571
+ if (ctx) {
572
+ testSuiteFinishCh.publish({ status, ...ctx.currentStore }, () => {})
573
+ } else {
574
+ log.warn('No ctx found for suite', file)
575
+ }
576
+ }
511
577
  return
512
578
  }
513
579
  const suitesInTestFile = suitesByTestFile[suite.file]
@@ -517,8 +583,9 @@ addHook({
517
583
  return
518
584
  }
519
585
 
586
+ const rootTests = rootTestsByFile.get(suite.file) || []
520
587
  let status = 'pass'
521
- if (suitesInTestFile.every(suite => suite.pending)) {
588
+ if (suitesInTestFile.every(suite => suite.pending) && rootTests.every(test => test.isPending())) {
522
589
  status = 'skip'
523
590
  } else {
524
591
  // has to check every test in the test file
@@ -530,6 +597,11 @@ addHook({
530
597
  }
531
598
  })
532
599
  })
600
+ for (const test of rootTests) {
601
+ if (test.state === 'failed' || test.timedOut) {
602
+ status = 'fail'
603
+ }
604
+ }
533
605
  }
534
606
 
535
607
  if (global.__coverage__) {
@@ -11,6 +11,14 @@ const startCh = channel('apm:mongodb:query:start')
11
11
  const finishCh = channel('apm:mongodb:query:finish')
12
12
  const errorCh = channel('apm:mongodb:query:error')
13
13
 
14
+ // Per-Connection cached topology shape (mongodb >= 4). The Connection's `address` is immutable
15
+ // for the lifetime of the connection, so we synthesize the `{ s: { options } }` envelope the
16
+ // plugin expects only once per connection. A WeakMap keeps the cache off the foreign Connection
17
+ // instance — no extra own-key visible to `Reflect.ownKeys`, `Object.freeze`, or another tracer's
18
+ // instrumentation walking the connection.
19
+ /** @type {WeakMap<object, { s: { options: { host?: string, port?: string } } }>} */
20
+ const topologyCache = new WeakMap()
21
+
14
22
  addHook({ name: 'mongodb-core', versions: ['2 - 3.1.9'] }, Server => {
15
23
  const serverProto = Server.Server.prototype
16
24
  shimmer.wrap(serverProto, 'command', command => wrapCommand(command, 'command'))
@@ -88,21 +96,36 @@ function wrapUnifiedCommand (command, operation, name) {
88
96
  }
89
97
 
90
98
  function wrapConnectionCommand (command, operation, name, instrumentFn = instrument) {
99
+ const opts = { name }
91
100
  return function (ns, ops) {
92
101
  if (!startCh.hasSubscribers) {
93
102
  return command.apply(this, arguments)
94
103
  }
95
- const hostParts = typeof this.address === 'string' ? this.address.split(':') : ''
96
- const options = hostParts.length === 2
97
- ? { host: hostParts[0], port: hostParts[1] }
98
- : {} // no port means the address is a random UUID so no host either
99
- const topology = { s: { options } }
100
-
101
- ns = `${ns.db}.${ns.collection}`
102
- return instrumentFn(operation, command, this, arguments, topology, ns, ops, { name })
104
+ let topology = topologyCache.get(this)
105
+ if (topology === undefined) {
106
+ topology = synthesizeTopology(this.address)
107
+ topologyCache.set(this, topology)
108
+ }
109
+ return instrumentFn(operation, command, this, arguments, topology, `${ns.db}.${ns.collection}`, ops, opts)
103
110
  }
104
111
  }
105
112
 
113
+ /**
114
+ * @param {string} address
115
+ * @returns {{ s: { options: { host?: string, port?: string } } }}
116
+ */
117
+ function synthesizeTopology (address) {
118
+ if (typeof address === 'string') {
119
+ const colon = address.indexOf(':')
120
+ // Match the previous `.split(':')` length-2 check: exactly one colon with non-empty parts on both sides.
121
+ if (colon > 0 && colon < address.length - 1 && !address.includes(':', colon + 1)) {
122
+ return { s: { options: { host: address.slice(0, colon), port: address.slice(colon + 1) } } }
123
+ }
124
+ }
125
+ // No port means the address is a random UUID, an IPv6 form, or otherwise unparseable, so no host either.
126
+ return { s: { options: {} } }
127
+ }
128
+
106
129
  function wrapQuery (query, operation, name) {
107
130
  return function (...args) {
108
131
  if (!startCh.hasSubscribers) {
@@ -171,6 +194,8 @@ function instrument (operation, command, instance, args, server, ns, ops, option
171
194
  })
172
195
  }
173
196
 
197
+ module.exports = { synthesizeTopology }
198
+
174
199
  function instrumentPromise (operation, command, instance, args, server, ns, ops, options = {}) {
175
200
  const name = options.name || (ops && Object.keys(ops)[0])
176
201
 
@@ -15,6 +15,10 @@ const errorCh = channel('apm:pg:query:error')
15
15
  const startPoolQueryCh = channel('datadog:pg:pool:query:start')
16
16
  const finishPoolQueryCh = channel('datadog:pg:pool:query:finish')
17
17
 
18
+ // Drivers like pg-promise reuse the same prepared-statement query object across executions; cache
19
+ // the un-injected `text` so the wrap doesn't capture a previous DBM injection as the new original.
20
+ const originalTextCache = new WeakMap()
21
+
18
22
  addHook({ name: 'pg', versions: ['>=8.0.3'], file: 'lib/native/client.js' }, Client => {
19
23
  shimmer.wrap(Client.prototype, 'query', query => wrapQuery(query))
20
24
  return Client
@@ -43,20 +47,14 @@ function wrapQuery (query) {
43
47
  : { text: args[0] }
44
48
 
45
49
  const textPropObj = pgQuery.cursor ?? pgQuery
46
- const textProp = Object.getOwnPropertyDescriptor(textPropObj, 'text')
47
50
  const stream = typeof textPropObj.read === 'function'
48
51
 
49
- // Only alter `text` property if safe to do so. Initially, it's a property, not a getter.
50
- let originalText
51
- if (!textProp || textProp.configurable) {
52
+ let originalText = originalTextCache.get(textPropObj)
53
+ if (originalText === undefined) {
52
54
  originalText = textPropObj.text
53
-
54
- Object.defineProperty(textPropObj, 'text', {
55
- get () {
56
- return this?.__ddInjectableQuery || originalText
57
- },
58
- })
55
+ originalTextCache.set(textPropObj, originalText)
59
56
  }
57
+
60
58
  const abortController = new AbortController()
61
59
  const ctx = {
62
60
  params: this.connectionParameters,
@@ -109,6 +107,22 @@ function wrapQuery (query) {
109
107
  return Promise.reject(error)
110
108
  }
111
109
 
110
+ const injected = ctx.injected
111
+ if (injected !== undefined) {
112
+ // Skip the per-read getter trampoline when `text` is a configurable, writable data
113
+ // property (the pg / pg-cursor common shape). Accessor descriptors and read-only data
114
+ // still go through `defineProperty(get)` so `get text ()` query objects keep working.
115
+ const textProp = Object.getOwnPropertyDescriptor(textPropObj, 'text')
116
+ if (textProp?.configurable === true && textProp.writable === true) {
117
+ textPropObj.text = injected
118
+ } else if (textProp === undefined || textProp.configurable === true) {
119
+ Object.defineProperty(textPropObj, 'text', {
120
+ configurable: true,
121
+ get () { return injected },
122
+ })
123
+ }
124
+ }
125
+
112
126
  args[0] = pgQuery
113
127
 
114
128
  const retval = query.apply(this, args)