dd-trace 4.0.0-pre-5d09d34 → 4.0.0-pre-2e8aecc

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 (25) hide show
  1. package/index.d.ts +20 -0
  2. package/package.json +1 -1
  3. package/packages/datadog-instrumentations/src/cucumber.js +74 -15
  4. package/packages/datadog-instrumentations/src/helpers/register.js +1 -1
  5. package/packages/datadog-instrumentations/src/jest.js +16 -22
  6. package/packages/datadog-instrumentations/src/mocha.js +4 -4
  7. package/packages/datadog-instrumentations/src/playwright.js +2 -4
  8. package/packages/datadog-plugin-cucumber/src/index.js +98 -11
  9. package/packages/datadog-plugin-cypress/src/plugin.js +119 -3
  10. package/packages/datadog-plugin-cypress/src/support.js +5 -0
  11. package/packages/datadog-plugin-jest/src/index.js +9 -8
  12. package/packages/datadog-plugin-mocha/src/index.js +4 -2
  13. package/packages/datadog-plugin-playwright/src/index.js +2 -1
  14. package/packages/dd-trace/src/appsec/reporter.js +14 -14
  15. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
  16. package/packages/dd-trace/src/config.js +55 -6
  17. package/packages/dd-trace/src/encode/0.4.js +1 -1
  18. package/packages/dd-trace/src/encode/0.5.js +1 -1
  19. package/packages/dd-trace/src/encode/tags-processors.js +3 -2
  20. package/packages/dd-trace/src/opentracing/propagation/text_map.js +186 -36
  21. package/packages/dd-trace/src/opentracing/propagation/tracestate.js +99 -0
  22. package/packages/dd-trace/src/opentracing/span.js +2 -1
  23. package/packages/dd-trace/src/opentracing/span_context.js +1 -0
  24. package/packages/dd-trace/src/plugins/ci_plugin.js +2 -2
  25. package/packages/dd-trace/src/telemetry/send-data.js +4 -1
package/index.d.ts CHANGED
@@ -221,6 +221,21 @@ export declare interface SpanSamplingRule {
221
221
  name?: string
222
222
  }
223
223
 
224
+ /**
225
+ * Selection and priority order of context propagation injection and extraction mechanisms.
226
+ */
227
+ export declare interface PropagationStyle {
228
+ /**
229
+ * Selection of context propagation injection mechanisms.
230
+ */
231
+ inject: string[],
232
+
233
+ /**
234
+ * Selection and priority order of context propagation extraction mechanisms.
235
+ */
236
+ extract: string[]
237
+ }
238
+
224
239
  /**
225
240
  * List of options available to the tracer.
226
241
  */
@@ -550,6 +565,11 @@ export declare interface TracerOptions {
550
565
  * Custom header name to source the http.client_ip tag from.
551
566
  */
552
567
  clientIpHeader?: string,
568
+
569
+ /**
570
+ * The selection and priority order of context propagation injection and extraction mechanisms.
571
+ */
572
+ propagationStyle?: string[] | PropagationStyle
553
573
  }
554
574
 
555
575
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "4.0.0-pre-5d09d34",
3
+ "version": "4.0.0-pre-2e8aecc",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -3,15 +3,35 @@
3
3
  const { addHook, channel, AsyncResource } = require('./helpers/instrument')
4
4
  const shimmer = require('../../datadog-shimmer')
5
5
 
6
- const runStartCh = channel('ci:cucumber:run:start')
7
- const runFinishCh = channel('ci:cucumber:run:finish')
8
- const runStepStartCh = channel('ci:cucumber:run-step:start')
6
+ const testStartCh = channel('ci:cucumber:test:start')
7
+ const testFinishCh = channel('ci:cucumber:test:finish') // used for test steps too
8
+
9
+ const testStepStartCh = channel('ci:cucumber:test-step:start')
10
+
9
11
  const errorCh = channel('ci:cucumber:error')
12
+
13
+ const testSuiteStartCh = channel('ci:cucumber:test-suite:start')
14
+ const testSuiteFinishCh = channel('ci:cucumber:test-suite:finish')
15
+
16
+ const sessionStartCh = channel('ci:cucumber:session:start')
10
17
  const sessionFinishCh = channel('ci:cucumber:session:finish')
11
18
 
12
19
  // TODO: remove in a later major version
13
20
  const patched = new WeakSet()
14
21
 
22
+ let pickleByFile = {}
23
+ const pickleResultByFile = {}
24
+
25
+ function getSuiteStatusFromTestStatuses (testStatuses) {
26
+ if (testStatuses.some(status => status === 'fail')) {
27
+ return 'fail'
28
+ }
29
+ if (testStatuses.every(status => status === 'skip')) {
30
+ return 'skip'
31
+ }
32
+ return 'pass'
33
+ }
34
+
15
35
  function getStatusFromResult (result) {
16
36
  if (result.status === 1) {
17
37
  return { status: 'pass' }
@@ -44,13 +64,18 @@ function wrapRun (pl, isLatestVersion) {
44
64
  patched.add(pl)
45
65
 
46
66
  shimmer.wrap(pl.prototype, 'run', run => function () {
47
- if (!runStartCh.hasSubscribers) {
67
+ if (!testStartCh.hasSubscribers) {
48
68
  return run.apply(this, arguments)
49
69
  }
50
70
 
51
71
  const asyncResource = new AsyncResource('bound-anonymous-fn')
52
72
  return asyncResource.runInAsyncScope(() => {
53
- runStartCh.publish({ testName: this.pickle.name, fullTestSuite: this.pickle.uri })
73
+ const testSuiteFullPath = this.pickle.uri
74
+
75
+ if (!pickleResultByFile[testSuiteFullPath]) { // first test in suite
76
+ testSuiteStartCh.publish(testSuiteFullPath)
77
+ }
78
+ testStartCh.publish({ testName: this.pickle.name, fullTestSuite: testSuiteFullPath })
54
79
  try {
55
80
  const promise = run.apply(this, arguments)
56
81
  promise.finally(() => {
@@ -58,7 +83,17 @@ function wrapRun (pl, isLatestVersion) {
58
83
  const { status, skipReason, errorMessage } = isLatestVersion
59
84
  ? getStatusFromResultLatest(result) : getStatusFromResult(result)
60
85
 
61
- runFinishCh.publish({ status, skipReason, errorMessage })
86
+ if (!pickleResultByFile[testSuiteFullPath]) {
87
+ pickleResultByFile[testSuiteFullPath] = [status]
88
+ } else {
89
+ pickleResultByFile[testSuiteFullPath].push(status)
90
+ }
91
+ // last test in suite
92
+ if (pickleResultByFile[testSuiteFullPath].length === pickleByFile[testSuiteFullPath].length) {
93
+ const testSuiteStatus = getSuiteStatusFromTestStatuses(pickleResultByFile[testSuiteFullPath])
94
+ testSuiteFinishCh.publish(testSuiteStatus)
95
+ }
96
+ testFinishCh.publish({ status, skipReason, errorMessage })
62
97
  })
63
98
  return promise
64
99
  } catch (err) {
@@ -68,7 +103,7 @@ function wrapRun (pl, isLatestVersion) {
68
103
  })
69
104
  })
70
105
  shimmer.wrap(pl.prototype, 'runStep', runStep => function () {
71
- if (!runStepStartCh.hasSubscribers) {
106
+ if (!testStepStartCh.hasSubscribers) {
72
107
  return runStep.apply(this, arguments)
73
108
  }
74
109
  const testStep = arguments[0]
@@ -82,7 +117,7 @@ function wrapRun (pl, isLatestVersion) {
82
117
 
83
118
  const asyncResource = new AsyncResource('bound-anonymous-fn')
84
119
  return asyncResource.runInAsyncScope(() => {
85
- runStepStartCh.publish({ resource })
120
+ testStepStartCh.publish({ resource })
86
121
  try {
87
122
  const promise = runStep.apply(this, arguments)
88
123
 
@@ -90,7 +125,7 @@ function wrapRun (pl, isLatestVersion) {
90
125
  const { status, skipReason, errorMessage } = isLatestVersion
91
126
  ? getStatusFromResultLatest(result) : getStatusFromResult(result)
92
127
 
93
- runFinishCh.publish({ isStep: true, status, skipReason, errorMessage })
128
+ testFinishCh.publish({ isStep: true, status, skipReason, errorMessage })
94
129
  })
95
130
  return promise
96
131
  } catch (err) {
@@ -129,16 +164,40 @@ addHook({
129
164
  file: 'lib/runtime/test_case_runner.js'
130
165
  }, testCaseHook)
131
166
 
167
+ function getPickleByFile (runtime) {
168
+ return runtime.pickleIds.reduce((acc, pickleId) => {
169
+ const test = runtime.eventDataCollector.getPickle(pickleId)
170
+ if (acc[test.uri]) {
171
+ acc[test.uri].push(test)
172
+ } else {
173
+ acc[test.uri] = [test]
174
+ }
175
+ return acc
176
+ }, {})
177
+ }
178
+
132
179
  addHook({
133
180
  name: '@cucumber/cucumber',
134
181
  versions: ['>=7.0.0'],
135
182
  file: 'lib/runtime/index.js'
136
- }, (Runtime) => {
137
- shimmer.wrap(Runtime.default.prototype, 'start', start => async function () {
138
- const result = await start.apply(this, arguments)
139
- sessionFinishCh.publish(undefined)
140
- return result
183
+ }, (runtimePackage, cucumberVersion) => {
184
+ shimmer.wrap(runtimePackage.default.prototype, 'start', start => async function () {
185
+ pickleByFile = getPickleByFile(this)
186
+
187
+ const processArgv = process.argv.slice(2).join(' ')
188
+ const command = process.env.npm_lifecycle_script || `cucumber-js ${processArgv}`
189
+
190
+ const asyncResource = new AsyncResource('bound-anonymous-fn')
191
+ asyncResource.runInAsyncScope(() => {
192
+ sessionStartCh.publish({ command, frameworkVersion: cucumberVersion })
193
+ })
194
+ const success = await start.apply(this, arguments)
195
+
196
+ asyncResource.runInAsyncScope(() => {
197
+ sessionFinishCh.publish(success ? 'pass' : 'fail')
198
+ })
199
+ return success
141
200
  })
142
201
 
143
- return Runtime
202
+ return runtimePackage
144
203
  })
@@ -32,7 +32,7 @@ for (const packageName of names) {
32
32
  try {
33
33
  loadChannel.publish({ name, version, file })
34
34
 
35
- moduleExports = hook(moduleExports)
35
+ moduleExports = hook(moduleExports, version)
36
36
  } catch (e) {
37
37
  log.error(e)
38
38
  }
@@ -72,7 +72,7 @@ function getTestEnvironmentOptions (config) {
72
72
  return {}
73
73
  }
74
74
 
75
- function getWrappedEnvironment (BaseEnvironment) {
75
+ function getWrappedEnvironment (BaseEnvironment, jestVersion) {
76
76
  return class DatadogEnvironment extends BaseEnvironment {
77
77
  constructor (config, context) {
78
78
  super(config, context)
@@ -116,7 +116,8 @@ function getWrappedEnvironment (BaseEnvironment) {
116
116
  name: getJestTestName(event.test),
117
117
  suite: this.testSuite,
118
118
  runner: 'jest-circus',
119
- testParameters
119
+ testParameters,
120
+ frameworkVersion: jestVersion
120
121
  })
121
122
  originalTestFns.set(event.test, event.test.fn)
122
123
  event.test.fn = asyncResource.bind(event.test.fn)
@@ -142,7 +143,8 @@ function getWrappedEnvironment (BaseEnvironment) {
142
143
  testSkippedCh.publish({
143
144
  name: getJestTestName(event.test),
144
145
  suite: this.testSuite,
145
- runner: 'jest-circus'
146
+ runner: 'jest-circus',
147
+ frameworkVersion: jestVersion
146
148
  })
147
149
  })
148
150
  }
@@ -150,14 +152,14 @@ function getWrappedEnvironment (BaseEnvironment) {
150
152
  }
151
153
  }
152
154
 
153
- function getTestEnvironment (pkg) {
155
+ function getTestEnvironment (pkg, jestVersion) {
154
156
  if (pkg.default) {
155
- const wrappedTestEnvironment = getWrappedEnvironment(pkg.default)
157
+ const wrappedTestEnvironment = getWrappedEnvironment(pkg.default, jestVersion)
156
158
  pkg.default = wrappedTestEnvironment
157
159
  pkg.TestEnvironment = wrappedTestEnvironment
158
160
  return pkg
159
161
  }
160
- return getWrappedEnvironment(pkg)
162
+ return getWrappedEnvironment(pkg, jestVersion)
161
163
  }
162
164
 
163
165
  addHook({
@@ -170,7 +172,7 @@ addHook({
170
172
  versions: ['>=24.8.0']
171
173
  }, getTestEnvironment)
172
174
 
173
- function cliWrapper (cli) {
175
+ function cliWrapper (cli, jestVersion) {
174
176
  const wrapped = shimmer.wrap(cli, 'runCLI', runCLI => async function () {
175
177
  let onDone
176
178
  const configurationPromise = new Promise((resolve) => {
@@ -215,19 +217,9 @@ function cliWrapper (cli) {
215
217
 
216
218
  const isSuitesSkipped = !!skippableSuites.length
217
219
 
218
- let testFrameworkVersion
219
- try {
220
- testFrameworkVersion = this.getVersion()
221
- } catch (e) {
222
- try {
223
- testFrameworkVersion = this.default.getVersion()
224
- } catch (e) {
225
- // ignore errors
226
- }
227
- }
228
220
  const processArgv = process.argv.slice(2).join(' ')
229
221
  sessionAsyncResource.runInAsyncScope(() => {
230
- testSessionStartCh.publish({ command: `jest ${processArgv}`, testFrameworkVersion })
222
+ testSessionStartCh.publish({ command: `jest ${processArgv}`, frameworkVersion: jestVersion })
231
223
  })
232
224
 
233
225
  const result = await runCLI.apply(this, arguments)
@@ -295,7 +287,7 @@ addHook({
295
287
  versions: ['>=24.8.0']
296
288
  }, cliWrapper)
297
289
 
298
- function jestAdapterWrapper (jestAdapter) {
290
+ function jestAdapterWrapper (jestAdapter, jestVersion) {
299
291
  const adapter = jestAdapter.default ? jestAdapter.default : jestAdapter
300
292
  const newAdapter = shimmer.wrap(adapter, function () {
301
293
  const environment = arguments[2]
@@ -306,7 +298,8 @@ function jestAdapterWrapper (jestAdapter) {
306
298
  return asyncResource.runInAsyncScope(() => {
307
299
  testSuiteStartCh.publish({
308
300
  testSuite: environment.testSuite,
309
- testEnvironmentOptions: environment.testEnvironmentOptions
301
+ testEnvironmentOptions: environment.testEnvironmentOptions,
302
+ frameworkVersion: jestVersion
310
303
  })
311
304
  return adapter.apply(this, arguments).then(suiteResults => {
312
305
  const { numFailingTests, skipped, failureMessage: errorMessage } = suiteResults
@@ -447,7 +440,7 @@ addHook({
447
440
  versions: ['24.8.0 - 24.9.0']
448
441
  }, jestConfigSyncWrapper)
449
442
 
450
- function jasmineAsyncInstallWraper (jasmineAsyncInstallExport) {
443
+ function jasmineAsyncInstallWraper (jasmineAsyncInstallExport, jestVersion) {
451
444
  return function (globalConfig, globalInput) {
452
445
  globalInput._ddtrace = global._ddtrace
453
446
  shimmer.wrap(globalInput.jasmine.Spec.prototype, 'execute', execute => function (onComplete) {
@@ -457,7 +450,8 @@ function jasmineAsyncInstallWraper (jasmineAsyncInstallExport) {
457
450
  testStartCh.publish({
458
451
  name: this.getFullName(),
459
452
  suite: testSuite,
460
- runner: 'jest-jasmine2'
453
+ runner: 'jest-jasmine2',
454
+ frameworkVersion: jestVersion
461
455
  })
462
456
  const spec = this
463
457
  const callback = asyncResource.bind(function () {
@@ -38,7 +38,7 @@ const testFileToSuiteAr = new Map()
38
38
  const originalCoverageMap = createCoverageMap()
39
39
 
40
40
  let suitesToSkip = []
41
- let mochaVersion
41
+ let frameworkVersion
42
42
 
43
43
  function getSuitesByTestFile (root) {
44
44
  const suitesByTestFile = {}
@@ -128,7 +128,7 @@ function mochaHook (Runner) {
128
128
  this.once('start', testRunAsyncResource.bind(function () {
129
129
  const processArgv = process.argv.slice(2).join(' ')
130
130
  const command = `mocha ${processArgv}`
131
- testSessionStartCh.publish({ command, frameworkVersion: mochaVersion })
131
+ testSessionStartCh.publish({ command, frameworkVersion })
132
132
  }))
133
133
 
134
134
  this.on('suite', function (suite) {
@@ -313,14 +313,14 @@ addHook({
313
313
  name: 'mocha',
314
314
  versions: ['>=5.2.0'],
315
315
  file: 'lib/mocha.js'
316
- }, (Mocha) => {
316
+ }, (Mocha, mochaVersion) => {
317
+ frameworkVersion = mochaVersion
317
318
  const mochaRunAsyncResource = new AsyncResource('bound-anonymous-fn')
318
319
  /**
319
320
  * Get ITR configuration and skippable suites
320
321
  * If ITR is disabled, `onDone` is called immediately on the subscriber
321
322
  */
322
323
  shimmer.wrap(Mocha.prototype, 'run', run => function () {
323
- mochaVersion = this.version
324
324
  if (!itrConfigurationCh.hasSubscribers) {
325
325
  return run.apply(this, arguments)
326
326
  }
@@ -181,17 +181,15 @@ function dispatcherHookNew (dispatcherExport) {
181
181
  return dispatcherExport
182
182
  }
183
183
 
184
- function runnerHook (runnerExport) {
184
+ function runnerHook (runnerExport, playwrightVersion) {
185
185
  shimmer.wrap(runnerExport.Runner.prototype, 'runAllTests', runAllTests => async function () {
186
186
  const testSessionAsyncResource = new AsyncResource('bound-anonymous-fn')
187
- const { version: frameworkVersion } = getPlaywrightConfig(this)
188
-
189
187
  const rootDir = getRootDir(this)
190
188
 
191
189
  const processArgv = process.argv.slice(2).join(' ')
192
190
  const command = `playwright ${processArgv}`
193
191
  testSessionAsyncResource.runInAsyncScope(() => {
194
- testSessionStartCh.publish({ command, frameworkVersion, rootDir })
192
+ testSessionStartCh.publish({ command, frameworkVersion: playwrightVersion, rootDir })
195
193
  })
196
194
 
197
195
  const res = await runAllTests.apply(this, arguments)
@@ -7,7 +7,16 @@ const {
7
7
  TEST_SKIP_REASON,
8
8
  TEST_STATUS,
9
9
  finishAllTraceSpans,
10
- getTestSuitePath
10
+ getTestSuitePath,
11
+ getTestParentSpan,
12
+ getTestSessionCommonTags,
13
+ getTestModuleCommonTags,
14
+ getTestSuiteCommonTags,
15
+ TEST_SUITE_ID,
16
+ TEST_MODULE_ID,
17
+ TEST_SESSION_ID,
18
+ TEST_COMMAND,
19
+ TEST_BUNDLE
11
20
  } = require('../../dd-trace/src/plugins/util/test')
12
21
  const { RESOURCE_NAME } = require('../../../ext/tags')
13
22
  const { COMPONENT, ERROR_MESSAGE } = require('../../dd-trace/src/constants')
@@ -20,21 +29,73 @@ class CucumberPlugin extends CiPlugin {
20
29
  constructor (...args) {
21
30
  super(...args)
22
31
 
23
- this.addSub('ci:cucumber:session:finish', () => {
32
+ this.sourceRoot = process.cwd()
33
+
34
+ this.addSub('ci:cucumber:session:start', ({ command, frameworkVersion }) => {
35
+ const childOf = getTestParentSpan(this.tracer)
36
+ const testSessionSpanMetadata = getTestSessionCommonTags(command, frameworkVersion)
37
+
38
+ this.command = command
39
+ this.frameworkVersion = frameworkVersion
40
+ this.testSessionSpan = this.tracer.startSpan('cucumber.test_session', {
41
+ childOf,
42
+ tags: {
43
+ [COMPONENT]: this.constructor.name,
44
+ ...this.testEnvironmentMetadata,
45
+ ...testSessionSpanMetadata
46
+ }
47
+ })
48
+
49
+ const testModuleSpanMetadata = getTestModuleCommonTags(command, frameworkVersion)
50
+ this.testModuleSpan = this.tracer.startSpan('cucumber.test_module', {
51
+ childOf: this.testSessionSpan,
52
+ tags: {
53
+ [COMPONENT]: this.constructor.name,
54
+ ...this.testEnvironmentMetadata,
55
+ ...testModuleSpanMetadata
56
+ }
57
+ })
58
+ })
59
+
60
+ this.addSub('ci:cucumber:session:finish', (status) => {
61
+ this.testSessionSpan.setTag(TEST_STATUS, status)
62
+ this.testModuleSpan.setTag(TEST_STATUS, status)
63
+ this.testModuleSpan.finish()
64
+ this.testSessionSpan.finish()
65
+ finishAllTraceSpans(this.testSessionSpan)
24
66
  this.tracer._exporter.flush()
25
67
  })
26
68
 
27
- this.addSub('ci:cucumber:run:start', ({ testName, fullTestSuite }) => {
28
- const store = storage.getStore()
29
- const childOf = store ? store.span : store
30
- const testSuite = getTestSuitePath(fullTestSuite, process.cwd())
69
+ this.addSub('ci:cucumber:test-suite:start', (testSuiteFullPath) => {
70
+ const testSuiteMetadata = getTestSuiteCommonTags(
71
+ this.command,
72
+ this.frameworkVersion,
73
+ getTestSuitePath(testSuiteFullPath, this.sourceRoot)
74
+ )
75
+ this.testSuiteSpan = this.tracer.startSpan('cucumber.test_suite', {
76
+ childOf: this.testModuleSpan,
77
+ tags: {
78
+ [COMPONENT]: this.constructor.name,
79
+ ...this.testEnvironmentMetadata,
80
+ ...testSuiteMetadata
81
+ }
82
+ })
83
+ })
31
84
 
32
- const testSpan = this.startTestSpan(testName, testSuite, childOf)
85
+ this.addSub('ci:cucumber:test-suite:finish', status => {
86
+ this.testSuiteSpan.setTag(TEST_STATUS, status)
87
+ this.testSuiteSpan.finish()
88
+ })
89
+
90
+ this.addSub('ci:cucumber:test:start', ({ testName, fullTestSuite }) => {
91
+ const store = storage.getStore()
92
+ const testSuite = getTestSuitePath(fullTestSuite, this.sourceRoot)
93
+ const testSpan = this.startTestSpan(testName, testSuite)
33
94
 
34
95
  this.enter(testSpan, store)
35
96
  })
36
97
 
37
- this.addSub('ci:cucumber:run-step:start', ({ resource }) => {
98
+ this.addSub('ci:cucumber:test-step:start', ({ resource }) => {
38
99
  const store = storage.getStore()
39
100
  const childOf = store ? store.span : store
40
101
  const span = this.tracer.startSpan('cucumber.step', {
@@ -48,7 +109,7 @@ class CucumberPlugin extends CiPlugin {
48
109
  this.enter(span, store)
49
110
  })
50
111
 
51
- this.addSub('ci:cucumber:run:finish', ({ isStep, status, skipReason, errorMessage }) => {
112
+ this.addSub('ci:cucumber:test:finish', ({ isStep, status, skipReason, errorMessage }) => {
52
113
  const span = storage.getStore().span
53
114
  const statusTag = isStep ? 'step.status' : TEST_STATUS
54
115
 
@@ -76,8 +137,34 @@ class CucumberPlugin extends CiPlugin {
76
137
  })
77
138
  }
78
139
 
79
- startTestSpan (testName, testSuite, childOf) {
80
- return super.startTestSpan(testName, testSuite, {}, childOf)
140
+ startTestSpan (testName, testSuite) {
141
+ const childOf = getTestParentSpan(this.tracer)
142
+ // TODO: move this logic to CiPlugin once every framework supports test suite level visibility
143
+ // This is a hack to get good time resolution on test events, while keeping
144
+ // the test event as the root span of its trace.
145
+ childOf._trace.startTime = this.testSessionSpan.context()._trace.startTime
146
+ childOf._trace.ticks = this.testSessionSpan.context()._trace.ticks
147
+
148
+ const testSuiteTags = {}
149
+ const testSuiteId = this.testSuiteSpan.context().toSpanId()
150
+ testSuiteTags[TEST_SUITE_ID] = testSuiteId
151
+
152
+ const testSessionId = this.testSessionSpan.context().toTraceId()
153
+ testSuiteTags[TEST_SESSION_ID] = testSessionId
154
+ testSuiteTags[TEST_COMMAND] = this.command
155
+
156
+ const testModuleId = this.testModuleSpan.context().toSpanId()
157
+ testSuiteTags[TEST_MODULE_ID] = testModuleId
158
+ testSuiteTags[TEST_COMMAND] = this.command
159
+ testSuiteTags[TEST_BUNDLE] = this.command
160
+
161
+ return super.startTestSpan(
162
+ testName,
163
+ testSuite,
164
+ childOf,
165
+ this.frameworkVersion,
166
+ testSuiteTags
167
+ )
81
168
  }
82
169
  }
83
170
 
@@ -7,7 +7,15 @@ const {
7
7
  getTestParentSpan,
8
8
  getCodeOwnersFileEntries,
9
9
  getCodeOwnersForFilename,
10
- getTestCommonTags
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_BUNDLE
11
19
  } = require('../../dd-trace/src/plugins/util/test')
12
20
 
13
21
  const { ORIGIN_KEY, COMPONENT } = require('../../dd-trace/src/constants')
@@ -30,6 +38,43 @@ function getTestSpanMetadata (tracer, testName, testSuite, cypressConfig) {
30
38
  }
31
39
  }
32
40
 
41
+ function getCypressVersion (details) {
42
+ if (details && details.cypressVersion) {
43
+ return details.cypressVersion
44
+ }
45
+ if (details && details.config && details.config.version) {
46
+ return details.config.version
47
+ }
48
+ return ''
49
+ }
50
+
51
+ function getCypressCommand (details) {
52
+ if (!details) {
53
+ return 'cypress'
54
+ }
55
+ return `cypress ${details.specPattern || ''}`
56
+ }
57
+
58
+ function getSessionStatus (summary) {
59
+ if (summary.totalFailed !== undefined && summary.totalFailed > 0) {
60
+ return 'fail'
61
+ }
62
+ if (summary.totalSkipped !== undefined && summary.totalSkipped === summary.totalTests) {
63
+ return 'skip'
64
+ }
65
+ return 'pass'
66
+ }
67
+
68
+ function getSuiteStatus (suiteStats) {
69
+ if (suiteStats.failures !== undefined && suiteStats.failures > 0) {
70
+ return 'fail'
71
+ }
72
+ if (suiteStats.tests !== undefined && suiteStats.tests === suiteStats.pending) {
73
+ return 'skip'
74
+ }
75
+ return 'pass'
76
+ }
77
+
33
78
  module.exports = (on, config) => {
34
79
  const tracer = require('../../dd-trace')
35
80
  const testEnvironmentMetadata = getTestEnvironmentMetadata('cypress')
@@ -37,14 +82,84 @@ module.exports = (on, config) => {
37
82
  const codeOwnersEntries = getCodeOwnersFileEntries()
38
83
 
39
84
  let activeSpan = null
40
- on('after:run', () => {
85
+ let testSessionSpan = null
86
+ let testModuleSpan = null
87
+ let testSuiteSpan = null
88
+ let command = null
89
+ let frameworkVersion
90
+
91
+ on('before:run', (details) => {
92
+ const childOf = getTestParentSpan(tracer)
93
+
94
+ command = getCypressCommand(details)
95
+ frameworkVersion = getCypressVersion(details)
96
+
97
+ const testSessionSpanMetadata = getTestSessionCommonTags(command, frameworkVersion)
98
+ const testModuleSpanMetadata = getTestModuleCommonTags(command, frameworkVersion)
99
+
100
+ testSessionSpan = tracer.startSpan('cypress.test_session', {
101
+ childOf,
102
+ tags: {
103
+ [COMPONENT]: 'cypress',
104
+ ...testEnvironmentMetadata,
105
+ ...testSessionSpanMetadata
106
+ }
107
+ })
108
+ testModuleSpan = tracer.startSpan('cypress.test_module', {
109
+ childOf: testSessionSpan,
110
+ tags: {
111
+ [COMPONENT]: 'cypress',
112
+ ...testEnvironmentMetadata,
113
+ ...testModuleSpanMetadata
114
+ }
115
+ })
116
+ })
117
+
118
+ on('after:run', (suiteStats) => {
119
+ const testStatus = getSessionStatus(suiteStats)
120
+ testModuleSpan.setTag(TEST_STATUS, testStatus)
121
+ testSessionSpan.setTag(TEST_STATUS, testStatus)
122
+
123
+ testModuleSpan.finish()
124
+ testSessionSpan.finish()
125
+
41
126
  return new Promise(resolve => {
42
127
  tracer._tracer._exporter._writer.flush(() => resolve(null))
43
128
  })
44
129
  })
45
130
  on('task', {
131
+ 'dd:testSuiteStart': (suite) => {
132
+ const testSuiteSpanMetadata = getTestSuiteCommonTags(command, frameworkVersion, suite)
133
+ testSuiteSpan = tracer.startSpan('cypress.test_suite', {
134
+ childOf: testModuleSpan,
135
+ tags: {
136
+ [COMPONENT]: 'cypress',
137
+ ...testEnvironmentMetadata,
138
+ ...testSuiteSpanMetadata
139
+ }
140
+ })
141
+ return null
142
+ },
143
+ 'dd:testSuiteFinish': (suiteStats) => {
144
+ const status = getSuiteStatus(suiteStats)
145
+ testSuiteSpan.setTag(TEST_STATUS, status)
146
+ testSuiteSpan.finish()
147
+ return null
148
+ },
46
149
  'dd:beforeEach': (test) => {
47
150
  const { testName, testSuite } = test
151
+ const testSuiteId = testSuiteSpan.context().toSpanId()
152
+ const testSessionId = testSessionSpan.context().toTraceId()
153
+ const testModuleId = testModuleSpan.context().toSpanId()
154
+
155
+ const testSuiteTags = {
156
+ [TEST_SUITE_ID]: testSuiteId,
157
+ [TEST_SESSION_ID]: testSessionId,
158
+ [TEST_COMMAND]: command,
159
+ [TEST_MODULE_ID]: testModuleId,
160
+ [TEST_COMMAND]: command,
161
+ [TEST_BUNDLE]: command
162
+ }
48
163
 
49
164
  const {
50
165
  childOf,
@@ -65,7 +180,8 @@ module.exports = (on, config) => {
65
180
  [COMPONENT]: 'cypress',
66
181
  [ORIGIN_KEY]: CI_APP_ORIGIN,
67
182
  ...testSpanMetadata,
68
- ...testEnvironmentMetadata
183
+ ...testEnvironmentMetadata,
184
+ ...testSuiteTags
69
185
  }
70
186
  })
71
187
  }
@@ -8,7 +8,12 @@ beforeEach(() => {
8
8
  })
9
9
  })
10
10
 
11
+ before(() => {
12
+ cy.task('dd:testSuiteStart', Cypress.mocha.getRootSuite().file)
13
+ })
14
+
11
15
  after(() => {
16
+ cy.task('dd:testSuiteFinish', Cypress.mocha.getRunner().stats)
12
17
  cy.window().then(win => {
13
18
  win.dispatchEvent(new Event('beforeunload'))
14
19
  })