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.
- package/README.md +108 -43
- package/ci/init.js +6 -1
- package/ext/exporters.d.ts +2 -1
- package/ext/exporters.js +2 -1
- package/index.d.ts +6 -1
- package/package.json +2 -2
- package/packages/datadog-instrumentations/src/http/server.js +4 -1
- package/packages/datadog-instrumentations/src/jest.js +26 -9
- package/packages/datadog-instrumentations/src/mocha.js +12 -3
- package/packages/datadog-instrumentations/src/opensearch.js +1 -1
- package/packages/datadog-instrumentations/src/router.js +1 -1
- package/packages/datadog-plugin-cucumber/src/index.js +1 -1
- package/packages/datadog-plugin-http/src/server.js +2 -1
- package/packages/datadog-plugin-jest/src/index.js +21 -25
- package/packages/datadog-plugin-mocha/src/index.js +11 -18
- package/packages/datadog-plugin-mongodb-core/src/index.js +3 -3
- package/packages/dd-trace/src/appsec/callbacks/ddwaf.js +4 -0
- package/packages/dd-trace/src/appsec/index.js +6 -1
- package/packages/dd-trace/src/appsec/remote_config/index.js +23 -2
- package/packages/dd-trace/src/appsec/remote_config/manager.js +1 -1
- package/packages/dd-trace/src/appsec/rule_manager.js +58 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +62 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +8 -1
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +11 -59
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +9 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +196 -0
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +20 -7
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-itr-configuration.js +22 -19
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +22 -18
- package/packages/dd-trace/src/config.js +10 -8
- package/packages/dd-trace/src/exporter.js +3 -0
- package/packages/dd-trace/src/exporters/agent/index.js +4 -0
- package/packages/dd-trace/src/exporters/common/agent-info-exporter.js +82 -0
- package/packages/dd-trace/src/plugin_manager.js +0 -8
- package/packages/dd-trace/src/plugins/ci_plugin.js +9 -50
- package/packages/dd-trace/src/plugins/util/ci.js +35 -2
- package/packages/dd-trace/src/plugins/util/test.js +4 -0
- package/packages/dd-trace/src/plugins/util/web.js +1 -2
- package/packages/dd-trace/src/profiling/exporters/agent.js +4 -0
- package/packages/dd-trace/src/proxy.js +4 -18
- package/packages/dd-trace/src/ritm.js +18 -13
- 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
|
|
|
@@ -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
|
-
|
|
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
|
-
|
|
8
|
-
|
|
9
|
-
class AgentlessCiVisibilityExporter {
|
|
8
|
+
class AgentlessCiVisibilityExporter extends CiVisibilityExporter {
|
|
10
9
|
constructor (config) {
|
|
11
|
-
|
|
12
|
-
const { tags, site, url,
|
|
13
|
-
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
this.
|
|
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
|