dd-trace 2.4.2 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/LICENSE-3rdparty.csv +1 -0
  2. package/ci/init.js +6 -0
  3. package/ci/jest/env.js +16 -3
  4. package/ext/exporters.d.ts +2 -1
  5. package/ext/exporters.js +2 -1
  6. package/package.json +3 -2
  7. package/packages/datadog-instrumentations/src/cypress.js +8 -0
  8. package/packages/datadog-plugin-aws-sdk/src/helpers.js +4 -4
  9. package/packages/datadog-plugin-aws-sdk/src/index.js +1 -1
  10. package/packages/datadog-plugin-cucumber/src/index.js +24 -12
  11. package/packages/datadog-plugin-cypress/src/index.js +10 -5
  12. package/packages/datadog-plugin-cypress/src/plugin.js +13 -1
  13. package/packages/datadog-plugin-mocha/src/index.js +10 -1
  14. package/packages/dd-trace/lib/version.js +1 -1
  15. package/packages/dd-trace/src/appsec/callbacks/ddwaf.js +3 -1
  16. package/packages/dd-trace/src/appsec/recommended.json +15 -5
  17. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +32 -0
  18. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +51 -0
  19. package/packages/dd-trace/src/config.js +8 -1
  20. package/packages/dd-trace/src/encode/0.4.js +0 -1
  21. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +193 -0
  22. package/packages/dd-trace/src/encode/tags-processors.js +116 -0
  23. package/packages/dd-trace/src/exporter.js +3 -0
  24. package/packages/dd-trace/src/exporters/agent/index.js +1 -1
  25. package/packages/dd-trace/src/exporters/agent/writer.js +7 -32
  26. package/packages/dd-trace/src/exporters/{agent → common}/docker.js +0 -0
  27. package/packages/dd-trace/src/exporters/common/request.js +83 -0
  28. package/packages/dd-trace/src/exporters/common/writer.js +36 -0
  29. package/packages/dd-trace/src/exporters/{agent/scheduler.js → scheduler.js} +0 -0
  30. package/packages/dd-trace/src/instrumenter.js +3 -0
  31. package/packages/dd-trace/src/pkg.js +11 -6
  32. package/packages/dd-trace/src/plugins/util/test.js +60 -1
  33. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  34. package/packages/dd-trace/src/proxy.js +2 -0
  35. package/packages/dd-trace/src/telemetry.js +187 -0
  36. package/packages/dd-trace/src/exporters/agent/request.js +0 -86
@@ -0,0 +1,193 @@
1
+ 'use strict'
2
+ const { truncateSpan, normalizeSpan } = require('./tags-processors')
3
+ const Chunk = require('./chunk')
4
+ const { AgentEncoder } = require('./0.4')
5
+
6
+ const ENCODING_VERSION = 1
7
+
8
+ function formatSpan (span) {
9
+ return {
10
+ type: span.type === 'test' ? 'test' : 'span',
11
+ version: ENCODING_VERSION,
12
+ content: normalizeSpan(truncateSpan(span))
13
+ }
14
+ }
15
+
16
+ class AgentlessCiVisibilityEncoder extends AgentEncoder {
17
+ constructor ({ runtimeId, service, env }) {
18
+ super(...arguments)
19
+ this._events = []
20
+ this.runtimeId = runtimeId
21
+ this.service = service
22
+ this.env = env
23
+ this._traceBytes = new Chunk()
24
+ this._stringBytes = new Chunk()
25
+ this._stringCount = 0
26
+ this._stringMap = {}
27
+
28
+ // Used to keep track of the number of encoded events to update the
29
+ // length of `payload.events` when calling `makePayload`
30
+ this._eventCount = 0
31
+
32
+ this.reset()
33
+ }
34
+
35
+ _encodeEventContent (bytes, content) {
36
+ this._encodeMapPrefix(bytes, content)
37
+ if (content.type) {
38
+ this._encodeString(bytes, 'type')
39
+ this._encodeString(bytes, content.type)
40
+ }
41
+ this._encodeString(bytes, 'trace_id')
42
+ this._encodeId(bytes, content.trace_id)
43
+ this._encodeString(bytes, 'span_id')
44
+ this._encodeId(bytes, content.span_id)
45
+ this._encodeString(bytes, 'parent_id')
46
+ this._encodeId(bytes, content.parent_id)
47
+ this._encodeString(bytes, 'name')
48
+ this._encodeString(bytes, content.name)
49
+ this._encodeString(bytes, 'resource')
50
+ this._encodeString(bytes, content.resource)
51
+ this._encodeString(bytes, 'service')
52
+ this._encodeString(bytes, content.service)
53
+ this._encodeString(bytes, 'error')
54
+ this._encodeNumber(bytes, content.error)
55
+ this._encodeString(bytes, 'start')
56
+ this._encodeNumber(bytes, content.start)
57
+ this._encodeString(bytes, 'duration')
58
+ this._encodeNumber(bytes, content.duration)
59
+ this._encodeString(bytes, 'meta')
60
+ this._encodeMap(bytes, content.meta)
61
+ this._encodeString(bytes, 'metrics')
62
+ this._encodeMap(bytes, content.metrics)
63
+ }
64
+
65
+ _encodeEvent (bytes, event) {
66
+ this._encodeMapPrefix(bytes, event)
67
+ this._encodeString(bytes, 'type')
68
+ this._encodeString(bytes, event.type)
69
+
70
+ this._encodeString(bytes, 'version')
71
+ this._encodeNumber(bytes, event.version)
72
+
73
+ this._encodeString(bytes, 'content')
74
+ this._encodeEventContent(bytes, event.content)
75
+ }
76
+
77
+ _encodeNumber (bytes, value) {
78
+ if (Math.floor(value) !== value) { // float 64
79
+ return this._encodeFloat(bytes, value)
80
+ }
81
+ return this._encodeLong(bytes, value)
82
+ }
83
+
84
+ _encodeLong (bytes, value) {
85
+ const isPositive = value >= 0
86
+
87
+ const hi = isPositive ? (value / Math.pow(2, 32)) >> 0 : Math.floor(value / Math.pow(2, 32))
88
+ const lo = value >>> 0
89
+ const flag = isPositive ? 0xcf : 0xd3
90
+
91
+ const buffer = bytes.buffer
92
+ const offset = bytes.length
93
+
94
+ // int 64
95
+ bytes.reserve(9)
96
+ bytes.length += 9
97
+
98
+ buffer[offset] = flag
99
+ buffer[offset + 1] = hi >> 24
100
+ buffer[offset + 2] = hi >> 16
101
+ buffer[offset + 3] = hi >> 8
102
+ buffer[offset + 4] = hi
103
+ buffer[offset + 5] = lo >> 24
104
+ buffer[offset + 6] = lo >> 16
105
+ buffer[offset + 7] = lo >> 8
106
+ buffer[offset + 8] = lo
107
+ }
108
+
109
+ _encodeMapPrefix (bytes, map) {
110
+ const keys = Object.keys(map)
111
+ const buffer = bytes.buffer
112
+ const offset = bytes.length
113
+
114
+ bytes.reserve(5)
115
+ bytes.length += 5
116
+ buffer[offset] = 0xdf
117
+ buffer[offset + 1] = keys.length >> 24
118
+ buffer[offset + 2] = keys.length >> 16
119
+ buffer[offset + 3] = keys.length >> 8
120
+ buffer[offset + 4] = keys.length
121
+ }
122
+
123
+ _encode (bytes, trace) {
124
+ this._eventCount += trace.length
125
+ const events = trace.map(formatSpan)
126
+
127
+ for (const event of events) {
128
+ this._encodeEvent(bytes, event)
129
+ }
130
+ }
131
+
132
+ makePayload () {
133
+ const bytes = this._traceBytes
134
+ const eventsOffset = this._eventsOffset
135
+ const eventsCount = this._eventCount
136
+
137
+ bytes.buffer[eventsOffset] = 0xdd
138
+ bytes.buffer[eventsOffset + 1] = eventsCount >> 24
139
+ bytes.buffer[eventsOffset + 2] = eventsCount >> 16
140
+ bytes.buffer[eventsOffset + 3] = eventsCount >> 8
141
+ bytes.buffer[eventsOffset + 4] = eventsCount
142
+
143
+ const traceSize = bytes.length
144
+ const buffer = Buffer.allocUnsafe(traceSize)
145
+
146
+ bytes.buffer.copy(buffer, 0, 0, bytes.length)
147
+
148
+ this.reset()
149
+
150
+ return buffer
151
+ }
152
+
153
+ _encodePayloadStart (bytes) {
154
+ // encodes the payload up to `events`. `events` will be encoded via _encode
155
+ const payload = {
156
+ version: ENCODING_VERSION,
157
+ metadata: {
158
+ '*': {
159
+ 'language': 'javascript'
160
+ }
161
+ },
162
+ events: []
163
+ }
164
+
165
+ if (this.env) {
166
+ payload.metadata['*'].env = this.env
167
+ }
168
+ if (this.runtimeId) {
169
+ payload.metadata['*']['runtime-id'] = this.runtimeId
170
+ }
171
+
172
+ this._encodeMapPrefix(bytes, payload)
173
+ this._encodeString(bytes, 'version')
174
+ this._encodeNumber(bytes, payload.version)
175
+ this._encodeString(bytes, 'metadata')
176
+ this._encodeMapPrefix(bytes, payload.metadata)
177
+ this._encodeString(bytes, '*')
178
+ this._encodeMap(bytes, payload.metadata['*'])
179
+ this._encodeString(bytes, 'events')
180
+ // Get offset of the events list to update the length of the array when calling `makePayload`
181
+ this._eventsOffset = bytes.length
182
+ bytes.reserve(5)
183
+ bytes.length += 5
184
+ }
185
+
186
+ reset () {
187
+ this._reset()
188
+ this._eventCount = 0
189
+ this._encodePayloadStart(this._traceBytes)
190
+ }
191
+ }
192
+
193
+ module.exports = { AgentlessCiVisibilityEncoder }
@@ -0,0 +1,116 @@
1
+ // From agent truncators: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/agent/truncator.go
2
+
3
+ // Values from: https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/traceutil/truncate.go#L22-L27
4
+ // MAX_RESOURCE_NAME_LENGTH the maximum length a span resource can have
5
+ const MAX_RESOURCE_NAME_LENGTH = 5000
6
+ // MAX_META_KEY_LENGTH the maximum length of metadata key
7
+ const MAX_META_KEY_LENGTH = 200
8
+ // MAX_META_VALUE_LENGTH the maximum length of metadata value
9
+ const MAX_META_VALUE_LENGTH = 25000
10
+ // MAX_METRIC_KEY_LENGTH the maximum length of a metric name key
11
+ const MAX_METRIC_KEY_LENGTH = MAX_META_KEY_LENGTH
12
+ // MAX_METRIC_VALUE_LENGTH the maximum length of a metric name value
13
+ const MAX_METRIC_VALUE_LENGTH = MAX_META_VALUE_LENGTH
14
+
15
+ // From agent normalizer:
16
+ // https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/traceutil/normalize.go
17
+ // DEFAULT_SPAN_NAME is the default name we assign a span if it's missing and we have no reasonable fallback
18
+ const DEFAULT_SPAN_NAME = 'unnamed_operation'
19
+ // DEFAULT_SERVICE_NAME is the default name we assign a service if it's missing and we have no reasonable fallback
20
+ const DEFAULT_SERVICE_NAME = 'unnamed-service'
21
+ // MAX_NAME_LENGTH the maximum length a name can have
22
+ const MAX_NAME_LENGTH = 100
23
+ // MAX_SERVICE_LENGTH the maximum length a service can have
24
+ const MAX_SERVICE_LENGTH = 100
25
+ // MAX_TYPE_LENGTH the maximum length a span type can have
26
+ const MAX_TYPE_LENGTH = 100
27
+
28
+ const fromEntries = Object.fromEntries || (entries =>
29
+ entries.reduce((obj, [k, v]) => Object.assign(obj, { [k]: v }), {}))
30
+
31
+ function truncateToLength (value, maxLength) {
32
+ if (!value) {
33
+ return value
34
+ }
35
+ if (value.length > maxLength) {
36
+ return `${value.slice(0, maxLength)}...`
37
+ }
38
+ return value
39
+ }
40
+
41
+ function truncateSpan (span) {
42
+ return fromEntries(Object.entries(span).map(([key, value]) => {
43
+ switch (key) {
44
+ case 'resource':
45
+ return ['resource', truncateToLength(value, MAX_RESOURCE_NAME_LENGTH)]
46
+ case 'meta':
47
+ return ['meta', fromEntries(Object.entries(value).map(([metaKey, metaValue]) =>
48
+ [truncateToLength(metaKey, MAX_META_KEY_LENGTH), truncateToLength(metaValue, MAX_META_VALUE_LENGTH)]
49
+ ))]
50
+ case 'metrics':
51
+ return ['metrics', fromEntries(Object.entries(value).map(([metricsKey, metricsValue]) =>
52
+ [truncateToLength(metricsKey, MAX_METRIC_KEY_LENGTH), truncateToLength(metricsValue, MAX_METRIC_VALUE_LENGTH)]
53
+ ))]
54
+ default:
55
+ return [key, value]
56
+ }
57
+ }))
58
+ }
59
+
60
+ function normalizeSpan (span) {
61
+ const normalizedSpan = fromEntries(Object.entries(span).map(([key, value]) => {
62
+ switch (key) {
63
+ case 'service':
64
+ if (!value) {
65
+ return [key, DEFAULT_SERVICE_NAME]
66
+ }
67
+ if (value.length > MAX_SERVICE_LENGTH) {
68
+ return [key, value.slice(0, MAX_SERVICE_LENGTH)]
69
+ }
70
+ break
71
+ case 'name':
72
+ if (!value) {
73
+ return [key, DEFAULT_SPAN_NAME]
74
+ }
75
+ if (value.length > MAX_NAME_LENGTH) {
76
+ return [key, value.slice(0, MAX_NAME_LENGTH)]
77
+ }
78
+ break
79
+ case 'resource':
80
+ if (!value) {
81
+ return [key, span.name || DEFAULT_SPAN_NAME]
82
+ }
83
+ break
84
+ case 'type':
85
+ if (!value) {
86
+ return [key, value]
87
+ }
88
+ if (value.length > MAX_TYPE_LENGTH) {
89
+ return [key, value.slice(0, MAX_TYPE_LENGTH)]
90
+ }
91
+ }
92
+ return [key, value]
93
+ }))
94
+ if (!normalizedSpan.service) {
95
+ normalizedSpan.service = DEFAULT_SERVICE_NAME
96
+ }
97
+ if (!normalizedSpan.name) {
98
+ normalizedSpan.name = DEFAULT_SPAN_NAME
99
+ }
100
+ return normalizedSpan
101
+ }
102
+
103
+ module.exports = {
104
+ truncateSpan,
105
+ normalizeSpan,
106
+ MAX_META_KEY_LENGTH,
107
+ MAX_META_VALUE_LENGTH,
108
+ MAX_METRIC_KEY_LENGTH,
109
+ MAX_METRIC_VALUE_LENGTH,
110
+ MAX_NAME_LENGTH,
111
+ MAX_SERVICE_LENGTH,
112
+ MAX_TYPE_LENGTH,
113
+ MAX_RESOURCE_NAME_LENGTH,
114
+ DEFAULT_SPAN_NAME,
115
+ DEFAULT_SERVICE_NAME
116
+ }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const AgentExporter = require('./exporters/agent')
4
4
  const LogExporter = require('./exporters/log')
5
+ const AgentlessCiVisibilityExporter = require('./ci-visibility/exporters/agentless')
5
6
  const exporters = require('../../../ext/exporters')
6
7
  const fs = require('fs')
7
8
  const constants = require('./constants')
@@ -15,6 +16,8 @@ module.exports = name => {
15
16
  return LogExporter
16
17
  case exporters.AGENT:
17
18
  return AgentExporter
19
+ case exporters.DATADOG:
20
+ return AgentlessCiVisibilityExporter
18
21
  default:
19
22
  return inAWSLambda && !usingLambdaExtension ? LogExporter : AgentExporter
20
23
  }
@@ -3,7 +3,7 @@
3
3
  const URL = require('url').URL
4
4
  const log = require('../../log')
5
5
  const Writer = require('./writer')
6
- const Scheduler = require('./scheduler')
6
+ const Scheduler = require('../scheduler')
7
7
 
8
8
  class AgentExporter {
9
9
  constructor ({ url, hostname, port, flushInterval, lookup, protocolVersion }, prioritySampler) {
@@ -1,28 +1,23 @@
1
1
  'use strict'
2
2
 
3
- const request = require('./request')
3
+ const request = require('../common/request')
4
4
  const { startupLog } = require('../../startup-log')
5
5
  const metrics = require('../../metrics')
6
6
  const log = require('../../log')
7
7
  const tracerVersion = require('../../../lib/version')
8
+ const BaseWriter = require('../common/writer')
8
9
 
9
10
  const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
10
11
 
11
- class Writer {
12
- constructor ({ url, prioritySampler, lookup, protocolVersion }) {
12
+ class Writer extends BaseWriter {
13
+ constructor ({ prioritySampler, lookup, protocolVersion }) {
14
+ super(...arguments)
13
15
  const AgentEncoder = getEncoder(protocolVersion)
14
16
 
15
- this._url = url
16
17
  this._prioritySampler = prioritySampler
17
18
  this._lookup = lookup
18
19
  this._protocolVersion = protocolVersion
19
- this._encoderForVersion = new AgentEncoder(this)
20
- }
21
-
22
- append (spans) {
23
- log.debug(() => `Encoding trace: ${JSON.stringify(spans)}`)
24
-
25
- this._encode(spans)
20
+ this._encoder = new AgentEncoder(this)
26
21
  }
27
22
 
28
23
  _sendPayload (data, count, done) {
@@ -62,26 +57,6 @@ class Writer {
62
57
  done()
63
58
  })
64
59
  }
65
-
66
- setUrl (url) {
67
- this._url = url
68
- }
69
-
70
- _encode (trace) {
71
- this._encoderForVersion.encode(trace)
72
- }
73
-
74
- flush (done = () => {}) {
75
- const count = this._encoderForVersion.count()
76
-
77
- if (count > 0) {
78
- const payload = this._encoderForVersion.makePayload()
79
-
80
- this._sendPayload(payload, count, done)
81
- } else {
82
- done()
83
- }
84
- }
85
60
  }
86
61
 
87
62
  function setHeader (headers, key, value) {
@@ -124,7 +99,7 @@ function makeRequest (version, data, count, url, lookup, needsStartupLog, cb) {
124
99
 
125
100
  log.debug(() => `Request to the agent: ${JSON.stringify(options)}`)
126
101
 
127
- request(Object.assign({ data }, options), (err, res, status) => {
102
+ request(data, options, true, (err, res, status) => {
128
103
  if (needsStartupLog) {
129
104
  // Note that logging will only happen once, regardless of how many times this is called.
130
105
  startupLog({
@@ -0,0 +1,83 @@
1
+ 'use strict'
2
+
3
+ const http = require('http')
4
+ const https = require('https')
5
+ const log = require('../../log')
6
+ const docker = require('./docker')
7
+ const { storage } = require('../../../../datadog-core')
8
+
9
+ const httpAgent = new http.Agent({ keepAlive: true })
10
+ const httpsAgent = new https.Agent({ keepAlive: true })
11
+ const containerId = docker.id()
12
+
13
+ function request (data, options, keepAlive, callback) {
14
+ if (!options.headers) {
15
+ options.headers = {}
16
+ }
17
+ const isSecure = options.protocol === 'https:'
18
+ const client = isSecure ? https : http
19
+ const dataArray = [].concat(data)
20
+ options.headers['Content-Length'] = byteLength(dataArray)
21
+
22
+ if (containerId) {
23
+ options.headers['Datadog-Container-ID'] = containerId
24
+ }
25
+
26
+ if (keepAlive) {
27
+ options.agent = isSecure ? httpsAgent : httpAgent
28
+ }
29
+
30
+ const firstRequest = retriableRequest(options, client, callback)
31
+ dataArray.forEach(buffer => firstRequest.write(buffer))
32
+
33
+ // The first request will be retried
34
+ const firstRequestErrorHandler = () => {
35
+ log.debug('Retrying request to the intake')
36
+ const retriedReq = retriableRequest(options, client, callback)
37
+ dataArray.forEach(buffer => retriedReq.write(buffer))
38
+ // The retried request will fail normally
39
+ retriedReq.on('error', e => callback(new Error(`Network error trying to reach the intake: ${e.message}`)))
40
+ retriedReq.end()
41
+ }
42
+
43
+ firstRequest.on('error', firstRequestErrorHandler)
44
+ firstRequest.end()
45
+
46
+ return firstRequest
47
+ }
48
+
49
+ function retriableRequest (options, client, callback) {
50
+ const store = storage.getStore()
51
+
52
+ storage.enterWith({ noop: true })
53
+
54
+ const timeout = options.timeout || 15000
55
+
56
+ const request = client.request(options, res => {
57
+ let responseData = ''
58
+
59
+ res.setTimeout(timeout)
60
+
61
+ res.on('data', chunk => { responseData += chunk })
62
+ res.on('end', () => {
63
+ if (res.statusCode >= 200 && res.statusCode <= 299) {
64
+ callback(null, responseData, res.statusCode)
65
+ } else {
66
+ const error = new Error(`Error from the endpoint: ${res.statusCode} ${http.STATUS_CODES[res.statusCode]}`)
67
+ error.status = res.statusCode
68
+
69
+ callback(error, null, res.statusCode)
70
+ }
71
+ })
72
+ })
73
+ request.setTimeout(timeout, request.abort)
74
+ storage.enterWith(store)
75
+
76
+ return request
77
+ }
78
+
79
+ function byteLength (data) {
80
+ return data.length > 0 ? data.reduce((prev, next) => prev + next.length, 0) : 0
81
+ }
82
+
83
+ module.exports = request
@@ -0,0 +1,36 @@
1
+ 'use strict'
2
+ const log = require('../../log')
3
+
4
+ class Writer {
5
+ constructor ({ url }) {
6
+ this._url = url
7
+ }
8
+
9
+ flush (done = () => {}) {
10
+ const count = this._encoder.count()
11
+
12
+ if (count > 0) {
13
+ const payload = this._encoder.makePayload()
14
+
15
+ this._sendPayload(payload, count, done)
16
+ } else {
17
+ done()
18
+ }
19
+ }
20
+
21
+ append (spans) {
22
+ log.debug(() => `Encoding trace: ${JSON.stringify(spans)}`)
23
+
24
+ this._encode(spans)
25
+ }
26
+
27
+ _encode (trace) {
28
+ this._encoder.encode(trace)
29
+ }
30
+
31
+ setUrl (url) {
32
+ this._url = url
33
+ }
34
+ }
35
+
36
+ module.exports = Writer
@@ -7,6 +7,7 @@ const Loader = require('./loader')
7
7
  const { isTrue } = require('./util')
8
8
  const plugins = require('./plugins')
9
9
  const Plugin = require('./plugins/plugin')
10
+ const telemetry = require('./telemetry')
10
11
 
11
12
  const disabledPlugins = process.env.DD_TRACE_DISABLED_PLUGINS
12
13
 
@@ -54,6 +55,7 @@ class Instrumenter {
54
55
 
55
56
  try {
56
57
  this._set(plugin, { name, config })
58
+ telemetry.updateIntegrations()
57
59
  } catch (e) {
58
60
  log.debug(`Could not find a plugin named "${name}".`)
59
61
  }
@@ -154,6 +156,7 @@ class Instrumenter {
154
156
 
155
157
  if (!instrumented) {
156
158
  this._instrumented.set(instrumentation, instrumented = new Set())
159
+ telemetry.updateIntegrations()
157
160
  }
158
161
 
159
162
  if (!instrumented.has(this._defaultExport(moduleExports))) {
@@ -11,7 +11,14 @@ function findRoot () {
11
11
 
12
12
  function findPkg () {
13
13
  const cwd = findRoot()
14
- const filePath = findUp('package.json', cwd)
14
+ const directory = path.resolve(cwd)
15
+ const res = path.parse(directory)
16
+
17
+ if (!res) return {}
18
+
19
+ const { root } = res
20
+
21
+ const filePath = findUp('package.json', root, directory)
15
22
 
16
23
  try {
17
24
  return JSON.parse(fs.readFileSync(filePath, 'utf8'))
@@ -20,18 +27,16 @@ function findPkg () {
20
27
  }
21
28
  }
22
29
 
23
- function findUp (name, cwd) {
24
- let directory = path.resolve(cwd)
25
- const { root } = path.parse(directory)
26
-
30
+ function findUp (name, root, directory) {
27
31
  while (true) {
28
32
  const current = path.resolve(directory, name)
29
33
 
30
34
  if (fs.existsSync(current)) return current
35
+
31
36
  if (directory === root) return
32
37
 
33
38
  directory = path.dirname(directory)
34
39
  }
35
40
  }
36
41
 
37
- module.exports = findPkg()
42
+ module.exports = Object.assign(findPkg(), { findRoot, findUp })
@@ -1,4 +1,7 @@
1
1
  const path = require('path')
2
+ const fs = require('fs')
3
+
4
+ const ignore = require('ignore')
2
5
 
3
6
  const { getGitMetadata } = require('./git')
4
7
  const { getUserProviderGitMetadata } = require('./user-provided-git')
@@ -25,6 +28,7 @@ const TEST_STATUS = 'test.status'
25
28
  const TEST_PARAMETERS = 'test.parameters'
26
29
  const TEST_SKIP_REASON = 'test.skip_reason'
27
30
  const TEST_IS_RUM_ACTIVE = 'test.is_rum_active'
31
+ const TEST_CODE_OWNERS = 'test.codeowners'
28
32
 
29
33
  const ERROR_TYPE = 'error.type'
30
34
  const ERROR_MESSAGE = 'error.msg'
@@ -35,6 +39,7 @@ const CI_APP_ORIGIN = 'ciapp-test'
35
39
  const JEST_TEST_RUNNER = 'test.jest.test_runner'
36
40
 
37
41
  module.exports = {
42
+ TEST_CODE_OWNERS,
38
43
  TEST_FRAMEWORK,
39
44
  TEST_FRAMEWORK_VERSION,
40
45
  JEST_TEST_RUNNER,
@@ -53,7 +58,9 @@ module.exports = {
53
58
  getTestParametersString,
54
59
  finishAllTraceSpans,
55
60
  getTestParentSpan,
56
- getTestSuitePath
61
+ getTestSuitePath,
62
+ getCodeOwnersFileEntries,
63
+ getCodeOwnersForFilename
57
64
  }
58
65
 
59
66
  function getTestEnvironmentMetadata (testFramework, config) {
@@ -140,3 +147,55 @@ function getTestSuitePath (testSuiteAbsolutePath, sourceRoot) {
140
147
 
141
148
  return testSuitePath.replace(path.sep, '/')
142
149
  }
150
+
151
+ const POSSIBLE_CODEOWNERS_LOCATIONS = [
152
+ 'CODEOWNERS',
153
+ '.github/CODEOWNERS',
154
+ 'docs/CODEOWNERS',
155
+ '.gitlab/CODEOWNERS'
156
+ ]
157
+
158
+ function getCodeOwnersFileEntries (rootDir = process.cwd()) {
159
+ let codeOwnersContent
160
+
161
+ POSSIBLE_CODEOWNERS_LOCATIONS.forEach(location => {
162
+ try {
163
+ codeOwnersContent = fs.readFileSync(`${rootDir}/${location}`).toString()
164
+ } catch (e) {
165
+ // retry with next path
166
+ }
167
+ })
168
+ if (!codeOwnersContent) {
169
+ return null
170
+ }
171
+
172
+ const entries = []
173
+ const lines = codeOwnersContent.split('\n')
174
+
175
+ for (const line of lines) {
176
+ const [content] = line.split('#')
177
+ const trimmed = content.trim()
178
+ if (trimmed === '') continue
179
+ const [pattern, ...owners] = trimmed.split(/\s+/)
180
+ entries.push({ pattern, owners })
181
+ }
182
+ // Reverse because rules defined last take precedence
183
+ return entries.reverse()
184
+ }
185
+
186
+ function getCodeOwnersForFilename (filename, entries) {
187
+ if (!entries) {
188
+ return null
189
+ }
190
+ for (const entry of entries) {
191
+ try {
192
+ const isResponsible = ignore().add(entry.pattern).ignores(filename)
193
+ if (isResponsible) {
194
+ return JSON.stringify(entry.owners)
195
+ }
196
+ } catch (e) {
197
+ return null
198
+ }
199
+ }
200
+ return null
201
+ }
@@ -5,7 +5,7 @@ const { request } = require('http')
5
5
  const FormData = require('form-data')
6
6
 
7
7
  // TODO: avoid using dd-trace internals. Make this a separate module?
8
- const docker = require('../../exporters/agent/docker')
8
+ const docker = require('../../exporters/common/docker')
9
9
  const version = require('../../../lib/version')
10
10
 
11
11
  const containerId = docker.id()