dd-trace 4.40.0 → 4.42.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 (74) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ext/exporters.d.ts +1 -1
  3. package/index.d.ts +54 -1
  4. package/init.js +40 -1
  5. package/initialize.mjs +8 -5
  6. package/package.json +24 -20
  7. package/packages/datadog-core/src/storage/index.js +1 -10
  8. package/packages/datadog-esbuild/index.js +5 -1
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +2 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +76 -34
  11. package/packages/datadog-instrumentations/src/helpers/hook.js +8 -3
  12. package/packages/datadog-instrumentations/src/helpers/hooks.js +3 -0
  13. package/packages/datadog-instrumentations/src/helpers/instrument.js +4 -3
  14. package/packages/datadog-instrumentations/src/helpers/register.js +56 -5
  15. package/packages/datadog-instrumentations/src/http/server.js +98 -0
  16. package/packages/datadog-instrumentations/src/mocha/main.js +12 -1
  17. package/packages/datadog-instrumentations/src/mocha/utils.js +58 -14
  18. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -0
  19. package/packages/datadog-instrumentations/src/playwright.js +1 -1
  20. package/packages/datadog-instrumentations/src/undici.js +18 -0
  21. package/packages/datadog-instrumentations/src/vitest.js +303 -0
  22. package/packages/datadog-plugin-aws-sdk/src/base.js +8 -1
  23. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +9 -3
  24. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +6 -1
  25. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +23 -5
  26. package/packages/datadog-plugin-child_process/src/index.js +1 -1
  27. package/packages/datadog-plugin-cucumber/src/index.js +24 -1
  28. package/packages/datadog-plugin-mocha/src/index.js +25 -4
  29. package/packages/datadog-plugin-openai/src/index.js +52 -30
  30. package/packages/datadog-plugin-openai/src/token-estimator.js +20 -0
  31. package/packages/datadog-plugin-undici/src/index.js +12 -0
  32. package/packages/datadog-plugin-vitest/src/index.js +156 -0
  33. package/packages/dd-trace/src/appsec/blocking.js +4 -0
  34. package/packages/dd-trace/src/appsec/channels.js +1 -0
  35. package/packages/dd-trace/src/appsec/iast/path-line.js +2 -19
  36. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -0
  37. package/packages/dd-trace/src/appsec/index.js +45 -11
  38. package/packages/dd-trace/src/appsec/rasp.js +32 -5
  39. package/packages/dd-trace/src/appsec/recommended.json +208 -3
  40. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +1 -0
  41. package/packages/dd-trace/src/appsec/remote_config/index.js +2 -0
  42. package/packages/dd-trace/src/appsec/reporter.js +64 -20
  43. package/packages/dd-trace/src/appsec/sdk/track_event.js +3 -0
  44. package/packages/dd-trace/src/appsec/stack_trace.js +90 -0
  45. package/packages/dd-trace/src/appsec/standalone.js +130 -0
  46. package/packages/dd-trace/src/appsec/telemetry.js +33 -1
  47. package/packages/dd-trace/src/appsec/waf/index.js +2 -2
  48. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +2 -2
  49. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -2
  50. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  51. package/packages/dd-trace/src/config.js +110 -40
  52. package/packages/dd-trace/src/constants.js +3 -1
  53. package/packages/dd-trace/src/datastreams/processor.js +2 -1
  54. package/packages/dd-trace/src/exporters/agent/index.js +2 -2
  55. package/packages/dd-trace/src/format.js +22 -2
  56. package/packages/dd-trace/src/opentelemetry/span.js +33 -7
  57. package/packages/dd-trace/src/opentracing/propagation/text_map.js +12 -0
  58. package/packages/dd-trace/src/opentracing/span.js +42 -1
  59. package/packages/dd-trace/src/opentracing/tracer.js +2 -2
  60. package/packages/dd-trace/src/plugins/ci_plugin.js +7 -0
  61. package/packages/dd-trace/src/plugins/index.js +3 -0
  62. package/packages/dd-trace/src/plugins/util/test.js +5 -1
  63. package/packages/dd-trace/src/priority_sampler.js +2 -5
  64. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  65. package/packages/dd-trace/src/proxy.js +3 -1
  66. package/packages/dd-trace/src/rate_limiter.js +2 -2
  67. package/packages/dd-trace/src/service-naming/schemas/v0/web.js +4 -0
  68. package/packages/dd-trace/src/service-naming/schemas/v1/web.js +4 -0
  69. package/packages/dd-trace/src/span_stats.js +4 -3
  70. package/packages/dd-trace/src/tagger.js +10 -1
  71. package/packages/dd-trace/src/telemetry/init-telemetry.js +75 -0
  72. package/packages/dd-trace/src/tracer.js +2 -2
  73. package/packages/dd-trace/src/util.js +6 -1
  74. package/packages/datadog-core/src/storage/async_hooks.js +0 -49
@@ -0,0 +1,303 @@
1
+ const { addHook, channel, AsyncResource } = require('./helpers/instrument')
2
+ const shimmer = require('../../datadog-shimmer')
3
+
4
+ // test hooks
5
+ const testStartCh = channel('ci:vitest:test:start')
6
+ const testFinishTimeCh = channel('ci:vitest:test:finish-time')
7
+ const testPassCh = channel('ci:vitest:test:pass')
8
+ const testErrorCh = channel('ci:vitest:test:error')
9
+ const testSkipCh = channel('ci:vitest:test:skip')
10
+
11
+ // test suite hooks
12
+ const testSuiteStartCh = channel('ci:vitest:test-suite:start')
13
+ const testSuiteFinishCh = channel('ci:vitest:test-suite:finish')
14
+ const testSuiteErrorCh = channel('ci:vitest:test-suite:error')
15
+
16
+ // test session hooks
17
+ const testSessionStartCh = channel('ci:vitest:session:start')
18
+ const testSessionFinishCh = channel('ci:vitest:session:finish')
19
+
20
+ const taskToAsync = new WeakMap()
21
+
22
+ const sessionAsyncResource = new AsyncResource('bound-anonymous-fn')
23
+
24
+ function isReporterPackage (vitestPackage) {
25
+ return vitestPackage.B?.name === 'BaseSequencer'
26
+ }
27
+
28
+ // from 2.0.0
29
+ function isReporterPackageNew (vitestPackage) {
30
+ return vitestPackage.e?.name === 'BaseSequencer'
31
+ }
32
+
33
+ function getSessionStatus (state) {
34
+ if (state.getCountOfFailedTests() > 0) {
35
+ return 'fail'
36
+ }
37
+ if (state.pathsSet.size === 0) {
38
+ return 'skip'
39
+ }
40
+ return 'pass'
41
+ }
42
+
43
+ // eslint-disable-next-line
44
+ // From https://github.com/vitest-dev/vitest/blob/51c04e2f44d91322b334f8ccbcdb368facc3f8ec/packages/runner/src/run.ts#L243-L250
45
+ function getVitestTestStatus (test, retryCount) {
46
+ if (test.result.state !== 'fail') {
47
+ if (!test.repeats) {
48
+ return 'pass'
49
+ } else if (test.repeats && (test.retry ?? 0) === retryCount) {
50
+ return 'pass'
51
+ }
52
+ }
53
+ return 'fail'
54
+ }
55
+
56
+ function getTypeTasks (fileTasks, type = 'test') {
57
+ const typeTasks = []
58
+
59
+ function getTasks (tasks) {
60
+ for (const task of tasks) {
61
+ if (task.type === type) {
62
+ typeTasks.push(task)
63
+ } else if (task.tasks) {
64
+ getTasks(task.tasks)
65
+ }
66
+ }
67
+ }
68
+
69
+ getTasks(fileTasks)
70
+
71
+ return typeTasks
72
+ }
73
+
74
+ function getTestName (task) {
75
+ let testName = task.name
76
+ let currentTask = task.suite
77
+
78
+ while (currentTask) {
79
+ if (currentTask.name) {
80
+ testName = `${currentTask.name} ${testName}`
81
+ }
82
+ currentTask = currentTask.suite
83
+ }
84
+
85
+ return testName
86
+ }
87
+
88
+ function getSortWrapper (sort) {
89
+ return async function () {
90
+ if (!testSessionFinishCh.hasSubscribers) {
91
+ return sort.apply(this, arguments)
92
+ }
93
+ shimmer.wrap(this.ctx, 'exit', exit => async function () {
94
+ let onFinish
95
+
96
+ const flushPromise = new Promise(resolve => {
97
+ onFinish = resolve
98
+ })
99
+ const failedSuites = this.state.getFailedFilepaths()
100
+ let error
101
+ if (failedSuites.length) {
102
+ error = new Error(`Test suites failed: ${failedSuites.length}.`)
103
+ }
104
+
105
+ sessionAsyncResource.runInAsyncScope(() => {
106
+ testSessionFinishCh.publish({
107
+ status: getSessionStatus(this.state),
108
+ onFinish,
109
+ error
110
+ })
111
+ })
112
+
113
+ await flushPromise
114
+
115
+ return exit.apply(this, arguments)
116
+ })
117
+
118
+ return sort.apply(this, arguments)
119
+ }
120
+ }
121
+
122
+ addHook({
123
+ name: 'vitest',
124
+ versions: ['>=1.6.0'],
125
+ file: 'dist/runners.js'
126
+ }, (vitestPackage) => {
127
+ const { VitestTestRunner } = vitestPackage
128
+ // test start (only tests that are not marked as skip or todo)
129
+ shimmer.wrap(VitestTestRunner.prototype, 'onBeforeTryTask', onBeforeTryTask => async function (task) {
130
+ if (!testStartCh.hasSubscribers) {
131
+ return onBeforeTryTask.apply(this, arguments)
132
+ }
133
+ const asyncResource = new AsyncResource('bound-anonymous-fn')
134
+ taskToAsync.set(task, asyncResource)
135
+
136
+ asyncResource.runInAsyncScope(() => {
137
+ testStartCh.publish({ testName: getTestName(task), testSuiteAbsolutePath: task.suite.file.filepath })
138
+ })
139
+ return onBeforeTryTask.apply(this, arguments)
140
+ })
141
+
142
+ // test finish (only passed tests)
143
+ shimmer.wrap(VitestTestRunner.prototype, 'onAfterTryTask', onAfterTryTask =>
144
+ async function (task, { retry: retryCount }) {
145
+ if (!testFinishTimeCh.hasSubscribers) {
146
+ return onAfterTryTask.apply(this, arguments)
147
+ }
148
+ const result = await onAfterTryTask.apply(this, arguments)
149
+
150
+ const status = getVitestTestStatus(task, retryCount)
151
+ const asyncResource = taskToAsync.get(task)
152
+
153
+ if (asyncResource) {
154
+ // We don't finish here because the test might fail in a later hook
155
+ asyncResource.runInAsyncScope(() => {
156
+ testFinishTimeCh.publish({ status, task })
157
+ })
158
+ }
159
+
160
+ return result
161
+ })
162
+
163
+ return vitestPackage
164
+ })
165
+
166
+ addHook({
167
+ name: 'vitest',
168
+ versions: ['>=2.0.0'],
169
+ filePattern: 'dist/vendor/index.*'
170
+ }, (vitestPackage) => {
171
+ // there are multiple index* files so we have to check the exported values
172
+ if (isReporterPackageNew(vitestPackage)) {
173
+ shimmer.wrap(vitestPackage.e.prototype, 'sort', getSortWrapper)
174
+ }
175
+
176
+ return vitestPackage
177
+ })
178
+
179
+ addHook({
180
+ name: 'vitest',
181
+ versions: ['>=1.6.0'],
182
+ filePattern: 'dist/vendor/index.*'
183
+ }, (vitestPackage) => {
184
+ // there are multiple index* files so we have to check the exported values
185
+ if (isReporterPackage(vitestPackage)) {
186
+ shimmer.wrap(vitestPackage.B.prototype, 'sort', getSortWrapper)
187
+ }
188
+
189
+ return vitestPackage
190
+ })
191
+
192
+ // Can't specify file because compiled vitest includes hashes in their files
193
+ addHook({
194
+ name: 'vitest',
195
+ versions: ['>=1.6.0'],
196
+ filePattern: 'dist/vendor/cac.*'
197
+ }, (vitestPackage, frameworkVersion) => {
198
+ shimmer.wrap(vitestPackage, 'c', oldCreateCli => function () {
199
+ if (!testSessionStartCh.hasSubscribers) {
200
+ return oldCreateCli.apply(this, arguments)
201
+ }
202
+ sessionAsyncResource.runInAsyncScope(() => {
203
+ const processArgv = process.argv.slice(2).join(' ')
204
+ testSessionStartCh.publish({ command: `vitest ${processArgv}`, frameworkVersion })
205
+ })
206
+ return oldCreateCli.apply(this, arguments)
207
+ })
208
+
209
+ return vitestPackage
210
+ })
211
+
212
+ // test suite start and finish
213
+ // only relevant for workers
214
+ addHook({
215
+ name: '@vitest/runner',
216
+ versions: ['>=1.6.0'],
217
+ file: 'dist/index.js'
218
+ }, vitestPackage => {
219
+ shimmer.wrap(vitestPackage, 'startTests', startTests => async function (testPath) {
220
+ let testSuiteError = null
221
+ if (!testSuiteStartCh.hasSubscribers) {
222
+ return startTests.apply(this, arguments)
223
+ }
224
+
225
+ const testSuiteAsyncResource = new AsyncResource('bound-anonymous-fn')
226
+ testSuiteAsyncResource.runInAsyncScope(() => {
227
+ testSuiteStartCh.publish(testPath[0])
228
+ })
229
+ const startTestsResponse = await startTests.apply(this, arguments)
230
+
231
+ let onFinish = null
232
+ const onFinishPromise = new Promise(resolve => {
233
+ onFinish = resolve
234
+ })
235
+
236
+ const testTasks = getTypeTasks(startTestsResponse[0].tasks)
237
+
238
+ testTasks.forEach(task => {
239
+ const testAsyncResource = taskToAsync.get(task)
240
+ const { result } = task
241
+
242
+ if (result) {
243
+ const { state, duration, errors } = result
244
+ if (state === 'skip') { // programmatic skip
245
+ testSkipCh.publish({ testName: getTestName(task), testSuiteAbsolutePath: task.suite.file.filepath })
246
+ } else if (state === 'pass') {
247
+ if (testAsyncResource) {
248
+ testAsyncResource.runInAsyncScope(() => {
249
+ testPassCh.publish({ task })
250
+ })
251
+ }
252
+ } else if (state === 'fail') {
253
+ // If it's failing, we have no accurate finish time, so we have to use `duration`
254
+ let testError
255
+
256
+ if (errors?.length) {
257
+ testError = errors[0]
258
+ }
259
+
260
+ if (testAsyncResource) {
261
+ testAsyncResource.runInAsyncScope(() => {
262
+ testErrorCh.publish({ duration, error: testError })
263
+ })
264
+ }
265
+ if (errors?.length) {
266
+ testSuiteError = testError // we store the error to bubble it up to the suite
267
+ }
268
+ }
269
+ } else { // test.skip or test.todo
270
+ testSkipCh.publish({ testName: getTestName(task), testSuiteAbsolutePath: task.suite.file.filepath })
271
+ }
272
+ })
273
+
274
+ const testSuiteResult = startTestsResponse[0].result
275
+
276
+ if (testSuiteResult.errors?.length) { // Errors from root level hooks
277
+ testSuiteError = testSuiteResult.errors[0]
278
+ } else if (testSuiteResult.state === 'fail') { // Errors from `describe` level hooks
279
+ const suiteTasks = getTypeTasks(startTestsResponse[0].tasks, 'suite')
280
+ const failedSuites = suiteTasks.filter(task => task.result?.state === 'fail')
281
+ if (failedSuites.length && failedSuites[0].result?.errors?.length) {
282
+ testSuiteError = failedSuites[0].result.errors[0]
283
+ }
284
+ }
285
+
286
+ if (testSuiteError) {
287
+ testSuiteAsyncResource.runInAsyncScope(() => {
288
+ testSuiteErrorCh.publish({ error: testSuiteError })
289
+ })
290
+ }
291
+
292
+ testSuiteAsyncResource.runInAsyncScope(() => {
293
+ testSuiteFinishCh.publish({ status: testSuiteResult.state, onFinish })
294
+ })
295
+
296
+ // TODO: fix too frequent flushes
297
+ await onFinishPromise
298
+
299
+ return startTestsResponse
300
+ })
301
+
302
+ return vitestPackage
303
+ })
@@ -64,11 +64,17 @@ class BaseAwsSdkPlugin extends ClientPlugin {
64
64
  span.setTag('region', region)
65
65
  })
66
66
 
67
- this.addSub(`apm:aws:request:complete:${this.serviceIdentifier}`, ({ response }) => {
67
+ this.addSub(`apm:aws:request:complete:${this.serviceIdentifier}`, ({ response, cbExists = false }) => {
68
68
  const store = storage.getStore()
69
69
  if (!store) return
70
70
  const { span } = store
71
71
  if (!span) return
72
+ // try to extract DSM context from response if no callback exists as extraction normally happens in CB
73
+ if (!cbExists && this.serviceIdentifier === 'sqs') {
74
+ const params = response.request.params
75
+ const operation = response.request.operation
76
+ this.responseExtractDSMContext(operation, params, response.data, span)
77
+ }
72
78
  this.addResponseTags(span, response)
73
79
  this.finish(span, response, response.error)
74
80
  })
@@ -159,6 +165,7 @@ function normalizeConfig (config, serviceIdentifier) {
159
165
 
160
166
  return Object.assign({}, config, specificConfig, {
161
167
  splitByAwsService: config.splitByAwsService !== false,
168
+ batchPropagationEnabled: config.batchPropagationEnabled !== false,
162
169
  hooks
163
170
  })
164
171
  }
@@ -52,7 +52,7 @@ class Kinesis extends BaseAwsSdkPlugin {
52
52
 
53
53
  // extract DSM context after as we might not have a parent-child but may have a DSM context
54
54
  this.responseExtractDSMContext(
55
- request.operation, response, span || null, streamName
55
+ request.operation, request.params, response, span || null, { streamName }
56
56
  )
57
57
  }
58
58
  })
@@ -100,7 +100,8 @@ class Kinesis extends BaseAwsSdkPlugin {
100
100
  }
101
101
  }
102
102
 
103
- responseExtractDSMContext (operation, response, span, streamName) {
103
+ responseExtractDSMContext (operation, params, response, span, kwargs = {}) {
104
+ const { streamName } = kwargs
104
105
  if (!this.config.dsmEnabled) return
105
106
  if (operation !== 'getRecords') return
106
107
  if (!response || !response.Records || !response.Records[0]) return
@@ -151,7 +152,12 @@ class Kinesis extends BaseAwsSdkPlugin {
151
152
  case 'putRecords':
152
153
  stream = params.StreamArn ? params.StreamArn : (params.StreamName ? params.StreamName : '')
153
154
  for (let i = 0; i < params.Records.length; i++) {
154
- this.injectToMessage(span, params.Records[i], stream, i === 0)
155
+ this.injectToMessage(
156
+ span,
157
+ params.Records[i],
158
+ stream,
159
+ i === 0 || (this.config.kinesis && this.config.kinesis.batchPropagationEnabled)
160
+ )
155
161
  }
156
162
  }
157
163
  }
@@ -59,7 +59,12 @@ class Sns extends BaseAwsSdkPlugin {
59
59
  break
60
60
  case 'publishBatch':
61
61
  for (let i = 0; i < params.PublishBatchRequestEntries.length; i++) {
62
- this.injectToMessage(span, params.PublishBatchRequestEntries[i], params.TopicArn, i === 0)
62
+ this.injectToMessage(
63
+ span,
64
+ params.PublishBatchRequestEntries[i],
65
+ params.TopicArn,
66
+ i === 0 || (this.config.sns && this.config.sns.batchPropagationEnabled)
67
+ )
63
68
  }
64
69
  break
65
70
  }
@@ -23,7 +23,7 @@ class Sqs extends BaseAwsSdkPlugin {
23
23
  const plugin = this
24
24
  const contextExtraction = this.responseExtract(request.params, request.operation, response)
25
25
  let span
26
- let parsedMessageAttributes
26
+ let parsedMessageAttributes = null
27
27
  if (contextExtraction && contextExtraction.datadogContext) {
28
28
  obj.needsFinish = true
29
29
  const options = {
@@ -39,8 +39,9 @@ class Sqs extends BaseAwsSdkPlugin {
39
39
  this.enter(span, store)
40
40
  }
41
41
  // extract DSM context after as we might not have a parent-child but may have a DSM context
42
+
42
43
  this.responseExtractDSMContext(
43
- request.operation, request.params, response, span || null, parsedMessageAttributes || null
44
+ request.operation, request.params, response, span || null, { parsedMessageAttributes }
44
45
  )
45
46
  })
46
47
 
@@ -165,7 +166,8 @@ class Sqs extends BaseAwsSdkPlugin {
165
166
  }
166
167
  }
167
168
 
168
- responseExtractDSMContext (operation, params, response, span, parsedAttributes) {
169
+ responseExtractDSMContext (operation, params, response, span, kwargs = {}) {
170
+ let { parsedAttributes } = kwargs
169
171
  if (!this.config.dsmEnabled) return
170
172
  if (operation !== 'receiveMessage') return
171
173
  if (!response || !response.Messages || !response.Messages[0]) return
@@ -188,7 +190,7 @@ class Sqs extends BaseAwsSdkPlugin {
188
190
  // SQS to SQS
189
191
  }
190
192
  }
191
- if (message.MessageAttributes && message.MessageAttributes._datadog) {
193
+ if (!parsedAttributes && message.MessageAttributes && message.MessageAttributes._datadog) {
192
194
  parsedAttributes = this.parseDatadogAttributes(message.MessageAttributes._datadog)
193
195
  }
194
196
  }
@@ -216,7 +218,23 @@ class Sqs extends BaseAwsSdkPlugin {
216
218
  break
217
219
  case 'sendMessageBatch':
218
220
  for (let i = 0; i < params.Entries.length; i++) {
219
- this.injectToMessage(span, params.Entries[i], params.QueueUrl, i === 0)
221
+ this.injectToMessage(
222
+ span,
223
+ params.Entries[i],
224
+ params.QueueUrl,
225
+ i === 0 || (this.config.sqs && this.config.sqs.batchPropagationEnabled)
226
+ )
227
+ }
228
+ break
229
+ case 'receiveMessage':
230
+ if (!params.MessageAttributeNames) {
231
+ params.MessageAttributeNames = ['_datadog']
232
+ } else if (
233
+ !params.MessageAttributeNames.includes('_datadog') &&
234
+ !params.MessageAttributeNames.includes('.*') &&
235
+ !params.MessageAttributeNames.includes('All')
236
+ ) {
237
+ params.MessageAttributeNames.push('_datadog')
220
238
  }
221
239
  break
222
240
  }
@@ -54,7 +54,7 @@ class ChildProcessPlugin extends TracingPlugin {
54
54
  }
55
55
 
56
56
  this.startSpan('command_execution', {
57
- service: this.config.service,
57
+ service: this.config.service || this._tracerConfig.service,
58
58
  resource: (shell === true) ? 'sh' : cmdFields[0],
59
59
  type: 'system',
60
60
  meta
@@ -195,6 +195,17 @@ class CucumberPlugin extends CiPlugin {
195
195
  this.enter(testSpan, store)
196
196
  })
197
197
 
198
+ this.addSub('ci:cucumber:test:retry', (isFlakyRetry) => {
199
+ const store = storage.getStore()
200
+ const span = store.span
201
+ if (isFlakyRetry) {
202
+ span.setTag(TEST_IS_RETRY, 'true')
203
+ }
204
+ span.setTag(TEST_STATUS, 'fail')
205
+ span.finish()
206
+ finishAllTraceSpans(span)
207
+ })
208
+
198
209
  this.addSub('ci:cucumber:test-step:start', ({ resource }) => {
199
210
  const store = storage.getStore()
200
211
  const childOf = store ? store.span : store
@@ -239,7 +250,15 @@ class CucumberPlugin extends CiPlugin {
239
250
  })
240
251
  })
241
252
 
242
- this.addSub('ci:cucumber:test:finish', ({ isStep, status, skipReason, errorMessage, isNew, isEfdRetry }) => {
253
+ this.addSub('ci:cucumber:test:finish', ({
254
+ isStep,
255
+ status,
256
+ skipReason,
257
+ errorMessage,
258
+ isNew,
259
+ isEfdRetry,
260
+ isFlakyRetry
261
+ }) => {
243
262
  const span = storage.getStore().span
244
263
  const statusTag = isStep ? 'step.status' : TEST_STATUS
245
264
 
@@ -260,6 +279,10 @@ class CucumberPlugin extends CiPlugin {
260
279
  span.setTag(ERROR_MESSAGE, errorMessage)
261
280
  }
262
281
 
282
+ if (isFlakyRetry > 0) {
283
+ span.setTag(TEST_IS_RETRY, 'true')
284
+ }
285
+
263
286
  span.finish()
264
287
  if (!isStep) {
265
288
  this.telemetry.ciVisEvent(
@@ -175,13 +175,15 @@ class MochaPlugin extends CiPlugin {
175
175
  this.tracer._exporter.flush()
176
176
  })
177
177
 
178
- this.addSub('ci:mocha:test:finish', (status) => {
178
+ this.addSub('ci:mocha:test:finish', ({ status, hasBeenRetried }) => {
179
179
  const store = storage.getStore()
180
180
  const span = store?.span
181
181
 
182
182
  if (span) {
183
183
  span.setTag(TEST_STATUS, status)
184
-
184
+ if (hasBeenRetried) {
185
+ span.setTag(TEST_IS_RETRY, 'true')
186
+ }
185
187
  span.finish()
186
188
  this.telemetry.ciVisEvent(
187
189
  TELEMETRY_EVENT_FINISHED,
@@ -204,8 +206,8 @@ class MochaPlugin extends CiPlugin {
204
206
 
205
207
  this.addSub('ci:mocha:test:error', (err) => {
206
208
  const store = storage.getStore()
207
- if (err && store && store.span) {
208
- const span = store.span
209
+ const span = store?.span
210
+ if (err && span) {
209
211
  if (err.constructor.name === 'Pending' && !this.forbidPending) {
210
212
  span.setTag(TEST_STATUS, 'skip')
211
213
  } else {
@@ -215,6 +217,25 @@ class MochaPlugin extends CiPlugin {
215
217
  }
216
218
  })
217
219
 
220
+ this.addSub('ci:mocha:test:retry', (isFirstAttempt) => {
221
+ const store = storage.getStore()
222
+ const span = store?.span
223
+ if (span) {
224
+ span.setTag(TEST_STATUS, 'fail')
225
+ if (!isFirstAttempt) {
226
+ span.setTag(TEST_IS_RETRY, 'true')
227
+ }
228
+
229
+ span.finish()
230
+ this.telemetry.ciVisEvent(
231
+ TELEMETRY_EVENT_FINISHED,
232
+ 'test',
233
+ { hasCodeOwners: !!span.context()._tags[TEST_CODE_OWNERS] }
234
+ )
235
+ finishAllTraceSpans(span)
236
+ }
237
+ })
238
+
218
239
  this.addSub('ci:mocha:test:parameterize', ({ title, params }) => {
219
240
  this._testTitleToParams[title] = params
220
241
  })