dd-trace 4.28.0 → 4.30.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 (58) hide show
  1. package/CONTRIBUTING.md +98 -0
  2. package/README.md +8 -99
  3. package/ci/cypress/after-run.js +1 -0
  4. package/ci/cypress/after-spec.js +1 -0
  5. package/index.d.ts +1499 -1486
  6. package/package.json +3 -3
  7. package/packages/datadog-core/src/utils/src/get.js +11 -0
  8. package/packages/datadog-core/src/utils/src/has.js +14 -0
  9. package/packages/datadog-core/src/utils/src/set.js +16 -0
  10. package/packages/datadog-instrumentations/src/amqplib.js +1 -1
  11. package/packages/datadog-instrumentations/src/cucumber.js +157 -42
  12. package/packages/datadog-instrumentations/src/grpc/server.js +3 -1
  13. package/packages/datadog-instrumentations/src/jest.js +80 -40
  14. package/packages/datadog-instrumentations/src/mocha.js +4 -1
  15. package/packages/datadog-instrumentations/src/mongodb-core.js +34 -3
  16. package/packages/datadog-instrumentations/src/playwright.js +78 -16
  17. package/packages/datadog-plugin-amqplib/src/consumer.js +8 -4
  18. package/packages/datadog-plugin-amqplib/src/producer.js +3 -4
  19. package/packages/datadog-plugin-aws-sdk/src/base.js +3 -2
  20. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +60 -57
  21. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +42 -22
  22. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +64 -30
  23. package/packages/datadog-plugin-cucumber/src/index.js +25 -9
  24. package/packages/datadog-plugin-cypress/src/after-run.js +3 -0
  25. package/packages/datadog-plugin-cypress/src/after-spec.js +3 -0
  26. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +625 -0
  27. package/packages/datadog-plugin-cypress/src/plugin.js +6 -549
  28. package/packages/datadog-plugin-cypress/src/support.js +50 -3
  29. package/packages/datadog-plugin-graphql/src/index.js +1 -1
  30. package/packages/datadog-plugin-graphql/src/resolve.js +10 -8
  31. package/packages/datadog-plugin-grpc/src/util.js +1 -1
  32. package/packages/datadog-plugin-jest/src/index.js +11 -2
  33. package/packages/datadog-plugin-kafkajs/src/consumer.js +4 -3
  34. package/packages/datadog-plugin-kafkajs/src/producer.js +3 -5
  35. package/packages/datadog-plugin-playwright/src/index.js +34 -3
  36. package/packages/datadog-plugin-rhea/src/consumer.js +8 -3
  37. package/packages/datadog-plugin-rhea/src/producer.js +3 -4
  38. package/packages/dd-trace/src/appsec/iast/index.js +10 -0
  39. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +18 -5
  40. package/packages/dd-trace/src/appsec/recommended.json +67 -27
  41. package/packages/dd-trace/src/appsec/remote_config/index.js +1 -1
  42. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -3
  43. package/packages/dd-trace/src/config.js +451 -459
  44. package/packages/dd-trace/src/data_streams_context.js +1 -1
  45. package/packages/dd-trace/src/datastreams/pathway.js +58 -1
  46. package/packages/dd-trace/src/datastreams/processor.js +3 -5
  47. package/packages/dd-trace/src/format.js +0 -1
  48. package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -2
  49. package/packages/dd-trace/src/opentracing/span.js +4 -4
  50. package/packages/dd-trace/src/plugins/util/test.js +2 -0
  51. package/packages/dd-trace/src/plugins/util/web.js +1 -1
  52. package/packages/dd-trace/src/profiling/exporters/agent.js +77 -32
  53. package/packages/dd-trace/src/telemetry/index.js +22 -34
  54. package/packages/dd-trace/src/tracer.js +3 -3
  55. package/register.js +4 -0
  56. /package/packages/{utils → datadog-core/src/utils}/src/kebabcase.js +0 -0
  57. /package/packages/{utils → datadog-core/src/utils}/src/pick.js +0 -0
  58. /package/packages/{utils → datadog-core/src/utils}/src/uniq.js +0 -0
@@ -0,0 +1,625 @@
1
+ const {
2
+ TEST_STATUS,
3
+ TEST_IS_RUM_ACTIVE,
4
+ TEST_CODE_OWNERS,
5
+ getTestEnvironmentMetadata,
6
+ CI_APP_ORIGIN,
7
+ getTestParentSpan,
8
+ getCodeOwnersFileEntries,
9
+ getCodeOwnersForFilename,
10
+ getTestCommonTags,
11
+ getTestSessionCommonTags,
12
+ getTestModuleCommonTags,
13
+ getTestSuiteCommonTags,
14
+ TEST_SUITE_ID,
15
+ TEST_MODULE_ID,
16
+ TEST_SESSION_ID,
17
+ TEST_COMMAND,
18
+ TEST_MODULE,
19
+ TEST_SOURCE_START,
20
+ finishAllTraceSpans,
21
+ getCoveredFilenamesFromCoverage,
22
+ getTestSuitePath,
23
+ addIntelligentTestRunnerSpanTags,
24
+ TEST_SKIPPED_BY_ITR,
25
+ TEST_ITR_UNSKIPPABLE,
26
+ TEST_ITR_FORCED_RUN,
27
+ ITR_CORRELATION_ID,
28
+ TEST_SOURCE_FILE,
29
+ TEST_IS_NEW,
30
+ TEST_EARLY_FLAKE_IS_RETRY,
31
+ TEST_EARLY_FLAKE_IS_ENABLED
32
+ } = require('../../dd-trace/src/plugins/util/test')
33
+ const { isMarkedAsUnskippable } = require('../../datadog-plugin-jest/src/util')
34
+ const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
35
+ const { appClosing: appClosingTelemetry } = require('../../dd-trace/src/telemetry')
36
+ const log = require('../../dd-trace/src/log')
37
+
38
+ const {
39
+ TELEMETRY_EVENT_CREATED,
40
+ TELEMETRY_EVENT_FINISHED,
41
+ TELEMETRY_ITR_FORCED_TO_RUN,
42
+ TELEMETRY_CODE_COVERAGE_EMPTY,
43
+ TELEMETRY_ITR_UNSKIPPABLE,
44
+ TELEMETRY_CODE_COVERAGE_NUM_FILES,
45
+ incrementCountMetric,
46
+ distributionMetric
47
+ } = require('../../dd-trace/src/ci-visibility/telemetry')
48
+
49
+ const {
50
+ GIT_REPOSITORY_URL,
51
+ GIT_COMMIT_SHA,
52
+ GIT_BRANCH,
53
+ CI_PROVIDER_NAME,
54
+ CI_WORKSPACE_PATH
55
+ } = require('../../dd-trace/src/plugins/util/tags')
56
+ const {
57
+ OS_VERSION,
58
+ OS_PLATFORM,
59
+ OS_ARCHITECTURE,
60
+ RUNTIME_NAME,
61
+ RUNTIME_VERSION
62
+ } = require('../../dd-trace/src/plugins/util/env')
63
+
64
+ const TEST_FRAMEWORK_NAME = 'cypress'
65
+
66
+ const CYPRESS_STATUS_TO_TEST_STATUS = {
67
+ passed: 'pass',
68
+ failed: 'fail',
69
+ pending: 'skip',
70
+ skipped: 'skip'
71
+ }
72
+
73
+ function getSessionStatus (summary) {
74
+ if (summary.totalFailed !== undefined && summary.totalFailed > 0) {
75
+ return 'fail'
76
+ }
77
+ if (summary.totalSkipped !== undefined && summary.totalSkipped === summary.totalTests) {
78
+ return 'skip'
79
+ }
80
+ return 'pass'
81
+ }
82
+
83
+ function getCypressVersion (details) {
84
+ if (details?.cypressVersion) {
85
+ return details.cypressVersion
86
+ }
87
+ if (details?.config?.version) {
88
+ return details.config.version
89
+ }
90
+ return ''
91
+ }
92
+
93
+ function getRootDir (details) {
94
+ if (details?.config) {
95
+ return details.config.projectRoot || details.config.repoRoot || process.cwd()
96
+ }
97
+ return process.cwd()
98
+ }
99
+
100
+ function getCypressCommand (details) {
101
+ if (!details) {
102
+ return TEST_FRAMEWORK_NAME
103
+ }
104
+ return `${TEST_FRAMEWORK_NAME} ${details.specPattern || ''}`
105
+ }
106
+
107
+ function getLibraryConfiguration (tracer, testConfiguration) {
108
+ return new Promise(resolve => {
109
+ if (!tracer._tracer._exporter?.getLibraryConfiguration) {
110
+ return resolve({ err: new Error('CI Visibility was not initialized correctly') })
111
+ }
112
+
113
+ tracer._tracer._exporter.getLibraryConfiguration(testConfiguration, (err, libraryConfig) => {
114
+ resolve({ err, libraryConfig })
115
+ })
116
+ })
117
+ }
118
+
119
+ function getSkippableTests (tracer, testConfiguration) {
120
+ return new Promise(resolve => {
121
+ if (!tracer._tracer._exporter?.getSkippableSuites) {
122
+ return resolve({ err: new Error('CI Visibility was not initialized correctly') })
123
+ }
124
+ tracer._tracer._exporter.getSkippableSuites(testConfiguration, (err, skippableTests, correlationId) => {
125
+ resolve({
126
+ err,
127
+ skippableTests,
128
+ correlationId
129
+ })
130
+ })
131
+ })
132
+ }
133
+
134
+ function getKnownTests (tracer, testConfiguration) {
135
+ return new Promise(resolve => {
136
+ if (!tracer._tracer._exporter?.getKnownTests) {
137
+ return resolve({ err: new Error('CI Visibility was not initialized correctly') })
138
+ }
139
+ tracer._tracer._exporter.getKnownTests(testConfiguration, (err, knownTests) => {
140
+ resolve({
141
+ err,
142
+ knownTests
143
+ })
144
+ })
145
+ })
146
+ }
147
+
148
+ function getSuiteStatus (suiteStats) {
149
+ if (!suiteStats) {
150
+ return 'skip'
151
+ }
152
+ if (suiteStats.failures !== undefined && suiteStats.failures > 0) {
153
+ return 'fail'
154
+ }
155
+ if (suiteStats.tests !== undefined &&
156
+ (suiteStats.tests === suiteStats.pending || suiteStats.tests === suiteStats.skipped)) {
157
+ return 'skip'
158
+ }
159
+ return 'pass'
160
+ }
161
+
162
+ class CypressPlugin {
163
+ constructor () {
164
+ this._isInit = false
165
+ this.testEnvironmentMetadata = getTestEnvironmentMetadata(TEST_FRAMEWORK_NAME)
166
+
167
+ const {
168
+ [GIT_REPOSITORY_URL]: repositoryUrl,
169
+ [GIT_COMMIT_SHA]: sha,
170
+ [OS_VERSION]: osVersion,
171
+ [OS_PLATFORM]: osPlatform,
172
+ [OS_ARCHITECTURE]: osArchitecture,
173
+ [RUNTIME_NAME]: runtimeName,
174
+ [RUNTIME_VERSION]: runtimeVersion,
175
+ [GIT_BRANCH]: branch,
176
+ [CI_PROVIDER_NAME]: ciProviderName,
177
+ [CI_WORKSPACE_PATH]: repositoryRoot
178
+ } = this.testEnvironmentMetadata
179
+
180
+ this.repositoryRoot = repositoryRoot
181
+ this.isUnsupportedCIProvider = !ciProviderName
182
+ this.codeOwnersEntries = getCodeOwnersFileEntries(repositoryRoot)
183
+
184
+ this.testConfiguration = {
185
+ repositoryUrl,
186
+ sha,
187
+ osVersion,
188
+ osPlatform,
189
+ osArchitecture,
190
+ runtimeName,
191
+ runtimeVersion,
192
+ branch,
193
+ testLevel: 'test'
194
+ }
195
+ this.finishedTestsByFile = {}
196
+
197
+ this.isTestsSkipped = false
198
+ this.isSuitesSkippingEnabled = false
199
+ this.isCodeCoverageEnabled = false
200
+ this.isEarlyFlakeDetectionEnabled = false
201
+ this.earlyFlakeDetectionNumRetries = 0
202
+ this.testsToSkip = []
203
+ this.skippedTests = []
204
+ this.hasForcedToRunSuites = false
205
+ this.hasUnskippableSuites = false
206
+ this.unskippableSuites = []
207
+ this.knownTests = []
208
+ }
209
+
210
+ init (tracer, cypressConfig) {
211
+ this._isInit = true
212
+ this.tracer = tracer
213
+ this.cypressConfig = cypressConfig
214
+ }
215
+
216
+ getTestSuiteSpan (suite) {
217
+ const testSuiteSpanMetadata =
218
+ getTestSuiteCommonTags(this.command, this.frameworkVersion, suite, TEST_FRAMEWORK_NAME)
219
+ this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'suite')
220
+ return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_suite`, {
221
+ childOf: this.testModuleSpan,
222
+ tags: {
223
+ [COMPONENT]: TEST_FRAMEWORK_NAME,
224
+ ...this.testEnvironmentMetadata,
225
+ ...testSuiteSpanMetadata
226
+ }
227
+ })
228
+ }
229
+
230
+ getTestSpan (testName, testSuite, isUnskippable, isForcedToRun) {
231
+ const testSuiteTags = {
232
+ [TEST_COMMAND]: this.command,
233
+ [TEST_COMMAND]: this.command,
234
+ [TEST_MODULE]: TEST_FRAMEWORK_NAME
235
+ }
236
+ if (this.testSuiteSpan) {
237
+ testSuiteTags[TEST_SUITE_ID] = this.testSuiteSpan.context().toSpanId()
238
+ }
239
+ if (this.testSessionSpan && this.testModuleSpan) {
240
+ testSuiteTags[TEST_SESSION_ID] = this.testSessionSpan.context().toTraceId()
241
+ testSuiteTags[TEST_MODULE_ID] = this.testModuleSpan.context().toSpanId()
242
+ // If testSuiteSpan couldn't be created, we'll use the testModuleSpan as the parent
243
+ if (!this.testSuiteSpan) {
244
+ testSuiteTags[TEST_SUITE_ID] = this.testModuleSpan.context().toSpanId()
245
+ }
246
+ }
247
+
248
+ const childOf = getTestParentSpan(this.tracer)
249
+ const {
250
+ resource,
251
+ ...testSpanMetadata
252
+ } = getTestCommonTags(testName, testSuite, this.cypressConfig.version, TEST_FRAMEWORK_NAME)
253
+
254
+ const codeOwners = getCodeOwnersForFilename(testSuite, this.codeOwnersEntries)
255
+
256
+ if (codeOwners) {
257
+ testSpanMetadata[TEST_CODE_OWNERS] = codeOwners
258
+ }
259
+
260
+ if (isUnskippable) {
261
+ this.hasUnskippableSuites = true
262
+ incrementCountMetric(TELEMETRY_ITR_UNSKIPPABLE, { testLevel: 'suite' })
263
+ testSpanMetadata[TEST_ITR_UNSKIPPABLE] = 'true'
264
+ }
265
+
266
+ if (isForcedToRun) {
267
+ this.hasForcedToRunSuites = true
268
+ incrementCountMetric(TELEMETRY_ITR_FORCED_TO_RUN, { testLevel: 'suite' })
269
+ testSpanMetadata[TEST_ITR_FORCED_RUN] = 'true'
270
+ }
271
+
272
+ this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'test', { hasCodeOwners: !!codeOwners })
273
+
274
+ return this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test`, {
275
+ childOf,
276
+ tags: {
277
+ [COMPONENT]: TEST_FRAMEWORK_NAME,
278
+ [ORIGIN_KEY]: CI_APP_ORIGIN,
279
+ ...testSpanMetadata,
280
+ ...this.testEnvironmentMetadata,
281
+ ...testSuiteTags
282
+ }
283
+ })
284
+ }
285
+
286
+ ciVisEvent (name, testLevel, tags = {}) {
287
+ incrementCountMetric(name, {
288
+ testLevel,
289
+ testFramework: 'cypress',
290
+ isUnsupportedCIProvider: this.isUnsupportedCIProvider,
291
+ ...tags
292
+ })
293
+ }
294
+
295
+ isNewTest (testName, testSuite) {
296
+ return !this.knownTestsByTestSuite?.[testSuite]?.includes(testName)
297
+ }
298
+
299
+ async beforeRun (details) {
300
+ this.command = getCypressCommand(details)
301
+ this.frameworkVersion = getCypressVersion(details)
302
+ this.rootDir = getRootDir(details)
303
+
304
+ const libraryConfigurationResponse = await getLibraryConfiguration(this.tracer, this.testConfiguration)
305
+
306
+ if (libraryConfigurationResponse.err) {
307
+ log.error(libraryConfigurationResponse.err)
308
+ } else {
309
+ const {
310
+ libraryConfig: {
311
+ isSuitesSkippingEnabled,
312
+ isCodeCoverageEnabled,
313
+ isEarlyFlakeDetectionEnabled,
314
+ earlyFlakeDetectionNumRetries
315
+ }
316
+ } = libraryConfigurationResponse
317
+ this.isSuitesSkippingEnabled = isSuitesSkippingEnabled
318
+ this.isCodeCoverageEnabled = isCodeCoverageEnabled
319
+ this.isEarlyFlakeDetectionEnabled = isEarlyFlakeDetectionEnabled
320
+ this.earlyFlakeDetectionNumRetries = earlyFlakeDetectionNumRetries
321
+ }
322
+
323
+ if (this.isEarlyFlakeDetectionEnabled) {
324
+ const knownTestsResponse = await getKnownTests(
325
+ this.tracer,
326
+ this.testConfiguration
327
+ )
328
+ if (knownTestsResponse.err) {
329
+ log.error(knownTestsResponse.err)
330
+ } else {
331
+ // We use TEST_FRAMEWORK_NAME for the name of the module
332
+ this.knownTestsByTestSuite = knownTestsResponse.knownTests[TEST_FRAMEWORK_NAME]
333
+ }
334
+ }
335
+
336
+ if (this.isSuitesSkippingEnabled) {
337
+ const skippableTestsResponse = await getSkippableTests(
338
+ this.tracer,
339
+ this.testConfiguration
340
+ )
341
+ if (skippableTestsResponse.err) {
342
+ log.error(skippableTestsResponse.err)
343
+ } else {
344
+ const { skippableTests, correlationId } = skippableTestsResponse
345
+ this.testsToSkip = skippableTests || []
346
+ this.itrCorrelationId = correlationId
347
+ }
348
+ }
349
+
350
+ // `details.specs` are test files
351
+ details.specs?.forEach(({ absolute, relative }) => {
352
+ const isUnskippableSuite = isMarkedAsUnskippable({ path: absolute })
353
+ if (isUnskippableSuite) {
354
+ this.unskippableSuites.push(relative)
355
+ }
356
+ })
357
+
358
+ const childOf = getTestParentSpan(this.tracer)
359
+
360
+ const testSessionSpanMetadata =
361
+ getTestSessionCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME)
362
+ const testModuleSpanMetadata =
363
+ getTestModuleCommonTags(this.command, this.frameworkVersion, TEST_FRAMEWORK_NAME)
364
+
365
+ if (this.isEarlyFlakeDetectionEnabled) {
366
+ testSessionSpanMetadata[TEST_EARLY_FLAKE_IS_ENABLED] = 'true'
367
+ }
368
+
369
+ this.testSessionSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_session`, {
370
+ childOf,
371
+ tags: {
372
+ [COMPONENT]: TEST_FRAMEWORK_NAME,
373
+ ...this.testEnvironmentMetadata,
374
+ ...testSessionSpanMetadata
375
+ }
376
+ })
377
+ this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'session')
378
+
379
+ this.testModuleSpan = this.tracer.startSpan(`${TEST_FRAMEWORK_NAME}.test_module`, {
380
+ childOf: this.testSessionSpan,
381
+ tags: {
382
+ [COMPONENT]: TEST_FRAMEWORK_NAME,
383
+ ...this.testEnvironmentMetadata,
384
+ ...testModuleSpanMetadata
385
+ }
386
+ })
387
+ this.ciVisEvent(TELEMETRY_EVENT_CREATED, 'module')
388
+
389
+ return details
390
+ }
391
+
392
+ afterRun (suiteStats) {
393
+ if (!this._isInit) {
394
+ log.warn('Attemping to call afterRun without initializating the plugin first')
395
+ return
396
+ }
397
+ if (this.testSessionSpan && this.testModuleSpan) {
398
+ const testStatus = getSessionStatus(suiteStats)
399
+ this.testModuleSpan.setTag(TEST_STATUS, testStatus)
400
+ this.testSessionSpan.setTag(TEST_STATUS, testStatus)
401
+
402
+ addIntelligentTestRunnerSpanTags(
403
+ this.testSessionSpan,
404
+ this.testModuleSpan,
405
+ {
406
+ isSuitesSkipped: this.isTestsSkipped,
407
+ isSuitesSkippingEnabled: this.isSuitesSkippingEnabled,
408
+ isCodeCoverageEnabled: this.isCodeCoverageEnabled,
409
+ skippingType: 'test',
410
+ skippingCount: this.skippedTests.length,
411
+ hasForcedToRunSuites: this.hasForcedToRunSuites,
412
+ hasUnskippableSuites: this.hasUnskippableSuites
413
+ }
414
+ )
415
+
416
+ this.testModuleSpan.finish()
417
+ this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'module')
418
+ this.testSessionSpan.finish()
419
+ this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'session')
420
+
421
+ finishAllTraceSpans(this.testSessionSpan)
422
+ }
423
+
424
+ return new Promise(resolve => {
425
+ const exporter = this.tracer._tracer._exporter
426
+ if (!exporter) {
427
+ return resolve(null)
428
+ }
429
+ if (exporter.flush) {
430
+ exporter.flush(() => {
431
+ appClosingTelemetry()
432
+ resolve(null)
433
+ })
434
+ } else if (exporter._writer) {
435
+ exporter._writer.flush(() => {
436
+ appClosingTelemetry()
437
+ resolve(null)
438
+ })
439
+ }
440
+ })
441
+ }
442
+
443
+ afterSpec (spec, results) {
444
+ const { tests, stats } = results || {}
445
+ const cypressTests = tests || []
446
+ const finishedTests = this.finishedTestsByFile[spec.relative] || []
447
+
448
+ if (!this.testSuiteSpan) {
449
+ // dd:testSuiteStart hasn't been triggered for whatever reason
450
+ // We will create the test suite span on the spot if that's the case
451
+ log.warn('There was an error creating the test suite event.')
452
+ this.testSuiteSpan = this.getTestSuiteSpan(spec.relative)
453
+ }
454
+
455
+ // Get tests that didn't go through `dd:afterEach`
456
+ // and create a skipped test span for each of them
457
+ cypressTests.filter(({ title }) => {
458
+ const cypressTestName = title.join(' ')
459
+ const isTestFinished = finishedTests.find(({ testName }) => cypressTestName === testName)
460
+
461
+ return !isTestFinished
462
+ }).forEach(({ title }) => {
463
+ const cypressTestName = title.join(' ')
464
+ const isSkippedByItr = this.testsToSkip.find(test =>
465
+ cypressTestName === test.name && spec.relative === test.suite
466
+ )
467
+ const skippedTestSpan = this.getTestSpan(cypressTestName, spec.relative)
468
+ if (spec.absolute && this.repositoryRoot) {
469
+ skippedTestSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot))
470
+ } else {
471
+ skippedTestSpan.setTag(TEST_SOURCE_FILE, spec.relative)
472
+ }
473
+ skippedTestSpan.setTag(TEST_STATUS, 'skip')
474
+ if (isSkippedByItr) {
475
+ skippedTestSpan.setTag(TEST_SKIPPED_BY_ITR, 'true')
476
+ }
477
+ if (this.itrCorrelationId) {
478
+ skippedTestSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
479
+ }
480
+ skippedTestSpan.finish()
481
+ })
482
+
483
+ // Make sure that reported test statuses are the same as Cypress reports.
484
+ // This is not always the case, such as when an `after` hook fails:
485
+ // Cypress will report the last run test as failed, but we don't know that yet at `dd:afterEach`
486
+ let latestError
487
+ finishedTests.forEach((finishedTest) => {
488
+ const cypressTest = cypressTests.find(test => test.title.join(' ') === finishedTest.testName)
489
+ if (!cypressTest) {
490
+ return
491
+ }
492
+ if (cypressTest.displayError) {
493
+ latestError = new Error(cypressTest.displayError)
494
+ }
495
+ const cypressTestStatus = CYPRESS_STATUS_TO_TEST_STATUS[cypressTest.state]
496
+ // update test status
497
+ if (cypressTestStatus !== finishedTest.testStatus) {
498
+ finishedTest.testSpan.setTag(TEST_STATUS, cypressTestStatus)
499
+ finishedTest.testSpan.setTag('error', latestError)
500
+ }
501
+ if (this.itrCorrelationId) {
502
+ finishedTest.testSpan.setTag(ITR_CORRELATION_ID, this.itrCorrelationId)
503
+ }
504
+ if (spec.absolute && this.repositoryRoot) {
505
+ finishedTest.testSpan.setTag(TEST_SOURCE_FILE, getTestSuitePath(spec.absolute, this.repositoryRoot))
506
+ } else {
507
+ finishedTest.testSpan.setTag(TEST_SOURCE_FILE, spec.relative)
508
+ }
509
+ finishedTest.testSpan.finish(finishedTest.finishTime)
510
+ })
511
+
512
+ if (this.testSuiteSpan) {
513
+ const status = getSuiteStatus(stats)
514
+ this.testSuiteSpan.setTag(TEST_STATUS, status)
515
+
516
+ if (latestError) {
517
+ this.testSuiteSpan.setTag('error', latestError)
518
+ }
519
+ this.testSuiteSpan.finish()
520
+ this.testSuiteSpan = null
521
+ this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'suite')
522
+ }
523
+ }
524
+
525
+ getTasks () {
526
+ return {
527
+ 'dd:testSuiteStart': (testSuite) => {
528
+ const suitePayload = {
529
+ isEarlyFlakeDetectionEnabled: this.isEarlyFlakeDetectionEnabled,
530
+ knownTestsForSuite: this.knownTestsByTestSuite?.[testSuite] || [],
531
+ earlyFlakeDetectionNumRetries: this.earlyFlakeDetectionNumRetries
532
+ }
533
+
534
+ if (this.testSuiteSpan) {
535
+ return suitePayload
536
+ }
537
+ this.testSuiteSpan = this.getTestSuiteSpan(testSuite)
538
+ return suitePayload
539
+ },
540
+ 'dd:beforeEach': (test) => {
541
+ const { testName, testSuite } = test
542
+ const shouldSkip = !!this.testsToSkip.find(test => {
543
+ return testName === test.name && testSuite === test.suite
544
+ })
545
+ const isUnskippable = this.unskippableSuites.includes(testSuite)
546
+ const isForcedToRun = shouldSkip && isUnskippable
547
+
548
+ // skip test
549
+ if (shouldSkip && !isUnskippable) {
550
+ this.skippedTests.push(test)
551
+ this.isTestsSkipped = true
552
+ return { shouldSkip: true }
553
+ }
554
+
555
+ if (!this.activeTestSpan) {
556
+ this.activeTestSpan = this.getTestSpan(testName, testSuite, isUnskippable, isForcedToRun)
557
+ }
558
+
559
+ return this.activeTestSpan ? { traceId: this.activeTestSpan.context().toTraceId() } : {}
560
+ },
561
+ 'dd:afterEach': ({ test, coverage }) => {
562
+ const { state, error, isRUMActive, testSourceLine, testSuite, testName, isNew, isEfdRetry } = test
563
+ if (this.activeTestSpan) {
564
+ if (coverage && this.isCodeCoverageEnabled && this.tracer._tracer._exporter?.exportCoverage) {
565
+ const coverageFiles = getCoveredFilenamesFromCoverage(coverage)
566
+ const relativeCoverageFiles = coverageFiles.map(file => getTestSuitePath(file, this.rootDir))
567
+ if (!relativeCoverageFiles.length) {
568
+ incrementCountMetric(TELEMETRY_CODE_COVERAGE_EMPTY)
569
+ }
570
+ distributionMetric(TELEMETRY_CODE_COVERAGE_NUM_FILES, {}, relativeCoverageFiles.length)
571
+ const { _traceId, _spanId } = this.testSuiteSpan.context()
572
+ const formattedCoverage = {
573
+ sessionId: _traceId,
574
+ suiteId: _spanId,
575
+ testId: this.activeTestSpan.context()._spanId,
576
+ files: relativeCoverageFiles
577
+ }
578
+ this.tracer._tracer._exporter.exportCoverage(formattedCoverage)
579
+ }
580
+ const testStatus = CYPRESS_STATUS_TO_TEST_STATUS[state]
581
+ this.activeTestSpan.setTag(TEST_STATUS, testStatus)
582
+
583
+ if (error) {
584
+ this.activeTestSpan.setTag('error', error)
585
+ }
586
+ if (isRUMActive) {
587
+ this.activeTestSpan.setTag(TEST_IS_RUM_ACTIVE, 'true')
588
+ }
589
+ if (testSourceLine) {
590
+ this.activeTestSpan.setTag(TEST_SOURCE_START, testSourceLine)
591
+ }
592
+ if (isNew) {
593
+ this.activeTestSpan.setTag(TEST_IS_NEW, 'true')
594
+ if (isEfdRetry) {
595
+ this.activeTestSpan.setTag(TEST_EARLY_FLAKE_IS_RETRY, 'true')
596
+ }
597
+ }
598
+ const finishedTest = {
599
+ testName,
600
+ testStatus,
601
+ finishTime: this.activeTestSpan._getTime(), // we store the finish time here
602
+ testSpan: this.activeTestSpan
603
+ }
604
+ if (this.finishedTestsByFile[testSuite]) {
605
+ this.finishedTestsByFile[testSuite].push(finishedTest)
606
+ } else {
607
+ this.finishedTestsByFile[testSuite] = [finishedTest]
608
+ }
609
+ // test spans are finished at after:spec
610
+ }
611
+ this.activeTestSpan = null
612
+ this.ciVisEvent(TELEMETRY_EVENT_FINISHED, 'test')
613
+ return null
614
+ },
615
+ 'dd:addTags': (tags) => {
616
+ if (this.activeTestSpan) {
617
+ this.activeTestSpan.addTags(tags)
618
+ }
619
+ return null
620
+ }
621
+ }
622
+ }
623
+ }
624
+
625
+ module.exports = new CypressPlugin()