dd-trace 2.22.2 → 2.23.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 (42) hide show
  1. package/README.md +108 -43
  2. package/ci/init.js +6 -1
  3. package/ext/exporters.d.ts +2 -1
  4. package/ext/exporters.js +2 -1
  5. package/index.d.ts +6 -1
  6. package/package.json +2 -2
  7. package/packages/datadog-instrumentations/src/http/server.js +4 -1
  8. package/packages/datadog-instrumentations/src/jest.js +26 -9
  9. package/packages/datadog-instrumentations/src/mocha.js +12 -3
  10. package/packages/datadog-instrumentations/src/opensearch.js +1 -1
  11. package/packages/datadog-instrumentations/src/router.js +1 -1
  12. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  13. package/packages/datadog-plugin-http/src/server.js +2 -1
  14. package/packages/datadog-plugin-jest/src/index.js +21 -25
  15. package/packages/datadog-plugin-mocha/src/index.js +11 -18
  16. package/packages/datadog-plugin-mongodb-core/src/index.js +3 -3
  17. package/packages/dd-trace/src/appsec/callbacks/ddwaf.js +4 -0
  18. package/packages/dd-trace/src/appsec/index.js +6 -1
  19. package/packages/dd-trace/src/appsec/remote_config/index.js +23 -2
  20. package/packages/dd-trace/src/appsec/remote_config/manager.js +1 -1
  21. package/packages/dd-trace/src/appsec/rule_manager.js +58 -1
  22. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +62 -0
  23. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +8 -1
  24. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +11 -59
  25. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +9 -1
  26. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +196 -0
  27. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +20 -7
  28. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +22 -19
  29. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -18
  30. package/packages/dd-trace/src/config.js +10 -8
  31. package/packages/dd-trace/src/exporter.js +3 -0
  32. package/packages/dd-trace/src/exporters/agent/index.js +4 -0
  33. package/packages/dd-trace/src/exporters/common/agent-info-exporter.js +82 -0
  34. package/packages/dd-trace/src/plugin_manager.js +0 -8
  35. package/packages/dd-trace/src/plugins/ci_plugin.js +9 -50
  36. package/packages/dd-trace/src/plugins/util/ci.js +35 -2
  37. package/packages/dd-trace/src/plugins/util/test.js +4 -0
  38. package/packages/dd-trace/src/plugins/util/web.js +1 -2
  39. package/packages/dd-trace/src/profiling/exporters/agent.js +4 -0
  40. package/packages/dd-trace/src/proxy.js +4 -18
  41. package/packages/dd-trace/src/ritm.js +18 -13
  42. package/packages/dd-trace/src/telemetry/dependencies.js +11 -1
@@ -14,7 +14,10 @@ const {
14
14
  getTestSuiteCommonTags,
15
15
  TEST_SUITE_ID,
16
16
  TEST_SESSION_ID,
17
- TEST_COMMAND
17
+ TEST_COMMAND,
18
+ TEST_ITR_TESTS_SKIPPED,
19
+ TEST_SESSION_ITR_CODE_COVERAGE_ENABLED,
20
+ TEST_SESSION_ITR_SKIPPING_ENABLED
18
21
  } = require('../../dd-trace/src/plugins/util/test')
19
22
  const { COMPONENT } = require('../../dd-trace/src/constants')
20
23
 
@@ -31,9 +34,6 @@ class MochaPlugin extends CiPlugin {
31
34
  this.sourceRoot = process.cwd()
32
35
 
33
36
  this.addSub('ci:mocha:test-suite:code-coverage', ({ coverageFiles, suiteFile }) => {
34
- if (!this.config.isAgentlessEnabled || !this.config.isIntelligentTestRunnerEnabled) {
35
- return
36
- }
37
37
  if (!this.itrConfig || !this.itrConfig.isCodeCoverageEnabled) {
38
38
  return
39
39
  }
@@ -49,9 +49,6 @@ class MochaPlugin extends CiPlugin {
49
49
  })
50
50
 
51
51
  this.addSub('ci:mocha:session:start', (command) => {
52
- if (!this.config.isAgentlessEnabled) {
53
- return
54
- }
55
52
  const childOf = getTestParentSpan(this.tracer)
56
53
  const testSessionSpanMetadata = getTestSessionCommonTags(command, this.tracer._version)
57
54
 
@@ -67,9 +64,6 @@ class MochaPlugin extends CiPlugin {
67
64
  })
68
65
 
69
66
  this.addSub('ci:mocha:test-suite:start', (suite) => {
70
- if (!this.config.isAgentlessEnabled) {
71
- return
72
- }
73
67
  const store = storage.getStore()
74
68
  const testSuiteMetadata = getTestSuiteCommonTags(
75
69
  this.command,
@@ -89,9 +83,6 @@ class MochaPlugin extends CiPlugin {
89
83
  })
90
84
 
91
85
  this.addSub('ci:mocha:test-suite:finish', (status) => {
92
- if (!this.config.isAgentlessEnabled) {
93
- return
94
- }
95
86
  const span = storage.getStore().span
96
87
  // the test status of the suite may have been set in ci:mocha:test-suite:error already
97
88
  if (!span.context()._tags[TEST_STATUS]) {
@@ -101,9 +92,6 @@ class MochaPlugin extends CiPlugin {
101
92
  })
102
93
 
103
94
  this.addSub('ci:mocha:test-suite:error', (err) => {
104
- if (!this.config.isAgentlessEnabled) {
105
- return
106
- }
107
95
  const span = storage.getStore().span
108
96
  span.setTag('error', err)
109
97
  span.setTag(TEST_STATUS, 'fail')
@@ -151,14 +139,19 @@ class MochaPlugin extends CiPlugin {
151
139
  this._testNameToParams[name] = params
152
140
  })
153
141
 
154
- this.addSub('ci:mocha:session:finish', (status) => {
142
+ this.addSub('ci:mocha:session:finish', ({ status, isSuitesSkipped }) => {
155
143
  if (this.testSessionSpan) {
144
+ const { isSuitesSkippingEnabled, isCodeCoverageEnabled } = this.itrConfig || {}
156
145
  this.testSessionSpan.setTag(TEST_STATUS, status)
146
+ this.testSessionSpan.setTag(TEST_ITR_TESTS_SKIPPED, isSuitesSkipped ? 'true' : 'false')
147
+ this.testSessionSpan.setTag(TEST_SESSION_ITR_SKIPPING_ENABLED, isSuitesSkippingEnabled ? 'true' : 'false')
148
+ this.testSessionSpan.setTag(TEST_SESSION_ITR_CODE_COVERAGE_ENABLED, isCodeCoverageEnabled ? 'true' : 'false')
149
+
157
150
  this.testSessionSpan.finish()
158
151
  finishAllTraceSpans(this.testSessionSpan)
159
152
  }
160
- this.tracer._exporter._writer.flush()
161
153
  this.itrConfig = null
154
+ this.tracer._exporter.flush()
162
155
  })
163
156
  }
164
157
 
@@ -8,7 +8,7 @@ class MongodbCorePlugin extends DatabasePlugin {
8
8
 
9
9
  start ({ ns, ops, options = {}, name }) {
10
10
  const query = getQuery(ops)
11
- const resource = truncate(getResource(ns, query, name))
11
+ const resource = truncate(getResource(this, ns, query, name))
12
12
 
13
13
  this.startSpan('mongodb.query', {
14
14
  service: this.config.service,
@@ -31,10 +31,10 @@ function getQuery (cmd) {
31
31
  if (cmd.filter) return JSON.stringify(limitDepth(cmd.filter))
32
32
  }
33
33
 
34
- function getResource (ns, query, operationName) {
34
+ function getResource (plugin, ns, query, operationName) {
35
35
  const parts = [operationName, ns]
36
36
 
37
- if (query) {
37
+ if (plugin.config.queryInResourceName && query) {
38
38
  parts.push(query)
39
39
  }
40
40
 
@@ -123,6 +123,10 @@ class WAFCallback {
123
123
  return result.actions
124
124
  }
125
125
 
126
+ updateRuleData (ruleData) {
127
+ this.ddwaf.updateRuleData(ruleData)
128
+ }
129
+
126
130
  clear () {
127
131
  this.ddwaf.dispose()
128
132
 
@@ -1,8 +1,10 @@
1
1
  'use strict'
2
2
 
3
3
  const fs = require('fs')
4
+ const path = require('path')
4
5
  const log = require('../log')
5
6
  const RuleManager = require('./rule_manager')
7
+ const remoteConfig = require('./remote_config')
6
8
  const { incomingHttpRequestStart, incomingHttpRequestEnd } = require('./gateway/channels')
7
9
  const Gateway = require('./gateway/engine')
8
10
  const addresses = require('./addresses')
@@ -17,16 +19,18 @@ function enable (config) {
17
19
  try {
18
20
  // TODO: enable dc_blocking: config.appsec.blocking === true
19
21
 
20
- let rules = fs.readFileSync(config.appsec.rules)
22
+ let rules = fs.readFileSync(config.appsec.rules || path.join(__dirname, 'recommended.json'))
21
23
  rules = JSON.parse(rules)
22
24
 
23
25
  RuleManager.applyRules(rules, config.appsec)
26
+ remoteConfig.enableAsmData(config.appsec)
24
27
  } catch (err) {
25
28
  log.error('Unable to start AppSec')
26
29
  log.error(err)
27
30
 
28
31
  // abort AppSec start
29
32
  RuleManager.clearAllRules()
33
+ remoteConfig.disableAsmData()
30
34
  return
31
35
  }
32
36
 
@@ -116,6 +120,7 @@ function disable () {
116
120
  isEnabled = false
117
121
 
118
122
  RuleManager.clearAllRules()
123
+ remoteConfig.disableAsmData()
119
124
 
120
125
  // Channel#unsubscribe() is undefined for non active channels
121
126
  if (incomingHttpRequestStart.hasSubscribers) incomingHttpRequestStart.unsubscribe(incomingHttpStartTranslator)
@@ -2,9 +2,12 @@
2
2
 
3
3
  const RemoteConfigManager = require('./manager')
4
4
  const RemoteConfigCapabilities = require('./capabilities')
5
+ const RuleManager = require('../rule_manager')
6
+
7
+ let rc
5
8
 
6
9
  function enable (config) {
7
- const rc = new RemoteConfigManager(config)
10
+ rc = new RemoteConfigManager(config)
8
11
 
9
12
  if (config.appsec.enabled === undefined) { // only activate ASM_FEATURES when conf is not set locally
10
13
  rc.updateCapabilities(RemoteConfigCapabilities.ASM_ACTIVATION, true)
@@ -29,6 +32,24 @@ function enable (config) {
29
32
  }
30
33
  }
31
34
 
35
+ function enableAsmData (appsecConfig) {
36
+ if (rc && appsecConfig && appsecConfig.rules === undefined) {
37
+ rc.on('ASM_DATA', _asmDataListener)
38
+ }
39
+ }
40
+
41
+ function disableAsmData () {
42
+ if (rc) {
43
+ rc.off('ASM_DATA', _asmDataListener)
44
+ }
45
+ }
46
+
47
+ function _asmDataListener (action, ruleData, ruleId) {
48
+ RuleManager.updateAsmData(action, ruleData, ruleId)
49
+ }
50
+
32
51
  module.exports = {
33
- enable
52
+ enable,
53
+ enableAsmData,
54
+ disableAsmData
34
55
  }
@@ -223,7 +223,7 @@ class RemoteConfigManager extends EventEmitter {
223
223
  for (const item of list) {
224
224
  try {
225
225
  // TODO: do we want to pass old and new config ?
226
- this.emit(item.product, action, item.file)
226
+ this.emit(item.product, action, item.file, item.id)
227
227
 
228
228
  item.apply_state = 2
229
229
  } catch (err) {
@@ -4,6 +4,7 @@ const callbacks = require('./callbacks')
4
4
  const Gateway = require('./gateway/engine')
5
5
 
6
6
  const appliedCallbacks = new Map()
7
+ const appliedAsmData = new Map()
7
8
 
8
9
  function applyRules (rules, config) {
9
10
  if (appliedCallbacks.has(rules)) return
@@ -14,6 +15,60 @@ function applyRules (rules, config) {
14
15
  appliedCallbacks.set(rules, callback)
15
16
  }
16
17
 
18
+ function updateAsmData (action, asmData, asmDataId) {
19
+ if (action === 'unapply') {
20
+ appliedAsmData.delete(asmDataId)
21
+ } else {
22
+ appliedAsmData.set(asmDataId, asmData)
23
+ }
24
+
25
+ const mergedRuleData = mergeRuleData(appliedAsmData.values())
26
+ for (const callback of appliedCallbacks.values()) {
27
+ callback.updateRuleData(mergedRuleData)
28
+ }
29
+ }
30
+
31
+ function mergeRuleData (asmDataValues) {
32
+ const mergedRulesData = new Map()
33
+ for (const asmData of asmDataValues) {
34
+ if (!asmData.rules_data) continue
35
+ for (const rulesData of asmData.rules_data) {
36
+ const key = `${rulesData.id}+${rulesData.type}`
37
+ if (mergedRulesData.has(key)) {
38
+ const existingRulesData = mergedRulesData.get(key)
39
+ rulesData.data.reduce(rulesReducer, existingRulesData.data)
40
+ } else {
41
+ mergedRulesData.set(key, copyRulesData(rulesData))
42
+ }
43
+ }
44
+ }
45
+ return [...mergedRulesData.values()]
46
+ }
47
+
48
+ function rulesReducer (existingEntries, rulesDataEntry) {
49
+ const existingEntry = existingEntries.find((entry) => entry.value === rulesDataEntry.value)
50
+ if (existingEntry && !('expiration' in existingEntry)) return existingEntries
51
+ if (existingEntry && 'expiration' in rulesDataEntry && rulesDataEntry.expiration > existingEntry.expiration) {
52
+ existingEntry.expiration = rulesDataEntry.expiration
53
+ } else if (existingEntry && !('expiration' in rulesDataEntry)) {
54
+ delete existingEntry.expiration
55
+ } else if (!existingEntry) {
56
+ existingEntries.push({ ...rulesDataEntry })
57
+ }
58
+ return existingEntries
59
+ }
60
+
61
+ function copyRulesData (rulesData) {
62
+ const copy = { ...rulesData }
63
+ if (copy.data) {
64
+ const data = []
65
+ copy.data.forEach(item => {
66
+ data.push({ ...item })
67
+ })
68
+ copy.data = data
69
+ }
70
+ return copy
71
+ }
17
72
  function clearAllRules () {
18
73
  Gateway.manager.clear()
19
74
 
@@ -22,9 +77,11 @@ function clearAllRules () {
22
77
 
23
78
  appliedCallbacks.delete(key)
24
79
  }
80
+ appliedAsmData.clear()
25
81
  }
26
82
 
27
83
  module.exports = {
28
84
  applyRules,
29
- clearAllRules
85
+ clearAllRules,
86
+ updateAsmData
30
87
  }
@@ -0,0 +1,62 @@
1
+ 'use strict'
2
+
3
+ const AgentWriter = require('../../../exporters/agent/writer')
4
+ const AgentlessWriter = require('../agentless/writer')
5
+ const CoverageWriter = require('../agentless/coverage-writer')
6
+ const CiVisibilityExporter = require('../ci-visibility-exporter')
7
+
8
+ const AGENT_EVP_PROXY_PATH = '/evp_proxy/v2'
9
+
10
+ function getIsEvpCompatible (err, agentInfo) {
11
+ return !err && agentInfo.endpoints.some(url => url.includes(AGENT_EVP_PROXY_PATH))
12
+ }
13
+
14
+ class AgentProxyCiVisibilityExporter extends CiVisibilityExporter {
15
+ constructor (config) {
16
+ super(config)
17
+
18
+ const {
19
+ tags,
20
+ prioritySampler,
21
+ lookup,
22
+ protocolVersion,
23
+ headers,
24
+ isGitUploadEnabled
25
+ } = config
26
+
27
+ this.getAgentInfo((err, agentInfo) => {
28
+ this._isInitialized = true
29
+ const isEvpCompatible = getIsEvpCompatible(err, agentInfo)
30
+ if (isEvpCompatible) {
31
+ this._isUsingEvpProxy = true
32
+ this._writer = new AgentlessWriter({
33
+ url: this._url,
34
+ tags,
35
+ evpProxyPrefix: AGENT_EVP_PROXY_PATH
36
+ })
37
+ this._coverageWriter = new CoverageWriter({
38
+ url: this._url,
39
+ evpProxyPrefix: AGENT_EVP_PROXY_PATH
40
+ })
41
+ if (isGitUploadEnabled) {
42
+ this.sendGitMetadata({ url: this._url, isEvpProxy: true })
43
+ }
44
+ } else {
45
+ this._writer = new AgentWriter({
46
+ url: this._url,
47
+ prioritySampler,
48
+ lookup,
49
+ protocolVersion,
50
+ headers
51
+ })
52
+ // coverages will never be used, so we discard them
53
+ this._coverageBuffer = []
54
+ }
55
+ this._resolveCanUseCiVisProtocol(isEvpCompatible)
56
+ this.exportUncodedTraces()
57
+ this.exportUncodedCoverages()
58
+ })
59
+ }
60
+ }
61
+
62
+ module.exports = AgentProxyCiVisibilityExporter
@@ -12,10 +12,11 @@ function safeJSONStringify (value) {
12
12
  }
13
13
 
14
14
  class Writer extends BaseWriter {
15
- constructor ({ url }) {
15
+ constructor ({ url, evpProxyPrefix = '' }) {
16
16
  super(...arguments)
17
17
  this._url = url
18
18
  this._encoder = new CoverageCIVisibilityEncoder(this)
19
+ this._evpProxyPrefix = evpProxyPrefix
19
20
  }
20
21
 
21
22
  _sendPayload (form, _, done) {
@@ -30,6 +31,12 @@ class Writer extends BaseWriter {
30
31
  url: this._url
31
32
  }
32
33
 
34
+ if (this._evpProxyPrefix) {
35
+ options.path = `${this._evpProxyPrefix}/api/v2/citestcov`
36
+ delete options.headers['dd-api-key']
37
+ options.headers['X-Datadog-EVP-Subdomain'] = 'event-platform-intake'
38
+ }
39
+
33
40
  log.debug(() => `Request to the intake: ${safeJSONStringify(options)}`)
34
41
 
35
42
  request(form, options, (err, res) => {
@@ -3,73 +3,25 @@
3
3
  const URL = require('url').URL
4
4
  const Writer = require('./writer')
5
5
  const CoverageWriter = require('./coverage-writer')
6
+ const CiVisibilityExporter = require('../ci-visibility-exporter')
6
7
 
7
- const log = require('../../../log')
8
-
9
- class AgentlessCiVisibilityExporter {
8
+ class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
10
9
  constructor (config) {
11
- this._config = config
12
- const { tags, site, url, isIntelligentTestRunnerEnabled } = config
13
- this._isIntelligentTestRunnerEnabled = isIntelligentTestRunnerEnabled
10
+ super(config)
11
+ const { tags, site, url, isGitUploadEnabled } = config
12
+ // we don't need to request /info because we are using agentless by configuration
13
+ this._isInitialized = true
14
+ this._resolveCanUseCiVisProtocol(true)
15
+
14
16
  this._url = url || new URL(`https://citestcycle-intake.${site}`)
15
17
  this._writer = new Writer({ url: this._url, tags })
16
- this._timer = undefined
17
- this._coverageTimer = undefined
18
18
 
19
19
  this._coverageUrl = url || new URL(`https://event-platform-intake.${site}`)
20
20
  this._coverageWriter = new CoverageWriter({ url: this._coverageUrl })
21
21
 
22
- process.once('beforeExit', () => {
23
- this._writer.flush()
24
- this._coverageWriter.flush()
25
- })
26
- }
27
-
28
- exportCoverage ({ span, coverageFiles }) {
29
- const formattedCoverage = {
30
- traceId: span.context()._traceId,
31
- spanId: span.context()._spanId,
32
- files: coverageFiles
33
- }
34
- this._coverageWriter.append(formattedCoverage)
35
-
36
- const { flushInterval } = this._config
37
-
38
- if (flushInterval === 0) {
39
- this._coverageWriter.flush()
40
- } else if (flushInterval > 0 && !this._coverageTimer) {
41
- this._coverageTimer = setTimeout(() => {
42
- this._coverageWriter.flush()
43
- this._coverageTimer = clearTimeout(this._coverageTimer)
44
- }, flushInterval).unref()
45
- }
46
- }
47
-
48
- export (trace) {
49
- this._writer.append(trace)
50
-
51
- const { flushInterval } = this._config
52
-
53
- if (flushInterval === 0) {
54
- this._writer.flush()
55
- } else if (flushInterval > 0 && !this._timer) {
56
- this._timer = setTimeout(() => {
57
- this._writer.flush()
58
- this._timer = clearTimeout(this._timer)
59
- }, flushInterval).unref()
60
- }
61
- }
62
-
63
- setUrl (url, coverageUrl = url) {
64
- try {
65
- url = new URL(url)
66
- coverageUrl = new URL(coverageUrl)
67
- this._url = url
68
- this._coverageUrl = coverageUrl
69
- this._writer.setUrl(url)
70
- this._coverageWriter.setUrl(coverageUrl)
71
- } catch (e) {
72
- log.error(e)
22
+ if (isGitUploadEnabled) {
23
+ const gitUrl = url || new URL(`https://api.${site}`)
24
+ this.sendGitMetadata({ url: gitUrl })
73
25
  }
74
26
  }
75
27
  }
@@ -12,11 +12,12 @@ function safeJSONStringify (value) {
12
12
  }
13
13
 
14
14
  class Writer extends BaseWriter {
15
- constructor ({ url, tags }) {
15
+ constructor ({ url, tags, evpProxyPrefix = '' }) {
16
16
  super(...arguments)
17
17
  const { 'runtime-id': runtimeId, env, service } = tags
18
18
  this._url = url
19
19
  this._encoder = new AgentlessCiVisibilityEncoder(this, { runtimeId, env, service })
20
+ this._evpProxyPrefix = evpProxyPrefix
20
21
  }
21
22
 
22
23
  _sendPayload (data, _, done) {
@@ -31,7 +32,14 @@ class Writer extends BaseWriter {
31
32
  url: this._url
32
33
  }
33
34
 
35
+ if (this._evpProxyPrefix) {
36
+ options.path = `${this._evpProxyPrefix}/api/v2/citestcycle`
37
+ delete options.headers['dd-api-key']
38
+ options.headers['X-Datadog-EVP-Subdomain'] = 'citestcycle-intake'
39
+ }
40
+
34
41
  log.debug(() => `Request to the intake: ${safeJSONStringify(options)}`)
42
+
35
43
  request(data, options, (err, res) => {
36
44
  if (err) {
37
45
  log.error(err)
@@ -0,0 +1,196 @@
1
+ 'use strict'
2
+
3
+ const URL = require('url').URL
4
+
5
+ const { sendGitMetadata: sendGitMetadataRequest } = require('./git/git_metadata')
6
+ const { getItrConfiguration: getItrConfigurationRequest } = require('../intelligent-test-runner/get-itr-configuration')
7
+ const { getSkippableSuites: getSkippableSuitesRequest } = require('../intelligent-test-runner/get-skippable-suites')
8
+ const log = require('../../log')
9
+ const AgentInfoExporter = require('../../exporters/common/agent-info-exporter')
10
+
11
+ function getIsTestSessionTrace (trace) {
12
+ return trace.some(span =>
13
+ span.type === 'test_session_end' || span.type === 'test_suite_end'
14
+ )
15
+ }
16
+
17
+ class CiVisibilityExporter extends AgentInfoExporter {
18
+ constructor (config) {
19
+ super(config)
20
+ this._timer = undefined
21
+ this._coverageTimer = undefined
22
+ this._coverageBuffer = []
23
+ // The library can use new features like ITR and test suite level visibility
24
+ // AKA CI Vis Protocol
25
+ this._canUseCiVisProtocol = false
26
+
27
+ // TODO: add timeout to reject this promise
28
+ this._gitUploadPromise = new Promise(resolve => {
29
+ this._resolveGit = resolve
30
+ })
31
+
32
+ // TODO: add timeout to reject this promise
33
+ this._canUseCiVisProtocolPromise = new Promise(resolve => {
34
+ this._resolveCanUseCiVisProtocol = (canUseCiVisProtocol) => {
35
+ this._canUseCiVisProtocol = canUseCiVisProtocol
36
+ resolve(canUseCiVisProtocol)
37
+ }
38
+ })
39
+
40
+ process.once('beforeExit', () => {
41
+ if (this._writer) {
42
+ this._writer.flush()
43
+ }
44
+ if (this._coverageWriter) {
45
+ this._coverageWriter.flush()
46
+ }
47
+ })
48
+ }
49
+
50
+ shouldRequestSkippableSuites () {
51
+ return !!(this._config.isIntelligentTestRunnerEnabled &&
52
+ this._canUseCiVisProtocol &&
53
+ this._itrConfig &&
54
+ this._itrConfig.isSuitesSkippingEnabled)
55
+ }
56
+
57
+ shouldRequestItrConfiguration () {
58
+ return this._config.isIntelligentTestRunnerEnabled
59
+ }
60
+
61
+ canReportSessionTraces () {
62
+ return this._canUseCiVisProtocol
63
+ }
64
+
65
+ canReportCodeCoverage () {
66
+ return this._canUseCiVisProtocol &&
67
+ this._itrConfig &&
68
+ this._itrConfig.isCodeCoverageEnabled
69
+ }
70
+
71
+ // We can't call the skippable endpoint until git upload has finished,
72
+ // hence the this._gitUploadPromise.then
73
+ getSkippableSuites (testConfiguration, callback) {
74
+ if (!this.shouldRequestSkippableSuites()) {
75
+ return callback(null, [])
76
+ }
77
+ this._gitUploadPromise.then(gitUploadError => {
78
+ if (gitUploadError) {
79
+ return callback(gitUploadError, [])
80
+ }
81
+ const configuration = {
82
+ url: this._url,
83
+ site: this._config.site,
84
+ env: this._config.env,
85
+ service: this._config.service,
86
+ isEvpProxy: !!this._isUsingEvpProxy,
87
+ ...testConfiguration
88
+ }
89
+ getSkippableSuitesRequest(configuration, callback)
90
+ })
91
+ }
92
+
93
+ /**
94
+ * We can't request ITR configuration until we know whether we can use the
95
+ * CI Visibility Protocol, hence the this._canUseCiVisProtocol promise.
96
+ */
97
+ getItrConfiguration (testConfiguration, callback) {
98
+ if (!this.shouldRequestItrConfiguration()) {
99
+ return callback(null, {})
100
+ }
101
+ this._canUseCiVisProtocolPromise.then((canUseCiVisProtocol) => {
102
+ if (!canUseCiVisProtocol) {
103
+ return callback(null, {})
104
+ }
105
+ const configuration = {
106
+ url: this._url,
107
+ env: this._config.env,
108
+ service: this._config.service,
109
+ isEvpProxy: !!this._isUsingEvpProxy,
110
+ ...testConfiguration
111
+ }
112
+ getItrConfigurationRequest(configuration, (err, itrConfig) => {
113
+ this._itrConfig = itrConfig
114
+ callback(err, itrConfig)
115
+ })
116
+ })
117
+ }
118
+
119
+ sendGitMetadata ({ url, isEvpProxy }) {
120
+ sendGitMetadataRequest(url, isEvpProxy, (err) => {
121
+ if (err) {
122
+ log.error(`Error uploading git metadata: ${err.message}`)
123
+ } else {
124
+ log.debug('Successfully uploaded git metadata')
125
+ }
126
+ this._resolveGit(err)
127
+ })
128
+ }
129
+
130
+ export (trace) {
131
+ // Until it's initialized, we just store the traces as is
132
+ if (!this._isInitialized) {
133
+ this._traceBuffer.push(trace)
134
+ return
135
+ }
136
+ if (!this.canReportSessionTraces() && getIsTestSessionTrace(trace)) {
137
+ return
138
+ }
139
+ this._export(trace)
140
+ }
141
+
142
+ exportCoverage (coveragePayload) {
143
+ // Until it's initialized, we just store the coverages as is
144
+ if (!this._isInitialized) {
145
+ this._coverageBuffer.push(coveragePayload)
146
+ return
147
+ }
148
+ if (!this.canReportCodeCoverage()) {
149
+ return
150
+ }
151
+
152
+ const { span, coverageFiles } = coveragePayload
153
+ const formattedCoverage = {
154
+ traceId: span.context()._traceId,
155
+ spanId: span.context()._spanId,
156
+ files: coverageFiles
157
+ }
158
+
159
+ this._export(formattedCoverage, this._coverageWriter, '_coverageTimer')
160
+ }
161
+
162
+ flush (done = () => {}) {
163
+ if (!this._isInitialized) {
164
+ return done()
165
+ }
166
+ this._writer.flush(() => {
167
+ if (this._coverageWriter) {
168
+ this._coverageWriter.flush(done)
169
+ } else {
170
+ done()
171
+ }
172
+ })
173
+ }
174
+
175
+ exportUncodedCoverages () {
176
+ this._coverageBuffer.forEach(oldCoveragePayload => {
177
+ this.exportCoverage(oldCoveragePayload)
178
+ })
179
+ this._coverageBuffer = []
180
+ }
181
+
182
+ setUrl (url, coverageUrl = url) {
183
+ try {
184
+ url = new URL(url)
185
+ coverageUrl = new URL(coverageUrl)
186
+ this._url = url
187
+ this._coverageUrl = coverageUrl
188
+ this._writer.setUrl(url)
189
+ this._coverageWriter.setUrl(coverageUrl)
190
+ } catch (e) {
191
+ log.error(e)
192
+ }
193
+ }
194
+ }
195
+
196
+ module.exports = CiVisibilityExporter