dd-trace 6.3.0 → 6.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (207) hide show
  1. package/README.electron.md +7 -0
  2. package/README.md +17 -0
  3. package/ci/diagnose.js +2100 -0
  4. package/ci/init.js +23 -2
  5. package/ci/runbook.md +198 -0
  6. package/ci/test-optimization-validation/approval-artifacts.js +143 -0
  7. package/ci/test-optimization-validation/approval.js +299 -0
  8. package/ci/test-optimization-validation/artifact-id.js +20 -0
  9. package/ci/test-optimization-validation/bounded-json.js +121 -0
  10. package/ci/test-optimization-validation/ci-command-candidate.js +83 -0
  11. package/ci/test-optimization-validation/ci-discovery.js +181 -0
  12. package/ci/test-optimization-validation/ci-remediation.js +210 -0
  13. package/ci/test-optimization-validation/cli.js +903 -0
  14. package/ci/test-optimization-validation/command-blocker.js +81 -0
  15. package/ci/test-optimization-validation/command-output-policy.js +206 -0
  16. package/ci/test-optimization-validation/command-runner.js +707 -0
  17. package/ci/test-optimization-validation/command-suitability.js +471 -0
  18. package/ci/test-optimization-validation/executable-approval.js +48 -0
  19. package/ci/test-optimization-validation/executable.js +480 -0
  20. package/ci/test-optimization-validation/generated-file-policy.js +63 -0
  21. package/ci/test-optimization-validation/generated-files.js +351 -0
  22. package/ci/test-optimization-validation/generated-verifier.js +196 -0
  23. package/ci/test-optimization-validation/init-probe-preload.js +163 -0
  24. package/ci/test-optimization-validation/init-probe.js +246 -0
  25. package/ci/test-optimization-validation/late-initialization.js +63 -0
  26. package/ci/test-optimization-validation/local-command.js +163 -0
  27. package/ci/test-optimization-validation/manifest-loader.js +133 -0
  28. package/ci/test-optimization-validation/manifest-scaffold.js +738 -0
  29. package/ci/test-optimization-validation/manifest-schema.js +862 -0
  30. package/ci/test-optimization-validation/offline-fixtures.js +327 -0
  31. package/ci/test-optimization-validation/offline-output.js +406 -0
  32. package/ci/test-optimization-validation/payload-normalizer.js +72 -0
  33. package/ci/test-optimization-validation/plan-writer.js +1120 -0
  34. package/ci/test-optimization-validation/preflight-runner.js +114 -0
  35. package/ci/test-optimization-validation/redaction.js +331 -0
  36. package/ci/test-optimization-validation/report-writer.js +1508 -0
  37. package/ci/test-optimization-validation/safe-files.js +203 -0
  38. package/ci/test-optimization-validation/scenarios/auto-test-retries.js +159 -0
  39. package/ci/test-optimization-validation/scenarios/basic-reporting.js +657 -0
  40. package/ci/test-optimization-validation/scenarios/ci-wiring.js +780 -0
  41. package/ci/test-optimization-validation/scenarios/early-flake-detection.js +141 -0
  42. package/ci/test-optimization-validation/scenarios/helpers.js +539 -0
  43. package/ci/test-optimization-validation/scenarios/test-management.js +218 -0
  44. package/ci/test-optimization-validation/setup-runner.js +97 -0
  45. package/ci/test-optimization-validation/static-diagnosis.js +159 -0
  46. package/ci/test-optimization-validation/test-output.js +129 -0
  47. package/ci/test-optimization-validation-manifest.schema.json +1 -0
  48. package/ci/validate-test-optimization.js +3 -0
  49. package/ext/exporters.js +1 -0
  50. package/index.d.ts +121 -8
  51. package/index.electron.js +3 -0
  52. package/init.js +18 -0
  53. package/initialize.mjs +1 -1
  54. package/loader-hook.mjs +28 -18
  55. package/openfeature.d.ts +1 -0
  56. package/openfeature.js +4 -0
  57. package/package.json +15 -8
  58. package/packages/datadog-instrumentations/src/ai.js +37 -26
  59. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  60. package/packages/datadog-instrumentations/src/claude-agent-sdk.js +1 -1
  61. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +9 -3
  62. package/packages/datadog-instrumentations/src/cucumber.js +24 -10
  63. package/packages/datadog-instrumentations/src/cypress-config.js +398 -52
  64. package/packages/datadog-instrumentations/src/express.js +20 -2
  65. package/packages/datadog-instrumentations/src/fastify.js +7 -11
  66. package/packages/datadog-instrumentations/src/grpc/server.js +18 -0
  67. package/packages/datadog-instrumentations/src/helpers/hook.js +27 -6
  68. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +8 -8
  69. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +28 -2
  70. package/packages/datadog-instrumentations/src/helpers/router-helper.js +8 -8
  71. package/packages/datadog-instrumentations/src/http2/server.js +143 -17
  72. package/packages/datadog-instrumentations/src/jest.js +36 -4
  73. package/packages/datadog-instrumentations/src/mariadb.js +1 -1
  74. package/packages/datadog-instrumentations/src/mocha/main.js +0 -2
  75. package/packages/datadog-instrumentations/src/mocha/utils.js +0 -4
  76. package/packages/datadog-instrumentations/src/mongodb.js +3 -3
  77. package/packages/datadog-instrumentations/src/mongoose.js +3 -3
  78. package/packages/datadog-instrumentations/src/mquery.js +2 -2
  79. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  80. package/packages/datadog-instrumentations/src/next.js +24 -0
  81. package/packages/datadog-instrumentations/src/nyc.js +0 -2
  82. package/packages/datadog-instrumentations/src/oracledb.js +2 -2
  83. package/packages/datadog-instrumentations/src/pg.js +2 -2
  84. package/packages/datadog-instrumentations/src/playwright.js +159 -22
  85. package/packages/datadog-instrumentations/src/process.js +3 -0
  86. package/packages/datadog-instrumentations/src/router.js +191 -106
  87. package/packages/datadog-instrumentations/src/selenium.js +52 -26
  88. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +2 -2
  89. package/packages/datadog-instrumentations/src/vitest-main.js +0 -2
  90. package/packages/datadog-instrumentations/src/ws.js +2 -1
  91. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/context.js +2 -2
  92. package/packages/datadog-plugin-aws-durable-execution-sdk-js/src/util.js +2 -1
  93. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +41 -16
  94. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +21 -21
  95. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +14 -8
  96. package/packages/datadog-plugin-aws-sdk/src/util.js +2 -24
  97. package/packages/datadog-plugin-cucumber/src/index.js +1 -1
  98. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +24 -62
  99. package/packages/datadog-plugin-cypress/src/index.js +6 -1
  100. package/packages/datadog-plugin-cypress/src/support.js +92 -26
  101. package/packages/datadog-plugin-electron/src/ipc.js +1 -1
  102. package/packages/datadog-plugin-graphql/src/execute.js +177 -59
  103. package/packages/datadog-plugin-graphql/src/parse.js +22 -1
  104. package/packages/datadog-plugin-graphql/src/request.js +32 -1
  105. package/packages/datadog-plugin-graphql/src/utils.js +42 -0
  106. package/packages/datadog-plugin-graphql/src/validate.js +15 -3
  107. package/packages/datadog-plugin-http2/src/client.js +1 -1
  108. package/packages/datadog-plugin-http2/src/server.js +38 -6
  109. package/packages/datadog-plugin-jest/src/util.js +21 -6
  110. package/packages/datadog-plugin-langchain/src/handlers/embedding.js +2 -1
  111. package/packages/datadog-plugin-langchain/src/handlers/language_models.js +2 -1
  112. package/packages/datadog-plugin-langchain/src/tokens.js +2 -2
  113. package/packages/datadog-plugin-playwright/src/index.js +177 -64
  114. package/packages/datadog-plugin-router/src/index.js +11 -2
  115. package/packages/datadog-plugin-selenium/src/index.js +5 -36
  116. package/packages/datadog-plugin-vitest/src/index.js +4 -2
  117. package/packages/datadog-plugin-ws/src/close.js +2 -1
  118. package/packages/datadog-plugin-ws/src/producer.js +2 -1
  119. package/packages/datadog-plugin-ws/src/receiver.js +2 -1
  120. package/packages/datadog-plugin-ws/src/server.js +2 -1
  121. package/packages/dd-trace/index.electron.js +3 -0
  122. package/packages/dd-trace/index.js +2 -37
  123. package/packages/dd-trace/src/aiguard/sdk.js +1 -1
  124. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +2 -2
  125. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +3 -1
  126. package/packages/dd-trace/src/azure_metadata.js +2 -1
  127. package/packages/dd-trace/src/bootstrap.js +39 -0
  128. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +93 -28
  129. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
  130. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/index.js +160 -0
  131. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/msgpack-to-json.js +288 -0
  132. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/payload-projection.js +84 -0
  133. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/sink.js +369 -0
  134. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +60 -0
  135. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +55 -5
  136. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +22 -6
  137. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +5 -0
  138. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +62 -7
  139. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +9 -2
  140. package/packages/dd-trace/src/ci-visibility/requests/upload-coverage-report.js +6 -2
  141. package/packages/dd-trace/src/ci-visibility/rum.js +7 -0
  142. package/packages/dd-trace/src/ci-visibility/test-api-manual/test-api-manual-plugin.js +4 -0
  143. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
  144. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache-schema.js +137 -9
  145. package/packages/dd-trace/src/ci-visibility/test-optimization-http-cache.js +173 -9
  146. package/packages/dd-trace/src/ci-visibility/test-screenshot.js +90 -0
  147. package/packages/dd-trace/src/config/generated-config-types.d.ts +20 -0
  148. package/packages/dd-trace/src/config/helper.js +1 -0
  149. package/packages/dd-trace/src/config/index.js +10 -0
  150. package/packages/dd-trace/src/config/supported-configurations.json +83 -0
  151. package/packages/dd-trace/src/constants.js +7 -0
  152. package/packages/dd-trace/src/datastreams/pathway.js +6 -4
  153. package/packages/dd-trace/src/exporter.js +2 -0
  154. package/packages/dd-trace/src/exporters/common/client-library-headers.js +21 -0
  155. package/packages/dd-trace/src/exporters/common/request.js +59 -30
  156. package/packages/dd-trace/src/exporters/common/url.js +15 -1
  157. package/packages/dd-trace/src/feature-registry.js +29 -0
  158. package/packages/dd-trace/src/llmobs/constants/tags.js +1 -0
  159. package/packages/dd-trace/src/llmobs/experiments/client.js +113 -0
  160. package/packages/dd-trace/src/llmobs/experiments/dataset.js +154 -0
  161. package/packages/dd-trace/src/llmobs/experiments/experiment.js +283 -0
  162. package/packages/dd-trace/src/llmobs/experiments/index.js +152 -0
  163. package/packages/dd-trace/src/llmobs/experiments/noop.js +30 -0
  164. package/packages/dd-trace/src/llmobs/experiments/result.js +34 -0
  165. package/packages/dd-trace/src/llmobs/index.js +10 -2
  166. package/packages/dd-trace/src/llmobs/noop.js +6 -0
  167. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +20 -3
  168. package/packages/dd-trace/src/llmobs/plugins/anthropic/index.js +2 -2
  169. package/packages/dd-trace/src/llmobs/plugins/langgraph/index.js +1 -1
  170. package/packages/dd-trace/src/llmobs/plugins/openai/constants.js +8 -0
  171. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +48 -14
  172. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +43 -0
  173. package/packages/dd-trace/src/llmobs/sdk.js +17 -0
  174. package/packages/dd-trace/src/llmobs/tagger.js +79 -7
  175. package/packages/dd-trace/src/llmobs/telemetry.js +2 -1
  176. package/packages/dd-trace/src/llmobs/util.js +32 -0
  177. package/packages/dd-trace/src/noop/proxy.js +5 -4
  178. package/packages/dd-trace/src/openfeature/agentless_configuration_source.js +322 -0
  179. package/packages/dd-trace/src/openfeature/configuration_source.js +90 -0
  180. package/packages/dd-trace/src/openfeature/flagging_provider.js +14 -23
  181. package/packages/dd-trace/src/openfeature/index.js +0 -2
  182. package/packages/dd-trace/src/openfeature/noop.js +1 -28
  183. package/packages/dd-trace/src/openfeature/register.js +35 -0
  184. package/packages/dd-trace/src/openfeature/remote_config.js +15 -18
  185. package/packages/dd-trace/src/openfeature/require-provider.js +18 -0
  186. package/packages/dd-trace/src/opentelemetry/span-ending-hook.js +10 -0
  187. package/packages/dd-trace/src/opentelemetry/span.js +5 -0
  188. package/packages/dd-trace/src/opentracing/propagation/log.js +11 -2
  189. package/packages/dd-trace/src/opentracing/propagation/text_map.js +80 -14
  190. package/packages/dd-trace/src/opentracing/propagation/text_map_dsm.js +10 -1
  191. package/packages/dd-trace/src/opentracing/tracer.js +7 -1
  192. package/packages/dd-trace/src/plugins/ci_plugin.js +20 -6
  193. package/packages/dd-trace/src/plugins/util/git.js +3 -2
  194. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +29 -5
  195. package/packages/dd-trace/src/plugins/util/test.js +65 -8
  196. package/packages/dd-trace/src/plugins/util/web.js +12 -0
  197. package/packages/dd-trace/src/proxy.js +99 -10
  198. package/packages/dd-trace/src/span_processor.js +5 -0
  199. package/packages/dd-trace/src/standalone/index.js +0 -36
  200. package/packages/dd-trace/src/telemetry/send-data.js +2 -2
  201. package/packages/dd-trace/src/util.js +26 -0
  202. package/vendor/dist/@apm-js-collab/code-transformer/index.js +1 -1
  203. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  204. package/vendor/dist/protobufjs/index.js +1 -1
  205. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  206. package/vendor/dist/shell-quote/index.js +1 -1
  207. package/packages/datadog-instrumentations/src/helpers/require-optional-peer.js +0 -17
@@ -0,0 +1,322 @@
1
+ 'use strict'
2
+
3
+ /* eslint-disable no-await-in-loop -- Polls and retries must remain sequential. */
4
+
5
+ const { setTimeout: sleep } = require('node:timers/promises')
6
+
7
+ const request = require('../exporters/common/request')
8
+ const { getClientLibraryHeaders } = require('../exporters/common/client-library-headers')
9
+ const log = require('../log')
10
+
11
+ const MAX_ATTEMPTS = 3
12
+ const FIRST_RETRY_MIN_MS = 2000
13
+ const FIRST_RETRY_MAX_MS = 10_000
14
+ const SECOND_RETRY_MIN_MS = 5000
15
+ const SECOND_RETRY_MAX_MS = 30_000
16
+ const RETRY_JITTER = 0.2
17
+
18
+ /**
19
+ * @typedef {object} AgentlessSourceConfig
20
+ * @property {URL} endpoint
21
+ * @property {number} pollIntervalMs
22
+ * @property {number} requestTimeoutMs
23
+ * @property {string} apiKey
24
+ */
25
+
26
+ /**
27
+ * @typedef {import('@datadog/openfeature-node-server').UniversalFlagConfigurationV1} UniversalFlagConfiguration
28
+ */
29
+
30
+ /**
31
+ * @typedef {object} PollResponse
32
+ * @property {Error | null | undefined} error
33
+ * @property {string | undefined} body
34
+ * @property {number | undefined} statusCode
35
+ * @property {import('node:http').IncomingHttpHeaders | undefined} headers
36
+ */
37
+
38
+ class AgentlessConfigurationSource {
39
+ /** @type {AbortController | undefined} */
40
+ #abortController
41
+
42
+ /** @type {(configuration: UniversalFlagConfiguration) => void} */
43
+ #applyConfiguration
44
+
45
+ #applicationFailureLogged = false
46
+
47
+ /** @type {AgentlessSourceConfig} */
48
+ #config
49
+
50
+ /** @type {string | undefined} */
51
+ #etag
52
+
53
+ /** @type {Set<string>} */
54
+ #failureWarnings = new Set()
55
+
56
+ #malformedPayloadLogged = false
57
+
58
+ /**
59
+ * @param {AgentlessSourceConfig} config
60
+ * @param {(configuration: UniversalFlagConfiguration) => void} applyConfiguration
61
+ */
62
+ constructor (config, applyConfiguration) {
63
+ this.#config = config
64
+ this.#applyConfiguration = applyConfiguration
65
+ }
66
+
67
+ /**
68
+ * @returns {void}
69
+ */
70
+ start () {
71
+ if (this.#abortController) return
72
+
73
+ const abortController = new AbortController()
74
+ this.#abortController = abortController
75
+ this.#poll(abortController)
76
+ }
77
+
78
+ /**
79
+ * @returns {void}
80
+ */
81
+ stop () {
82
+ this.#abortController?.abort()
83
+ this.#abortController = undefined
84
+ }
85
+
86
+ /**
87
+ * @param {AbortController} abortController
88
+ * @returns {Promise<void>}
89
+ */
90
+ async #poll (abortController) {
91
+ try {
92
+ do {
93
+ await this.#pollOnce(abortController)
94
+ } while (
95
+ this.#abortController === abortController &&
96
+ await wait(this.#config.pollIntervalMs, abortController.signal)
97
+ )
98
+ } catch (error) {
99
+ if (this.#abortController === abortController) {
100
+ this.#warnFailure(undefined, error, 1)
101
+ }
102
+ } finally {
103
+ if (this.#abortController === abortController) {
104
+ this.#abortController = undefined
105
+ }
106
+ }
107
+ }
108
+
109
+ /**
110
+ * @param {AbortController} abortController
111
+ * @returns {Promise<void>}
112
+ */
113
+ async #pollOnce (abortController) {
114
+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
115
+ const response = await this.#request(abortController.signal)
116
+ if (this.#abortController !== abortController) return
117
+
118
+ const retryable = response.statusCode === undefined || isRetryableStatus(response.statusCode)
119
+ if (!retryable) {
120
+ this.#apply(response)
121
+ return
122
+ }
123
+
124
+ if (attempt === MAX_ATTEMPTS) {
125
+ this.#warnFailure(response.statusCode, response.error, attempt)
126
+ return
127
+ }
128
+
129
+ const delay = retryDelay(this.#config.pollIntervalMs, attempt, Math.random())
130
+ if (!await wait(delay, abortController.signal)) return
131
+ }
132
+ }
133
+
134
+ /**
135
+ * @param {AbortSignal} signal
136
+ * @returns {Promise<PollResponse>}
137
+ */
138
+ #request (signal) {
139
+ const headers = getClientLibraryHeaders()
140
+ headers['Accept-Encoding'] = 'gzip'
141
+ headers['DD-API-KEY'] = this.#config.apiKey
142
+ if (this.#etag) headers['If-None-Match'] = this.#etag
143
+
144
+ /**
145
+ * @param {(response: PollResponse) => void} resolve
146
+ */
147
+ const execute = (resolve) => {
148
+ /**
149
+ * @param {Error | null} error
150
+ * @param {string | import('node:http').IncomingMessage | null | undefined} body
151
+ * @param {number | undefined} statusCode
152
+ * @param {import('node:http').IncomingHttpHeaders | undefined} responseHeaders
153
+ */
154
+ const onResponse = (error, body, statusCode, responseHeaders) => {
155
+ resolve({
156
+ error,
157
+ body: typeof body === 'string' ? body : undefined,
158
+ statusCode,
159
+ headers: responseHeaders,
160
+ })
161
+ }
162
+
163
+ request('', {
164
+ url: this.#config.endpoint,
165
+ method: 'GET',
166
+ headers,
167
+ retry: false,
168
+ signal,
169
+ timeout: this.#config.requestTimeoutMs,
170
+ }, onResponse)
171
+ }
172
+
173
+ return new Promise(execute)
174
+ }
175
+
176
+ /**
177
+ * @param {PollResponse} response
178
+ * @returns {void}
179
+ */
180
+ #apply (response) {
181
+ const statusCode = response.statusCode
182
+ if (statusCode === 304) return
183
+
184
+ if (statusCode === 401 || statusCode === 403) {
185
+ this.#warnFailure(statusCode, undefined, 1)
186
+ return
187
+ }
188
+
189
+ if (statusCode !== 200) return
190
+
191
+ let configuration
192
+ try {
193
+ configuration = parseConfiguration(response.body)
194
+ } catch {
195
+ if (!this.#malformedPayloadLogged) {
196
+ this.#malformedPayloadLogged = true
197
+ log.error('Feature Flagging agentless endpoint returned malformed UFC payload')
198
+ }
199
+ return
200
+ }
201
+
202
+ try {
203
+ this.#applyConfiguration(configuration)
204
+ } catch (error) {
205
+ if (!this.#applicationFailureLogged) {
206
+ this.#applicationFailureLogged = true
207
+ log.warn('Feature Flagging agentless UFC payload could not be applied: %s', errorMessage(error))
208
+ }
209
+ return
210
+ }
211
+
212
+ const etag = response.headers?.etag
213
+ const value = Array.isArray(etag) ? etag[0] : etag
214
+ this.#etag = value?.trim() || undefined
215
+ }
216
+
217
+ /**
218
+ * @param {number | undefined} statusCode
219
+ * @param {unknown} error
220
+ * @param {number} attempts
221
+ * @returns {void}
222
+ */
223
+ #warnFailure (statusCode, error, attempts) {
224
+ const category = statusCode === 401 || statusCode === 403
225
+ ? 'authentication'
226
+ : statusCode ? 'http' : 'request'
227
+ if (this.#failureWarnings.has(category)) return
228
+ this.#failureWarnings.add(category)
229
+
230
+ if (statusCode === 401 || statusCode === 403) {
231
+ log.warn(
232
+ 'Feature Flagging agentless endpoint returned HTTP %d; verify DD_API_KEY is configured and valid',
233
+ statusCode
234
+ )
235
+ } else if (statusCode) {
236
+ log.warn('Feature Flagging agentless endpoint returned HTTP %d after %d attempts', statusCode, attempts)
237
+ } else if (attempts > 1) {
238
+ log.warn('Feature Flagging agentless request failed after %d attempts: %s', attempts, errorMessage(error))
239
+ } else {
240
+ log.warn('Feature Flagging agentless request failed: %s', errorMessage(error))
241
+ }
242
+ }
243
+ }
244
+
245
+ /**
246
+ * @param {number} delay
247
+ * @param {AbortSignal} signal
248
+ * @returns {Promise<boolean>}
249
+ */
250
+ async function wait (delay, signal) {
251
+ try {
252
+ await sleep(delay, undefined, { ref: false, signal })
253
+ return true
254
+ } catch (error) {
255
+ if (error?.name === 'AbortError') return false
256
+ throw error
257
+ }
258
+ }
259
+
260
+ /**
261
+ * @param {string | undefined} body
262
+ * @returns {UniversalFlagConfiguration}
263
+ */
264
+ function parseConfiguration (body) {
265
+ const { data } = JSON.parse(body)
266
+ if (data?.type !== 'universal-flag-configuration') {
267
+ throw new Error('Expected a JSON:API Universal Flag Configuration resource')
268
+ }
269
+
270
+ const { attributes } = data
271
+ if (typeof attributes?.format !== 'string' ||
272
+ typeof attributes.createdAt !== 'string' ||
273
+ typeof attributes.environment?.name !== 'string' ||
274
+ !attributes.flags ||
275
+ typeof attributes.flags !== 'object' ||
276
+ Array.isArray(attributes.flags)) {
277
+ throw new Error('Expected a Universal Flag Configuration v1 object')
278
+ }
279
+
280
+ return attributes
281
+ }
282
+
283
+ /**
284
+ * @param {unknown} error
285
+ * @returns {string}
286
+ */
287
+ function errorMessage (error) {
288
+ return error instanceof Error ? error.message : String(error ?? 'request was not sent')
289
+ }
290
+
291
+ /**
292
+ * @param {number | undefined} status
293
+ * @returns {boolean}
294
+ */
295
+ function isRetryableStatus (status) {
296
+ return status === 408 || status === 429 || (status >= 500 && status <= 599)
297
+ }
298
+
299
+ /**
300
+ * @param {number} pollIntervalMs
301
+ * @param {number} attempt
302
+ * @param {number} random
303
+ * @returns {number}
304
+ */
305
+ function retryDelay (pollIntervalMs, attempt, random) {
306
+ const base = attempt === 1
307
+ ? clamp(pollIntervalMs / 6, FIRST_RETRY_MIN_MS, FIRST_RETRY_MAX_MS)
308
+ : clamp(pollIntervalMs / 3, SECOND_RETRY_MIN_MS, SECOND_RETRY_MAX_MS)
309
+ return Math.max(1, Math.round(base * (1 - RETRY_JITTER + random * RETRY_JITTER * 2)))
310
+ }
311
+
312
+ /**
313
+ * @param {number} value
314
+ * @param {number} minimum
315
+ * @param {number} maximum
316
+ * @returns {number}
317
+ */
318
+ function clamp (value, minimum, maximum) {
319
+ return Math.max(minimum, Math.min(maximum, value))
320
+ }
321
+
322
+ module.exports = AgentlessConfigurationSource
@@ -0,0 +1,90 @@
1
+ 'use strict'
2
+
3
+ const { isLoopbackHost } = require('../exporters/common/url')
4
+ const log = require('../log')
5
+
6
+ const DEFAULT_AGENTLESS_PATH = '/api/v2/feature-flagging/config/rules-based/server'
7
+ const MAX_POLL_INTERVAL_SECONDS = 60 * 60
8
+
9
+ /**
10
+ * @typedef {import('@datadog/openfeature-node-server').UniversalFlagConfigurationV1} UniversalFlagConfiguration
11
+ */
12
+
13
+ /**
14
+ * @param {import('../config/config-base')} config
15
+ * @param {(configuration: UniversalFlagConfiguration) => void} applyConfiguration
16
+ */
17
+ function create (config, applyConfiguration) {
18
+ const {
19
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE: source,
20
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_BASE_URL: baseUrl,
21
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_POLL_INTERVAL_SECONDS: pollIntervalSeconds,
22
+ DD_FEATURE_FLAGS_CONFIGURATION_SOURCE_AGENTLESS_REQUEST_TIMEOUT_SECONDS: requestTimeoutSeconds,
23
+ DD_FEATURE_FLAGS_ENABLED: enabled,
24
+ } = config.featureFlags
25
+
26
+ if (!enabled || source !== 'agentless') {
27
+ return
28
+ }
29
+
30
+ try {
31
+ if (!config.DD_API_KEY) {
32
+ throw new Error('DD_API_KEY is required for Feature Flagging agentless delivery')
33
+ }
34
+
35
+ const AgentlessConfigurationSource = require('./agentless_configuration_source')
36
+ return new AgentlessConfigurationSource({
37
+ endpoint: endpoint(config, baseUrl),
38
+ pollIntervalMs: Math.min(pollIntervalSeconds, MAX_POLL_INTERVAL_SECONDS) * 1000,
39
+ requestTimeoutMs: requestTimeoutSeconds * 1000,
40
+ apiKey: config.DD_API_KEY,
41
+ }, applyConfiguration)
42
+ } catch (error) {
43
+ log.error('Unable to configure Feature Flagging configuration source', error)
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Builds the agentless rules-based server endpoint.
49
+ *
50
+ * A configured URL with a non-root path is treated as the exact endpoint. A
51
+ * configured origin (or root URL) receives the standard rules-based server
52
+ * path.
53
+ *
54
+ * @param {import('../config/config-base')} config - Tracer configuration.
55
+ * @param {string | undefined} configuredBaseUrl - Optional endpoint or origin override.
56
+ * @returns {URL} Agentless endpoint.
57
+ */
58
+ function endpoint (config, configuredBaseUrl) {
59
+ const configured = configuredBaseUrl?.trim()
60
+
61
+ if (!configured) {
62
+ const url = new URL(`https://ufc-server.ff-cdn.${config.site.toLowerCase()}${DEFAULT_AGENTLESS_PATH}`)
63
+ if (config.env) url.searchParams.set('dd_env', config.env)
64
+ return url
65
+ }
66
+
67
+ let url
68
+ try {
69
+ url = new URL(configured)
70
+ } catch {
71
+ throw new Error('Invalid Feature Flagging agentless URL')
72
+ }
73
+
74
+ if (url.protocol !== 'https:' && url.protocol !== 'http:') {
75
+ throw new Error('Feature Flagging agentless URL must use HTTP or HTTPS')
76
+ }
77
+ if (url.protocol === 'http:' && !isLoopbackHost(url.hostname)) {
78
+ throw new Error('Feature Flagging agentless URL must use HTTPS unless it targets loopback')
79
+ }
80
+
81
+ if (url.pathname === '' || url.pathname === '/') {
82
+ url.pathname = DEFAULT_AGENTLESS_PATH
83
+ }
84
+
85
+ return url
86
+ }
87
+
88
+ module.exports = {
89
+ create,
90
+ }
@@ -1,40 +1,40 @@
1
1
  'use strict'
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
- const requireOptionalPeer = require('../../../datadog-instrumentations/src/helpers/require-optional-peer')
5
4
  const log = require('../log')
5
+ const configurationSource = require('./configuration_source')
6
6
  const { EXPOSURE_CHANNEL } = require('./constants/constants')
7
7
  const EvalMetricsHook = require('./eval-metrics-hook')
8
8
  const SpanEnrichmentHook = require('./span-enrichment-hook')
9
9
 
10
- const { DatadogNodeServerProvider } = requireOptionalPeer('@datadog/openfeature-node-server')
10
+ const { DatadogNodeServerProvider } = require('./require-provider')
11
11
 
12
12
  /**
13
13
  * OpenFeature provider that integrates with Datadog's feature flagging system.
14
14
  * Extends DatadogNodeServerProvider to add tracer integration and configuration management.
15
15
  */
16
16
  class FlaggingProvider extends DatadogNodeServerProvider {
17
- /** @type {SpanEnrichmentHook?} */
17
+ /** @type {SpanEnrichmentHook | undefined} */
18
18
  #spanEnrichmentHook
19
19
 
20
+ /** @type {{ start: Function, stop: Function } | undefined} */
21
+ #configurationSource
22
+
20
23
  /**
21
24
  * @param {import('../tracer')} tracer - Datadog tracer instance
22
- * @param {import('../config')} config - Tracer configuration object
25
+ * @param {import('../config/config-base')} config - Tracer configuration object
23
26
  */
24
27
  constructor (tracer, config) {
25
- // Call parent constructor with required options and timeout
26
28
  super({
27
29
  exposureChannel: channel(EXPOSURE_CHANNEL),
28
30
  initializationTimeoutMs: config.experimental.flaggingProvider.initializationTimeoutMs,
29
31
  })
30
32
 
31
- this._tracer = tracer
32
- this._config = config
33
-
34
33
  this.hooks.push(new EvalMetricsHook(config))
35
34
 
36
35
  if (config.experimental.flaggingProvider.spanEnrichment?.enabled) {
37
36
  this.#spanEnrichmentHook = new SpanEnrichmentHook(tracer)
37
+ // @ts-expect-error The upstream constructor always initializes its optional hooks property.
38
38
  this.hooks.push(this.#spanEnrichmentHook)
39
39
  log.info('%s span enrichment enabled', this.constructor.name)
40
40
  } else {
@@ -43,6 +43,9 @@ class FlaggingProvider extends DatadogNodeServerProvider {
43
43
 
44
44
  log.debug('%s created with timeout: %dms', this.constructor.name,
45
45
  config.experimental.flaggingProvider.initializationTimeoutMs)
46
+
47
+ this.#configurationSource = configurationSource.create(config, this.setConfiguration.bind(this))
48
+ this.#configurationSource?.start()
46
49
  }
47
50
 
48
51
  /**
@@ -50,22 +53,10 @@ class FlaggingProvider extends DatadogNodeServerProvider {
50
53
  * Cleans up resources including channel subscriptions.
51
54
  */
52
55
  onClose () {
56
+ this.#configurationSource?.stop()
57
+ this.#configurationSource = undefined
53
58
  this.#spanEnrichmentHook?.destroy()
54
- }
55
-
56
- /**
57
- * Internal method to update flag configuration from Remote Config.
58
- * This method is called automatically when Remote Config delivers UFC updates.
59
- *
60
- * @internal
61
- * @param {import('@datadog/openfeature-node-server').UniversalFlagConfigurationV1} ufc
62
- * - Universal Flag Configuration object
63
- */
64
- _setConfiguration (ufc) {
65
- if (typeof this.setConfiguration === 'function') {
66
- this.setConfiguration(ufc)
67
- }
68
- log.debug('%s provider configuration updated', this.constructor.name)
59
+ this.#spanEnrichmentHook = undefined
69
60
  }
70
61
  }
71
62
 
@@ -47,8 +47,6 @@ function enable (config) {
47
47
  setAgentStrategy(config, hasAgent => {
48
48
  exposuresWriter?.setEnabled(hasAgent)
49
49
  })
50
-
51
- log.debug('OpenFeature module enabled')
52
50
  }
53
51
 
54
52
  /**
@@ -20,12 +20,7 @@ function resolveDefault (defaultValue) {
20
20
  * https://openfeature.dev/docs/reference/concepts/provider/
21
21
  */
22
22
  class NoopFlaggingProvider {
23
- /**
24
- * @param {object} [noopTracer] - Optional noop tracer instance
25
- */
26
- constructor (noopTracer) {
27
- this._tracer = noopTracer
28
- this._config = {}
23
+ constructor () {
29
24
  this.metadata = { name: 'NoopFlaggingProvider' }
30
25
  this.status = 'NOT_READY'
31
26
  this.runsOn = 'server'
@@ -78,28 +73,6 @@ class NoopFlaggingProvider {
78
73
  resolveObjectEvaluation (flagKey, defaultValue, context, logger) {
79
74
  return resolveDefault(defaultValue)
80
75
  }
81
-
82
- /**
83
- * @returns {object} Current configuration
84
- */
85
- getConfiguration () {
86
- return this._config
87
- }
88
-
89
- /**
90
- * @param {object} config - Configuration to set
91
- */
92
- setConfiguration (config) {
93
- this._config = config
94
- }
95
-
96
- /**
97
- * @internal
98
- * @param {object} ufc - Universal Flag Configuration object
99
- */
100
- _setConfiguration (ufc) {
101
- this.setConfiguration(ufc)
102
- }
103
76
  }
104
77
 
105
78
  module.exports = NoopFlaggingProvider
@@ -0,0 +1,35 @@
1
+ 'use strict'
2
+
3
+ const { registerFeature } = require('../feature-registry')
4
+
5
+ const noop = new (require('./noop'))()
6
+
7
+ /** @typedef {import('../proxy') & { openfeature: object }} OpenFeatureProxy */
8
+
9
+ registerFeature({
10
+ name: 'openfeature',
11
+ noop,
12
+ factory: () => require('./index'),
13
+ provider: () => require('./flagging_provider'),
14
+
15
+ /** @param {import('../config/config-base')} config */
16
+ isEnabled (config) {
17
+ return config.featureFlags.DD_FEATURE_FLAGS_ENABLED
18
+ },
19
+
20
+ /**
21
+ * @param {import('../remote_config')} rc - RemoteConfig instance
22
+ * @param {import('../config/config-base')} config
23
+ * @param {OpenFeatureProxy} proxy
24
+ */
25
+ remoteConfig (rc, config, proxy) {
26
+ const openfeatureRemoteConfig = require('./remote_config')
27
+ const subscribe = config.featureFlags.DD_FEATURE_FLAGS_ENABLED &&
28
+ config.featureFlags.DD_FEATURE_FLAGS_CONFIGURATION_SOURCE === 'remote_config'
29
+ openfeatureRemoteConfig.enable(
30
+ rc,
31
+ () => proxy.openfeature,
32
+ subscribe
33
+ )
34
+ },
35
+ })
@@ -5,30 +5,27 @@ const RemoteConfigCapabilities = require('../remote_config/capabilities')
5
5
  /**
6
6
  * Configures remote config handlers for openfeature feature flagging
7
7
  *
8
- * @param {object} rc - RemoteConfig instance
9
- * @param {object} config - Tracer config
10
- * @param {Function} getOpenfeatureProxy - Function that returns the OpenFeature proxy from tracer
8
+ * @param {import('../remote_config')} rc - RemoteConfig instance
9
+ * @param {() => import('./flagging_provider')} getOpenfeatureProxy
10
+ * @param {boolean} subscribe - Whether Agent Remote Config owns UFC delivery
11
11
  */
12
- function enable (rc, config, getOpenfeatureProxy) {
13
- // Always enable capability for feature flag configuration
14
- // This indicates the library supports this capability via remote config
15
- rc.updateCapabilities(RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, true)
12
+ function enable (rc, getOpenfeatureProxy, subscribe) {
13
+ if (!subscribe) return
16
14
 
17
- // Only register product handler if the experimental feature is enabled
18
- if (!config.experimental.flaggingProvider.enabled) return
15
+ rc.updateCapabilities(RemoteConfigCapabilities.FFE_FLAG_CONFIGURATION_RULES, true)
19
16
 
20
- // Set product handler for FFE_FLAGS
21
- rc.setProductHandler('FFE_FLAGS', (action, conf) => {
17
+ /**
18
+ * @param {string} action
19
+ * @param {import('@datadog/openfeature-node-server').UniversalFlagConfigurationV1} conf
20
+ */
21
+ const updateConfiguration = (action, conf) => {
22
22
  if (action === 'apply' || action === 'modify') {
23
- // Feed UFC config directly to OpenFeature provider
24
- getOpenfeatureProxy()._setConfiguration(conf)
23
+ getOpenfeatureProxy().setConfiguration(conf)
25
24
  } else if (action === 'unapply') {
26
- // Clear the configuration so evaluations return PROVIDER_NOT_READY,
27
- // consistent with Go and Python which also set config to null on RC deletion.
28
- // The evaluator returns PROVIDER_NOT_READY when config is null/undefined.
29
- getOpenfeatureProxy()._setConfiguration(null)
25
+ getOpenfeatureProxy().setConfiguration(undefined)
30
26
  }
31
- })
27
+ }
28
+ rc.setProductHandler('FFE_FLAGS', updateConfiguration)
32
29
  }
33
30
 
34
31
  module.exports = {
@@ -0,0 +1,18 @@
1
+ 'use strict'
2
+
3
+ /** @type {(request: string) => typeof import('@datadog/openfeature-node-server')} */
4
+ let requireOptionalPeer
5
+
6
+ // @ts-expect-error webpack exposes this escape hatch as a free variable.
7
+ // eslint-disable-next-line camelcase
8
+ if (typeof __non_webpack_require__ === 'function') {
9
+ // eslint-disable-next-line camelcase, no-undef
10
+ requireOptionalPeer = __non_webpack_require__
11
+ } else {
12
+ // nft recognizes createRequire through a binding named `module`.
13
+ const module = require('node:module')
14
+ const runtimeRequire = module.createRequire(__filename)
15
+ requireOptionalPeer = runtimeRequire
16
+ }
17
+
18
+ module.exports = requireOptionalPeer('@datadog/openfeature-node-server')
@@ -0,0 +1,10 @@
1
+ 'use strict'
2
+
3
+ // Pre-finish hook for OTel-bridge spans. `Span.end()` runs it before `finish()` formats and
4
+ // (synchronously, for the last span in a trace) exports the trace, so a framework instrumentation
5
+ // can rewrite the operation name / resource while the DD span is still unfinished; `onEnd` only sees
6
+ // the already-built payload. Its own dependency-free module so an instrumentation can register
7
+ // without loading the OTel bridge, and a plain holder so the caller gates on `hook` existing.
8
+
9
+ /** @type {{ hook: ((ddSpan: import('../opentracing/span')) => void) | undefined }} */
10
+ module.exports = { hook: undefined }
@@ -15,6 +15,7 @@ const kinds = require('../../../../ext/kinds')
15
15
  const id = require('../id')
16
16
  const BridgeSpanBase = require('./bridge-span-base')
17
17
  const SpanContext = require('./span_context')
18
+ const spanEndingHook = require('./span-ending-hook')
18
19
  const { setOtelOperationName, setOtelResource } = require('./span-helpers')
19
20
 
20
21
  const spanKindNames = {
@@ -255,6 +256,10 @@ class Span extends BridgeSpanBase {
255
256
  const hrEndTime = timeInputToHrTime(timeInput || (performance.now() + timeOrigin))
256
257
  const endTime = hrTimeToMilliseconds(hrEndTime)
257
258
 
259
+ // Must run before `finish()`, while the DD span is still unfinished. See span-ending-hook.js.
260
+ if (spanEndingHook.hook !== undefined) {
261
+ spanEndingHook.hook(this._ddSpan)
262
+ }
258
263
  this._ddSpan.finish(endTime)
259
264
  this._spanProcessor.onEnd(this)
260
265
  }