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
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
4
- const serverless = require('../../dd-trace/src/plugins/util/serverless')
5
4
  const web = require('../../dd-trace/src/plugins/util/web')
6
5
 
7
6
  const triggerMap = {
@@ -26,19 +25,51 @@ class AzureFunctionsPlugin extends TracingPlugin {
26
25
  bindStart (ctx) {
27
26
  const meta = getMetaForTrigger(ctx)
28
27
  const triggerType = triggerMap[ctx.methodName]
28
+ const isHttpTrigger = triggerType === 'Http'
29
29
  const isMessagingService = (triggerType === 'ServiceBus' || triggerType === 'EventHubs')
30
- const childOf = isMessagingService ? null : extractTraceContext(this._tracer, ctx)
31
- const span = this.startSpan(this.operationName(), {
32
- childOf,
33
- service: this.serviceName(),
34
- type: 'serverless',
35
- meta,
36
- }, ctx)
37
-
38
- if (isMessagingService) {
39
- setSpanLinks(triggerType, this.tracer, span, ctx)
40
- }
41
30
 
31
+ let span
32
+
33
+ if (isHttpTrigger) {
34
+ const { httpRequest } = ctx
35
+ const path = (new URL(httpRequest.url)).pathname
36
+ const req = {
37
+ method: httpRequest.method,
38
+ headers: Object.fromEntries(httpRequest.headers),
39
+ url: path,
40
+ }
41
+ // Patch the request to create web context
42
+ const webContext = web.patch(req)
43
+ webContext.config = this.config
44
+ webContext.tracer = this.tracer
45
+ webContext.paths = [path]
46
+ // Creates a standard span and an inferred proxy span if headers are present
47
+ span = web.startServerlessSpanWithInferredProxy(
48
+ this.tracer,
49
+ this.config,
50
+ this.operationName(),
51
+ req,
52
+ ctx
53
+ )
54
+
55
+ span._integrationName = 'azure-functions'
56
+ span.context()._tags.component = 'azure-functions'
57
+ span.addTags(meta)
58
+ webContext.span = span
59
+ webContext.azureFunctionCtx = ctx
60
+ ctx.webContext = webContext
61
+ } else {
62
+ // For non-HTTP triggers, use standard flow
63
+ span = this.startSpan(this.operationName(), {
64
+ service: this.serviceName(),
65
+ type: 'serverless',
66
+ meta,
67
+ }, ctx)
68
+
69
+ if (isMessagingService) {
70
+ setSpanLinks(triggerType, this.tracer, span, ctx)
71
+ }
72
+ }
42
73
  ctx.span = span
43
74
  return ctx.currentStore
44
75
  }
@@ -48,24 +79,16 @@ class AzureFunctionsPlugin extends TracingPlugin {
48
79
  ctx.currentStore.span.setTag('error.message', ctx.error)
49
80
  }
50
81
 
51
- asyncEnd (ctx) {
52
- const { httpRequest, methodName, result = {} } = ctx
53
- if (triggerMap[methodName] === 'Http') {
54
- // If the method is an HTTP trigger, we need to patch the request and finish the span
55
- const path = (new URL(httpRequest.url)).pathname
56
- const req = {
57
- method: httpRequest.method,
58
- headers: Object.fromEntries(httpRequest.headers),
59
- url: path,
82
+ asyncStart (ctx) {
83
+ const { methodName, result = {}, webContext } = ctx
84
+ const triggerType = triggerMap[methodName]
85
+
86
+ // For HTTP triggers, use web utilities to finish all spans (including inferred proxy)
87
+ if (triggerType === 'Http') {
88
+ if (webContext) {
89
+ webContext.res = { statusCode: result.status }
90
+ web.finishAll(webContext, 'serverless')
60
91
  }
61
- const context = web.patch(req)
62
- context.config = this.config
63
- context.paths = [path]
64
- context.res = { statusCode: result.status }
65
- context.span = ctx.currentStore.span
66
-
67
- serverless.finishSpan(context)
68
- // Fallback for other trigger types
69
92
  } else {
70
93
  super.finish()
71
94
  }
@@ -80,6 +103,7 @@ function getMetaForTrigger ({ functionName, methodName, invocationContext }) {
80
103
  let meta = {
81
104
  'aas.function.name': functionName,
82
105
  'aas.function.trigger': mapTriggerTag(methodName),
106
+ 'span.type': 'serverless',
83
107
  }
84
108
 
85
109
  if (triggerMap[methodName] === 'ServiceBus') {
@@ -112,14 +136,6 @@ function mapTriggerTag (methodName) {
112
136
  return triggerMap[methodName] || 'Unknown'
113
137
  }
114
138
 
115
- function extractTraceContext (tracer, ctx) {
116
- if (triggerMap[ctx.methodName] === 'Http') {
117
- return tracer.extract('http_headers', Object.fromEntries(ctx.httpRequest.headers))
118
- }
119
- // Returning null indicates that the span is a root span
120
- return null
121
- }
122
-
123
139
  // message & messages & batch with cardinality of 1 == applicationProperties
124
140
  // messages with cardinality of many == applicationPropertiesArray
125
141
  function setSpanLinks (triggerType, tracer, span, ctx) {
@@ -11,17 +11,24 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
11
11
  ctx.currentStore?.span?.finish()
12
12
  }
13
13
 
14
+ start (ctx) {
15
+ if (!this.config.dsmEnabled) return
16
+ const { span } = ctx.currentStore
17
+ this.setConsumerCheckpoint(span, ctx)
18
+ }
19
+
14
20
  bindStart (ctx) {
15
21
  const job = ctx.arguments?.[0]
16
22
  const queueName = job?.queueName || job?.queue?.name || 'bullmq'
17
23
 
18
24
  let childOf
19
- const datadogContext = job?.data?._datadog
20
- if (datadogContext) {
21
- childOf = this.tracer.extract('text_map', datadogContext)
25
+ const ddCarrier = this._extractDatadog(job)
26
+ if (ddCarrier) {
27
+ ctx._ddCarrier = ddCarrier
28
+ childOf = this.tracer.extract('text_map', ddCarrier)
22
29
  }
23
30
 
24
- const span = this.startSpan({
31
+ this.startSpan({
25
32
  childOf,
26
33
  resource: queueName,
27
34
  meta: {
@@ -33,10 +40,6 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
33
40
  },
34
41
  }, ctx)
35
42
 
36
- if (this.config.dsmEnabled) {
37
- this.setConsumerCheckpoint(span, ctx)
38
- }
39
-
40
43
  return ctx.currentStore
41
44
  }
42
45
 
@@ -47,14 +50,33 @@ class BullmqConsumerPlugin extends ConsumerPlugin {
47
50
  const queueName = job.queueName || job.queue?.name || 'bullmq'
48
51
  const payloadSize = job.data ? getMessageSize(job.data) : 0
49
52
 
50
- const datadogContext = job.data?._datadog
51
- if (datadogContext) {
52
- this.tracer.decodeDataStreamsContext(datadogContext)
53
+ const ddCarrier = ctx._ddCarrier
54
+ if (ddCarrier) {
55
+ this.tracer.decodeDataStreamsContext(ddCarrier)
53
56
  }
54
57
 
55
58
  const edgeTags = ['direction:in', `topic:${queueName}`, 'type:bullmq']
56
59
  this.tracer.setCheckpoint(edgeTags, span, payloadSize)
57
60
  }
61
+
62
+ _extractDatadog (job) {
63
+ const metadataStr = job?.opts?.telemetry?.metadata
64
+ if (!metadataStr) return
65
+
66
+ try {
67
+ const metadata = JSON.parse(metadataStr)
68
+ const ddCarrier = metadata._datadog
69
+ if (!ddCarrier) return
70
+
71
+ // Clean up only our _datadog key, preserve other metadata
72
+ delete metadata._datadog
73
+ job.opts.telemetry.metadata = JSON.stringify(metadata)
74
+
75
+ return ddCarrier
76
+ } catch {
77
+ // Ignore malformed metadata
78
+ }
79
+ }
58
80
  }
59
81
 
60
82
  module.exports = BullmqConsumerPlugin
@@ -10,6 +10,12 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
10
10
  ctx.currentStore?.span?.finish()
11
11
  }
12
12
 
13
+ start (ctx) {
14
+ if (!this.config.dsmEnabled) return
15
+ const { span } = ctx.currentStore
16
+ this.setProducerCheckpoint(span, ctx)
17
+ }
18
+
13
19
  bindStart (ctx) {
14
20
  const { resource, meta } = this.getSpanData(ctx)
15
21
  const span = this.startSpan({
@@ -25,10 +31,6 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
25
31
 
26
32
  this.injectTraceContext(span, ctx)
27
33
 
28
- if (this.config.dsmEnabled) {
29
- this.setProducerCheckpoint(span, ctx)
30
- }
31
-
32
34
  return ctx.currentStore
33
35
  }
34
36
 
@@ -40,13 +42,24 @@ class BaseBullmqProducerPlugin extends ProducerPlugin {
40
42
  throw new Error('injectTraceContext must be implemented by subclass')
41
43
  }
42
44
 
45
+ _injectIntoOpts (span, opts) {
46
+ const carrier = {}
47
+ this.tracer.inject(span, 'text_map', carrier)
48
+ const existing = opts.telemetry?.metadata ? JSON.parse(opts.telemetry.metadata) : {}
49
+ existing._datadog = carrier
50
+ opts.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
51
+ }
52
+
43
53
  setProducerCheckpoint (span, ctx) {
44
- const { queueName, payloadSize, injectTarget } = this.getDsmData(ctx)
54
+ const { queueName, payloadSize, optsTarget } = this.getDsmData(ctx)
45
55
  const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq']
46
56
  const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
47
- if (injectTarget && typeof injectTarget === 'object') {
48
- injectTarget._datadog = injectTarget._datadog || {}
49
- DsmPathwayCodec.encode(dataStreamsContext, injectTarget._datadog)
57
+
58
+ if (optsTarget && typeof optsTarget === 'object') {
59
+ const existing = optsTarget.telemetry?.metadata ? JSON.parse(optsTarget.telemetry.metadata) : {}
60
+ DsmPathwayCodec.encode(dataStreamsContext, existing._datadog || existing)
61
+ if (!existing._datadog) existing._datadog = {}
62
+ optsTarget.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
50
63
  }
51
64
  }
52
65
 
@@ -68,12 +81,22 @@ class QueueAddPlugin extends BaseBullmqProducerPlugin {
68
81
  }
69
82
  }
70
83
 
71
- injectTraceContext (span, ctx) {
72
- const data = ctx.arguments?.[1]
73
- if (data?.constructor?.name === 'Object') {
74
- data._datadog = data._datadog || {}
75
- this.tracer.inject(span, 'text_map', data._datadog)
84
+ #ensureOpts (ctx) {
85
+ let opts = ctx.arguments?.[2]
86
+ if (!opts || typeof opts !== 'object') {
87
+ opts = {}
88
+ if (ctx.arguments.length <= 2) {
89
+ Array.prototype.push.call(ctx.arguments, opts)
90
+ } else {
91
+ ctx.arguments[2] = opts
92
+ }
76
93
  }
94
+ return opts
95
+ }
96
+
97
+ injectTraceContext (span, ctx) {
98
+ const opts = this.#ensureOpts(ctx)
99
+ this._injectIntoOpts(span, opts)
77
100
  }
78
101
 
79
102
  getDsmData (ctx) {
@@ -81,7 +104,7 @@ class QueueAddPlugin extends BaseBullmqProducerPlugin {
81
104
  return {
82
105
  queueName: ctx.self?.name || 'bullmq',
83
106
  payloadSize: data ? getMessageSize(data) : 0,
84
- injectTarget: data,
107
+ optsTarget: this.#ensureOpts(ctx),
85
108
  }
86
109
  }
87
110
  }
@@ -108,10 +131,11 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
108
131
  injectTraceContext (span, ctx) {
109
132
  const jobs = ctx.arguments?.[0]
110
133
  if (!Array.isArray(jobs)) return
134
+
111
135
  for (const job of jobs) {
112
- if (job?.data?.constructor?.name !== 'Object') continue
113
- job.data._datadog = job.data._datadog || {}
114
- this.tracer.inject(span, 'text_map', job.data._datadog)
136
+ if (!job) continue
137
+ job.opts = job.opts || {}
138
+ this._injectIntoOpts(span, job.opts)
115
139
  }
116
140
  }
117
141
 
@@ -123,7 +147,7 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
123
147
  return {
124
148
  queueName: ctx.self?.name || 'bullmq',
125
149
  payloadSize,
126
- injectTarget: jobs[0]?.data,
150
+ optsTarget: jobs[0]?.opts,
127
151
  }
128
152
  }
129
153
 
@@ -133,12 +157,14 @@ class QueueAddBulkPlugin extends BaseBullmqProducerPlugin {
133
157
  const edgeTags = ['direction:out', `topic:${queueName}`, 'type:bullmq']
134
158
 
135
159
  for (const job of jobs) {
136
- if (job?.data && job.data !== null && job.data.constructor.name === 'Object') {
137
- const payloadSize = getMessageSize(job.data)
138
- const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
139
- job.data._datadog = job.data._datadog || {}
140
- DsmPathwayCodec.encode(dataStreamsContext, job.data._datadog)
141
- }
160
+ if (!job?.data) continue
161
+ const payloadSize = getMessageSize(job.data)
162
+ const dataStreamsContext = this.tracer.setCheckpoint(edgeTags, span, payloadSize)
163
+ job.opts = job.opts || {}
164
+ const existing = job.opts.telemetry?.metadata ? JSON.parse(job.opts.telemetry.metadata) : {}
165
+ DsmPathwayCodec.encode(dataStreamsContext, existing._datadog || existing)
166
+ if (!existing._datadog) existing._datadog = {}
167
+ job.opts.telemetry = { metadata: JSON.stringify(existing), omitContext: true }
142
168
  }
143
169
  }
144
170
  }
@@ -159,18 +185,21 @@ class FlowProducerAddPlugin extends BaseBullmqProducerPlugin {
159
185
 
160
186
  injectTraceContext (span, ctx) {
161
187
  const flow = ctx.arguments?.[0]
162
- if (flow?.data?.constructor?.name === 'Object') {
163
- flow.data._datadog = flow.data._datadog || {}
164
- this.tracer.inject(span, 'text_map', flow.data._datadog)
165
- }
188
+ if (!flow) return
189
+ flow.opts = flow.opts || {}
190
+ this._injectIntoOpts(span, flow.opts)
166
191
  }
167
192
 
168
193
  getDsmData (ctx) {
169
194
  const flow = ctx.arguments?.[0]
195
+ if (!flow) {
196
+ return { queueName: 'bullmq', payloadSize: 0, optsTarget: undefined }
197
+ }
198
+ flow.opts = flow.opts || {}
170
199
  return {
171
- queueName: flow?.queueName || 'bullmq',
172
- payloadSize: flow?.data ? getMessageSize(flow.data) : 0,
173
- injectTarget: flow?.data,
200
+ queueName: flow.queueName || 'bullmq',
201
+ payloadSize: flow.data ? getMessageSize(flow.data) : 0,
202
+ optsTarget: flow.opts,
174
203
  }
175
204
  }
176
205
  }
@@ -128,12 +128,15 @@ class CucumberPlugin extends CiPlugin {
128
128
  const testSuitePath = getTestSuitePath(testFileAbsolutePath, process.cwd())
129
129
  const testSourceFile = getTestSuitePath(testFileAbsolutePath, this.repositoryRoot)
130
130
 
131
- const testSuiteMetadata = getTestSuiteCommonTags(
132
- this.command,
133
- this.frameworkVersion,
134
- testSuitePath,
135
- 'cucumber'
136
- )
131
+ const testSuiteMetadata = {
132
+ ...getTestSuiteCommonTags(
133
+ this.command,
134
+ this.frameworkVersion,
135
+ testSuitePath,
136
+ 'cucumber'
137
+ ),
138
+ ...this.getSessionRequestErrorTags(),
139
+ }
137
140
  if (isUnskippable) {
138
141
  this.telemetry.count(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' })
139
142
  testSuiteMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
@@ -46,6 +46,8 @@ const {
46
46
  TEST_RETRY_REASON_TYPES,
47
47
  getPullRequestDiff,
48
48
  getModifiedFilesFromDiff,
49
+ getSessionRequestErrorTags,
50
+ DD_CI_LIBRARY_CONFIGURATION_ERROR,
49
51
  TEST_IS_MODIFIED,
50
52
  getPullRequestBaseBranch,
51
53
  } = require('../../dd-trace/src/plugins/util/test')
@@ -306,6 +308,9 @@ class CypressPlugin {
306
308
 
307
309
  this.isTestIsolationEnabled = getIsTestIsolationEnabled(cypressConfig)
308
310
 
311
+ const envFlushWait = Number(getValueFromEnvSources('DD_CIVISIBILITY_RUM_FLUSH_WAIT_MILLIS'))
312
+ this.rumFlushWaitMillis = Number.isFinite(envFlushWait) ? envFlushWait : undefined
313
+
309
314
  if (!this.isTestIsolationEnabled) {
310
315
  log.warn('Test isolation is disabled, retries will not be enabled')
311
316
  }
@@ -314,10 +319,15 @@ class CypressPlugin {
314
319
  this.testEnvironmentMetadata[DD_TEST_IS_USER_PROVIDED_SERVICE] =
315
320
  tracer._tracer._config.isServiceUserProvided ? 'true' : 'false'
316
321
 
322
+ this._pendingRequestErrorTags = []
317
323
  this.libraryConfigurationPromise = getLibraryConfiguration(this.tracer, this.testConfiguration)
318
324
  .then((libraryConfigurationResponse) => {
319
325
  if (libraryConfigurationResponse.err) {
320
326
  log.error('Cypress plugin library config response error', libraryConfigurationResponse.err)
327
+ this._pendingRequestErrorTags.push({
328
+ tag: DD_CI_LIBRARY_CONFIGURATION_ERROR,
329
+ value: 'true',
330
+ })
321
331
  } else {
322
332
  const {
323
333
  libraryConfig: {
@@ -412,6 +422,7 @@ class CypressPlugin {
412
422
  if (this.testSessionSpan && this.testModuleSpan) {
413
423
  testSuiteTags[TEST_SESSION_ID] = this.testSessionSpan.context().toTraceId()
414
424
  testSuiteTags[TEST_MODULE_ID] = this.testModuleSpan.context().toSpanId()
425
+ Object.assign(testSuiteTags, this.getSessionRequestErrorTags())
415
426
  // If testSuiteSpan couldn't be created, we'll use the testModuleSpan as the parent
416
427
  if (!this.testSuiteSpan) {
417
428
  testSuiteTags[TEST_SUITE_ID] = this.testModuleSpan.context().toSpanId()
@@ -468,6 +479,14 @@ class CypressPlugin {
468
479
  })
469
480
  }
470
481
 
482
+ /**
483
+ * Returns request error tags from the test session span for propagation to test spans.
484
+ * @returns {Record<string, string>}
485
+ */
486
+ getSessionRequestErrorTags () {
487
+ return getSessionRequestErrorTags(this.testSessionSpan)
488
+ }
489
+
471
490
  ciVisEvent (name, testLevel, tags = {}) {
472
491
  incrementCountMetric(name, {
473
492
  testLevel,
@@ -596,14 +615,20 @@ class CypressPlugin {
596
615
  },
597
616
  integrationName: TEST_FRAMEWORK_NAME,
598
617
  })
618
+ for (const { tag, value } of this._pendingRequestErrorTags) {
619
+ this.testSessionSpan.setTag(tag, value)
620
+ }
621
+ this._pendingRequestErrorTags = []
599
622
  this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session')
600
623
 
624
+ const sessionRequestErrorTags = getSessionRequestErrorTags(this.testSessionSpan)
601
625
  this.testModuleSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_module`, {
602
626
  childOf: this.testSessionSpan,
603
627
  tags: {
604
628
  [COMPONENT]: TEST_FRAMEWORK_NAME,
605
629
  ...this.testEnvironmentMetadata,
606
630
  ...testModuleSpanMetadata,
631
+ ...sessionRequestErrorTags,
607
632
  },
608
633
  integrationName: TEST_FRAMEWORK_NAME,
609
634
  })
@@ -823,6 +848,7 @@ class CypressPlugin {
823
848
  isModifiedTest: this.getIsTestModified(testSuiteAbsolutePath),
824
849
  repositoryRoot: this.repositoryRoot,
825
850
  isTestIsolationEnabled: this.isTestIsolationEnabled,
851
+ rumFlushWaitMillis: this.rumFlushWaitMillis,
826
852
  }
827
853
 
828
854
  if (this.testSuiteSpan) {
@@ -937,6 +963,13 @@ class CypressPlugin {
937
963
  this.activeTestSpan.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.efd)
938
964
  }
939
965
  }
966
+ // Check if all EFD retries failed
967
+ if ((isNew || isModified) && this.earlyFlakeDetectionNumRetries > 0) {
968
+ const isLastEfdAttempt = testStatuses.length === this.earlyFlakeDetectionNumRetries + 1
969
+ if (isLastEfdAttempt && testStatuses.every(status => status === 'fail')) {
970
+ this.activeTestSpan.setTag(TEST_HAS_FAILED_ALL_RETRIES, 'true')
971
+ }
972
+ }
940
973
  if (isAttemptToFix) {
941
974
  this.activeTestSpan.setTag(TEST_MANAGEMENT_IS_ATTEMPT_TO_FIX, 'true')
942
975
  if (testStatuses.length > 1) {
@@ -1,5 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME = 'datadog-ci-visibility-test-execution-id'
4
+ let rumFlushWaitMillis = 500
5
+
3
6
  let isEarlyFlakeDetectionEnabled = false
4
7
  let isKnownTestsEnabled = false
5
8
  let knownTestsForSuite = []
@@ -15,9 +18,10 @@ const retryReasonsByTestName = new Map()
15
18
  // Track quarantined test errors - we catch them in Cypress.on('fail') but need to report to Datadog
16
19
  const quarantinedTestErrors = new Map()
17
20
 
18
- // We need to grab the original window as soon as possible,
19
- // in case the test changes the origin. If the test does change the origin,
20
- // any call to `cy.window()` will result in a cross origin error.
21
+ // Track the most recently loaded window in the AUT. Updated via the 'window:load'
22
+ // event so we always get the real app window (after cy.visit()), not the
23
+ // about:blank window that exists when beforeEach runs. If the test later navigates
24
+ // to a cross-origin URL, safeGetRum() handles the access error.
21
25
  let originalWindow
22
26
 
23
27
  // If the test is using multi domain with cy.origin, trying to access
@@ -165,18 +169,45 @@ beforeEach(function () {
165
169
  retryReasonsByTestName.delete(testName)
166
170
  }
167
171
 
172
+ cy.on('window:load', (win) => {
173
+ originalWindow = win
174
+ })
175
+
168
176
  cy.task('dd:beforeEach', {
169
177
  testName,
170
178
  testSuite: Cypress.mocha.getRootSuite().file,
171
179
  }).then(({ traceId, shouldSkip }) => {
172
- Cypress.env('traceId', traceId)
180
+ if (traceId) {
181
+ cy.setCookie(DD_CIVISIBILITY_TEST_EXECUTION_ID_COOKIE_NAME, traceId).then(() => {
182
+ // When testIsolation:false, the page is not reset between tests, so the RUM session
183
+ // stopped in afterEach must be explicitly restarted so events in this test are
184
+ // associated with the new testExecutionId.
185
+ //
186
+ // After stopSession(), the RUM SDK creates a new session upon a user interaction
187
+ // (click, scroll, keydown, or touchstart). We dispatch a synthetic click on the window
188
+ // to trigger session renewal, then call startView() to establish a view boundary.
189
+ if (!isTestIsolationEnabled && originalWindow) {
190
+ const rum = safeGetRum(originalWindow)
191
+ if (rum) {
192
+ try {
193
+ const evt = new originalWindow.MouseEvent('click', { bubbles: true, cancelable: true })
194
+ // The browser-sdk addEventListener wrapper filters out untrusted synthetic events
195
+ // unless __ddIsTrusted is set. Set it so the click triggers expandOrRenewSession().
196
+ // See: https://github.com/DataDog/browser-sdk/blob/v6.27.1/packages/core/src/browser/addEventListener.ts#L119
197
+ Object.defineProperty(evt, '__ddIsTrusted', { value: true })
198
+ originalWindow.dispatchEvent(evt)
199
+ } catch {}
200
+ if (rum.startView) {
201
+ rum.startView()
202
+ }
203
+ }
204
+ }
205
+ })
206
+ }
173
207
  if (shouldSkip) {
174
208
  this.skip()
175
209
  }
176
210
  })
177
- cy.window().then(win => {
178
- originalWindow = win
179
- })
180
211
  })
181
212
 
182
213
  before(function () {
@@ -195,6 +226,9 @@ before(function () {
195
226
  isImpactedTestsEnabled = suiteConfig.isImpactedTestsEnabled
196
227
  isModifiedTest = suiteConfig.isModifiedTest
197
228
  isTestIsolationEnabled = suiteConfig.isTestIsolationEnabled
229
+ if (Number.isFinite(suiteConfig.rumFlushWaitMillis)) {
230
+ rumFlushWaitMillis = suiteConfig.rumFlushWaitMillis
231
+ }
198
232
  }
199
233
  })
200
234
  })
@@ -241,8 +275,14 @@ afterEach(function () {
241
275
  testInfo.testSourceLine = Cypress.mocha.getRunner().currentRunnable.invocationDetails.line
242
276
  } catch {}
243
277
 
244
- if (safeGetRum(originalWindow)) {
278
+ const rum = safeGetRum(originalWindow)
279
+ if (rum) {
245
280
  testInfo.isRUMActive = true
281
+ if (rum.stopSession) {
282
+ rum.stopSession()
283
+ // eslint-disable-next-line cypress/no-unnecessary-waiting
284
+ cy.wait(rumFlushWaitMillis)
285
+ }
246
286
  }
247
287
  let coverage
248
288
  try {
@@ -182,9 +182,10 @@ class JestPlugin extends CiPlugin {
182
182
  config._ddTestSessionId = this.testSessionSpan.context().toTraceId()
183
183
  config._ddTestModuleId = this.testModuleSpan.context().toSpanId()
184
184
  config._ddTestCommand = this.testSessionSpan.context()._tags[TEST_COMMAND]
185
+ config._ddRequestErrorTags = this.getSessionRequestErrorTags()
185
186
  config._ddItrCorrelationId = this.itrCorrelationId
186
187
  config._ddIsEarlyFlakeDetectionEnabled = !!this.libraryConfig?.isEarlyFlakeDetectionEnabled
187
- config._ddEarlyFlakeDetectionNumRetries = this.libraryConfig?.earlyFlakeDetectionNumRetries ?? 0
188
+ config._ddEarlyFlakeDetectionSlowTestRetries = this.libraryConfig?.earlyFlakeDetectionSlowTestRetries ?? {}
188
189
  config._ddRepositoryRoot = this.repositoryRoot
189
190
  config._ddIsFlakyTestRetriesEnabled = this.libraryConfig?.isFlakyTestRetriesEnabled ?? false
190
191
  config._ddIsTestManagementTestsEnabled = this.libraryConfig?.isTestManagementEnabled ?? false
@@ -208,6 +209,7 @@ class JestPlugin extends CiPlugin {
208
209
  _ddTestSessionId: testSessionId,
209
210
  _ddTestCommand: testCommand,
210
211
  _ddTestModuleId: testModuleId,
212
+ _ddRequestErrorTags: requestErrorTags,
211
213
  _ddItrCorrelationId: itrCorrelationId,
212
214
  _ddForcedToRun,
213
215
  _ddUnskippable,
@@ -219,7 +221,11 @@ class JestPlugin extends CiPlugin {
219
221
  'x-datadog-parent-id': testModuleId,
220
222
  })
221
223
 
222
- const testSuiteMetadata = getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, 'jest')
224
+ const testSuiteMetadata = {
225
+ ...getTestSuiteCommonTags(testCommand, frameworkVersion, testSuite, 'jest'),
226
+ // requestErrorTags from test env options may be undefined
227
+ ...(requestErrorTags !== undefined && requestErrorTags !== null ? requestErrorTags : {}),
228
+ }
223
229
 
224
230
  if (_ddUnskippable) {
225
231
  const unskippableSuites = this.getUnskippableSuites(_ddUnskippable)
@@ -389,6 +395,7 @@ class JestPlugin extends CiPlugin {
389
395
  attemptToFixFailed,
390
396
  isAtrRetry,
391
397
  finalStatus,
398
+ earlyFlakeAbortReason,
392
399
  }) => {
393
400
  span.setTag(TEST_STATUS, status)
394
401
  if (finalStatus) {
@@ -409,6 +416,9 @@ class JestPlugin extends CiPlugin {
409
416
  span.setTag(TEST_IS_RETRY, 'true')
410
417
  span.setTag(TEST_RETRY_REASON, TEST_RETRY_REASON_TYPES.atr)
411
418
  }
419
+ if (earlyFlakeAbortReason) {
420
+ span.setTag(TEST_EARLY_FLAKE_ABORT_REASON, earlyFlakeAbortReason)
421
+ }
412
422
 
413
423
  this.telemetry.ciVisEvent(
414
424
  TELEMETRY_EVENT_FINISHED,
@@ -3,7 +3,7 @@
3
3
  const { readFileSync } = require('fs')
4
4
  const { parse } = require('../../../vendor/dist/jest-docblock')
5
5
 
6
- const { getTestSuitePath } = require('../../dd-trace/src/plugins/util/test')
6
+ const { getTestSuitePath, getEfdRetryCount } = require('../../dd-trace/src/plugins/util/test')
7
7
  const log = require('../../dd-trace/src/log')
8
8
 
9
9
  /**
@@ -172,4 +172,5 @@ module.exports = {
172
172
  getJestTestName,
173
173
  getJestSuitesToRun,
174
174
  isMarkedAsUnskippable,
175
+ getEfdRetryCount,
175
176
  }