dd-trace 5.108.0 → 5.110.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 (174) hide show
  1. package/ci/init.js +4 -3
  2. package/index.d.ts +88 -25
  3. package/loader-hook.mjs +88 -2
  4. package/package.json +19 -14
  5. package/packages/datadog-esbuild/index.js +26 -0
  6. package/packages/datadog-esbuild/src/utils.js +46 -3
  7. package/packages/datadog-instrumentations/src/ai.js +43 -48
  8. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js-context-methods.js +18 -0
  9. package/packages/datadog-instrumentations/src/aws-durable-execution-sdk-js.js +111 -0
  10. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -1
  11. package/packages/datadog-instrumentations/src/connect.js +4 -3
  12. package/packages/datadog-instrumentations/src/electron.js +1 -1
  13. package/packages/datadog-instrumentations/src/fastify.js +4 -12
  14. package/packages/datadog-instrumentations/src/graphql.js +17 -6
  15. package/packages/datadog-instrumentations/src/helpers/check-require-cache.js +81 -8
  16. package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
  17. package/packages/datadog-instrumentations/src/helpers/instrument.js +27 -2
  18. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +65 -0
  19. package/packages/datadog-instrumentations/src/helpers/register.js +6 -4
  20. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +17 -0
  21. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/aws-durable-execution-sdk-js.js +31 -0
  22. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  23. package/packages/datadog-instrumentations/src/helpers/rewriter/loader.mjs +18 -2
  24. package/packages/datadog-instrumentations/src/http/client.js +12 -2
  25. package/packages/datadog-instrumentations/src/ioredis.js +0 -1
  26. package/packages/datadog-instrumentations/src/iovalkey.js +1 -2
  27. package/packages/datadog-instrumentations/src/koa.js +3 -2
  28. package/packages/datadog-instrumentations/src/mariadb.js +4 -2
  29. package/packages/datadog-instrumentations/src/mocha/main.js +3 -3
  30. package/packages/datadog-instrumentations/src/next.js +44 -10
  31. package/packages/datadog-instrumentations/src/openai.js +77 -18
  32. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +7 -6
  33. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  34. package/packages/datadog-instrumentations/src/redis.js +0 -1
  35. package/packages/datadog-instrumentations/src/restify.js +4 -3
  36. package/packages/datadog-instrumentations/src/router.js +18 -5
  37. package/packages/datadog-instrumentations/src/selenium.js +1 -2
  38. package/packages/datadog-instrumentations/src/tedious.js +28 -0
  39. package/packages/datadog-instrumentations/src/vitest.js +323 -7
  40. package/packages/datadog-plugin-avsc/src/schema_iterator.js +2 -2
  41. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/checkpoint.js +31 -0
  42. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/client.js +55 -0
  43. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +114 -0
  44. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/handler.js +128 -0
  45. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/index.js +19 -0
  46. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/trace-checkpoint.js +224 -0
  47. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +43 -0
  48. package/packages/datadog-plugin-aws-sdk/src/base.js +14 -16
  49. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +43 -6
  50. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +100 -37
  51. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +108 -99
  52. package/packages/datadog-plugin-bullmq/src/filter.js +35 -0
  53. package/packages/datadog-plugin-bullmq/src/producer.js +84 -4
  54. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +9 -1
  55. package/packages/datadog-plugin-fs/src/index.js +1 -0
  56. package/packages/datadog-plugin-http/src/client.js +8 -3
  57. package/packages/datadog-plugin-http2/src/client.js +5 -2
  58. package/packages/datadog-plugin-mysql/src/index.js +7 -2
  59. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +2 -2
  60. package/packages/datadog-plugin-redis/src/index.js +1 -2
  61. package/packages/datadog-plugin-undici/src/index.js +5 -2
  62. package/packages/datadog-plugin-vitest/src/index.js +11 -2
  63. package/packages/datadog-webpack/index.js +17 -1
  64. package/packages/datadog-webpack/src/optional-peer-loader.js +17 -0
  65. package/packages/dd-trace/src/aiguard/channels.js +0 -1
  66. package/packages/dd-trace/src/aiguard/index.js +11 -49
  67. package/packages/dd-trace/src/aiguard/integrations/evaluate.js +46 -0
  68. package/packages/dd-trace/src/aiguard/integrations/openai.js +66 -0
  69. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +78 -0
  70. package/packages/{datadog-instrumentations/src/helpers/ai-messages.js → dd-trace/src/aiguard/messages/openai.js} +85 -193
  71. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +185 -0
  72. package/packages/dd-trace/src/aiguard/sdk.js +13 -1
  73. package/packages/dd-trace/src/appsec/api_security/index.js +55 -0
  74. package/packages/dd-trace/src/appsec/api_security/sampler.js +147 -0
  75. package/packages/dd-trace/src/appsec/channels.js +1 -0
  76. package/packages/dd-trace/src/appsec/downstream_requests.js +111 -58
  77. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/ldap-sensitive-analyzer.js +54 -12
  78. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +5 -1
  79. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +29 -4
  80. package/packages/dd-trace/src/appsec/index.js +10 -6
  81. package/packages/dd-trace/src/appsec/rasp/ssrf.js +19 -11
  82. package/packages/dd-trace/src/appsec/reporter.js +1 -0
  83. package/packages/dd-trace/src/appsec/telemetry/api_security.js +34 -0
  84. package/packages/dd-trace/src/appsec/telemetry/index.js +26 -0
  85. package/packages/dd-trace/src/ci-visibility/requests/request.js +1 -17
  86. package/packages/dd-trace/src/config/defaults.js +22 -3
  87. package/packages/dd-trace/src/config/generated-config-types.d.ts +538 -1
  88. package/packages/dd-trace/src/config/helper.js +133 -33
  89. package/packages/dd-trace/src/config/index.js +4 -1
  90. package/packages/dd-trace/src/config/parsers.js +19 -1
  91. package/packages/dd-trace/src/config/supported-configurations.json +45 -5
  92. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +49 -35
  93. package/packages/dd-trace/src/debugger/devtools_client/index.js +31 -66
  94. package/packages/dd-trace/src/debugger/devtools_client/probe_sampler.js +83 -0
  95. package/packages/dd-trace/src/debugger/devtools_client/state.js +1 -0
  96. package/packages/dd-trace/src/debugger/index.js +5 -0
  97. package/packages/dd-trace/src/debugger/probe_sampler.js +112 -0
  98. package/packages/dd-trace/src/debugger/probe_sampler_constants.js +19 -0
  99. package/packages/dd-trace/src/dogstatsd.js +15 -8
  100. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +77 -26
  101. package/packages/dd-trace/src/exporters/agentless/index.js +7 -5
  102. package/packages/dd-trace/src/exporters/agentless/intake.js +43 -0
  103. package/packages/dd-trace/src/exporters/agentless/writer.js +5 -4
  104. package/packages/dd-trace/src/exporters/common/request.js +1 -20
  105. package/packages/dd-trace/src/exporters/common/url.js +34 -0
  106. package/packages/dd-trace/src/exporters/span-stats/writer.js +1 -3
  107. package/packages/dd-trace/src/guardrails/index.js +8 -5
  108. package/packages/dd-trace/src/index.js +6 -4
  109. package/packages/dd-trace/src/llmobs/constants/tags.js +6 -0
  110. package/packages/dd-trace/src/llmobs/index.js +16 -5
  111. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +3 -3
  112. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +5 -1
  113. package/packages/dd-trace/src/llmobs/sdk.js +5 -3
  114. package/packages/dd-trace/src/llmobs/span_processor.js +4 -0
  115. package/packages/dd-trace/src/llmobs/tagger.js +55 -0
  116. package/packages/dd-trace/src/llmobs/telemetry.js +1 -0
  117. package/packages/dd-trace/src/log/index.js +18 -20
  118. package/packages/dd-trace/src/openfeature/flagging_provider.js +3 -1
  119. package/packages/dd-trace/src/opentelemetry/bridge-span-base.js +5 -4
  120. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +2 -1
  121. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +4 -3
  122. package/packages/dd-trace/src/opentelemetry/metrics/time.js +19 -0
  123. package/packages/dd-trace/src/opentelemetry/otlp/common.proto +2 -2
  124. package/packages/dd-trace/src/opentelemetry/otlp/logs.proto +1 -1
  125. package/packages/dd-trace/src/opentelemetry/otlp/logs_service.proto +1 -1
  126. package/packages/dd-trace/src/opentelemetry/otlp/metrics.proto +1 -1
  127. package/packages/dd-trace/src/opentelemetry/otlp/metrics_service.proto +1 -1
  128. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +11 -4
  129. package/packages/dd-trace/src/opentelemetry/span-helpers.js +29 -13
  130. package/packages/dd-trace/src/opentelemetry/span.js +14 -3
  131. package/packages/dd-trace/src/opentelemetry/span_context.js +1 -0
  132. package/packages/dd-trace/src/opentelemetry/trace/index.js +2 -1
  133. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +3 -2
  134. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +26 -13
  135. package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -3
  136. package/packages/dd-trace/src/opentracing/span_context.js +17 -0
  137. package/packages/dd-trace/src/plugin_manager.js +7 -1
  138. package/packages/dd-trace/src/plugins/ci_plugin.js +36 -3
  139. package/packages/dd-trace/src/plugins/index.js +3 -0
  140. package/packages/dd-trace/src/plugins/util/http-otel-semantics.js +276 -0
  141. package/packages/dd-trace/src/plugins/util/test.js +80 -0
  142. package/packages/dd-trace/src/plugins/util/url.js +68 -1
  143. package/packages/dd-trace/src/plugins/util/web.js +19 -36
  144. package/packages/dd-trace/src/priority_sampler.js +1 -13
  145. package/packages/dd-trace/src/profiling/config.js +129 -198
  146. package/packages/dd-trace/src/profiling/constants.js +4 -7
  147. package/packages/dd-trace/src/profiling/exporter_cli.js +10 -10
  148. package/packages/dd-trace/src/profiling/exporters/agent.js +23 -16
  149. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +45 -14
  150. package/packages/dd-trace/src/profiling/exporters/file.js +16 -5
  151. package/packages/dd-trace/src/profiling/oom.js +71 -0
  152. package/packages/dd-trace/src/profiling/profiler.js +35 -23
  153. package/packages/dd-trace/src/profiling/profilers/events.js +14 -7
  154. package/packages/dd-trace/src/profiling/profilers/space.js +27 -18
  155. package/packages/dd-trace/src/profiling/profilers/wall.js +35 -25
  156. package/packages/dd-trace/src/ritm.js +4 -2
  157. package/packages/dd-trace/src/serverless.js +1 -2
  158. package/packages/dd-trace/src/service-naming/schemas/util.js +36 -5
  159. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  160. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  161. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +5 -3
  162. package/packages/dd-trace/src/span_format.js +2 -1
  163. package/packages/dd-trace/src/span_processor.js +6 -0
  164. package/packages/dd-trace/src/telemetry/send-data.js +12 -10
  165. package/packages/dd-trace/src/tracer.js +15 -1
  166. package/packages/dd-trace/src/util.js +14 -0
  167. package/register.js +58 -1
  168. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  169. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  170. package/vendor/dist/pprof-format/index.js +1 -1
  171. package/vendor/dist/protobufjs/index.js +1 -1
  172. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  173. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +0 -284
  174. package/packages/dd-trace/src/appsec/api_security_sampler.js +0 -117
@@ -8,11 +8,18 @@ function recordDataAsString (data) {
8
8
  return Buffer.isBuffer(data) ? data.toString('utf8') : Buffer.from(data).toString('utf8')
9
9
  }
10
10
 
11
+ // Caps the promise-path iterator→stream cache so abandoned shard iterators
12
+ // (AWS expires them after 5 minutes) can't grow it without bound. Polling loops
13
+ // delete on consume, so their working set is ~the active shard count.
14
+ const MAX_TRACKED_SHARD_ITERATORS = 1000
15
+
11
16
  class Kinesis extends BaseAwsSdkPlugin {
12
17
  static id = 'kinesis'
13
18
  static peerServicePrecursors = ['streamname']
14
19
  static isPayloadReporter = true
15
20
 
21
+ #shardIteratorStreams = new Map()
22
+
16
23
  constructor (...args) {
17
24
  super(...args)
18
25
 
@@ -20,51 +27,107 @@ class Kinesis extends BaseAwsSdkPlugin {
20
27
  // in the base class
21
28
  this.requestTags = new WeakMap()
22
29
 
23
- this.addBind('apm:aws:response:start:kinesis', ctx => {
24
- const { request, response } = ctx
25
- const plugin = this
30
+ this.addBind('apm:aws:response:start:kinesis', ctx => this.#startResponseSpan(ctx))
31
+
32
+ // Promise / event-emitter calls never publish response:start, so create and finish the
33
+ // consumer span from request:complete instead. Callback calls handle it via the bind above.
34
+ this.addSub('apm:aws:request:complete:kinesis', ctx => {
35
+ if (ctx.cbExists) return
36
+ // v2 nests the SDK payload under response.data; v3 spreads the output onto response.
37
+ const response = ctx.response?.data ?? ctx.response
38
+ const responseCtx = { request: ctx.request, response }
39
+ this.#startResponseSpan(responseCtx)
40
+ if (responseCtx.needsFinish) this.finish(responseCtx)
41
+ // The async store that carries streamName to getRecords on the callback path is
42
+ // absent here, so map each shard iterator to its stream for the DSM topic tag.
43
+ if (this.config.dsmEnabled) this.#trackShardStream(ctx.request, response)
44
+ })
26
45
 
27
- let store = this._parentMap.get(request)
46
+ this.addSub('apm:aws:response:finish:kinesis', ctx => {
47
+ if (!ctx.needsFinish) return
48
+ this.finish(ctx)
49
+ })
50
+ }
28
51
 
29
- // if we have either of these operations, we want to store the streamName param
30
- // since it is not typically available during get/put records requests
31
- if (request.operation === 'getShardIterator' || request.operation === 'listShards') {
32
- return this.storeStreamName(request.params, request.operation, store)
33
- }
52
+ /**
53
+ * @param {object} ctx Completion context carrying the SDK request and response.
54
+ */
55
+ #startResponseSpan (ctx) {
56
+ const { request, response } = ctx
34
57
 
35
- if (request.operation === 'getRecords') {
36
- let span
37
- const responseExtraction = this.responseExtract(request.params, request.operation, response)
38
- if (responseExtraction && responseExtraction.maybeChildOf) {
39
- ctx.needsFinish = true
40
- const options = {
41
- childOf: responseExtraction.maybeChildOf,
42
- meta: {
43
- ...this.requestTags.get(request),
44
- 'span.kind': 'server',
45
- },
46
- integrationName: 'aws-sdk',
47
- }
48
- span = plugin.startSpan('aws.response', options, ctx)
49
- store = ctx.currentStore
50
- }
58
+ let store = this._parentMap.get(request)
51
59
 
52
- // get the stream name that should have been stored previously
53
- const { streamName } = store
60
+ // if we have either of these operations, we want to store the streamName param
61
+ // since it is not typically available during get/put records requests
62
+ if (request.operation === 'getShardIterator' || request.operation === 'listShards') {
63
+ return this.storeStreamName(request.params, request.operation, store)
64
+ }
54
65
 
55
- // extract DSM context after as we might not have a parent-child but may have a DSM context
56
- this.responseExtractDSMContext(
57
- request.operation, request.params, response, span || null, { streamName }
58
- )
66
+ if (request.operation === 'getRecords') {
67
+ let span
68
+ const responseExtraction = this.responseExtract(request.params, request.operation, response)
69
+ if (responseExtraction && responseExtraction.maybeChildOf) {
70
+ ctx.needsFinish = true
71
+ const options = {
72
+ childOf: responseExtraction.maybeChildOf,
73
+ meta: {
74
+ ...this.requestTags.get(request),
75
+ 'span.kind': 'server',
76
+ },
77
+ integrationName: 'aws-sdk',
78
+ }
79
+ span = this.startSpan('aws.response', options, ctx)
80
+ store = ctx.currentStore
59
81
  }
60
82
 
61
- return store
62
- })
83
+ if (this.config.dsmEnabled) {
84
+ // streamName rides the async store on the callback path; the promise path has no
85
+ // such link, so fall back to the iterator the producer returned.
86
+ const streamName = store?.streamName ?? this.#shardIteratorStreams.get(request.params.ShardIterator)
87
+ this.responseExtractDSMContext(request.operation, request.params, response, span || null, { streamName })
88
+ }
89
+ }
63
90
 
64
- this.addSub('apm:aws:response:finish:kinesis', ctx => {
65
- if (!ctx.needsFinish) return
66
- this.finish(ctx)
67
- })
91
+ return store
92
+ }
93
+
94
+ /**
95
+ * @param {object} request SDK request; reads `operation` and `params`.
96
+ * @param {object} response SDK output; reads `ShardIterator` / `NextShardIterator`.
97
+ */
98
+ #trackShardStream (request, response) {
99
+ if (request.operation === 'getShardIterator') {
100
+ this.#rememberShardStream(response?.ShardIterator, request.params?.StreamName)
101
+ } else if (request.operation === 'getRecords') {
102
+ this.#advanceShardStream(request.params?.ShardIterator, response?.NextShardIterator)
103
+ }
104
+ }
105
+
106
+ /**
107
+ * @param {string} [iterator] Shard iterator the producer returned.
108
+ * @param {string} [streamName] Stream the iterator belongs to.
109
+ */
110
+ #rememberShardStream (iterator, streamName) {
111
+ if (!iterator || streamName === undefined) return
112
+ // FIFO-evict the oldest entry (Map keeps insertion order) when the cap is hit; only
113
+ // abandoned iterators get here, so no realistic test drives the cap (eviction ignored).
114
+ /* istanbul ignore if */
115
+ if (this.#shardIteratorStreams.size >= MAX_TRACKED_SHARD_ITERATORS) {
116
+ this.#shardIteratorStreams.delete(this.#shardIteratorStreams.keys().next().value)
117
+ }
118
+ this.#shardIteratorStreams.set(iterator, streamName)
119
+ }
120
+
121
+ /**
122
+ * @param {string} [consumedIterator] Iterator just passed to getRecords.
123
+ * @param {string} [nextIterator] NextShardIterator for the following poll.
124
+ */
125
+ #advanceShardStream (consumedIterator, nextIterator) {
126
+ const streamName = this.#shardIteratorStreams.get(consumedIterator)
127
+ if (streamName === undefined) return
128
+ this.#shardIteratorStreams.delete(consumedIterator)
129
+ // carry the stream onto the next iterator so the polling loop keeps its topic
130
+ if (nextIterator) this.#rememberShardStream(nextIterator, streamName)
68
131
  }
69
132
 
70
133
  generateTags (params, operation, response) {
@@ -54,41 +54,16 @@ class Sqs extends BaseAwsSdkPlugin {
54
54
  // in the base class
55
55
  this.requestTags = new WeakMap()
56
56
 
57
- this.addBind('apm:aws:response:start:sqs', ctx => {
58
- const { request, response } = ctx
59
- const contextExtraction = this.responseExtract(request.params, request.operation, response)
60
-
61
- let store = this._parentMap.get(request)
62
- let span
63
- let parsedMessageAttributes
64
- let parsedFirstBody
65
- let firstBodyChecked = false
66
- if (contextExtraction !== undefined) {
67
- parsedFirstBody = contextExtraction.parsedBody
68
- firstBodyChecked = contextExtraction.bodyChecked === true
69
- if (contextExtraction.datadogContext !== undefined) {
70
- ctx.needsFinish = true
71
- const options = {
72
- childOf: contextExtraction.datadogContext,
73
- meta: {
74
- ...this.requestTags.get(request),
75
- 'span.kind': 'server',
76
- },
77
- integrationName: 'aws-sdk',
78
- }
79
- parsedMessageAttributes = contextExtraction.parsedAttributes
80
- span = this.startSpan('aws.response', options, ctx)
81
- store = ctx.currentStore
82
- }
83
- }
84
-
85
- // Extract DSM context after, as we might not have a parent-child but may have a DSM context.
86
- this.responseExtractDSMContext(
87
- request.operation, request.params, response, span ?? null,
88
- { parsedAttributes: parsedMessageAttributes, parsedFirstBody, firstBodyChecked }
89
- )
90
-
91
- return store
57
+ this.addBind('apm:aws:response:start:sqs', ctx => this.#startResponseSpan(ctx))
58
+
59
+ // No-callback receives (promises, event emitters) never publish response:start, so link and
60
+ // finish the consumer span here instead. Callback paths reach the same logic via the bind above.
61
+ this.addSub('apm:aws:request:complete:sqs', ctx => {
62
+ if (ctx.cbExists) return
63
+ // v2 nests the SDK payload under response.data; v3 spreads the output onto response.
64
+ const responseCtx = { request: ctx.request, response: ctx.response?.data ?? ctx.response }
65
+ this.#startResponseSpan(responseCtx)
66
+ if (responseCtx.needsFinish) this.finish(responseCtx)
92
67
  })
93
68
 
94
69
  this.addSub('apm:aws:response:finish:sqs', ctx => {
@@ -97,6 +72,57 @@ class Sqs extends BaseAwsSdkPlugin {
97
72
  })
98
73
  }
99
74
 
75
+ /**
76
+ * Start the consumer (`aws.response`) span for a receive. The first message carrying trace
77
+ * context becomes the parent; every additional one fans in as a span link.
78
+ *
79
+ * @param {{ request: object, response: object, needsFinish?: boolean, currentStore?: object }} ctx
80
+ * @returns {object | undefined} The store to activate for the consumer span, else the parent store.
81
+ */
82
+ #startResponseSpan (ctx) {
83
+ const { request, response } = ctx
84
+ const carriers = this.responseExtract(request.params, request.operation, response)
85
+
86
+ let store = this._parentMap.get(request)
87
+ let span
88
+
89
+ if (carriers !== undefined) {
90
+ // request:start records requestTags only after the isEnabled gate, so an absent entry
91
+ // means this consumer is disabled — gate on it instead of paying isEnabled again here.
92
+ const requestTags = this.requestTags.get(request)
93
+ if (requestTags !== undefined) {
94
+ // A receive can return messages from many producers; fanning the extra ones in as span
95
+ // links is the shape dd-trace-java and dd-trace-py use for batch SQS receives.
96
+ for (const carrier of carriers) {
97
+ if (carrier === undefined) continue
98
+ const datadogContext = this.tracer.extract('text_map', carrier)
99
+ // A DSM-only carrier (a non-first sendMessageBatch entry when batchPropagationEnabled
100
+ // is off) extracts to null; span.addLink dereferences the context and would throw on it.
101
+ if (datadogContext === null) continue
102
+ if (span === undefined) {
103
+ ctx.needsFinish = true
104
+ span = this.startSpan('aws.response', {
105
+ childOf: datadogContext,
106
+ meta: {
107
+ ...requestTags,
108
+ 'span.kind': 'server',
109
+ },
110
+ integrationName: 'aws-sdk',
111
+ }, ctx)
112
+ store = ctx.currentStore
113
+ } else {
114
+ span.addLink({ context: datadogContext })
115
+ }
116
+ }
117
+ }
118
+ }
119
+
120
+ // Extract DSM context after, as we might not have a parent-child but may have a DSM context.
121
+ this.responseExtractDSMContext(request.operation, request.params, response, span ?? null, carriers)
122
+
123
+ return store
124
+ }
125
+
100
126
  operationFromRequest (request) {
101
127
  switch (request.operation) {
102
128
  case 'receiveMessage':
@@ -166,19 +192,39 @@ class Sqs extends BaseAwsSdkPlugin {
166
192
  return tags
167
193
  }
168
194
 
195
+ /**
196
+ * Parse the trace-context carrier of every received message, in message order.
197
+ * Entries are `undefined` for messages that carry no `_datadog` context.
198
+ *
199
+ * @param {{ MaxNumberOfMessages?: number }} params
200
+ * @param {string} operation
201
+ * @param {{ Messages?: object[] }} response
202
+ * @returns {Array<Record<string, string> | undefined> | undefined}
203
+ */
169
204
  responseExtract (params, operation, response) {
170
205
  if (operation !== 'receiveMessage') return
171
- if (params.MaxNumberOfMessages && params.MaxNumberOfMessages !== 1) return
172
- if (!response || !response.Messages || !response.Messages[0]) return
206
+ if (!response?.Messages?.length) return
173
207
 
174
- let message = response.Messages[0]
175
- let parsedBody
208
+ return response.Messages.map(message => this.parseMessageCarrier(message))
209
+ }
176
210
 
211
+ /**
212
+ * Resolve the trace-context carrier for a single received message. The
213
+ * `MessageAttributes._datadog` text map (direct SQS or SNS to SQS) wins;
214
+ * otherwise the EventBridge envelope, optionally wrapped in an SNS
215
+ * `Notification` (see getEventBridgeContext). Checking MessageAttributes first
216
+ * avoids parsing a large SNS `Message` just to rule out an EventBridge envelope.
217
+ *
218
+ * @param {object} message A single `response.Messages` entry.
219
+ * @returns {Record<string, string> | undefined}
220
+ */
221
+ parseMessageCarrier (message) {
222
+ let parsedBody
177
223
  if (message.Body) {
178
224
  try {
179
225
  parsedBody = JSON.parse(message.Body)
180
226
  } catch {
181
- // SQS to SQS
227
+ // Opaque, non-JSON body (SQS to SQS).
182
228
  }
183
229
  // SNS to SQS
184
230
  if (parsedBody?.Type === 'Notification') {
@@ -186,34 +232,9 @@ class Sqs extends BaseAwsSdkPlugin {
186
232
  }
187
233
  }
188
234
 
189
- // Check MessageAttributes first (common direct-SQS/SNS path): avoids parsing
190
- // the body (e.g. a large SNS `Message`) just to rule out an EventBridge
191
- // envelope. Precedence matches responseExtractDSMContext.
192
235
  const datadogAttribute = message.MessageAttributes?._datadog
193
- if (datadogAttribute) {
194
- const parsedAttributes = this.parseDatadogAttributes(datadogAttribute)
195
- if (parsedAttributes) {
196
- return {
197
- datadogContext: this.tracer.extract('text_map', parsedAttributes),
198
- parsedAttributes,
199
- parsedBody,
200
- bodyChecked: true,
201
- }
202
- }
203
- }
204
-
205
- // Then the EventBridge envelope (optionally via SNS); see getEventBridgeContext.
206
- const eventBridgeContext = getEventBridgeContext(parsedBody)
207
- if (eventBridgeContext) {
208
- return {
209
- datadogContext: this.tracer.extract('text_map', eventBridgeContext),
210
- parsedAttributes: eventBridgeContext,
211
- parsedBody,
212
- bodyChecked: true,
213
- }
214
- }
215
-
216
- return { parsedBody, bodyChecked: true }
236
+ const carrier = datadogAttribute ? this.parseDatadogAttributes(datadogAttribute) : undefined
237
+ return carrier ?? getEventBridgeContext(parsedBody)
217
238
  }
218
239
 
219
240
  parseDatadogAttributes (attributes) {
@@ -230,51 +251,39 @@ class Sqs extends BaseAwsSdkPlugin {
230
251
  }
231
252
  }
232
253
 
233
- responseExtractDSMContext (operation, params, response, span, kwargs = {}) {
234
- let { parsedAttributes } = kwargs
235
- const { parsedFirstBody, firstBodyChecked } = kwargs
254
+ /**
255
+ * @param {string} operation
256
+ * @param {{ QueueUrl: string }} params
257
+ * @param {{ Messages?: object[] }} response
258
+ * @param {import('../../../dd-trace/src/opentracing/span') | null} span
259
+ * @param {Array<Record<string, string> | undefined>} [carriers] Per-message carriers already
260
+ * parsed by `responseExtract`; reused so each message body is parsed once. When omitted, the
261
+ * carriers are parsed here.
262
+ */
263
+ responseExtractDSMContext (operation, params, response, span, carriers) {
236
264
  if (!this.config.dsmEnabled) return
237
265
  if (operation !== 'receiveMessage') return
238
- if (!response || !response.Messages || !response.Messages[0]) return
266
+ if (!response?.Messages?.length) return
239
267
 
268
+ const messages = response.Messages
240
269
  // Only attribute payloadSize to the span when there is a single message.
241
- span = response.Messages.length > 1 ? null : span
270
+ span = messages.length > 1 ? null : span
242
271
 
243
272
  // QueueUrl is the same for the whole receive batch.
244
273
  const queue = params.QueueUrl.slice(params.QueueUrl.lastIndexOf('/') + 1)
245
274
 
246
- for (let i = 0; i < response.Messages.length; i++) {
247
- let message = response.Messages[i]
248
- if (!parsedAttributes) {
249
- let body
250
- // responseExtract already parsed message[0]; reuse that result instead of re-parsing.
251
- if (i === 0 && firstBodyChecked) {
252
- body = parsedFirstBody
253
- } else if (message.Body) {
254
- try {
255
- body = JSON.parse(message.Body)
256
- } catch {
257
- // SQS to SQS
258
- }
259
- }
260
- // SNS to SQS
261
- if (body?.Type === 'Notification') {
262
- message = body
263
- }
264
- // MessageAttributes for direct SQS/SNS; else the EventBridge envelope.
265
- parsedAttributes = message.MessageAttributes?._datadog
266
- ? this.parseDatadogAttributes(message.MessageAttributes._datadog)
267
- : getEventBridgeContext(body)
275
+ for (let i = 0; i < messages.length; i++) {
276
+ const message = messages[i]
277
+ const carrier = carriers === undefined ? this.parseMessageCarrier(message) : carriers[i]
278
+ if (carrier) {
279
+ // Inert for EventBridge until its producer emits a pathway (separate
280
+ // change) no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
281
+ this.tracer.decodeDataStreamsContext(carrier)
268
282
  }
269
283
  const payloadSize = getHeadersSize({
270
284
  Body: message.Body,
271
285
  MessageAttributes: message.MessageAttributes,
272
286
  })
273
- if (parsedAttributes) {
274
- // Inert for EventBridge until its producer emits a pathway (separate
275
- // change) — no `dd-pathway-ctx-base64` to decode yet; SQS/SNS decode now.
276
- this.tracer.decodeDataStreamsContext(parsedAttributes)
277
- }
278
287
  this.tracer
279
288
  .setCheckpoint(['direction:in', `topic:${queue}`, 'type:sqs'], span, payloadSize)
280
289
  }
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../dd-trace/src/log')
4
+
5
+ /**
6
+ * @typedef {object} BullmqJobShape
7
+ * @property {string} [name]
8
+ * @property {unknown} [data]
9
+ * @property {unknown} [opts]
10
+ * @property {string} [queueName]
11
+ */
12
+
13
+ /**
14
+ * @typedef {(job: BullmqJobShape) => boolean} BullmqFilter
15
+ */
16
+
17
+ /**
18
+ * Resolve a user-provided filter from plugin config. Returns `undefined` when
19
+ * no filter is configured so callers can short-circuit the filtering path on a
20
+ * cheap truthy check. If `producerFilter` is present but not a function, log
21
+ * an error and fall back to no filter.
22
+ *
23
+ * @param {{ producerFilter?: unknown }} config Plugin config that may carry a `producerFilter`.
24
+ * @returns {BullmqFilter | undefined}
25
+ */
26
+ function getFilter (config) {
27
+ if (typeof config?.producerFilter === 'function') {
28
+ return /** @type {BullmqFilter} */ (config.producerFilter)
29
+ }
30
+ if (config?.producerFilter !== undefined) {
31
+ log.error('Expected `producerFilter` to be a function. Ignoring.')
32
+ }
33
+ }
34
+
35
+ module.exports = { getFilter }
@@ -3,6 +3,9 @@
3
3
  const log = require('../../dd-trace/src/log')
4
4
  const ProducerPlugin = require('../../dd-trace/src/plugins/producer')
5
5
  const { DsmPathwayCodec, getMessageSize } = require('../../dd-trace/src/datastreams')
6
+ const { getFilter } = require('./filter')
7
+
8
+ const filteredJobs = Symbol('bullmq.filteredJobs')
6
9
 
7
10
  // Customer-controlled metadata may be malformed JSON. Returning a fresh `{}`
8
11
  // on parse failure keeps the publish path alive instead of throwing into
@@ -31,6 +34,19 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
31
34
  }
32
35
 
33
36
  bindStart (ctx) {
37
+ let instrument = true
38
+ if (this.config.producerFilter) {
39
+ try {
40
+ instrument = this.shouldInstrument(ctx)
41
+ } catch (error) {
42
+ log.error('bullmq: producerFilter threw, filtering is disabled: %s', error.message)
43
+ }
44
+ }
45
+
46
+ if (!instrument) {
47
+ return { noop: true }
48
+ }
49
+
34
50
  const { resource, meta } = this.getSpanData(ctx)
35
51
  const span = this.startSpan({
36
52
  resource,
@@ -48,6 +64,17 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
48
64
  return ctx.currentStore
49
65
  }
50
66
 
67
+ configure (config) {
68
+ if (typeof config === 'boolean') {
69
+ return super.configure(config)
70
+ }
71
+ return super.configure({ ...config, producerFilter: getFilter(config) })
72
+ }
73
+
74
+ shouldInstrument (ctx) {
75
+ throw new Error('shouldInstrument must be implemented by subclass')
76
+ }
77
+
51
78
  getSpanData (ctx) {
52
79
  throw new Error('getSpanData must be implemented by subclass')
53
80
  }
@@ -87,6 +114,15 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
87
114
  class QueueAddPlugin extends BaseBullmqProducerPlugin {
88
115
  static prefix = 'tracing:orchestrion:bullmq:Queue_add'
89
116
 
117
+ shouldInstrument (ctx) {
118
+ return this.config.producerFilter({
119
+ name: ctx.arguments?.[0],
120
+ data: ctx.arguments?.[1],
121
+ opts: ctx.arguments?.[2],
122
+ queueName: ctx.self?.name,
123
+ })
124
+ }
125
+
90
126
  getSpanData (ctx) {
91
127
  const queueName = ctx.self?.name || 'bullmq'
92
128
  return {
@@ -128,6 +164,40 @@ class QueueAddPlugin extends BaseBullmqProducerPlugin {
128
164
  class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
129
165
  static prefix = 'tracing:orchestrion:bullmq:Queue_addBulk'
130
166
 
167
+ shouldInstrument (ctx) {
168
+ const jobs = this.#getFilteredJobs(ctx)
169
+ // Empty bulk calls are publish attempts and should keep producing a span.
170
+ return jobs === undefined || jobs.length > 0 || ctx.arguments?.[0].length === 0
171
+ }
172
+
173
+ #getFilteredJobs (ctx) {
174
+ if (Object.hasOwn(ctx, filteredJobs)) return ctx[filteredJobs]
175
+
176
+ const jobs = ctx.arguments?.[0]
177
+ if (!Array.isArray(jobs)) return
178
+
179
+ let allowedJobs
180
+ if (this.config.producerFilter) {
181
+ const queueName = ctx.self?.name
182
+ try {
183
+ allowedJobs = []
184
+ for (const job of jobs) {
185
+ if (job && this.config.producerFilter({ name: job.name, data: job.data, opts: job.opts, queueName })) {
186
+ allowedJobs.push(job)
187
+ }
188
+ }
189
+ } catch (error) {
190
+ log.error('bullmq: producerFilter threw, filtering is disabled: %s', error.message)
191
+ allowedJobs = jobs.filter(Boolean)
192
+ }
193
+ } else {
194
+ allowedJobs = jobs
195
+ }
196
+
197
+ ctx[filteredJobs] = allowedJobs
198
+ return allowedJobs
199
+ }
200
+
131
201
  operationName () {
132
202
  return 'bullmq.addBulk'
133
203
  }
@@ -139,14 +209,14 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
139
209
  resource: queueName,
140
210
  meta: {
141
211
  'messaging.destination.name': ctx.self?.name,
142
- 'messaging.batch.message_count': Array.isArray(jobs) ? jobs.length : undefined,
212
+ 'messaging.batch.message_count': jobs?.length,
143
213
  },
144
214
  }
145
215
  }
146
216
 
147
217
  injectTraceContext (span, ctx) {
148
- const jobs = ctx.arguments?.[0]
149
- if (!Array.isArray(jobs)) return
218
+ const jobs = this.#getFilteredJobs(ctx)
219
+ if (!jobs) return
150
220
 
151
221
  const cache = []
152
222
  for (let i = 0; i < jobs.length; i++) {
@@ -159,7 +229,7 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
159
229
  }
160
230
 
161
231
  setProducerCheckpoint (span, ctx) {
162
- const jobs = ctx.arguments?.[0] || []
232
+ const jobs = this.#getFilteredJobs(ctx) || []
163
233
  const queueName = ctx.self?.name || 'bullmq'
164
234
  const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq']
165
235
  const cache = ctx._ddMetadata
@@ -180,6 +250,16 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
180
250
  class FlowProducerAddPlugin extends BaseBullmqProducerPlugin {
181
251
  static prefix = 'tracing:orchestrion:bullmq:FlowProducer_add'
182
252
 
253
+ shouldInstrument (ctx) {
254
+ const flow = ctx.arguments?.[0]
255
+ return this.config.producerFilter({
256
+ name: flow?.name,
257
+ data: flow?.data,
258
+ opts: flow?.opts,
259
+ queueName: flow?.queueName,
260
+ })
261
+ }
262
+
183
263
  getSpanData (ctx) {
184
264
  const flow = ctx.arguments?.[0]
185
265
  const queueName = flow?.queueName || 'bullmq'
@@ -11,6 +11,7 @@ const {
11
11
  TEST_IS_RUM_ACTIVE,
12
12
  TEST_CODE_OWNERS,
13
13
  getTestEnvironmentMetadata,
14
+ getTestLevelsMetadataTags,
14
15
  CI_APP_ORIGIN,
15
16
  getTestParentSpan,
16
17
  getCodeOwnersFileEntries,
@@ -23,6 +24,7 @@ const {
23
24
  TEST_MODULE_ID,
24
25
  TEST_SESSION_ID,
25
26
  TEST_COMMAND,
27
+ TEST_LEVELS_METADATA,
26
28
  TEST_MODULE,
27
29
  TEST_SOURCE_START,
28
30
  finishAllTraceSpans,
@@ -1043,7 +1045,13 @@ class CypressPlugin {
1043
1045
  )
1044
1046
 
1045
1047
  if (this.tracer._tracer._exporter?.addMetadataTags) {
1046
- const metadataTags = { '*': { [TEST_COMMAND]: this.command, [TEST_SESSION_NAME]: testSessionName } }
1048
+ const metadataTags = {
1049
+ [TEST_LEVELS_METADATA]: {
1050
+ [TEST_COMMAND]: this.command,
1051
+ [TEST_SESSION_NAME]: testSessionName,
1052
+ ...getTestLevelsMetadataTags(this.testEnvironmentMetadata),
1053
+ },
1054
+ }
1047
1055
  const libraryCapabilitiesTags = getLibraryCapabilitiesTags(this.constructor.id, this.frameworkVersion)
1048
1056
  metadataTags.test = {
1049
1057
  ...libraryCapabilitiesTags,
@@ -5,6 +5,7 @@ const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
5
5
  class FsPlugin extends TracingPlugin {
6
6
  static id = 'fs'
7
7
  static operation = 'operation'
8
+ static experimental = true
8
9
 
9
10
  configure (...args) {
10
11
  return super.configure(...args)