dd-trace 5.2.0 → 5.4.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 (86) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/README.md +1 -32
  3. package/ci/init.js +1 -4
  4. package/index.d.ts +21 -0
  5. package/package.json +7 -6
  6. package/packages/datadog-instrumentations/src/amqplib.js +1 -1
  7. package/packages/datadog-instrumentations/src/child_process.js +150 -0
  8. package/packages/datadog-instrumentations/src/cucumber.js +12 -12
  9. package/packages/datadog-instrumentations/src/express.js +20 -0
  10. package/packages/datadog-instrumentations/src/grpc/client.js +56 -36
  11. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -2
  12. package/packages/datadog-instrumentations/src/jest.js +149 -11
  13. package/packages/datadog-instrumentations/src/mocha.js +142 -16
  14. package/packages/datadog-instrumentations/src/mongoose.js +23 -10
  15. package/packages/datadog-instrumentations/src/next.js +17 -3
  16. package/packages/datadog-instrumentations/src/playwright.js +41 -9
  17. package/packages/datadog-plugin-amqplib/src/consumer.js +10 -1
  18. package/packages/datadog-plugin-amqplib/src/producer.js +14 -1
  19. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +107 -1
  20. package/packages/datadog-plugin-child_process/src/index.js +91 -0
  21. package/packages/datadog-plugin-child_process/src/scrub-cmd-params.js +125 -0
  22. package/packages/datadog-plugin-cucumber/src/index.js +16 -11
  23. package/packages/datadog-plugin-cypress/src/plugin.js +52 -23
  24. package/packages/datadog-plugin-grpc/src/client.js +16 -2
  25. package/packages/datadog-plugin-http/src/client.js +1 -1
  26. package/packages/datadog-plugin-jest/src/index.js +43 -6
  27. package/packages/datadog-plugin-kafkajs/src/consumer.js +16 -0
  28. package/packages/datadog-plugin-mocha/src/index.js +47 -17
  29. package/packages/datadog-plugin-playwright/src/index.js +19 -5
  30. package/packages/datadog-plugin-rhea/src/consumer.js +11 -1
  31. package/packages/datadog-plugin-rhea/src/producer.js +11 -0
  32. package/packages/dd-trace/src/appsec/addresses.js +2 -0
  33. package/packages/dd-trace/src/appsec/api_security_sampler.js +16 -3
  34. package/packages/dd-trace/src/appsec/channels.js +2 -1
  35. package/packages/dd-trace/src/appsec/iast/analyzers/command-injection-analyzer.js +1 -1
  36. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +7 -28
  37. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +10 -6
  38. package/packages/dd-trace/src/appsec/iast/context/context-plugin.js +90 -0
  39. package/packages/dd-trace/src/appsec/iast/context/kafka-ctx-plugin.js +14 -0
  40. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +12 -1
  41. package/packages/dd-trace/src/appsec/iast/index.js +4 -4
  42. package/packages/dd-trace/src/appsec/iast/overhead-controller.js +1 -1
  43. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
  44. package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +10 -0
  45. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations-taint-object.js +53 -0
  46. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +10 -46
  47. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +13 -9
  48. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugins/kafka.js +47 -0
  49. package/packages/dd-trace/src/appsec/iast/taint-tracking/source-types.js +3 -1
  50. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +29 -2
  51. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +1 -1
  52. package/packages/dd-trace/src/appsec/index.js +17 -2
  53. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +2 -1
  54. package/packages/dd-trace/src/appsec/remote_config/index.js +1 -0
  55. package/packages/dd-trace/src/appsec/rule_manager.js +2 -2
  56. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +83 -0
  57. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +25 -6
  58. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +2 -0
  59. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +83 -41
  60. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +30 -8
  61. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -1
  62. package/packages/dd-trace/src/ci-visibility/{intelligent-test-runner/get-itr-configuration.js → requests/get-library-configuration.js} +18 -6
  63. package/packages/dd-trace/src/config.js +22 -9
  64. package/packages/dd-trace/src/datastreams/processor.js +6 -0
  65. package/packages/dd-trace/src/datastreams/writer.js +2 -5
  66. package/packages/dd-trace/src/dogstatsd.js +3 -5
  67. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +5 -3
  68. package/packages/dd-trace/src/exporters/common/request.js +21 -3
  69. package/packages/dd-trace/src/format.js +25 -1
  70. package/packages/dd-trace/src/noop/span.js +1 -0
  71. package/packages/dd-trace/src/opentelemetry/span.js +9 -2
  72. package/packages/dd-trace/src/opentracing/span.js +38 -0
  73. package/packages/dd-trace/src/opentracing/span_context.js +12 -6
  74. package/packages/dd-trace/src/opentracing/tracer.js +2 -1
  75. package/packages/dd-trace/src/plugins/ci_plugin.js +25 -9
  76. package/packages/dd-trace/src/plugins/index.js +1 -0
  77. package/packages/dd-trace/src/plugins/util/git.js +6 -0
  78. package/packages/dd-trace/src/plugins/util/test.js +53 -8
  79. package/packages/dd-trace/src/profiling/config.js +22 -22
  80. package/packages/dd-trace/src/proxy.js +31 -23
  81. package/packages/dd-trace/src/span_processor.js +5 -1
  82. package/packages/dd-trace/src/telemetry/index.js +6 -0
  83. package/packages/dd-trace/src/telemetry/logs/index.js +2 -2
  84. package/packages/dd-trace/src/telemetry/send-data.js +0 -3
  85. package/packages/datadog-instrumentations/src/child-process.js +0 -29
  86. package/packages/dd-trace/src/plugins/util/exec.js +0 -34
@@ -13,6 +13,7 @@ const { GIT_REPOSITORY_URL, GIT_COMMIT_SHA } = require('./plugins/util/tags')
13
13
  const { getGitMetadataFromGitProperties, removeUserSensitiveInfo } = require('./git_properties')
14
14
  const { updateConfig } = require('./telemetry')
15
15
  const { getIsGCPFunction, getIsAzureFunctionConsumptionPlan } = require('./serverless')
16
+ const { ORIGIN_KEY } = require('./constants')
16
17
 
17
18
  const fromEntries = Object.fromEntries || (entries =>
18
19
  entries.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {}))
@@ -109,10 +110,6 @@ class Config {
109
110
  log.use(this.logger)
110
111
  log.toggle(this.debug, this.logLevel, this)
111
112
 
112
- const DD_TRACING_ENABLED = coalesce(
113
- process.env.DD_TRACING_ENABLED,
114
- true
115
- )
116
113
  const DD_PROFILING_ENABLED = coalesce(
117
114
  options.profiling, // TODO: remove when enabled by default
118
115
  process.env.DD_EXPERIMENTAL_PROFILING_ENABLED,
@@ -172,6 +169,11 @@ class Config {
172
169
  false
173
170
  )
174
171
 
172
+ const DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED = coalesce(
173
+ process.env.DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED,
174
+ true
175
+ )
176
+
175
177
  const DD_TRACE_MEMCACHED_COMMAND_ENABLED = coalesce(
176
178
  process.env.DD_TRACE_MEMCACHED_COMMAND_ENABLED,
177
179
  false
@@ -416,10 +418,11 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
416
418
  process.env.DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING,
417
419
  'safe'
418
420
  ).toLowerCase()
419
- const DD_EXPERIMENTAL_API_SECURITY_ENABLED = coalesce(
421
+ const DD_API_SECURITY_ENABLED = coalesce(
420
422
  appsec?.apiSecurity?.enabled,
421
- isTrue(process.env.DD_EXPERIMENTAL_API_SECURITY_ENABLED),
422
- false
423
+ process.env.DD_API_SECURITY_ENABLED && isTrue(process.env.DD_API_SECURITY_ENABLED),
424
+ process.env.DD_EXPERIMENTAL_API_SECURITY_ENABLED && isTrue(process.env.DD_EXPERIMENTAL_API_SECURITY_ENABLED),
425
+ true
423
426
  )
424
427
  const DD_API_SECURITY_REQUEST_SAMPLE_RATE = coalesce(
425
428
  appsec?.apiSecurity?.requestSampling,
@@ -563,7 +566,6 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
563
566
 
564
567
  const defaultFlushInterval = inAWSLambda ? 0 : 2000
565
568
 
566
- this.tracing = !isFalse(DD_TRACING_ENABLED)
567
569
  this.dbmPropagationMode = DD_DBM_PROPAGATION_MODE
568
570
  this.dsmEnabled = isTrue(DD_DATA_STREAMS_ENABLED)
569
571
  this.openAiLogsEnabled = DD_OPENAI_LOGS_ENABLED
@@ -636,7 +638,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
636
638
  mode: DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING
637
639
  },
638
640
  apiSecurity: {
639
- enabled: DD_EXPERIMENTAL_API_SECURITY_ENABLED,
641
+ enabled: DD_API_SECURITY_ENABLED,
640
642
  // Coerce value between 0 and 1
641
643
  requestSampling: Math.min(1, Math.max(0, DD_API_SECURITY_REQUEST_SAMPLE_RATE))
642
644
  }
@@ -666,6 +668,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
666
668
 
667
669
  this.gitMetadataEnabled = isTrue(DD_TRACE_GIT_METADATA_ENABLED)
668
670
  this.isManualApiEnabled = this.isCiVisibility && isTrue(DD_CIVISIBILITY_MANUAL_API_ENABLED)
671
+ this.isEarlyFlakeDetectionEnabled = this.isCiVisibility && isTrue(DD_CIVISIBILITY_EARLY_FLAKE_DETECTION_ENABLED)
669
672
 
670
673
  this.openaiSpanCharLimit = DD_OPENAI_SPAN_CHAR_LIMIT
671
674
 
@@ -703,6 +706,12 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
703
706
  'runtime-id': uuid()
704
707
  })
705
708
 
709
+ if (this.isCiVisibility) {
710
+ tagger.add(this.tags, {
711
+ [ORIGIN_KEY]: 'ciapp-test'
712
+ })
713
+ }
714
+
706
715
  if (this.gitMetadataEnabled) {
707
716
  this.repositoryUrl = removeUserSensitiveInfo(
708
717
  coalesce(
@@ -772,6 +781,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
772
781
  this._setBoolean(defaults, 'logInjection', false)
773
782
  this._setArray(defaults, 'headerTags', [])
774
783
  this._setValue(defaults, 'tags', {})
784
+ this._setBoolean(defaults, 'tracing', true)
775
785
  }
776
786
 
777
787
  _applyEnvironment () {
@@ -785,6 +795,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
785
795
  DD_TRACE_HEADER_TAGS,
786
796
  DD_TRACE_SAMPLE_RATE,
787
797
  DD_TRACE_TAGS,
798
+ DD_TRACING_ENABLED,
788
799
  DD_VERSION
789
800
  } = process.env
790
801
 
@@ -802,6 +813,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
802
813
  this._setBoolean(env, 'logInjection', DD_LOGS_INJECTION)
803
814
  this._setArray(env, 'headerTags', DD_TRACE_HEADER_TAGS)
804
815
  this._setTags(env, 'tags', tags)
816
+ this._setBoolean(env, 'tracing', DD_TRACING_ENABLED)
805
817
  }
806
818
 
807
819
  _applyOptions (options) {
@@ -836,6 +848,7 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
836
848
  this._setBoolean(opts, 'logInjection', options.log_injection_enabled)
837
849
  this._setArray(opts, 'headerTags', headerTags)
838
850
  this._setTags(opts, 'tags', tags)
851
+ this._setBoolean(opts, 'tracing', options.tracing_enabled)
839
852
  }
840
853
 
841
854
  _setBoolean (obj, name, value) {
@@ -153,6 +153,11 @@ function getMessageSize (message) {
153
153
  return getSizeOrZero(key) + getSizeOrZero(value) + getHeadersSize(headers)
154
154
  }
155
155
 
156
+ function getAmqpMessageSize (message) {
157
+ const { headers, content } = message
158
+ return getSizeOrZero(content) + getHeadersSize(headers)
159
+ }
160
+
156
161
  class TimeBuckets extends Map {
157
162
  forTime (time) {
158
163
  if (!this.has(time)) {
@@ -358,6 +363,7 @@ module.exports = {
358
363
  getMessageSize,
359
364
  getHeadersSize,
360
365
  getSizeOrZero,
366
+ getAmqpMessageSize,
361
367
  ENTRY_PARENT_HASH,
362
368
  CONTEXT_PROPAGATION_KEY
363
369
  }
@@ -15,13 +15,10 @@ function makeRequest (data, url, cb) {
15
15
  'Datadog-Meta-Tracer-Version': pkg.version,
16
16
  'Content-Type': 'application/msgpack',
17
17
  'Content-Encoding': 'gzip'
18
- }
18
+ },
19
+ url
19
20
  }
20
21
 
21
- options.protocol = url.protocol
22
- options.hostname = url.hostname
23
- options.port = url.port
24
-
25
22
  log.debug(() => `Request to the intake: ${JSON.stringify(options)}`)
26
23
 
27
24
  request(data, options, (err, res) => {
@@ -67,16 +67,14 @@ class DogStatsDClient {
67
67
  request(buffer, this._httpOptions, (err) => {
68
68
  if (err) {
69
69
  log.error('HTTP error from agent: ' + err.stack)
70
- if (err.status) {
70
+ if (err.status === 404) {
71
71
  // Inside this if-block, we have connectivity to the agent, but
72
72
  // we're not getting a 200 from the proxy endpoint. If it's a 404,
73
73
  // then we know we'll never have the endpoint, so just clear out the
74
74
  // options. Either way, we can give UDP a try.
75
- if (err.status === 404) {
76
- this._httpOptions = null
77
- }
78
- this._sendUdp(queue)
75
+ this._httpOptions = null
79
76
  }
77
+ this._sendUdp(queue)
80
78
  }
81
79
  })
82
80
  }
@@ -16,6 +16,7 @@ const ALLOWED_CONTENT_TYPES = ['test_session_end', 'test_module_end', 'test_suit
16
16
  const TEST_SUITE_KEYS_LENGTH = 12
17
17
  const TEST_MODULE_KEYS_LENGTH = 11
18
18
  const TEST_SESSION_KEYS_LENGTH = 10
19
+ const TEST_AND_SPAN_KEYS_LENGTH = 11
19
20
 
20
21
  const INTAKE_SOFT_LIMIT = 2 * 1024 * 1024 // 2MB
21
22
 
@@ -145,9 +146,7 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
145
146
  }
146
147
 
147
148
  _encodeEventContent (bytes, content) {
148
- const keysLength = Object.keys(content).length
149
-
150
- let totalKeysLength = keysLength
149
+ let totalKeysLength = TEST_AND_SPAN_KEYS_LENGTH
151
150
  if (content.meta.test_session_id) {
152
151
  totalKeysLength = totalKeysLength + 1
153
152
  }
@@ -161,6 +160,9 @@ class AgentlessCiVisibilityEncoder extends AgentEncoder {
161
160
  if (itrCorrelationId) {
162
161
  totalKeysLength = totalKeysLength + 1
163
162
  }
163
+ if (content.type) {
164
+ totalKeysLength = totalKeysLength + 1
165
+ }
164
166
  this._encodeMapPrefix(bytes, totalKeysLength)
165
167
  if (content.type) {
166
168
  this._encodeString(bytes, 'type')
@@ -7,6 +7,8 @@ const { Readable } = require('stream')
7
7
  const http = require('http')
8
8
  const https = require('https')
9
9
  const { parse: urlParse } = require('url')
10
+ const zlib = require('zlib')
11
+
10
12
  const docker = require('./docker')
11
13
  const { httpAgent, httpsAgent } = require('./agents')
12
14
  const { storage } = require('../../../../datadog-core')
@@ -93,16 +95,31 @@ function request (data, options, callback) {
93
95
  options.agent = isSecure ? httpsAgent : httpAgent
94
96
 
95
97
  const onResponse = res => {
96
- let responseData = ''
98
+ const chunks = []
97
99
 
98
100
  res.setTimeout(timeout)
99
101
 
100
- res.on('data', chunk => { responseData += chunk })
102
+ res.on('data', chunk => {
103
+ chunks.push(chunk)
104
+ })
101
105
  res.on('end', () => {
102
106
  activeRequests--
107
+ const buffer = Buffer.concat(chunks)
103
108
 
104
109
  if (res.statusCode >= 200 && res.statusCode <= 299) {
105
- callback(null, responseData, res.statusCode)
110
+ const isGzip = res.headers['content-encoding'] === 'gzip'
111
+ if (isGzip) {
112
+ zlib.gunzip(buffer, (err, result) => {
113
+ if (err) {
114
+ log.error(`Could not gunzip response: ${err.message}`)
115
+ callback(null, '', res.statusCode)
116
+ } else {
117
+ callback(null, result.toString(), res.statusCode)
118
+ }
119
+ })
120
+ } else {
121
+ callback(null, buffer.toString(), res.statusCode)
122
+ }
106
123
  } else {
107
124
  let errorMessage = ''
108
125
  try {
@@ -114,6 +131,7 @@ function request (data, options, callback) {
114
131
  } catch (e) {
115
132
  // ignore error
116
133
  }
134
+ const responseData = buffer.toString()
117
135
  if (responseData) {
118
136
  errorMessage += ` Response from the endpoint: "${responseData}"`
119
137
  }
@@ -33,6 +33,7 @@ const map = {
33
33
  function format (span) {
34
34
  const formatted = formatSpan(span)
35
35
 
36
+ extractSpanLinks(formatted, span)
36
37
  extractRootTags(formatted, span)
37
38
  extractChunkTags(formatted, span)
38
39
  extractTags(formatted, span)
@@ -53,7 +54,8 @@ function formatSpan (span) {
53
54
  meta: {},
54
55
  metrics: {},
55
56
  start: Math.round(span._startTime * 1e6),
56
- duration: Math.round(span._duration * 1e6)
57
+ duration: Math.round(span._duration * 1e6),
58
+ links: []
57
59
  }
58
60
  }
59
61
 
@@ -64,6 +66,28 @@ function setSingleSpanIngestionTags (span, options) {
64
66
  addTag({}, span.metrics, SPAN_SAMPLING_MAX_PER_SECOND, options.maxPerSecond)
65
67
  }
66
68
 
69
+ function extractSpanLinks (trace, span) {
70
+ const links = []
71
+ if (span._links) {
72
+ for (const link of span._links) {
73
+ const { context, attributes } = link
74
+ const formattedLink = {}
75
+
76
+ formattedLink.trace_id = context.toTraceId(true)
77
+ formattedLink.span_id = context.toSpanId(true)
78
+
79
+ if (attributes && Object.keys(attributes).length > 0) {
80
+ formattedLink.attributes = attributes
81
+ }
82
+ if (context?._sampling?.priority >= 0) formattedLink.flags = context._sampling.priority > 0 ? 1 : 0
83
+ if (context?._tracestate) formattedLink.tracestate = context._tracestate.toString()
84
+
85
+ links.push(formattedLink)
86
+ }
87
+ }
88
+ if (links.length > 0) { trace.meta['_dd.span_links'] = JSON.stringify(links) }
89
+ }
90
+
67
91
  function extractTags (trace, span) {
68
92
  const context = span.context()
69
93
  const origin = context._trace.origin
@@ -18,6 +18,7 @@ class NoopSpan {
18
18
  getBaggageItem (key) {}
19
19
  setTag (key, value) { return this }
20
20
  addTags (keyValueMap) { return this }
21
+ addLink (link) { return this }
21
22
  log () { return this }
22
23
  logEvent () {}
23
24
  finish (finishTime) {}
@@ -132,7 +132,8 @@ class Span {
132
132
  tags: {
133
133
  [SERVICE_NAME]: _tracer._service,
134
134
  [RESOURCE_NAME]: spanName
135
- }
135
+ },
136
+ links
136
137
  }, _tracer._debug)
137
138
 
138
139
  if (attributes) {
@@ -148,7 +149,6 @@ class Span {
148
149
  // math for computing opentracing timestamps is apparently lossy...
149
150
  this.startTime = hrStartTime
150
151
  this.kind = kind
151
- this.links = links
152
152
  this._spanProcessor.onStart(this, context)
153
153
  }
154
154
 
@@ -191,6 +191,13 @@ class Span {
191
191
  return this
192
192
  }
193
193
 
194
+ addLink (context, attributes) {
195
+ // extract dd context
196
+ const ddSpanContext = context._ddContext
197
+ this._ddSpan.addLink(ddSpanContext, attributes)
198
+ return this
199
+ }
200
+
194
201
  setStatus ({ code, message }) {
195
202
  if (!this.ended && !this._hasStatus && code) {
196
203
  this._hasStatus = true
@@ -26,6 +26,7 @@ const unfinishedRegistry = createRegistry('unfinished')
26
26
  const finishedRegistry = createRegistry('finished')
27
27
 
28
28
  const OTEL_ENABLED = !!process.env.DD_TRACE_OTEL_ENABLED
29
+ const ALLOWED = ['string', 'number', 'boolean']
29
30
 
30
31
  const integrationCounters = {
31
32
  span_created: {},
@@ -82,6 +83,9 @@ class DatadogSpan {
82
83
 
83
84
  this._startTime = fields.startTime || this._getTime()
84
85
 
86
+ this._links = []
87
+ fields.links && fields.links.forEach(link => this.addLink(link.context, link.attributes))
88
+
85
89
  if (DD_TRACE_EXPERIMENTAL_SPAN_COUNTS && finishedRegistry) {
86
90
  runtimeMetrics.increment('runtime.node.spans.unfinished')
87
91
  runtimeMetrics.increment('runtime.node.spans.unfinished.by.name', `span_name:${operationName}`)
@@ -150,6 +154,13 @@ class DatadogSpan {
150
154
 
151
155
  logEvent () {}
152
156
 
157
+ addLink (context, attributes) {
158
+ this._links.push({
159
+ context: context._ddContext ? context._ddContext : context,
160
+ attributes: this._sanitizeAttributes(attributes)
161
+ })
162
+ }
163
+
153
164
  finish (finishTime) {
154
165
  if (this._duration !== undefined) {
155
166
  return
@@ -185,6 +196,33 @@ class DatadogSpan {
185
196
  this._processor.process(this)
186
197
  }
187
198
 
199
+ _sanitizeAttributes (attributes = {}) {
200
+ const sanitizedAttributes = {}
201
+
202
+ const addArrayOrScalarAttributes = (key, maybeArray) => {
203
+ if (Array.isArray(maybeArray)) {
204
+ for (const subkey in maybeArray) {
205
+ addArrayOrScalarAttributes(`${key}.${subkey}`, maybeArray[subkey])
206
+ }
207
+ } else {
208
+ const maybeScalar = maybeArray
209
+ if (ALLOWED.includes(typeof maybeScalar)) {
210
+ // Wrap the value as a string if it's not already a string
211
+ sanitizedAttributes[key] = typeof maybeScalar === 'string' ? maybeScalar : String(maybeScalar)
212
+ } else {
213
+ log.warn(`Dropping span link attribute. It is not of an allowed type`)
214
+ }
215
+ }
216
+ }
217
+
218
+ Object.entries(attributes).forEach(entry => {
219
+ const [key, value] = entry
220
+ addArrayOrScalarAttributes(key, value)
221
+ })
222
+
223
+ return sanitizedAttributes
224
+ }
225
+
188
226
  _createContext (parent, fields) {
189
227
  let spanContext
190
228
  let startTime
@@ -28,20 +28,26 @@ class DatadogSpanContext {
28
28
  }
29
29
  }
30
30
 
31
- toTraceId () {
31
+ toTraceId (get128bitId = false) {
32
+ if (get128bitId) {
33
+ return this._traceId.toBuffer().length <= 8 && this._trace.tags[TRACE_ID_128]
34
+ ? this._trace.tags[TRACE_ID_128] + this._traceId.toString(16).padStart(16, '0')
35
+ : this._traceId.toString(16).padStart(32, '0')
36
+ }
32
37
  return this._traceId.toString(10)
33
38
  }
34
39
 
35
- toSpanId () {
40
+ toSpanId (get128bitId = false) {
41
+ if (get128bitId) {
42
+ return this._spanId.toString(16).padStart(16, '0')
43
+ }
36
44
  return this._spanId.toString(10)
37
45
  }
38
46
 
39
47
  toTraceparent () {
40
48
  const flags = this._sampling.priority >= AUTO_KEEP ? '01' : '00'
41
- const traceId = this._traceId.toBuffer().length <= 8 && this._trace.tags[TRACE_ID_128]
42
- ? this._trace.tags[TRACE_ID_128] + this._traceId.toString(16).padStart(16, '0')
43
- : this._traceId.toString(16).padStart(32, '0')
44
- const spanId = this._spanId.toString(16).padStart(16, '0')
49
+ const traceId = this.toTraceId(true)
50
+ const spanId = this.toSpanId(true)
45
51
  const version = (this._traceparent && this._traceparent.version) || '00'
46
52
  return `${version}-${traceId}-${spanId}-${flags}`
47
53
  }
@@ -61,7 +61,8 @@ class DatadogTracer {
61
61
  startTime: options.startTime,
62
62
  hostname: this._hostname,
63
63
  traceId128BitGenerationEnabled: this._traceId128BitGenerationEnabled,
64
- integrationName: options.integrationName
64
+ integrationName: options.integrationName,
65
+ links: options.links
65
66
  }, this._debug)
66
67
 
67
68
  span.addTags(this._config.tags)
@@ -27,7 +27,7 @@ const {
27
27
  TELEMETRY_EVENT_CREATED,
28
28
  TELEMETRY_ITR_SKIPPED
29
29
  } = require('../ci-visibility/telemetry')
30
- const { CI_PROVIDER_NAME, GIT_REPOSITORY_URL, GIT_COMMIT_SHA, GIT_BRANCH } = require('./util/tags')
30
+ const { CI_PROVIDER_NAME, GIT_REPOSITORY_URL, GIT_COMMIT_SHA, GIT_BRANCH, CI_WORKSPACE_PATH } = require('./util/tags')
31
31
  const { OS_VERSION, OS_PLATFORM, OS_ARCHITECTURE, RUNTIME_NAME, RUNTIME_VERSION } = require('./util/env')
32
32
 
33
33
  module.exports = class CiPlugin extends Plugin {
@@ -36,22 +36,22 @@ module.exports = class CiPlugin extends Plugin {
36
36
 
37
37
  this.rootDir = process.cwd() // fallback in case :session:start events are not emitted
38
38
 
39
- this.addSub(`ci:${this.constructor.id}:itr-configuration`, ({ onDone }) => {
40
- if (!this.tracer._exporter || !this.tracer._exporter.getItrConfiguration) {
39
+ this.addSub(`ci:${this.constructor.id}:library-configuration`, ({ onDone }) => {
40
+ if (!this.tracer._exporter || !this.tracer._exporter.getLibraryConfiguration) {
41
41
  return onDone({ err: new Error('CI Visibility was not initialized correctly') })
42
42
  }
43
- this.tracer._exporter.getItrConfiguration(this.testConfiguration, (err, itrConfig) => {
43
+ this.tracer._exporter.getLibraryConfiguration(this.testConfiguration, (err, libraryConfig) => {
44
44
  if (err) {
45
45
  log.error(`Intelligent Test Runner configuration could not be fetched. ${err.message}`)
46
46
  } else {
47
- this.itrConfig = itrConfig
47
+ this.libraryConfig = libraryConfig
48
48
  }
49
- onDone({ err, itrConfig })
49
+ onDone({ err, libraryConfig })
50
50
  })
51
51
  })
52
52
 
53
53
  this.addSub(`ci:${this.constructor.id}:test-suite:skippable`, ({ onDone }) => {
54
- if (!this.tracer._exporter || !this.tracer._exporter.getSkippableSuites) {
54
+ if (!this.tracer._exporter?.getSkippableSuites) {
55
55
  return onDone({ err: new Error('CI Visibility was not initialized correctly') })
56
56
  }
57
57
  this.tracer._exporter.getSkippableSuites(this.testConfiguration, (err, skippableSuites, itrCorrelationId) => {
@@ -115,6 +115,18 @@ module.exports = class CiPlugin extends Plugin {
115
115
  })
116
116
  this.telemetry.count(TELEMETRY_ITR_SKIPPED, { testLevel: 'suite' }, skippedSuites.length)
117
117
  })
118
+
119
+ this.addSub(`ci:${this.constructor.id}:known-tests`, ({ onDone }) => {
120
+ if (!this.tracer._exporter?.getKnownTests) {
121
+ return onDone({ err: new Error('CI Visibility was not initialized correctly') })
122
+ }
123
+ this.tracer._exporter.getKnownTests(this.testConfiguration, (err, knownTests) => {
124
+ if (err) {
125
+ log.error(`Known tests could not be fetched. ${err.message}`)
126
+ }
127
+ onDone({ err, knownTests })
128
+ })
129
+ })
118
130
  }
119
131
 
120
132
  get telemetry () {
@@ -140,7 +152,6 @@ module.exports = class CiPlugin extends Plugin {
140
152
  configure (config) {
141
153
  super.configure(config)
142
154
  this.testEnvironmentMetadata = getTestEnvironmentMetadata(this.constructor.id, this.config)
143
- this.codeOwnersEntries = getCodeOwnersFileEntries()
144
155
 
145
156
  const {
146
157
  [GIT_REPOSITORY_URL]: repositoryUrl,
@@ -151,9 +162,14 @@ module.exports = class CiPlugin extends Plugin {
151
162
  [RUNTIME_NAME]: runtimeName,
152
163
  [RUNTIME_VERSION]: runtimeVersion,
153
164
  [GIT_BRANCH]: branch,
154
- [CI_PROVIDER_NAME]: ciProviderName
165
+ [CI_PROVIDER_NAME]: ciProviderName,
166
+ [CI_WORKSPACE_PATH]: repositoryRoot
155
167
  } = this.testEnvironmentMetadata
156
168
 
169
+ this.repositoryRoot = repositoryRoot || process.cwd()
170
+
171
+ this.codeOwnersEntries = getCodeOwnersFileEntries(repositoryRoot)
172
+
157
173
  this.isUnsupportedCIProvider = !ciProviderName
158
174
 
159
175
  this.testConfiguration = {
@@ -23,6 +23,7 @@ module.exports = {
23
23
  get 'aws-sdk' () { return require('../../../datadog-plugin-aws-sdk/src') },
24
24
  get 'bunyan' () { return require('../../../datadog-plugin-bunyan/src') },
25
25
  get 'cassandra-driver' () { return require('../../../datadog-plugin-cassandra-driver/src') },
26
+ get 'child_process' () { return require('../../../datadog-plugin-child_process/src') },
26
27
  get 'connect' () { return require('../../../datadog-plugin-connect/src') },
27
28
  get 'couchbase' () { return require('../../../datadog-plugin-couchbase/src') },
28
29
  get 'cypress' () { return require('../../../datadog-plugin-cypress/src') },
@@ -26,6 +26,7 @@ const {
26
26
  TELEMETRY_GIT_COMMAND_ERRORS
27
27
  } = require('../../ci-visibility/telemetry')
28
28
  const { filterSensitiveInfoFromRepository } = require('./url')
29
+ const { storage } = require('../../../../datadog-core')
29
30
 
30
31
  const GIT_REV_LIST_MAX_BUFFER = 8 * 1024 * 1024 // 8MB
31
32
 
@@ -36,6 +37,9 @@ function sanitizedExec (
36
37
  durationMetric,
37
38
  errorMetric
38
39
  ) {
40
+ const store = storage.getStore()
41
+ storage.enterWith({ noop: true })
42
+
39
43
  let startTime
40
44
  if (operationMetric) {
41
45
  incrementCountMetric(operationMetric.name, operationMetric.tags)
@@ -55,6 +59,8 @@ function sanitizedExec (
55
59
  }
56
60
  log.error(e)
57
61
  return ''
62
+ } finally {
63
+ storage.enterWith(store)
58
64
  }
59
65
  }
60
66
 
@@ -48,6 +48,12 @@ const TEST_MODULE_ID = 'test_module_id'
48
48
  const TEST_SUITE_ID = 'test_suite_id'
49
49
  const TEST_TOOLCHAIN = 'test.toolchain'
50
50
  const TEST_SKIPPED_BY_ITR = 'test.skipped_by_itr'
51
+ // Browser used in browser test. Namespaced by test.configuration because it affects the fingerprint
52
+ const TEST_CONFIGURATION_BROWSER_NAME = 'test.configuration.browser_name'
53
+ // Early flake detection
54
+ const TEST_IS_NEW = 'test.is_new'
55
+ const TEST_EARLY_FLAKE_IS_RETRY = 'test.early_flake.is_retry'
56
+ const TEST_EARLY_FLAKE_IS_ENABLED = 'test.early_flake.is_enabled'
51
57
 
52
58
  const CI_APP_ORIGIN = 'ciapp-test'
53
59
 
@@ -68,6 +74,10 @@ const TEST_CODE_COVERAGE_LINES_PCT = 'test.code_coverage.lines_pct'
68
74
  const JEST_WORKER_TRACE_PAYLOAD_CODE = 60
69
75
  const JEST_WORKER_COVERAGE_PAYLOAD_CODE = 61
70
76
 
77
+ // Early flake detection util strings
78
+ const EFD_STRING = "Retried by Datadog's Early Flake Detection"
79
+ const EFD_TEST_NAME_REGEX = new RegExp(EFD_STRING + ' \\(#\\d+\\): ', 'g')
80
+
71
81
  module.exports = {
72
82
  TEST_CODE_OWNERS,
73
83
  TEST_FRAMEWORK,
@@ -87,6 +97,10 @@ module.exports = {
87
97
  JEST_WORKER_COVERAGE_PAYLOAD_CODE,
88
98
  TEST_SOURCE_START,
89
99
  TEST_SKIPPED_BY_ITR,
100
+ TEST_CONFIGURATION_BROWSER_NAME,
101
+ TEST_IS_NEW,
102
+ TEST_EARLY_FLAKE_IS_RETRY,
103
+ TEST_EARLY_FLAKE_IS_ENABLED,
90
104
  getTestEnvironmentMetadata,
91
105
  getTestParametersString,
92
106
  finishAllTraceSpans,
@@ -121,7 +135,11 @@ module.exports = {
121
135
  getTestLineStart,
122
136
  getCallSites,
123
137
  removeInvalidMetadata,
124
- parseAnnotations
138
+ parseAnnotations,
139
+ EFD_STRING,
140
+ EFD_TEST_NAME_REGEX,
141
+ removeEfdStringFromTestName,
142
+ addEfdStringToTestName
125
143
  }
126
144
 
127
145
  // Returns pkg manager and its version, separated by '-', e.g. npm-8.15.0 or yarn-1.22.19
@@ -253,7 +271,6 @@ function getTestCommonTags (name, suite, version, testFramework) {
253
271
  [SAMPLING_PRIORITY]: AUTO_KEEP,
254
272
  [TEST_NAME]: name,
255
273
  [TEST_SUITE]: suite,
256
- [TEST_SOURCE_FILE]: suite,
257
274
  [RESOURCE_NAME]: `${suite}.${name}`,
258
275
  [TEST_FRAMEWORK_VERSION]: version,
259
276
  [LIBRARY_VERSION]: ddTraceVersion
@@ -281,16 +298,36 @@ const POSSIBLE_CODEOWNERS_LOCATIONS = [
281
298
  '.gitlab/CODEOWNERS'
282
299
  ]
283
300
 
284
- function getCodeOwnersFileEntries (rootDir = process.cwd()) {
285
- let codeOwnersContent
286
-
287
- POSSIBLE_CODEOWNERS_LOCATIONS.forEach(location => {
301
+ function readCodeOwners (rootDir) {
302
+ for (const location of POSSIBLE_CODEOWNERS_LOCATIONS) {
288
303
  try {
289
- codeOwnersContent = fs.readFileSync(`${rootDir}/${location}`).toString()
304
+ return fs.readFileSync(path.join(rootDir, location)).toString()
290
305
  } catch (e) {
291
306
  // retry with next path
292
307
  }
293
- })
308
+ }
309
+ return ''
310
+ }
311
+
312
+ function getCodeOwnersFileEntries (rootDir) {
313
+ let codeOwnersContent
314
+ let usedRootDir = rootDir
315
+ let isTriedCwd = false
316
+
317
+ const processCwd = process.cwd()
318
+
319
+ if (!usedRootDir || usedRootDir === processCwd) {
320
+ usedRootDir = processCwd
321
+ isTriedCwd = true
322
+ }
323
+
324
+ codeOwnersContent = readCodeOwners(usedRootDir)
325
+
326
+ // If we haven't found CODEOWNERS in the provided root dir, we try with process.cwd()
327
+ if (!codeOwnersContent && !isTriedCwd) {
328
+ codeOwnersContent = readCodeOwners(processCwd)
329
+ }
330
+
294
331
  if (!codeOwnersContent) {
295
332
  return null
296
333
  }
@@ -523,3 +560,11 @@ function parseAnnotations (annotations) {
523
560
  return tags
524
561
  }, {})
525
562
  }
563
+
564
+ function addEfdStringToTestName (testName, numAttempt) {
565
+ return `${EFD_STRING} (#${numAttempt}): ${testName}`
566
+ }
567
+
568
+ function removeEfdStringFromTestName (testName) {
569
+ return testName.replace(EFD_TEST_NAME_REGEX, '')
570
+ }