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