dd-trace 5.31.0 → 5.33.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 (85) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +17 -14
  3. package/index.d.ts +11 -1
  4. package/package.json +6 -5
  5. package/packages/datadog-instrumentations/src/aws-sdk.js +4 -1
  6. package/packages/datadog-instrumentations/src/cucumber.js +31 -14
  7. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  8. package/packages/datadog-instrumentations/src/jest.js +105 -56
  9. package/packages/datadog-instrumentations/src/mocha/main.js +9 -4
  10. package/packages/datadog-instrumentations/src/mocha/utils.js +27 -9
  11. package/packages/datadog-instrumentations/src/mocha/worker.js +4 -2
  12. package/packages/datadog-instrumentations/src/node-serialize.js +22 -0
  13. package/packages/datadog-instrumentations/src/openai.js +2 -0
  14. package/packages/datadog-instrumentations/src/playwright.js +8 -3
  15. package/packages/datadog-instrumentations/src/vitest.js +134 -62
  16. package/packages/datadog-instrumentations/src/vm.js +49 -0
  17. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/index.js +16 -0
  18. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +63 -0
  19. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +287 -0
  20. package/packages/datadog-plugin-aws-sdk/src/services/index.js +1 -0
  21. package/packages/datadog-plugin-cucumber/src/index.js +31 -31
  22. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +19 -8
  23. package/packages/datadog-plugin-cypress/src/support.js +6 -2
  24. package/packages/datadog-plugin-fetch/src/index.js +3 -3
  25. package/packages/datadog-plugin-http/src/client.js +5 -33
  26. package/packages/datadog-plugin-jest/src/index.js +37 -37
  27. package/packages/datadog-plugin-langchain/src/index.js +12 -80
  28. package/packages/datadog-plugin-langchain/src/tracing.js +89 -0
  29. package/packages/datadog-plugin-mocha/src/index.js +19 -35
  30. package/packages/datadog-plugin-playwright/src/index.js +3 -1
  31. package/packages/datadog-plugin-vitest/src/index.js +33 -35
  32. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +1 -0
  33. package/packages/dd-trace/src/appsec/iast/analyzers/code-injection-analyzer.js +2 -0
  34. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +3 -3
  35. package/packages/dd-trace/src/appsec/iast/analyzers/untrusted-deserialization-analyzer.js +16 -0
  36. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +41 -24
  37. package/packages/dd-trace/src/appsec/iast/iast-context.js +12 -0
  38. package/packages/dd-trace/src/appsec/iast/path-line.js +19 -23
  39. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +9 -8
  40. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/index.js +1 -0
  41. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +1 -0
  42. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +75 -24
  43. package/packages/dd-trace/src/appsec/rasp/utils.js +10 -5
  44. package/packages/dd-trace/src/appsec/stack_trace.js +38 -28
  45. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +37 -0
  46. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +65 -28
  47. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +57 -17
  48. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -4
  49. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +18 -3
  50. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +5 -3
  51. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +20 -3
  52. package/packages/dd-trace/src/config.js +43 -3
  53. package/packages/dd-trace/src/crashtracking/crashtracker.js +9 -0
  54. package/packages/dd-trace/src/crashtracking/noop.js +3 -0
  55. package/packages/dd-trace/src/datastreams/fnv.js +1 -1
  56. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -2
  57. package/packages/dd-trace/src/debugger/devtools_client/config.js +1 -0
  58. package/packages/dd-trace/src/debugger/devtools_client/defaults.js +1 -0
  59. package/packages/dd-trace/src/debugger/devtools_client/index.js +30 -13
  60. package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -8
  61. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +35 -1
  62. package/packages/dd-trace/src/debugger/devtools_client/snapshot/redaction.js +112 -0
  63. package/packages/dd-trace/src/debugger/devtools_client/status.js +12 -10
  64. package/packages/dd-trace/src/debugger/index.js +2 -13
  65. package/packages/dd-trace/src/llmobs/plugins/base.js +40 -11
  66. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +59 -0
  67. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chain.js +24 -0
  68. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/chat_model.js +111 -0
  69. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/embedding.js +42 -0
  70. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/index.js +102 -0
  71. package/packages/dd-trace/src/llmobs/plugins/langchain/handlers/llm.js +32 -0
  72. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +131 -0
  73. package/packages/dd-trace/src/llmobs/plugins/openai.js +1 -1
  74. package/packages/dd-trace/src/llmobs/tagger.js +11 -3
  75. package/packages/dd-trace/src/llmobs/util.js +7 -1
  76. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +3 -3
  77. package/packages/dd-trace/src/opentelemetry/context_manager.js +43 -3
  78. package/packages/dd-trace/src/plugins/ci_plugin.js +58 -27
  79. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +0 -2
  80. package/packages/dd-trace/src/plugins/util/test.js +44 -12
  81. package/packages/dd-trace/src/priority_sampler.js +4 -1
  82. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +21 -0
  83. package/packages/dd-trace/src/profiling/profiler.js +11 -8
  84. package/packages/dd-trace/src/profiling/profilers/events.js +17 -1
  85. package/packages/dd-trace/src/proxy.js +6 -3
@@ -0,0 +1,63 @@
1
+ 'use strict'
2
+
3
+ const BaseAwsSdkPlugin = require('../../base')
4
+ const { parseModelId, extractRequestParams, extractTextAndResponseReason } = require('./utils')
5
+
6
+ const enabledOperations = ['invokeModel']
7
+
8
+ class BedrockRuntime extends BaseAwsSdkPlugin {
9
+ static get id () { return 'bedrockruntime' }
10
+
11
+ isEnabled (request) {
12
+ const operation = request.operation
13
+ if (!enabledOperations.includes(operation)) {
14
+ return false
15
+ }
16
+
17
+ return super.isEnabled(request)
18
+ }
19
+
20
+ generateTags (params, operation, response) {
21
+ const { modelProvider, modelName } = parseModelId(params.modelId)
22
+
23
+ const requestParams = extractRequestParams(params, modelProvider)
24
+ const textAndResponseReason = extractTextAndResponseReason(response, modelProvider, modelName)
25
+
26
+ const tags = buildTagsFromParams(requestParams, textAndResponseReason, modelProvider, modelName, operation)
27
+
28
+ return tags
29
+ }
30
+ }
31
+
32
+ function buildTagsFromParams (requestParams, textAndResponseReason, modelProvider, modelName, operation) {
33
+ const tags = {}
34
+
35
+ // add request tags
36
+ tags['resource.name'] = operation
37
+ tags['aws.bedrock.request.model'] = modelName
38
+ tags['aws.bedrock.request.model_provider'] = modelProvider.toLowerCase()
39
+ tags['aws.bedrock.request.prompt'] = requestParams.prompt
40
+ tags['aws.bedrock.request.temperature'] = requestParams.temperature
41
+ tags['aws.bedrock.request.top_p'] = requestParams.topP
42
+ tags['aws.bedrock.request.top_k'] = requestParams.topK
43
+ tags['aws.bedrock.request.max_tokens'] = requestParams.maxTokens
44
+ tags['aws.bedrock.request.stop_sequences'] = requestParams.stopSequences
45
+ tags['aws.bedrock.request.input_type'] = requestParams.inputType
46
+ tags['aws.bedrock.request.truncate'] = requestParams.truncate
47
+ tags['aws.bedrock.request.stream'] = requestParams.stream
48
+ tags['aws.bedrock.request.n'] = requestParams.n
49
+
50
+ // add response tags
51
+ if (modelName.includes('embed')) {
52
+ tags['aws.bedrock.response.embedding_length'] = textAndResponseReason.message.length
53
+ }
54
+ if (textAndResponseReason.choiceId) {
55
+ tags['aws.bedrock.response.choices.id'] = textAndResponseReason.choiceId
56
+ }
57
+ tags['aws.bedrock.response.choices.text'] = textAndResponseReason.message
58
+ tags['aws.bedrock.response.choices.finish_reason'] = textAndResponseReason.finishReason
59
+
60
+ return tags
61
+ }
62
+
63
+ module.exports = BedrockRuntime
@@ -0,0 +1,287 @@
1
+ 'use strict'
2
+
3
+ const log = require('../../../../dd-trace/src/log')
4
+
5
+ const MODEL_TYPE_IDENTIFIERS = [
6
+ 'foundation-model/',
7
+ 'custom-model/',
8
+ 'provisioned-model/',
9
+ 'imported-module/',
10
+ 'prompt/',
11
+ 'endpoint/',
12
+ 'inference-profile/',
13
+ 'default-prompt-router/'
14
+ ]
15
+
16
+ const PROVIDER = {
17
+ AI21: 'AI21',
18
+ AMAZON: 'AMAZON',
19
+ ANTHROPIC: 'ANTHROPIC',
20
+ COHERE: 'COHERE',
21
+ META: 'META',
22
+ STABILITY: 'STABILITY',
23
+ MISTRAL: 'MISTRAL'
24
+ }
25
+
26
+ class Generation {
27
+ constructor ({ message = '', finishReason = '', choiceId = '' } = {}) {
28
+ // stringify message as it could be a single generated message as well as a list of embeddings
29
+ this.message = typeof message === 'string' ? message : JSON.stringify(message) || ''
30
+ this.finishReason = finishReason || ''
31
+ this.choiceId = choiceId || undefined
32
+ }
33
+ }
34
+
35
+ class RequestParams {
36
+ constructor ({
37
+ prompt = '',
38
+ temperature = undefined,
39
+ topP = undefined,
40
+ topK = undefined,
41
+ maxTokens = undefined,
42
+ stopSequences = [],
43
+ inputType = '',
44
+ truncate = '',
45
+ stream = '',
46
+ n = undefined
47
+ } = {}) {
48
+ // stringify prompt as it could be a single prompt as well as a list of message objects
49
+ this.prompt = typeof prompt === 'string' ? prompt : JSON.stringify(prompt) || ''
50
+ this.temperature = temperature !== undefined ? temperature : undefined
51
+ this.topP = topP !== undefined ? topP : undefined
52
+ this.topK = topK !== undefined ? topK : undefined
53
+ this.maxTokens = maxTokens !== undefined ? maxTokens : undefined
54
+ this.stopSequences = stopSequences || []
55
+ this.inputType = inputType || ''
56
+ this.truncate = truncate || ''
57
+ this.stream = stream || ''
58
+ this.n = n !== undefined ? n : undefined
59
+ }
60
+ }
61
+
62
+ function parseModelId (modelId) {
63
+ // Best effort to extract the model provider and model name from the bedrock model ID.
64
+ // modelId can be a 1/2 period-separated string or a full AWS ARN, based on the following formats:
65
+ // 1. Base model: "{model_provider}.{model_name}"
66
+ // 2. Cross-region model: "{region}.{model_provider}.{model_name}"
67
+ // 3. Other: Prefixed by AWS ARN "arn:aws{+region?}:bedrock:{region}:{account-id}:"
68
+ // a. Foundation model: ARN prefix + "foundation-model/{region?}.{model_provider}.{model_name}"
69
+ // b. Custom model: ARN prefix + "custom-model/{model_provider}.{model_name}"
70
+ // c. Provisioned model: ARN prefix + "provisioned-model/{model-id}"
71
+ // d. Imported model: ARN prefix + "imported-module/{model-id}"
72
+ // e. Prompt management: ARN prefix + "prompt/{prompt-id}"
73
+ // f. Sagemaker: ARN prefix + "endpoint/{model-id}"
74
+ // g. Inference profile: ARN prefix + "{application-?}inference-profile/{model-id}"
75
+ // h. Default prompt router: ARN prefix + "default-prompt-router/{prompt-id}"
76
+ // If model provider cannot be inferred from the modelId formatting, then default to "custom"
77
+ modelId = modelId.toLowerCase()
78
+ if (!modelId.startsWith('arn:aws')) {
79
+ const modelMeta = modelId.split('.')
80
+ if (modelMeta.length < 2) {
81
+ return { modelProvider: 'custom', modelName: modelMeta[0] }
82
+ }
83
+ return { modelProvider: modelMeta[modelMeta.length - 2], modelName: modelMeta[modelMeta.length - 1] }
84
+ }
85
+
86
+ for (const identifier of MODEL_TYPE_IDENTIFIERS) {
87
+ if (!modelId.includes(identifier)) {
88
+ continue
89
+ }
90
+ modelId = modelId.split(identifier).pop()
91
+ if (['foundation-model/', 'custom-model/'].includes(identifier)) {
92
+ const modelMeta = modelId.split('.')
93
+ if (modelMeta.length < 2) {
94
+ return { modelProvider: 'custom', modelName: modelId }
95
+ }
96
+ return { modelProvider: modelMeta[modelMeta.length - 2], modelName: modelMeta[modelMeta.length - 1] }
97
+ }
98
+ return { modelProvider: 'custom', modelName: modelId }
99
+ }
100
+
101
+ return { modelProvider: 'custom', modelName: 'custom' }
102
+ }
103
+
104
+ function extractRequestParams (params, provider) {
105
+ const requestBody = JSON.parse(params.body)
106
+ const modelId = params.modelId
107
+
108
+ switch (provider.toUpperCase()) {
109
+ case PROVIDER.AI21: {
110
+ let userPrompt = requestBody.prompt
111
+ if (modelId.includes('jamba')) {
112
+ for (const message of requestBody.messages) {
113
+ if (message.role === 'user') {
114
+ userPrompt = message.content // Return the content of the most recent user message
115
+ }
116
+ }
117
+ }
118
+ return new RequestParams({
119
+ prompt: userPrompt,
120
+ temperature: requestBody.temperature,
121
+ topP: requestBody.top_p,
122
+ maxTokens: requestBody.max_tokens,
123
+ stopSequences: requestBody.stop_sequences
124
+ })
125
+ }
126
+ case PROVIDER.AMAZON: {
127
+ if (modelId.includes('embed')) {
128
+ return new RequestParams({ prompt: requestBody.inputText })
129
+ }
130
+ const textGenerationConfig = requestBody.textGenerationConfig || {}
131
+ return new RequestParams({
132
+ prompt: requestBody.inputText,
133
+ temperature: textGenerationConfig.temperature,
134
+ topP: textGenerationConfig.topP,
135
+ maxTokens: textGenerationConfig.maxTokenCount,
136
+ stopSequences: textGenerationConfig.stopSequences
137
+ })
138
+ }
139
+ case PROVIDER.ANTHROPIC: {
140
+ const prompt = requestBody.prompt || requestBody.messages
141
+ return new RequestParams({
142
+ prompt,
143
+ temperature: requestBody.temperature,
144
+ topP: requestBody.top_p,
145
+ maxTokens: requestBody.max_tokens_to_sample,
146
+ stopSequences: requestBody.stop_sequences
147
+ })
148
+ }
149
+ case PROVIDER.COHERE: {
150
+ if (modelId.includes('embed')) {
151
+ return new RequestParams({
152
+ prompt: requestBody.texts,
153
+ inputType: requestBody.input_type,
154
+ truncate: requestBody.truncate
155
+ })
156
+ }
157
+ return new RequestParams({
158
+ prompt: requestBody.prompt,
159
+ temperature: requestBody.temperature,
160
+ topP: requestBody.p,
161
+ maxTokens: requestBody.max_tokens,
162
+ stopSequences: requestBody.stop_sequences,
163
+ stream: requestBody.stream,
164
+ n: requestBody.num_generations
165
+ })
166
+ }
167
+ case PROVIDER.META: {
168
+ return new RequestParams({
169
+ prompt: requestBody.prompt,
170
+ temperature: requestBody.temperature,
171
+ topP: requestBody.top_p,
172
+ maxTokens: requestBody.max_gen_len
173
+ })
174
+ }
175
+ case PROVIDER.MISTRAL: {
176
+ return new RequestParams({
177
+ prompt: requestBody.prompt,
178
+ temperature: requestBody.temperature,
179
+ topP: requestBody.top_p,
180
+ maxTokens: requestBody.max_tokens,
181
+ stopSequences: requestBody.stop,
182
+ topK: requestBody.top_k
183
+ })
184
+ }
185
+ case PROVIDER.STABILITY: {
186
+ return new RequestParams()
187
+ }
188
+ default: {
189
+ return new RequestParams()
190
+ }
191
+ }
192
+ }
193
+
194
+ function extractTextAndResponseReason (response, provider, modelName) {
195
+ const body = JSON.parse(Buffer.from(response.body).toString('utf8'))
196
+ const shouldSetChoiceIds = provider.toUpperCase() === PROVIDER.COHERE && !modelName.includes('embed')
197
+ try {
198
+ switch (provider.toUpperCase()) {
199
+ case PROVIDER.AI21: {
200
+ if (modelName.includes('jamba')) {
201
+ const generations = body.choices || []
202
+ if (generations.length > 0) {
203
+ const generation = generations[0]
204
+ return new Generation({
205
+ message: generation.message,
206
+ finishReason: generation.finish_reason,
207
+ choiceId: shouldSetChoiceIds ? generation.id : undefined
208
+ })
209
+ }
210
+ }
211
+ const completions = body.completions || []
212
+ if (completions.length > 0) {
213
+ const completion = completions[0]
214
+ return new Generation({
215
+ message: completion.data?.text,
216
+ finishReason: completion?.finishReason,
217
+ choiceId: shouldSetChoiceIds ? completion?.id : undefined
218
+ })
219
+ }
220
+ return new Generation()
221
+ }
222
+ case PROVIDER.AMAZON: {
223
+ if (modelName.includes('embed')) {
224
+ return new Generation({ message: body.embedding })
225
+ }
226
+ const results = body.results || []
227
+ if (results.length > 0) {
228
+ const result = results[0]
229
+ return new Generation({ message: result.outputText, finishReason: result.completionReason })
230
+ }
231
+ break
232
+ }
233
+ case PROVIDER.ANTHROPIC: {
234
+ return new Generation({ message: body.completion || body.content, finishReason: body.stop_reason })
235
+ }
236
+ case PROVIDER.COHERE: {
237
+ if (modelName.includes('embed')) {
238
+ const embeddings = body.embeddings || [[]]
239
+ if (embeddings.length > 0) {
240
+ return new Generation({ message: embeddings[0] })
241
+ }
242
+ }
243
+ const generations = body.generations || []
244
+ if (generations.length > 0) {
245
+ const generation = generations[0]
246
+ return new Generation({
247
+ message: generation.text,
248
+ finishReason: generation.finish_reason,
249
+ choiceId: shouldSetChoiceIds ? generation.id : undefined
250
+ })
251
+ }
252
+ break
253
+ }
254
+ case PROVIDER.META: {
255
+ return new Generation({ message: body.generation, finishReason: body.stop_reason })
256
+ }
257
+ case PROVIDER.MISTRAL: {
258
+ const mistralGenerations = body.outputs || []
259
+ if (mistralGenerations.length > 0) {
260
+ const generation = mistralGenerations[0]
261
+ return new Generation({ message: generation.text, finishReason: generation.stop_reason })
262
+ }
263
+ break
264
+ }
265
+ case PROVIDER.STABILITY: {
266
+ return new Generation()
267
+ }
268
+ default: {
269
+ return new Generation()
270
+ }
271
+ }
272
+ } catch (error) {
273
+ log.warn('Unable to extract text/finishReason from response body. Defaulting to empty text/finishReason.')
274
+ return new Generation()
275
+ }
276
+
277
+ return new Generation()
278
+ }
279
+
280
+ module.exports = {
281
+ Generation,
282
+ RequestParams,
283
+ parseModelId,
284
+ extractRequestParams,
285
+ extractTextAndResponseReason,
286
+ PROVIDER
287
+ }
@@ -12,4 +12,5 @@ exports.sns = require('./sns')
12
12
  exports.sqs = require('./sqs')
13
13
  exports.states = require('./states')
14
14
  exports.stepfunctions = require('./stepfunctions')
15
+ exports.bedrockruntime = require('./bedrockruntime')
15
16
  exports.default = require('./default')
@@ -27,11 +27,7 @@ const {
27
27
  TEST_MODULE_ID,
28
28
  TEST_SUITE,
29
29
  CUCUMBER_IS_PARALLEL,
30
- TEST_NAME,
31
- DI_ERROR_DEBUG_INFO_CAPTURED,
32
- DI_DEBUG_ERROR_SNAPSHOT_ID,
33
- DI_DEBUG_ERROR_FILE,
34
- DI_DEBUG_ERROR_LINE
30
+ TEST_RETRY_REASON
35
31
  } = require('../../dd-trace/src/plugins/util/test')
36
32
  const { RESOURCE_NAME } = require('../../../ext/tags')
37
33
  const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants')
@@ -50,8 +46,8 @@ const {
50
46
  } = require('../../dd-trace/src/ci-visibility/telemetry')
51
47
  const id = require('../../dd-trace/src/id')
52
48
 
49
+ const BREAKPOINT_HIT_GRACE_PERIOD_MS = 200
53
50
  const isCucumberWorker = !!process.env.CUCUMBER_WORKER_ID
54
- const debuggerParameterPerTest = new Map()
55
51
 
56
52
  function getTestSuiteTags (testSuiteSpan) {
57
53
  const suiteTags = {
@@ -210,7 +206,13 @@ class CucumberPlugin extends CiPlugin {
210
206
  this.telemetry.ciVisEvent(TELEMETRY_CODE_COVERAGE_FINISHED, 'suite', { library: 'istanbul' })
211
207
  })
212
208
 
213
- this.addSub('ci:cucumber:test:start', ({ testName, testFileAbsolutePath, testSourceLine, isParallel }) => {
209
+ this.addSub('ci:cucumber:test:start', ({
210
+ testName,
211
+ testFileAbsolutePath,
212
+ testSourceLine,
213
+ isParallel,
214
+ promises
215
+ }) => {
214
216
  const store = storage.getStore()
215
217
  const testSuite = getTestSuitePath(testFileAbsolutePath, this.sourceRoot)
216
218
  const testSourceFile = getTestSuitePath(testFileAbsolutePath, this.repositoryRoot)
@@ -227,38 +229,30 @@ class CucumberPlugin extends CiPlugin {
227
229
 
228
230
  this.enter(testSpan, store)
229
231
 
230
- const debuggerParameters = debuggerParameterPerTest.get(testName)
231
-
232
- if (debuggerParameters) {
233
- const spanContext = testSpan.context()
234
-
235
- // TODO: handle race conditions with this.retriedTestIds
236
- this.retriedTestIds = {
237
- spanId: spanContext.toSpanId(),
238
- traceId: spanContext.toTraceId()
239
- }
240
- const { snapshotId, file, line } = debuggerParameters
241
-
242
- // TODO: should these be added on test:end if and only if the probe is hit?
243
- // Sync issues: `hitProbePromise` might be resolved after the test ends
244
- testSpan.setTag(DI_ERROR_DEBUG_INFO_CAPTURED, 'true')
245
- testSpan.setTag(DI_DEBUG_ERROR_SNAPSHOT_ID, snapshotId)
246
- testSpan.setTag(DI_DEBUG_ERROR_FILE, file)
247
- testSpan.setTag(DI_DEBUG_ERROR_LINE, line)
232
+ this.activeTestSpan = testSpan
233
+ // Time we give the breakpoint to be hit
234
+ if (promises && this.runningTestProbeId) {
235
+ promises.hitBreakpointPromise = new Promise((resolve) => {
236
+ setTimeout(resolve, BREAKPOINT_HIT_GRACE_PERIOD_MS)
237
+ })
248
238
  }
249
239
  })
250
240
 
251
- this.addSub('ci:cucumber:test:retry', ({ isRetry, error }) => {
241
+ this.addSub('ci:cucumber:test:retry', ({ isFirstAttempt, error }) => {
252
242
  const store = storage.getStore()
253
243
  const span = store.span
254
- if (isRetry) {
244
+ if (!isFirstAttempt) {
255
245
  span.setTag(TEST_IS_RETRY, 'true')
256
246
  }
257
247
  span.setTag('error', error)
258
- if (this.di && error && this.libraryConfig?.isDiEnabled) {
259
- const testName = span.context()._tags[TEST_NAME]
260
- const debuggerParameters = this.addDiProbe(error)
261
- debuggerParameterPerTest.set(testName, debuggerParameters)
248
+ if (isFirstAttempt && this.di && error && this.libraryConfig?.isDiEnabled) {
249
+ const probeInformation = this.addDiProbe(error)
250
+ if (probeInformation) {
251
+ const { probeId, stackIndex } = probeInformation
252
+ this.runningTestProbeId = probeId
253
+ this.testErrorStackIndex = stackIndex
254
+ // TODO: we're not waiting for setProbePromise to be resolved, so there might be race conditions
255
+ }
262
256
  }
263
257
  span.setTag(TEST_STATUS, 'fail')
264
258
  span.finish()
@@ -328,6 +322,7 @@ class CucumberPlugin extends CiPlugin {
328
322
  span.setTag(TEST_IS_NEW, 'true')
329
323
  if (isEfdRetry) {
330
324
  span.setTag(TEST_IS_RETRY, 'true')
325
+ span.setTag(TEST_RETRY_REASON, 'efd')
331
326
  }
332
327
  }
333
328
 
@@ -363,6 +358,11 @@ class CucumberPlugin extends CiPlugin {
363
358
  if (isCucumberWorker) {
364
359
  this.tracer._exporter.flush()
365
360
  }
361
+ this.activeTestSpan = null
362
+ if (this.runningTestProbeId) {
363
+ this.removeDiProbe(this.runningTestProbeId)
364
+ this.runningTestProbeId = null
365
+ }
366
366
  }
367
367
  })
368
368
 
@@ -31,7 +31,8 @@ const {
31
31
  TEST_EARLY_FLAKE_ENABLED,
32
32
  getTestSessionName,
33
33
  TEST_SESSION_NAME,
34
- TEST_LEVEL_EVENT_TYPES
34
+ TEST_LEVEL_EVENT_TYPES,
35
+ TEST_RETRY_REASON
35
36
  } = require('../../dd-trace/src/plugins/util/test')
36
37
  const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
37
38
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
@@ -112,7 +113,7 @@ function getCypressCommand (details) {
112
113
  function getLibraryConfiguration (tracer, testConfiguration) {
113
114
  return new Promise(resolve => {
114
115
  if (!tracer._tracer._exporter?.getLibraryConfiguration) {
115
- return resolve({ err: new Error('CI Visibility was not initialized correctly') })
116
+ return resolve({ err: new Error('Test Optimization was not initialized correctly') })
116
117
  }
117
118
 
118
119
  tracer._tracer._exporter.getLibraryConfiguration(testConfiguration, (err, libraryConfig) => {
@@ -124,7 +125,7 @@ function getLibraryConfiguration (tracer, testConfiguration) {
124
125
  function getSkippableTests (tracer, testConfiguration) {
125
126
  return new Promise(resolve => {
126
127
  if (!tracer._tracer._exporter?.getSkippableSuites) {
127
- return resolve({ err: new Error('CI Visibility was not initialized correctly') })
128
+ return resolve({ err: new Error('Test Optimization was not initialized correctly') })
128
129
  }
129
130
  tracer._tracer._exporter.getSkippableSuites(testConfiguration, (err, skippableTests, correlationId) => {
130
131
  resolve({
@@ -139,7 +140,7 @@ function getSkippableTests (tracer, testConfiguration) {
139
140
  function getKnownTests (tracer, testConfiguration) {
140
141
  return new Promise(resolve => {
141
142
  if (!tracer._tracer._exporter?.getKnownTests) {
142
- return resolve({ err: new Error('CI Visibility was not initialized correctly') })
143
+ return resolve({ err: new Error('Test Optimization was not initialized correctly') })
143
144
  }
144
145
  tracer._tracer._exporter.getKnownTests(testConfiguration, (err, knownTests) => {
145
146
  resolve({
@@ -203,6 +204,7 @@ class CypressPlugin {
203
204
  this.isSuitesSkippingEnabled = false
204
205
  this.isCodeCoverageEnabled = false
205
206
  this.isEarlyFlakeDetectionEnabled = false
207
+ this.isKnownTestsEnabled = false
206
208
  this.earlyFlakeDetectionNumRetries = 0
207
209
  this.testsToSkip = []
208
210
  this.skippedTests = []
@@ -232,13 +234,15 @@ class CypressPlugin {
232
234
  isEarlyFlakeDetectionEnabled,
233
235
  earlyFlakeDetectionNumRetries,
234
236
  isFlakyTestRetriesEnabled,
235
- flakyTestRetriesCount
237
+ flakyTestRetriesCount,
238
+ isKnownTestsEnabled
236
239
  }
237
240
  } = libraryConfigurationResponse
238
241
  this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
239
242
  this.isCodeCoverageEnabled = isCodeCoverageEnabled
240
243
  this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
241
244
  this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
245
+ this.isKnownTestsEnabled = isKnownTestsEnabled
242
246
  if (isFlakyTestRetriesEnabled) {
243
247
  this.cypressConfig.retries.runMode = flakyTestRetriesCount
244
248
  }
@@ -354,7 +358,7 @@ class CypressPlugin {
354
358
  this.frameworkVersion = getCypressVersion(details)
355
359
  this.rootDir = getRootDir(details)
356
360
 
357
- if (this.isEarlyFlakeDetectionEnabled) {
361
+ if (this.isKnownTestsEnabled) {
358
362
  const knownTestsResponse = await getKnownTests(
359
363
  this.tracer,
360
364
  this.testConfiguration
@@ -362,6 +366,7 @@ class CypressPlugin {
362
366
  if (knownTestsResponse.err) {
363
367
  log.error('Cypress known tests response error', knownTestsResponse.err)
364
368
  this.isEarlyFlakeDetectionEnabled = false
369
+ this.isKnownTestsEnabled = false
365
370
  } else {
366
371
  // We use TEST_FRAMEWORK_NAME for the name of the module
367
372
  this.knownTestsByTestSuite = knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME]
@@ -567,6 +572,9 @@ class CypressPlugin {
567
572
  cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.attempts[attemptIndex].state]
568
573
  if (attemptIndex > 0) {
569
574
  finishedTest.testSpan.setTag(TEST_IS_RETRY, 'true')
575
+ if (finishedTest.isEfdRetry) {
576
+ finishedTest.testSpan.setTag(TEST_RETRY_REASON, 'efd')
577
+ }
570
578
  }
571
579
  }
572
580
  if (cypressTest.displayError) {
@@ -618,7 +626,8 @@ class CypressPlugin {
618
626
  const suitePayload = {
619
627
  isEarlyFlakeDetectionEnabled: this.isEarlyFlakeDetectionEnabled,
620
628
  knownTestsForSuite: this.knownTestsByTestSuite?.[testSuite] || [],
621
- earlyFlakeDetectionNumRetries: this.earlyFlakeDetectionNumRetries
629
+ earlyFlakeDetectionNumRetries: this.earlyFlakeDetectionNumRetries,
630
+ isKnownTestsEnabled: this.isKnownTestsEnabled
622
631
  }
623
632
 
624
633
  if (this.testSuiteSpan) {
@@ -703,13 +712,15 @@ class CypressPlugin {
703
712
  this.activeTestSpan.setTag(TEST_IS_NEW, 'true')
704
713
  if (isEfdRetry) {
705
714
  this.activeTestSpan.setTag(TEST_IS_RETRY, 'true')
715
+ this.activeTestSpan.setTag(TEST_RETRY_REASON, 'efd')
706
716
  }
707
717
  }
708
718
  const finishedTest = {
709
719
  testName,
710
720
  testStatus,
711
721
  finishTime: this.activeTestSpan._getTime(), // we store the finish time here
712
- testSpan: this.activeTestSpan
722
+ testSpan: this.activeTestSpan,
723
+ isEfdRetry
713
724
  }
714
725
  if (this.finishedTestsByFile[testSuite]) {
715
726
  this.finishedTestsByFile[testSuite].push(finishedTest)
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable */
2
2
  let isEarlyFlakeDetectionEnabled = false
3
+ let isKnownTestsEnabled = false
3
4
  let knownTestsForSuite = []
4
5
  let suiteTests = []
5
6
  let earlyFlakeDetectionNumRetries = 0
@@ -33,7 +34,7 @@ function retryTest (test, suiteTests) {
33
34
 
34
35
  const oldRunTests = Cypress.mocha.getRunner().runTests
35
36
  Cypress.mocha.getRunner().runTests = function (suite, fn) {
36
- if (!isEarlyFlakeDetectionEnabled) {
37
+ if (!isKnownTestsEnabled) {
37
38
  return oldRunTests.apply(this, arguments)
38
39
  }
39
40
  // We copy the new tests at the beginning of the suite run (runTests), so that they're run
@@ -41,7 +42,9 @@ Cypress.mocha.getRunner().runTests = function (suite, fn) {
41
42
  suite.tests.forEach(test => {
42
43
  if (!test._ddIsNew && !test.isPending() && isNewTest(test)) {
43
44
  test._ddIsNew = true
44
- retryTest(test, suite.tests)
45
+ if (isEarlyFlakeDetectionEnabled) {
46
+ retryTest(test, suite.tests)
47
+ }
45
48
  }
46
49
  })
47
50
 
@@ -67,6 +70,7 @@ before(function () {
67
70
  }).then((suiteConfig) => {
68
71
  if (suiteConfig) {
69
72
  isEarlyFlakeDetectionEnabled = suiteConfig.isEarlyFlakeDetectionEnabled
73
+ isKnownTestsEnabled = suiteConfig.isKnownTestsEnabled
70
74
  knownTestsForSuite = suiteConfig.knownTestsForSuite
71
75
  earlyFlakeDetectionNumRetries = suiteConfig.earlyFlakeDetectionNumRetries
72
76
  }
@@ -9,7 +9,7 @@ class FetchPlugin extends HttpClientPlugin {
9
9
  bindStart (ctx) {
10
10
  const req = ctx.req
11
11
  const options = new URL(req.url)
12
- const headers = options.headers = Object.fromEntries(req.headers.entries())
12
+ options.headers = Object.fromEntries(req.headers.entries())
13
13
 
14
14
  options.method = req.method
15
15
 
@@ -17,9 +17,9 @@ class FetchPlugin extends HttpClientPlugin {
17
17
 
18
18
  const store = super.bindStart(ctx)
19
19
 
20
- for (const name in headers) {
20
+ for (const name in options.headers) {
21
21
  if (!req.headers.has(name)) {
22
- req.headers.set(name, headers[name])
22
+ req.headers.set(name, options.headers[name])
23
23
  }
24
24
  }
25
25