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.
- package/LICENSE-3rdparty.csv +5 -0
- package/index.d.ts +20 -3
- package/package.json +18 -17
- package/packages/datadog-instrumentations/src/cucumber.js +4 -4
- package/packages/datadog-instrumentations/src/mocha/utils.js +5 -2
- package/packages/datadog-instrumentations/src/playwright-reporter.js +149 -1
- package/packages/datadog-instrumentations/src/playwright.js +33 -33
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +6 -8
- package/packages/datadog-plugin-playwright/src/index.js +4 -0
- package/packages/dd-trace/src/appsec/reporter.js +2 -0
- package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
- package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
- package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
- package/packages/dd-trace/src/carrier.js +107 -48
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
- package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +5 -3
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +4 -2
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +5 -3
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
- package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +4 -2
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
- package/packages/dd-trace/src/debugger/constants.js +1 -0
- package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -10
- package/packages/dd-trace/src/debugger/index.js +4 -2
- package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
- package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
- package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
- package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
- package/packages/dd-trace/src/evp_proxy/path.js +22 -0
- package/packages/dd-trace/src/exporters/common/request.js +5 -2
- package/packages/dd-trace/src/llmobs/experiments/client.js +6 -2
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +249 -11
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +23 -10
- package/packages/dd-trace/src/llmobs/experiments/index.js +18 -6
- package/packages/dd-trace/src/llmobs/experiments/noop.js +64 -4
- package/packages/dd-trace/src/llmobs/experiments/util.js +60 -4
- package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
- package/packages/dd-trace/src/openfeature/index.js +7 -4
- package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
- package/packages/dd-trace/src/openfeature/writers/exposures.js +62 -14
- package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +22 -13
- package/packages/dd-trace/src/telemetry/metrics.js +62 -8
- package/packages/dd-trace/src/telemetry/send-data.js +1 -0
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/https-proxy-agent/LICENSE +22 -0
- package/vendor/dist/https-proxy-agent/index.js +4 -0
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/proxy-from-env/LICENSE +20 -0
- 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
|
|
188
|
+
const tagsByKey = new Map()
|
|
169
189
|
if ((userTags) != null) {
|
|
170
190
|
for (const [key, value] of Object.entries(userTags)) {
|
|
171
|
-
|
|
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 !== '')
|
|
196
|
+
if (value !== undefined && value !== null && value !== '') tagsByKey.set(key, [`${key}:${value}`])
|
|
176
197
|
}
|
|
177
|
-
|
|
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 {
|
|
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
|
-
|
|
43
|
+
const writer = new ExposuresWriter(config)
|
|
44
|
+
exposuresWriter = writer
|
|
44
45
|
exposureSubmitCh.subscribe(_handleExposureSubmit)
|
|
45
46
|
flushCh.subscribe(_handleFlush)
|
|
46
47
|
|
|
47
|
-
|
|
48
|
-
exposuresWriter
|
|
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] -
|
|
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
|
-
*
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
*
|
|
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
|
|
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
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
12
|
+
|
|
13
|
+
let missingRouteWarningLogged = false
|
|
6
14
|
|
|
7
15
|
/**
|
|
8
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
883
|
-
const
|
|
884
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
899
|
+
} else {
|
|
900
|
+
tags[key] = value
|
|
894
901
|
}
|
|
895
|
-
|
|
902
|
+
|
|
903
|
+
start = end + 1
|
|
904
|
+
if (hasSeparator) separator = header.indexOf('=', start)
|
|
896
905
|
}
|
|
897
906
|
|
|
898
|
-
|
|
907
|
+
return tags
|
|
899
908
|
}
|
|
900
909
|
}
|
|
901
910
|
|