dd-trace 6.11.0 → 6.12.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 (55) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/index.d.ts +20 -3
  3. package/package.json +18 -17
  4. package/packages/datadog-instrumentations/src/cucumber.js +4 -4
  5. package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
  6. package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
  7. package/packages/datadog-instrumentations/src/playwright.js +33 -33
  8. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +6 -8
  9. package/packages/datadog-plugin-playwright/src/index.js +4 -0
  10. package/packages/dd-trace/src/appsec/reporter.js +2 -0
  11. package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
  12. package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
  13. package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
  14. package/packages/dd-trace/src/carrier.js +107 -48
  15. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
  16. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
  17. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
  18. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
  19. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
  20. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
  21. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  22. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
  23. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
  24. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
  25. package/packages/dd-trace/src/debugger/constants.js +1 -0
  26. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  27. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
  28. package/packages/dd-trace/src/debugger/index.js +4 -2
  29. package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
  30. package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
  31. package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
  32. package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
  33. package/packages/dd-trace/src/evp_proxy/path.js +22 -0
  34. package/packages/dd-trace/src/exporters/common/request.js +5 -2
  35. package/packages/dd-trace/src/llmobs/experiments/client.js +6 -2
  36. package/packages/dd-trace/src/llmobs/experiments/dataset.js +249 -11
  37. package/packages/dd-trace/src/llmobs/experiments/experiment.js +23 -10
  38. package/packages/dd-trace/src/llmobs/experiments/index.js +18 -6
  39. package/packages/dd-trace/src/llmobs/experiments/noop.js +64 -4
  40. package/packages/dd-trace/src/llmobs/experiments/util.js +60 -4
  41. package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
  42. package/packages/dd-trace/src/openfeature/index.js +7 -4
  43. package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
  44. package/packages/dd-trace/src/openfeature/writers/exposures.js +62 -14
  45. package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
  46. package/packages/dd-trace/src/opentracing/propagation/text_map.js +22 -13
  47. package/packages/dd-trace/src/telemetry/metrics.js +62 -8
  48. package/packages/dd-trace/src/telemetry/send-data.js +1 -0
  49. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  50. package/vendor/dist/https-proxy-agent/LICENSE +22 -0
  51. package/vendor/dist/https-proxy-agent/index.js +4 -0
  52. package/vendor/dist/protobufjs/index.js +1 -1
  53. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  54. package/vendor/dist/proxy-from-env/LICENSE +20 -0
  55. package/vendor/dist/proxy-from-env/index.js +1 -0
@@ -22,6 +22,26 @@ function hasEntries (value) {
22
22
  return false
23
23
  }
24
24
 
25
+ function validateTagsList (tags) {
26
+ if (tags == null) return []
27
+ if (!Array.isArray(tags)) throw new TypeError('Tags must be an array of strings')
28
+ for (const tag of tags) {
29
+ if (typeof tag !== 'string') throw new TypeError('Each tag must be a string')
30
+ if (tag.indexOf(':') <= 0) {
31
+ throw new Error(`Tag '${tag}' is malformed. Tags must be in 'key:value' format (e.g., 'env:prod').`)
32
+ }
33
+ }
34
+ return [...tags]
35
+ }
36
+
37
+ function tagOperationsAreEmpty (operations) {
38
+ return operations == null || (
39
+ !Object.hasOwn(operations, 'replace') &&
40
+ !Object.hasOwn(operations, 'add') &&
41
+ !Object.hasOwn(operations, 'remove')
42
+ )
43
+ }
44
+
25
45
  /**
26
46
  * @param {string} name
27
47
  */
@@ -165,16 +185,20 @@ function stringify (value) {
165
185
  * @returns {string[]}
166
186
  */
167
187
  function buildTags (userTags, autoTags) {
168
- const tags = new Map()
188
+ const tagsByKey = new Map()
169
189
  if ((userTags) != null) {
170
190
  for (const [key, value] of Object.entries(userTags)) {
171
- tags.set(key, `${key}:${value}`)
191
+ const values = Array.isArray(value) ? value : [value]
192
+ tagsByKey.set(key, values.map(item => `${key}:${item}`))
172
193
  }
173
194
  }
174
195
  for (const [key, value] of Object.entries(autoTags)) {
175
- if (value !== undefined && value !== null && value !== '') tags.set(key, `${key}:${value}`)
196
+ if (value !== undefined && value !== null && value !== '') tagsByKey.set(key, [`${key}:${value}`])
176
197
  }
177
- return [...tags.values()]
198
+
199
+ const tags = []
200
+ for (const values of tagsByKey.values()) tags.push(...values)
201
+ return tags
178
202
  }
179
203
 
180
204
  /**
@@ -186,6 +210,35 @@ function mergeTags (baseTags, overrideTags) {
186
210
  return { ...baseTags, ...overrideTags }
187
211
  }
188
212
 
213
+ /**
214
+ * @param {string[] | undefined} tags
215
+ * @returns {Record<string, string | string[]>}
216
+ */
217
+ function recordTagsToObject (tags) {
218
+ const result = {}
219
+ if (!Array.isArray(tags)) return result
220
+ for (const tag of tags) {
221
+ const separator = tag.indexOf(':')
222
+ if (separator <= 0) continue
223
+
224
+ const key = tag.slice(0, separator)
225
+ const value = tag.slice(separator + 1)
226
+ if (!Object.hasOwn(result, key)) {
227
+ Object.defineProperty(result, key, {
228
+ configurable: true,
229
+ enumerable: true,
230
+ value,
231
+ writable: true,
232
+ })
233
+ } else if (Array.isArray(result[key])) {
234
+ result[key].push(value)
235
+ } else {
236
+ result[key] = [result[key], value]
237
+ }
238
+ }
239
+ return result
240
+ }
241
+
189
242
  /**
190
243
  * @param {number} ms
191
244
  * @returns {Promise<void>}
@@ -249,8 +302,11 @@ module.exports = {
249
302
  mergeTags,
250
303
  normalizeEvaluators,
251
304
  normalizeJsonMetricValue,
305
+ recordTagsToObject,
252
306
  sleep,
253
307
  stringify,
308
+ tagOperationsAreEmpty,
254
309
  timestampMs,
255
310
  validateEvaluatorName,
311
+ validateTagsList,
256
312
  }
@@ -1,24 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  module.exports = {
4
- /**
5
- * @constant
6
- * @type {string} Base path for EVP proxy agent endpoint
7
- */
8
- EVP_PROXY_AGENT_BASE_PATH: '/evp_proxy/v2/',
9
-
10
- /**
11
- * @constant
12
- * @type {string} HTTP header name for EVP subdomain routing
13
- */
14
- EVP_SUBDOMAIN_HEADER_NAME: 'X-Datadog-EVP-Subdomain',
15
-
16
- /**
17
- * @constant
18
- * @type {string} EVP subdomain value for event platform intake
19
- */
20
- EVP_SUBDOMAIN_VALUE: 'event-platform-intake',
21
-
22
4
  /**
23
5
  * @constant
24
6
  * @type {string} API endpoint for exposure events EVP track
@@ -3,7 +3,7 @@
3
3
  const { channel } = require('dc-polyfill')
4
4
  const log = require('../log')
5
5
  const ExposuresWriter = require('./writers/exposures')
6
- const { setAgentStrategy } = require('./writers/util')
6
+ const { setExposureDeliveryStrategy } = require('./writers/util')
7
7
 
8
8
  const exposureSubmitCh = channel('ffe:exposure:submit')
9
9
  const flushCh = channel('ffe:writers:flush')
@@ -40,12 +40,15 @@ function enable (config) {
40
40
  return
41
41
  }
42
42
 
43
- exposuresWriter = new ExposuresWriter(config)
43
+ const writer = new ExposuresWriter(config)
44
+ exposuresWriter = writer
44
45
  exposureSubmitCh.subscribe(_handleExposureSubmit)
45
46
  flushCh.subscribe(_handleFlush)
46
47
 
47
- setAgentStrategy(config, hasAgent => {
48
- exposuresWriter?.setEnabled(hasAgent)
48
+ setExposureDeliveryStrategy(config, (enabled, route) => {
49
+ if (exposuresWriter !== writer) return
50
+
51
+ writer.setEnabled(enabled, route)
49
52
  })
50
53
  }
51
54
 
@@ -11,14 +11,52 @@ const log = require('../../log')
11
11
  * @property {number} [timeout] - Request timeout in milliseconds
12
12
  * @property {object} config - Tracer configuration object
13
13
  * @property {string} endpoint - API endpoint path
14
- * @property {URL} [agentUrl] - Base URL for the agent
14
+ * @property {URL} [agentUrl] - Initial delivery URL
15
15
  * @property {number} [payloadSizeLimit] - Maximum payload size in bytes
16
16
  * @property {number} [eventSizeLimit] - Maximum individual event size in bytes
17
17
  * @property {object} [headers] - Additional HTTP headers
18
18
  */
19
19
 
20
20
  /**
21
- * BaseFFEWriter is the base class for sending Feature Flagging & Exposure Events payloads to the Datadog Agent.
21
+ * @typedef {object} WriterRoute
22
+ * @property {URL} url - Route base URL
23
+ * @property {string} endpoint - Route endpoint
24
+ * @property {object} headers - Route-specific headers
25
+ * @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
26
+ */
27
+
28
+ /**
29
+ * @typedef {object} ActiveWriterRoute
30
+ * @property {URL} url - Route base URL
31
+ * @property {string} endpoint - Route endpoint
32
+ * @property {object} requestOptions - HTTP request options
33
+ */
34
+
35
+ /**
36
+ * Tests whether a local route definitively rejected an event batch.
37
+ *
38
+ * @param {Error | null} error - Request error
39
+ * @param {number | undefined} statusCode - HTTP response status
40
+ * @returns {boolean} Whether direct retry is safe
41
+ */
42
+ function isDefinitiveRejection (error, statusCode) {
43
+ return error?.code === 'EAI_AGAIN' || error?.code === 'ECONNREFUSED' ||
44
+ error?.code === 'ENOENT' || error?.code === 'ENOTFOUND' ||
45
+ statusCode === 403 || statusCode === 404 || statusCode === 405
46
+ }
47
+
48
+ /**
49
+ * Tests whether a local route can have accepted an event batch before failing.
50
+ *
51
+ * @param {Error | null} error - Request error
52
+ * @returns {boolean} Whether the delivery result is ambiguous
53
+ */
54
+ function isAmbiguousNetworkFailure (error) {
55
+ return error?.code === 'ECONNRESET' || error?.code === 'EPIPE' || error?.code === 'ETIMEDOUT'
56
+ }
57
+
58
+ /**
59
+ * Base writer for Feature Flagging and Experimentation event delivery.
22
60
  * @class BaseFFEWriter
23
61
  */
24
62
  class BaseFFEWriter {
@@ -40,6 +78,7 @@ class BaseFFEWriter {
40
78
  this._payloadSizeLimit = payloadSizeLimit
41
79
  this._eventSizeLimit = eventSizeLimit
42
80
  this._headers = headers || {}
81
+ this._fallbackRoute = undefined
43
82
 
44
83
  this._requestOptions = {
45
84
  headers: {
@@ -47,6 +86,7 @@ class BaseFFEWriter {
47
86
  'Content-Type': 'application/json',
48
87
  },
49
88
  method: 'POST',
89
+ retry: true,
50
90
  timeout: this._timeout,
51
91
  url: this._baseUrl,
52
92
  path: this._endpoint,
@@ -115,15 +155,8 @@ class BaseFFEWriter {
115
155
  // eslint-disable-next-line eslint-rules/eslint-log-printf-style
116
156
  log.debug(() => `${this.constructor.name} flushing payload: ${safeJSONStringify(payload)}`)
117
157
 
118
- request(payload, this._requestOptions, (err, resp, code) => {
119
- if (err) {
120
- log.error('Failed to send events to %s%s', this._baseUrl.href, this._endpoint, err)
121
- } else if (code >= 200 && code < 300) {
122
- log.debug('Successfully sent %d events', events.length)
123
- } else {
124
- log.warn('Events request returned status %d', code)
125
- }
126
- })
158
+ const route = this.#createActiveRoute()
159
+ this.#sendRequest(payload, events.length, route, this._fallbackRoute)
127
160
  }
128
161
 
129
162
  /**
@@ -161,6 +194,115 @@ class BaseFFEWriter {
161
194
  _encode (payload) {
162
195
  return JSON.stringify(payload)
163
196
  }
197
+
198
+ /**
199
+ * Applies the active route and an optional direct fallback route.
200
+ *
201
+ * @param {WriterRoute} route - Active route
202
+ * @param {WriterRoute} [fallbackRoute] - Direct fallback route
203
+ * @returns {void}
204
+ */
205
+ _setRoutes (route, fallbackRoute) {
206
+ this.#activateRoute(this.#createRoute(route))
207
+ this._fallbackRoute = fallbackRoute ? this.#createRoute(fallbackRoute) : undefined
208
+ }
209
+
210
+ /**
211
+ * Creates request state for a configured writer route.
212
+ *
213
+ * @param {WriterRoute} route - Configured route
214
+ * @returns {ActiveWriterRoute} Active route state
215
+ */
216
+ #createRoute (route) {
217
+ return {
218
+ url: route.url,
219
+ endpoint: route.endpoint,
220
+ requestOptions: {
221
+ ...(route.agent && { agent: route.agent }),
222
+ headers: {
223
+ ...route.headers,
224
+ 'Content-Type': 'application/json',
225
+ },
226
+ method: 'POST',
227
+ retry: true,
228
+ timeout: this._timeout,
229
+ url: route.url,
230
+ path: route.endpoint,
231
+ },
232
+ }
233
+ }
234
+
235
+ /**
236
+ * Captures the current route for one event batch.
237
+ *
238
+ * @returns {ActiveWriterRoute} Active route state
239
+ */
240
+ #createActiveRoute () {
241
+ return {
242
+ url: this._baseUrl,
243
+ endpoint: this._endpoint,
244
+ requestOptions: this._requestOptions,
245
+ }
246
+ }
247
+
248
+ /**
249
+ * Makes a route active for future event batches.
250
+ *
251
+ * @param {ActiveWriterRoute} route - Route state
252
+ * @returns {void}
253
+ */
254
+ #activateRoute (route) {
255
+ this._baseUrl = route.url
256
+ this._endpoint = route.endpoint
257
+ this._requestOptions = route.requestOptions
258
+ }
259
+
260
+ /**
261
+ * Sends an encoded batch and retries it through direct intake after a local route failure.
262
+ *
263
+ * @param {string} payload - Encoded event batch
264
+ * @param {number} eventCount - Event count
265
+ * @param {ActiveWriterRoute} route - Selected route
266
+ * @param {ActiveWriterRoute} [fallbackRoute] - Direct fallback route
267
+ * @returns {void}
268
+ */
269
+ #sendRequest (payload, eventCount, route, fallbackRoute) {
270
+ request(payload, route.requestOptions, (error, response, statusCode) => {
271
+ if (fallbackRoute && isDefinitiveRejection(error, statusCode)) {
272
+ log.debug(
273
+ '%s switching from %s%s to direct intake after definitive rejection',
274
+ this.constructor.name,
275
+ route.url.href,
276
+ route.endpoint
277
+ )
278
+ this.#activateRoute(fallbackRoute)
279
+ this._fallbackRoute = undefined
280
+ this.#sendRequest(payload, eventCount, fallbackRoute)
281
+ return
282
+ }
283
+
284
+ if (fallbackRoute && isAmbiguousNetworkFailure(error)) {
285
+ log.debug(
286
+ '%s retrying through direct intake and switching future batches from %s%s after ambiguous failure',
287
+ this.constructor.name,
288
+ route.url.href,
289
+ route.endpoint
290
+ )
291
+ this.#activateRoute(fallbackRoute)
292
+ this._fallbackRoute = undefined
293
+ this.#sendRequest(payload, eventCount, fallbackRoute)
294
+ return
295
+ }
296
+
297
+ if (error) {
298
+ log.error('Failed to send events to %s%s: %s', route.url.href, route.endpoint, error.message)
299
+ } else if (statusCode >= 200 && statusCode < 300) {
300
+ log.debug('Successfully sent %d events', eventCount)
301
+ } else {
302
+ log.warn('Events request returned status %d', statusCode)
303
+ }
304
+ })
305
+ }
164
306
  }
165
307
 
166
308
  module.exports = BaseFFEWriter
@@ -2,12 +2,15 @@
2
2
 
3
3
  const {
4
4
  EXPOSURES_ENDPOINT,
5
- EVP_PROXY_AGENT_BASE_PATH,
6
- EVP_SUBDOMAIN_HEADER_NAME,
7
- EVP_SUBDOMAIN_VALUE,
8
5
  EVP_PAYLOAD_SIZE_LIMIT,
9
6
  EVP_EVENT_SIZE_LIMIT,
10
7
  } = require('../constants/constants')
8
+ const {
9
+ EVP_EVENT_PLATFORM_SUBDOMAIN,
10
+ EVP_PROXY_PATH_V2,
11
+ EVP_SUBDOMAIN_HEADER_NAME,
12
+ } = require('../../evp_proxy/constants')
13
+ const { joinEVPProxyPath } = require('../../evp_proxy/path')
11
14
  const log = require('../../log')
12
15
  const BaseFFEWriter = require('./base')
13
16
 
@@ -16,6 +19,15 @@ const BaseFFEWriter = require('./base')
16
19
  // drop emits a warning and `droppedEventCount` accumulates the cumulative loss.
17
20
  const PENDING_MAX_EVENTS = 1000
18
21
 
22
+ /**
23
+ * @typedef {object} ExposureRoute
24
+ * @property {URL} url - Route base URL
25
+ * @property {string} basePath - EVP base path
26
+ * @property {object} [headers] - Route-specific headers
27
+ * @property {import('node:https').Agent} [agent] - Optional HTTPS proxy agent
28
+ * @property {ExposureRoute} [fallback] - Optional direct fallback route
29
+ */
30
+
19
31
  /**
20
32
  * @typedef {object} ExposureEvent
21
33
  * @property {number} timestamp - Unix timestamp in milliseconds
@@ -45,10 +57,10 @@ const PENDING_MAX_EVENTS = 1000
45
57
  */
46
58
 
47
59
  /**
48
- * ExposuresWriter is responsible for sending exposure events to the Datadog Agent.
60
+ * Sends exposure events through the selected local EVP proxy or direct intake route.
49
61
  */
50
62
  class ExposuresWriter extends BaseFFEWriter {
51
- // Disabled until the agent strategy probe resolves.
63
+ // Disabled until route selection resolves.
52
64
  #enabled = false
53
65
 
54
66
  /** @type {ExposureEvent[]} */
@@ -61,22 +73,27 @@ class ExposuresWriter extends BaseFFEWriter {
61
73
 
62
74
  /**
63
75
  * @param {import('../../config/config-base')} config - Tracer configuration object
76
+ * @param {ExposureRoute} [route] - Caller-supplied route
64
77
  */
65
- constructor (config) {
66
- const basePath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/$/, '')
67
- const endpoint = EXPOSURES_ENDPOINT.replace(/^\/+/, '')
68
- const fullEndpoint = `${basePath}/${endpoint}`
78
+ constructor (config, route) {
79
+ route ??= { url: config.url, basePath: EVP_PROXY_PATH_V2 }
80
+ const headers = route.headers ?? {
81
+ [EVP_SUBDOMAIN_HEADER_NAME]: EVP_EVENT_PLATFORM_SUBDOMAIN,
82
+ }
69
83
 
70
84
  super({
71
85
  config,
72
- endpoint: fullEndpoint,
86
+ agentUrl: route.url,
87
+ endpoint: joinEVPProxyPath(route.basePath, EXPOSURES_ENDPOINT),
73
88
  payloadSizeLimit: EVP_PAYLOAD_SIZE_LIMIT,
74
89
  eventSizeLimit: EVP_EVENT_SIZE_LIMIT,
75
- headers: {
76
- [EVP_SUBDOMAIN_HEADER_NAME]: EVP_SUBDOMAIN_VALUE,
77
- },
90
+ headers,
78
91
  })
79
92
 
93
+ if (route.agent || route.fallback) {
94
+ this.#setRoute({ ...route, headers })
95
+ }
96
+
80
97
  /** @type {ExposureContext} */
81
98
  const context = {
82
99
  service: config.service,
@@ -95,8 +112,14 @@ class ExposuresWriter extends BaseFFEWriter {
95
112
 
96
113
  /**
97
114
  * @param {boolean} enabled - Whether to enable the writer
115
+ * @param {ExposureRoute} [route] - Selected EVP route
116
+ * @returns {void}
98
117
  */
99
- setEnabled (enabled) {
118
+ setEnabled (enabled, route) {
119
+ if (route) {
120
+ this.#setRoute(route)
121
+ }
122
+
100
123
  this.#enabled = enabled
101
124
 
102
125
  if (enabled && this.#pendingEvents.length > 0) {
@@ -106,6 +129,31 @@ class ExposuresWriter extends BaseFFEWriter {
106
129
  }
107
130
  }
108
131
 
132
+ /**
133
+ * Applies caller-supplied route data without performing discovery.
134
+ *
135
+ * @param {ExposureRoute} route - Selected EVP route
136
+ * @returns {void}
137
+ */
138
+ #setRoute (route) {
139
+ const fallbackRoute = route.fallback && {
140
+ url: route.fallback.url,
141
+ endpoint: joinEVPProxyPath(route.fallback.basePath, EXPOSURES_ENDPOINT),
142
+ headers: route.fallback.headers ?? {},
143
+ agent: route.fallback.agent,
144
+ }
145
+ const headers = route.headers ?? {
146
+ [EVP_SUBDOMAIN_HEADER_NAME]: EVP_EVENT_PLATFORM_SUBDOMAIN,
147
+ }
148
+
149
+ this._setRoutes({
150
+ url: route.url,
151
+ endpoint: joinEVPProxyPath(route.basePath, EXPOSURES_ENDPOINT),
152
+ headers,
153
+ agent: route.agent,
154
+ }, fallbackRoute)
155
+ }
156
+
109
157
  /**
110
158
  * Appends exposure event(s) to the buffer
111
159
  * @param {ExposureEvent|ExposureEvent[]} events - Exposure event(s) to append
@@ -1,30 +1,51 @@
1
1
  'use strict'
2
2
 
3
+ const {
4
+ EVP_EVENT_PLATFORM_SUBDOMAIN,
5
+ EVP_PROXY_PATH_V2,
6
+ EVP_PROXY_PATH_V4,
7
+ EVP_SUBDOMAIN_HEADER_NAME,
8
+ } = require('../../evp_proxy/constants')
9
+ const { createDirectEVPRoute } = require('../../evp_proxy/direct')
10
+ const { discoverEVPProxy } = require('../../evp_proxy/discovery')
3
11
  const logger = require('../../log')
4
- const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/constants')
5
- const { fetchAgentInfo } = require('../../agent/info')
12
+
13
+ let missingRouteWarningLogged = false
6
14
 
7
15
  /**
8
- * Determines if the agent supports EVP proxy and sets the writer enabled state accordingly
16
+ * Logs the unavailable exposure-delivery warning once.
17
+ *
18
+ * @returns {void}
19
+ */
20
+ function warnExposureDeliveryUnavailable () {
21
+ if (missingRouteWarningLogged) return
22
+ missingRouteWarningLogged = true
23
+ logger.warn(
24
+ 'Feature Flags exposure delivery is disabled because no compatible local EVP route or direct intake ' +
25
+ 'credentials are available.'
26
+ )
27
+ }
28
+
29
+ /**
30
+ * Preserves Agent exposure delivery for the Remote Configuration source.
31
+ *
9
32
  * @param {import('../../config')} config - Tracer configuration object
10
33
  * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
34
+ * @returns {void}
11
35
  */
12
36
  function setAgentStrategy (config, setWriterEnabledValue) {
13
- fetchAgentInfo(config.url, (err, agentInfo) => {
14
- if (err) {
15
- logger.debug('FFE Writer disabled - error getting agent info:', err.message)
37
+ discoverEVPProxy(config.url, {
38
+ supportedPaths: [EVP_PROXY_PATH_V2],
39
+ }, (error, route) => {
40
+ if (error) {
41
+ logger.debug('FFE Writer disabled - error getting agent info: %s', error.message)
16
42
  setWriterEnabledValue(false)
17
43
  return
18
44
  }
19
45
 
20
- const endpoints = agentInfo.endpoints
21
- const normalizedPath = EVP_PROXY_AGENT_BASE_PATH.replace(/\/$/, '')
22
- const hasEndpoint = Array.isArray(endpoints) &&
23
- endpoints.includes(normalizedPath) || endpoints.includes(normalizedPath + '/')
24
-
25
- if (hasEndpoint) {
46
+ if (route) {
26
47
  logger.debug('FFE Writer enabled - agent has EVP proxy support')
27
- setWriterEnabledValue(true)
48
+ setWriterEnabledValue(true, route)
28
49
  } else {
29
50
  logger.debug('FFE Writer disabled - agent does not have EVP proxy support')
30
51
  setWriterEnabledValue(false)
@@ -32,6 +53,67 @@ function setAgentStrategy (config, setWriterEnabledValue) {
32
53
  })
33
54
  }
34
55
 
35
- module.exports = {
36
- setAgentStrategy,
56
+ /**
57
+ * Selects a local serverless receiver or authenticated direct intake.
58
+ *
59
+ * Local discovery is optional for delivery. A missing listener, discovery
60
+ * error, or incompatible receiver selects direct intake when credentials exist.
61
+ *
62
+ * @param {import('../../config')} config - Tracer configuration object
63
+ * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
64
+ * @returns {void}
65
+ */
66
+ function setAgentlessStrategy (config, setWriterEnabledValue) {
67
+ const directRoute = createDirectEVPRoute(config, EVP_EVENT_PLATFORM_SUBDOMAIN)
68
+
69
+ discoverEVPProxy(config.url, {
70
+ supportedPaths: [EVP_PROXY_PATH_V4, EVP_PROXY_PATH_V2],
71
+ }, (error, localRoute) => {
72
+ if (localRoute) {
73
+ const route = {
74
+ ...localRoute,
75
+ headers: {
76
+ [EVP_SUBDOMAIN_HEADER_NAME]: EVP_EVENT_PLATFORM_SUBDOMAIN,
77
+ },
78
+ ...(directRoute && { fallback: directRoute }),
79
+ }
80
+ logger.debug('FFE Writer enabled with local EVP route %s', route.basePath)
81
+ setWriterEnabledValue(true, route)
82
+ return
83
+ }
84
+
85
+ if (directRoute) {
86
+ if (error) {
87
+ logger.debug('FFE Writer using direct EVP intake after local discovery failed: %s', error.message)
88
+ } else {
89
+ logger.debug('FFE Writer using direct EVP intake because no compatible local route was advertised')
90
+ }
91
+ setWriterEnabledValue(true, directRoute)
92
+ return
93
+ }
94
+
95
+ if (error) {
96
+ logger.debug('FFE Writer disabled - error getting local receiver info: %s', error.message)
97
+ }
98
+ warnExposureDeliveryUnavailable()
99
+ setWriterEnabledValue(false)
100
+ })
37
101
  }
102
+
103
+ /**
104
+ * Applies the exposure-delivery strategy for the configured Feature Flags source.
105
+ *
106
+ * @param {import('../../config')} config - Tracer configuration object
107
+ * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
108
+ * @returns {void}
109
+ */
110
+ function setExposureDeliveryStrategy (config, setWriterEnabledValue) {
111
+ if (config.featureFlags?.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE === 'agentless') {
112
+ setAgentlessStrategy(config, setWriterEnabledValue)
113
+ return
114
+ }
115
+
116
+ setAgentStrategy(config, setWriterEnabledValue)
117
+ }
118
+
119
+ module.exports = { setExposureDeliveryStrategy }
@@ -543,7 +543,8 @@ class TextMapPropagator {
543
543
  this._extractOrigin(carrier, spanContext)
544
544
  this._extractLegacyBaggageItems(carrier, spanContext)
545
545
  this._extractSamplingPriority(carrier, spanContext)
546
- this._extractTags(carrier, spanContext)
546
+ const traceTags = this._extractTags(carrier)
547
+ if (traceTags) spanContext._trace.tags = traceTags
547
548
 
548
549
  if (this._config.DD_TRACE_PROPAGATION_EXTRACT_FIRST) return spanContext
549
550
 
@@ -865,37 +866,45 @@ class TextMapPropagator {
865
866
  }
866
867
  }
867
868
 
868
- _extractTags (carrier, spanContext) {
869
+ /**
870
+ * @param {Record<string, unknown>} carrier
871
+ * @returns {Record<string, string> | undefined}
872
+ */
873
+ _extractTags (carrier) {
869
874
  const header = readDatadogTags(carrier)
870
875
  if (!header) return
871
876
 
872
- const trace = spanContext._trace
873
-
874
877
  if (this._config.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH === 0) {
875
878
  log.debug('Trace tag propagation is disabled, skipping extraction.')
876
879
  } else if (header.length > this._config.DD_TRACE_X_DATADOG_TAGS_MAX_LENGTH) {
877
880
  log.error('Trace tags from carrier are too large, skipping extraction.')
878
881
  } else {
879
- const pairs = header.split(',')
880
882
  const tags = {}
883
+ let start = 0
884
+ let separator = header.indexOf('=')
881
885
 
882
- for (const pair of pairs) {
883
- const [key, ...rest] = pair.split('=')
884
- const value = rest.join('=')
886
+ while (start <= header.length) {
887
+ const comma = header.indexOf(',', start)
888
+ const end = comma === -1 ? header.length : comma
889
+ const hasSeparator = separator !== -1 && separator < end
890
+ const key = header.slice(start, hasSeparator ? separator : end)
891
+ const value = hasSeparator ? header.slice(separator + 1, end) : ''
885
892
 
886
893
  if (!this._validateTagKey(key) || !this._validateTagValue(value)) {
887
894
  log.error('Trace tags from carrier are invalid, skipping extraction.')
888
895
  return
889
896
  }
890
- // Check if value is a valid 16 character lower-case hexadecimal encoded number as per spec
891
- if (key === '_dd.p.tid' && !(hex16.test(value))) {
897
+ if (key === '_dd.p.tid' && !hex16.test(value)) {
892
898
  log.error('Invalid _dd.p.tid tag %s, skipping', value)
893
- continue
899
+ } else {
900
+ tags[key] = value
894
901
  }
895
- tags[key] = value
902
+
903
+ start = end + 1
904
+ if (hasSeparator) separator = header.indexOf('=', start)
896
905
  }
897
906
 
898
- Object.assign(trace.tags, tags)
907
+ return tags
899
908
  }
900
909
  }
901
910