dd-trace 6.10.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 (173) hide show
  1. package/LICENSE-3rdparty.csv +5 -0
  2. package/README.md +0 -17
  3. package/index.d.ts +128 -6
  4. package/initialize.mjs +28 -17
  5. package/openfeature.js +2 -2
  6. package/package.json +27 -23
  7. package/packages/datadog-esbuild/index.js +0 -34
  8. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  10. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  11. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  12. package/packages/datadog-instrumentations/src/cucumber.js +30 -15
  13. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  14. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  15. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  16. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  17. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  18. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  19. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +9 -4
  20. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  21. package/packages/datadog-instrumentations/src/jest.js +25 -17
  22. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  23. package/packages/datadog-instrumentations/src/mocha/main.js +539 -55
  24. package/packages/datadog-instrumentations/src/mocha/utils.js +135 -19
  25. package/packages/datadog-instrumentations/src/playwright-reporter.js +247 -0
  26. package/packages/datadog-instrumentations/src/playwright.js +182 -35
  27. package/packages/datadog-instrumentations/src/router.js +2 -0
  28. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +6 -8
  29. package/packages/datadog-instrumentations/src/vitest-main.js +11 -3
  30. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  31. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  32. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -8
  33. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  34. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  35. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  36. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  37. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  38. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  39. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +81 -29
  40. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  41. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  42. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  43. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  44. package/packages/datadog-plugin-electron/src/index.js +2 -0
  45. package/packages/datadog-plugin-fs/src/index.js +1 -1
  46. package/packages/datadog-plugin-jest/src/index.js +6 -0
  47. package/packages/datadog-plugin-mocha/src/index.js +56 -1
  48. package/packages/datadog-plugin-nats/src/index.js +1 -1
  49. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  50. package/packages/datadog-plugin-openai-agents/src/integration.js +1 -0
  51. package/packages/datadog-plugin-playwright/src/index.js +46 -13
  52. package/packages/datadog-plugin-vitest/src/index.js +6 -0
  53. package/packages/datadog-webpack/index.js +0 -11
  54. package/packages/dd-trace/index.js +0 -1
  55. package/packages/dd-trace/src/agent/info.js +6 -5
  56. package/packages/dd-trace/src/aiguard/client.js +100 -1
  57. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  58. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  59. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  60. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  61. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  62. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  63. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  64. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  65. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  66. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  67. package/packages/dd-trace/src/appsec/reporter.js +2 -0
  68. package/packages/dd-trace/src/appsec/telemetry/index.js +13 -0
  69. package/packages/dd-trace/src/appsec/telemetry/rasp.js +16 -0
  70. package/packages/dd-trace/src/appsec/telemetry/waf.js +24 -0
  71. package/packages/dd-trace/src/carrier.js +107 -48
  72. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  73. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +5 -5
  74. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +29 -3
  75. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +27 -9
  76. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  77. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  78. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +26 -8
  79. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  80. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +6 -4
  81. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  82. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  83. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  84. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +7 -4
  85. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -2
  86. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  87. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  88. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +4 -2
  89. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +10 -4
  90. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +5 -5
  91. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  92. package/packages/dd-trace/src/config/index.js +13 -2
  93. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  94. package/packages/dd-trace/src/config/supported-configurations.json +14 -0
  95. package/packages/dd-trace/src/datastreams/index.js +2 -0
  96. package/packages/dd-trace/src/datastreams/manager.js +9 -2
  97. package/packages/dd-trace/src/datastreams/processor.js +15 -13
  98. package/packages/dd-trace/src/datastreams/size.js +5 -0
  99. package/packages/dd-trace/src/debugger/constants.js +1 -0
  100. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  101. package/packages/dd-trace/src/debugger/devtools_client/index.js +5 -11
  102. package/packages/dd-trace/src/debugger/index.js +4 -2
  103. package/packages/dd-trace/src/debugger/inspect-segment.js +154 -0
  104. package/packages/dd-trace/src/evp_proxy/constants.js +8 -0
  105. package/packages/dd-trace/src/evp_proxy/direct.js +64 -0
  106. package/packages/dd-trace/src/evp_proxy/discovery.js +133 -0
  107. package/packages/dd-trace/src/evp_proxy/path.js +22 -0
  108. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  109. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  110. package/packages/dd-trace/src/exporters/common/request.js +5 -2
  111. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  112. package/packages/dd-trace/src/llmobs/experiments/client.js +167 -5
  113. package/packages/dd-trace/src/llmobs/experiments/dataset.js +484 -92
  114. package/packages/dd-trace/src/llmobs/experiments/experiment.js +302 -29
  115. package/packages/dd-trace/src/llmobs/experiments/index.js +75 -64
  116. package/packages/dd-trace/src/llmobs/experiments/noop.js +135 -7
  117. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  118. package/packages/dd-trace/src/llmobs/experiments/util.js +101 -9
  119. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  120. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  121. package/packages/dd-trace/src/noop/proxy.js +3 -4
  122. package/packages/dd-trace/src/openfeature/constants/constants.js +0 -18
  123. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  124. package/packages/dd-trace/src/openfeature/index.js +7 -4
  125. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  126. package/packages/dd-trace/src/openfeature/writers/base.js +153 -11
  127. package/packages/dd-trace/src/openfeature/writers/exposures.js +62 -14
  128. package/packages/dd-trace/src/openfeature/writers/util.js +97 -15
  129. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  130. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  131. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  132. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  133. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  134. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  135. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  136. package/packages/dd-trace/src/opentracing/propagation/text_map.js +22 -13
  137. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  138. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  139. package/packages/dd-trace/src/plugin_manager.js +3 -3
  140. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  141. package/packages/dd-trace/src/plugins/index.js +1 -0
  142. package/packages/dd-trace/src/plugins/util/jest.js +4 -3
  143. package/packages/dd-trace/src/plugins/util/test.js +9 -9
  144. package/packages/dd-trace/src/plugins/util/web.js +3 -0
  145. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  146. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  147. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  148. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  149. package/packages/dd-trace/src/proxy.js +38 -29
  150. package/packages/dd-trace/src/ritm.js +9 -2
  151. package/packages/dd-trace/src/serverless.js +37 -0
  152. package/packages/dd-trace/src/span_format.js +1 -0
  153. package/packages/dd-trace/src/span_stats.js +30 -3
  154. package/packages/dd-trace/src/storage-channels.js +86 -0
  155. package/packages/dd-trace/src/telemetry/metrics.js +62 -8
  156. package/packages/dd-trace/src/telemetry/send-data.js +1 -0
  157. package/packages/dd-trace/src/tracer.js +9 -2
  158. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  159. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  160. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  161. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  162. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  163. package/vendor/dist/https-proxy-agent/LICENSE +22 -0
  164. package/vendor/dist/https-proxy-agent/index.js +4 -0
  165. package/vendor/dist/protobufjs/index.js +1 -1
  166. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  167. package/vendor/dist/proxy-from-env/LICENSE +20 -0
  168. package/vendor/dist/proxy-from-env/index.js +1 -0
  169. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  170. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  171. package/packages/dd-trace/src/feature-registry.js +0 -29
  172. package/packages/dd-trace/src/openfeature/register.js +0 -35
  173. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -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 }
@@ -21,14 +21,14 @@ class ContextManager {
21
21
  const storedSpan = store ? trace.getSpan(store) : null
22
22
 
23
23
  // Convert DD baggage to OTel format
24
- const baggages = getAllBaggageItems()
25
- const hasBaggage = Object.keys(baggages).length > 0
24
+ let entries
25
+ for (const [key, value] of Object.entries(getAllBaggageItems())) {
26
+ entries ??= {}
27
+ entries[key] = { value }
28
+ }
29
+
26
30
  let otelBaggages
27
- if (hasBaggage) {
28
- const entries = {}
29
- for (const [key, value] of Object.entries(baggages)) {
30
- entries[key] = { value }
31
- }
31
+ if (entries !== undefined) {
32
32
  otelBaggages = propagation.createBaggage(entries)
33
33
  }
34
34
 
@@ -42,7 +42,8 @@ class BatchLogRecordProcessor {
42
42
  */
43
43
  onEmit (logRecord, instrumentationScope) {
44
44
  // Store the log record (already enriched by Logger.emit)
45
- this.#logRecords.push({ ...logRecord, instrumentationScope })
45
+ logRecord.instrumentationScope = instrumentationScope
46
+ this.#logRecords.push(logRecord)
46
47
 
47
48
  if (this.#logRecords.length >= this.#maxExportBatchSize) {
48
49
  this.#export()
@@ -1,8 +1,65 @@
1
1
  'use strict'
2
2
 
3
3
  const { context } = require('@opentelemetry/api')
4
- const { sanitizeAttributes } = require('../../../../../vendor/dist/@opentelemetry/core')
4
+ const {
5
+ millisToHrTime,
6
+ sanitizeAttributes,
7
+ timeInputToHrTime,
8
+ } = require('../../../../../vendor/dist/@opentelemetry/core')
5
9
  const { VERSION: packageVersion } = require('../../../../../version')
10
+ const log = require('../../log')
11
+
12
+ const MAX_DATE_MILLISECONDS = 8.64e15
13
+ const NANOSECONDS_PER_SECOND = 1e9
14
+ const MAX_UINT64_SECONDS = 18_446_744_073
15
+ const MAX_UINT64_NANOSECONDS = 709_551_615
16
+ let invalidTimestampWarningLogged = false
17
+
18
+ function isValidHrTime (hrTime) {
19
+ return Array.isArray(hrTime) &&
20
+ hrTime.length === 2 &&
21
+ Number.isInteger(hrTime[0]) &&
22
+ Number.isInteger(hrTime[1]) &&
23
+ hrTime[0] >= 0 &&
24
+ hrTime[1] >= 0 &&
25
+ hrTime[1] < NANOSECONDS_PER_SECOND &&
26
+ (hrTime[0] < MAX_UINT64_SECONDS ||
27
+ (hrTime[0] === MAX_UINT64_SECONDS && hrTime[1] <= MAX_UINT64_NANOSECONDS))
28
+ }
29
+
30
+ function toHrTime (timestamp) {
31
+ if (typeof timestamp === 'number') {
32
+ if (!Number.isFinite(timestamp)) return
33
+
34
+ // Older versions documented numeric timestamps as Unix nanoseconds.
35
+ if (Math.abs(timestamp) > MAX_DATE_MILLISECONDS) {
36
+ const seconds = Math.trunc(timestamp / NANOSECONDS_PER_SECOND)
37
+ const hrTime = [seconds, timestamp - seconds * NANOSECONDS_PER_SECOND]
38
+ return isValidHrTime(hrTime) ? hrTime : undefined
39
+ }
40
+ }
41
+
42
+ try {
43
+ const hrTime = timeInputToHrTime(timestamp)
44
+ return isValidHrTime(hrTime) ? hrTime : undefined
45
+ } catch {
46
+ // Invalid timestamps fall back to the current time in normalizeTimestamp.
47
+ }
48
+ }
49
+
50
+ function normalizeTimestamp (timestamp) {
51
+ if (timestamp !== undefined) {
52
+ const hrTime = toHrTime(timestamp)
53
+ if (hrTime) return hrTime
54
+
55
+ if (!invalidTimestampWarningLogged) {
56
+ invalidTimestampWarningLogged = true
57
+ log.warn('Invalid OpenTelemetry log timestamp; using the current time instead')
58
+ }
59
+ }
60
+
61
+ return millisToHrTime(Date.now())
62
+ }
6
63
 
7
64
  /**
8
65
  * @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord
@@ -62,21 +119,20 @@ class Logger {
62
119
  return
63
120
  }
64
121
 
65
- if (logRecord.attributes) {
66
- logRecord.attributes = sanitizeAttributes(logRecord.attributes)
122
+ const record = {
123
+ ...logRecord,
124
+ timestamp: normalizeTimestamp(logRecord.timestamp),
125
+ context: logRecord.context || context.active(),
67
126
  }
68
127
 
69
- // Note: timestamp is in nanoseconds (as defined by OpenTelemetry LogRecord API)
70
- if (!logRecord.timestamp) {
71
- logRecord.timestamp = Number(process.hrtime.bigint())
128
+ if (logRecord.observedTimestamp !== undefined) {
129
+ record.observedTimestamp = normalizeTimestamp(logRecord.observedTimestamp)
72
130
  }
73
-
74
- if (!logRecord.context) {
75
- // Store span context in the log record context for trace correlation
76
- logRecord.context = context.active()
131
+ if (logRecord.attributes) {
132
+ record.attributes = sanitizeAttributes(logRecord.attributes)
77
133
  }
78
134
 
79
- this.loggerProvider.processor.onEmit(logRecord, this.#instrumentationScope)
135
+ this.loggerProvider.processor.onEmit(record, this.#instrumentationScope)
80
136
  }
81
137
  }
82
138
 
@@ -38,6 +38,17 @@ const SEVERITY_MAP = {
38
38
  [SeverityNumber.FATAL4]: 'SEVERITY_NUMBER_FATAL4',
39
39
  }
40
40
 
41
+ const NANOSECONDS_PER_SECOND = 1_000_000_000n
42
+
43
+ /**
44
+ * Converts an OpenTelemetry HrTime to an exact OTLP uint64 decimal string.
45
+ * @param {import('@opentelemetry/api').HrTime} hrTime
46
+ * @returns {string}
47
+ */
48
+ function encodeHrTime (hrTime) {
49
+ return (BigInt(hrTime[0]) * NANOSECONDS_PER_SECOND + BigInt(hrTime[1])).toString()
50
+ }
51
+
41
52
  /**
42
53
  * OtlpTransformer transforms log records to OTLP format.
43
54
  *
@@ -138,13 +149,13 @@ class OtlpTransformer extends OtlpTransformerBase {
138
149
  const spanContext = this.#extractSpanContext(logRecord.context)
139
150
 
140
151
  const result = {
141
- timeUnixNano: logRecord.timestamp,
152
+ timeUnixNano: encodeHrTime(logRecord.timestamp),
142
153
  body: this.#transformBody(logRecord.body),
143
154
  }
144
155
 
145
156
  // Add optional fields only if they are set
146
- if (logRecord.observedTimestamp) {
147
- result.observedTimeUnixNano = logRecord.observedTimestamp
157
+ if (logRecord.observedTimestamp !== undefined) {
158
+ result.observedTimeUnixNano = encodeHrTime(logRecord.observedTimestamp)
148
159
  }
149
160
 
150
161
  if (logRecord.severityNumber !== undefined) {