dd-trace 6.10.0 → 6.11.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 (137) hide show
  1. package/README.md +0 -17
  2. package/index.d.ts +108 -3
  3. package/initialize.mjs +28 -17
  4. package/openfeature.js +2 -2
  5. package/package.json +11 -8
  6. package/packages/datadog-esbuild/index.js +0 -34
  7. package/packages/datadog-instrumentations/src/anthropic.js +210 -15
  8. package/packages/datadog-instrumentations/src/aws-sdk.js +9 -1
  9. package/packages/datadog-instrumentations/src/browser-bunyan.js +11 -0
  10. package/packages/datadog-instrumentations/src/bunyan.js +3 -16
  11. package/packages/datadog-instrumentations/src/cucumber.js +26 -11
  12. package/packages/datadog-instrumentations/src/cypress-config.js +182 -16
  13. package/packages/datadog-instrumentations/src/cypress-legacy-finalizers.js +30 -0
  14. package/packages/datadog-instrumentations/src/cypress.js +2 -0
  15. package/packages/datadog-instrumentations/src/fastify.js +2 -0
  16. package/packages/datadog-instrumentations/src/helpers/bunyan.js +22 -0
  17. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -1
  18. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +9 -4
  19. package/packages/datadog-instrumentations/src/helpers/register.js +10 -8
  20. package/packages/datadog-instrumentations/src/jest.js +25 -17
  21. package/packages/datadog-instrumentations/src/mariadb.js +278 -21
  22. package/packages/datadog-instrumentations/src/mocha/main.js +539 -55
  23. package/packages/datadog-instrumentations/src/mocha/utils.js +130 -17
  24. package/packages/datadog-instrumentations/src/playwright-reporter.js +99 -0
  25. package/packages/datadog-instrumentations/src/playwright.js +149 -2
  26. package/packages/datadog-instrumentations/src/router.js +2 -0
  27. package/packages/datadog-instrumentations/src/vitest-main.js +11 -3
  28. package/packages/datadog-plugin-aws-sdk/src/base.js +1 -0
  29. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +163 -44
  30. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +2 -8
  31. package/packages/datadog-plugin-azure-functions/src/index.js +1 -1
  32. package/packages/datadog-plugin-browser-bunyan/src/index.js +9 -0
  33. package/packages/datadog-plugin-bunyan/src/index.js +5 -8
  34. package/packages/datadog-plugin-cucumber/src/index.js +11 -0
  35. package/packages/datadog-plugin-cypress/src/after-run.js +5 -1
  36. package/packages/datadog-plugin-cypress/src/after-spec.js +5 -1
  37. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +81 -29
  38. package/packages/datadog-plugin-cypress/src/finalization.js +76 -0
  39. package/packages/datadog-plugin-cypress/src/index.js +12 -6
  40. package/packages/datadog-plugin-cypress/src/plugin.js +16 -3
  41. package/packages/datadog-plugin-dd-trace-api/src/index.js +4 -0
  42. package/packages/datadog-plugin-electron/src/index.js +2 -0
  43. package/packages/datadog-plugin-fs/src/index.js +1 -1
  44. package/packages/datadog-plugin-jest/src/index.js +6 -0
  45. package/packages/datadog-plugin-mocha/src/index.js +56 -1
  46. package/packages/datadog-plugin-nats/src/index.js +1 -1
  47. package/packages/datadog-plugin-openai/src/tracing.js +41 -30
  48. package/packages/datadog-plugin-openai-agents/src/integration.js +1 -0
  49. package/packages/datadog-plugin-playwright/src/index.js +42 -13
  50. package/packages/datadog-plugin-vitest/src/index.js +6 -0
  51. package/packages/datadog-webpack/index.js +0 -11
  52. package/packages/dd-trace/index.js +0 -1
  53. package/packages/dd-trace/src/agent/info.js +6 -5
  54. package/packages/dd-trace/src/aiguard/client.js +100 -1
  55. package/packages/dd-trace/src/aiguard/errors.js +41 -0
  56. package/packages/dd-trace/src/aiguard/evaluation.js +357 -0
  57. package/packages/dd-trace/src/aiguard/integrations/anthropic.js +70 -0
  58. package/packages/dd-trace/src/aiguard/integrations/index.js +4 -0
  59. package/packages/dd-trace/src/aiguard/messages/anthropic.js +423 -0
  60. package/packages/dd-trace/src/aiguard/messages/openai.js +1 -24
  61. package/packages/dd-trace/src/aiguard/messages/utils.js +28 -0
  62. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +1 -11
  63. package/packages/dd-trace/src/aiguard/sdk.js +22 -278
  64. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +9 -12
  65. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +1 -0
  66. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +27 -3
  67. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +22 -6
  68. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +21 -5
  69. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +164 -0
  70. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +22 -6
  71. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +365 -32
  72. package/packages/dd-trace/src/ci-visibility/exporters/request.js +201 -0
  73. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +5 -3
  74. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +9 -5
  75. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -1
  76. package/packages/dd-trace/src/ci-visibility/requests/rate-limit.js +39 -0
  77. package/packages/dd-trace/src/ci-visibility/requests/request.js +3 -6
  78. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +6 -2
  79. package/packages/dd-trace/src/config/generated-config-types.d.ts +4 -0
  80. package/packages/dd-trace/src/config/index.js +13 -2
  81. package/packages/dd-trace/src/config/major-overrides.js +7 -0
  82. package/packages/dd-trace/src/config/supported-configurations.json +14 -0
  83. package/packages/dd-trace/src/datastreams/index.js +2 -0
  84. package/packages/dd-trace/src/datastreams/manager.js +9 -2
  85. package/packages/dd-trace/src/datastreams/processor.js +15 -13
  86. package/packages/dd-trace/src/datastreams/size.js +5 -0
  87. package/packages/dd-trace/src/debugger/devtools_client/index.js +3 -2
  88. package/packages/dd-trace/src/exporters/agent/writer.js +49 -9
  89. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +6 -4
  90. package/packages/dd-trace/src/exporters/common/writer.js +11 -6
  91. package/packages/dd-trace/src/llmobs/experiments/client.js +163 -5
  92. package/packages/dd-trace/src/llmobs/experiments/dataset.js +241 -87
  93. package/packages/dd-trace/src/llmobs/experiments/experiment.js +280 -20
  94. package/packages/dd-trace/src/llmobs/experiments/index.js +58 -59
  95. package/packages/dd-trace/src/llmobs/experiments/noop.js +71 -3
  96. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  97. package/packages/dd-trace/src/llmobs/experiments/util.js +41 -5
  98. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +12 -3
  99. package/packages/dd-trace/src/llmobs/plugins/base.js +4 -2
  100. package/packages/dd-trace/src/noop/proxy.js +3 -4
  101. package/packages/dd-trace/src/openfeature/flagging_provider.js +18 -2
  102. package/packages/dd-trace/src/openfeature/remote_config.js +1 -1
  103. package/packages/dd-trace/src/opentelemetry/context_manager.js +7 -7
  104. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +2 -1
  105. package/packages/dd-trace/src/opentelemetry/logs/logger.js +67 -11
  106. package/packages/dd-trace/src/opentelemetry/logs/otlp_transformer.js +14 -3
  107. package/packages/dd-trace/src/opentelemetry/metrics/index.js +25 -12
  108. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +2 -5
  109. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_transformer.js +96 -43
  110. package/packages/dd-trace/src/opentracing/tracer.js +5 -2
  111. package/packages/dd-trace/src/otel-thread-ctx.js +387 -0
  112. package/packages/dd-trace/src/plugin_manager.js +3 -3
  113. package/packages/dd-trace/src/plugins/ci_plugin.js +6 -1
  114. package/packages/dd-trace/src/plugins/index.js +1 -0
  115. package/packages/dd-trace/src/plugins/util/jest.js +4 -3
  116. package/packages/dd-trace/src/plugins/util/test.js +9 -9
  117. package/packages/dd-trace/src/plugins/util/web.js +3 -0
  118. package/packages/dd-trace/src/profiling/profiler.js +1 -1
  119. package/packages/dd-trace/src/profiling/profilers/space.js +1 -3
  120. package/packages/dd-trace/src/profiling/profilers/wall.js +37 -90
  121. package/packages/dd-trace/src/profiling/webspan-utils.js +45 -0
  122. package/packages/dd-trace/src/proxy.js +38 -29
  123. package/packages/dd-trace/src/ritm.js +9 -2
  124. package/packages/dd-trace/src/serverless.js +37 -0
  125. package/packages/dd-trace/src/span_format.js +1 -0
  126. package/packages/dd-trace/src/span_stats.js +30 -3
  127. package/packages/dd-trace/src/storage-channels.js +86 -0
  128. package/packages/dd-trace/src/tracer.js +9 -2
  129. package/packages/dd-trace/src/tracer_metadata.js +14 -1
  130. package/packages/dd-trace/src/web-tags-cache.js +132 -0
  131. package/vendor/dist/@datadog/openfeature-node-server/LICENSE +201 -0
  132. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -0
  133. package/packages/datadog-instrumentations/src/helpers/optional-peer-bundler.js +0 -65
  134. package/packages/datadog-webpack/src/optional-peer-loader.js +0 -17
  135. package/packages/dd-trace/src/feature-registry.js +0 -29
  136. package/packages/dd-trace/src/openfeature/register.js +0 -35
  137. package/packages/dd-trace/src/openfeature/require-provider.js +0 -18
@@ -1,13 +1,20 @@
1
1
  'use strict'
2
2
 
3
3
  const log = require('../../log')
4
+ const { ExternalExperiment } = require('./experiment')
5
+
6
+ const NOOP_EXPERIMENT_ID = '00000000-0000-0000-0000-000000000000'
7
+ const NOOP_SPAN_ID = '0000000000000000'
8
+ const NOOP_TRACE_ID = '00000000000000000000000000000000'
4
9
 
5
10
  class NoopDataset {
6
11
  #name
12
+ #description
7
13
  #records
8
14
 
9
15
  constructor (name = '', options = {}) {
10
16
  this.#name = name
17
+ this.#description = typeof options === 'string' ? options : (options.description ?? '')
11
18
  this.#records = (typeof options === 'string' ? [] : (options.records ?? [])).map(record => ({
12
19
  id: record.id ?? null,
13
20
  input: record.inputData,
@@ -21,6 +28,20 @@ class NoopDataset {
21
28
  return this
22
29
  }
23
30
 
31
+ update (index, fields) {
32
+ const record = this.#records[index]
33
+ if (record == null) return this
34
+ if (Object.hasOwn(fields, 'input')) record.input = fields.input
35
+ if (Object.hasOwn(fields, 'expectedOutput')) record.expectedOutput = fields.expectedOutput ?? null
36
+ if (Object.hasOwn(fields, 'metadata')) record.metadata = fields.metadata ?? {}
37
+ return this
38
+ }
39
+
40
+ delete (index) {
41
+ this.#records.splice(index, 1)
42
+ return this
43
+ }
44
+
24
45
  push () {
25
46
  return Promise.resolve({ pushedCount: 0, totalCount: 0 })
26
47
  }
@@ -29,6 +50,10 @@ class NoopDataset {
29
50
  return this.#name
30
51
  }
31
52
 
53
+ description () {
54
+ return this.#description
55
+ }
56
+
32
57
  id () {
33
58
  return null
34
59
  }
@@ -60,9 +85,11 @@ class NoopDataset {
60
85
 
61
86
  class NoopExperiment {
62
87
  #name
88
+ #external
63
89
 
64
- constructor (name = '') {
90
+ constructor (name = '', external = false) {
65
91
  this.#name = name
92
+ this.#external = external
66
93
  }
67
94
 
68
95
  name () {
@@ -70,7 +97,7 @@ class NoopExperiment {
70
97
  }
71
98
 
72
99
  experimentId () {
73
- return null
100
+ return this.#external ? NOOP_EXPERIMENT_ID : null
74
101
  }
75
102
 
76
103
  url () {
@@ -80,6 +107,32 @@ class NoopExperiment {
80
107
  run () {
81
108
  return Promise.resolve({ experimentId: null, rows: [], url: null })
82
109
  }
110
+
111
+ /**
112
+ * @returns {Promise<{experimentId: string, spanId: string, traceId: string, url: null}>}
113
+ */
114
+ submitSpan () {
115
+ return Promise.resolve({
116
+ experimentId: NOOP_EXPERIMENT_ID,
117
+ spanId: NOOP_SPAN_ID,
118
+ traceId: NOOP_TRACE_ID,
119
+ url: null,
120
+ })
121
+ }
122
+
123
+ /**
124
+ * @returns {Promise<void>}
125
+ */
126
+ submitEvaluationMetrics () {
127
+ return Promise.resolve()
128
+ }
129
+
130
+ /**
131
+ * @returns {Promise<void>}
132
+ */
133
+ close () {
134
+ return Promise.resolve()
135
+ }
83
136
  }
84
137
 
85
138
  // No-op Experiments used when LLM Observability is disabled or the API/APP keys
@@ -87,9 +140,11 @@ class NoopExperiment {
87
140
  // throwing, so intentionally disabled experiments remain graceful.
88
141
  class NoopExperiments {
89
142
  #reason
143
+ #startExperiment
90
144
 
91
- constructor (reason) {
145
+ constructor (reason, options = {}) {
92
146
  this.#reason = reason || 'LLMObs experiments are not available'
147
+ this.#startExperiment = options.startExperiment
93
148
  }
94
149
 
95
150
  #warn () {
@@ -110,6 +165,19 @@ class NoopExperiments {
110
165
  this.#warn()
111
166
  return new NoopExperiment(options.name)
112
167
  }
168
+
169
+ /**
170
+ * @param {object} options
171
+ * @returns {Promise<ExternalExperiment>}
172
+ */
173
+ startExperiment (options = {}) {
174
+ if (this.#startExperiment !== undefined && options.projectName) {
175
+ return this.#startExperiment(options)
176
+ }
177
+
178
+ this.#warn()
179
+ return Promise.resolve(new ExternalExperiment(new NoopExperiment(options.name, true)))
180
+ }
113
181
  }
114
182
 
115
183
  module.exports = NoopExperiments
@@ -13,6 +13,7 @@ class Row {
13
13
  this.expectedOutput = fields.expectedOutput
14
14
  this.errorType = fields.errorType
15
15
  this.errorMessage = fields.errorMessage
16
+ this.errorStack = fields.errorStack
16
17
  this.evaluations = fields.evaluations
17
18
  this.evaluationErrors = fields.evaluationErrors
18
19
  }
@@ -178,12 +178,12 @@ function buildTags (userTags, autoTags) {
178
178
  }
179
179
 
180
180
  /**
181
- * @param {Record<string, unknown> | undefined} userTags
182
- * @param {Record<string, unknown>} autoTags
181
+ * @param {Record<string, unknown> | undefined} baseTags
182
+ * @param {Record<string, unknown> | undefined} overrideTags
183
183
  * @returns {Record<string, unknown>}
184
184
  */
185
- function buildExperimentTagObject (userTags, autoTags) {
186
- return userTags ? { ...userTags, ...autoTags } : { ...autoTags }
185
+ function mergeTags (baseTags, overrideTags) {
186
+ return { ...baseTags, ...overrideTags }
187
187
  }
188
188
 
189
189
  /**
@@ -195,6 +195,40 @@ function sleep (ms) {
195
195
  return new Promise((resolve) => setTimeout(resolve, ms))
196
196
  }
197
197
 
198
+ /**
199
+ * @param {unknown} value
200
+ * @param {number} fallback
201
+ * @returns {number}
202
+ */
203
+ function timestampMs (value, fallback = Date.now()) {
204
+ if (value === null || value === undefined) return fallback
205
+ if (value instanceof Date) {
206
+ const timestamp = value.getTime()
207
+ return Number.isFinite(timestamp) ? timestamp : fallback
208
+ }
209
+ if (typeof value === 'number' && Number.isFinite(value)) return value
210
+ const parsed = Date.parse(String(value))
211
+ return Number.isFinite(parsed) ? parsed : fallback
212
+ }
213
+
214
+ /**
215
+ * @param {{ durationMs?: unknown, completedAt?: unknown }} row
216
+ * @param {number} startMs
217
+ * @returns {number}
218
+ */
219
+ function durationNs (row, startMs) {
220
+ if (typeof row.durationMs === 'number' && Number.isFinite(row.durationMs)) {
221
+ return Math.max(0, Math.round(row.durationMs * 1e6))
222
+ }
223
+
224
+ if (row.completedAt !== undefined) {
225
+ const completedMs = timestampMs(row.completedAt, startMs)
226
+ return Math.max(0, Math.round((completedMs - startMs) * 1e6))
227
+ }
228
+
229
+ return 0
230
+ }
231
+
198
232
  /**
199
233
  * @param {Record<string, unknown> | undefined} recordMetadata
200
234
  * @param {Record<string, unknown>} config
@@ -207,14 +241,16 @@ function buildSpanMetadata (recordMetadata, config) {
207
241
  }
208
242
 
209
243
  module.exports = {
210
- buildExperimentTagObject,
211
244
  buildSpanMetadata,
212
245
  buildTags,
246
+ durationNs,
213
247
  hasEntries,
214
248
  inferMetricType,
249
+ mergeTags,
215
250
  normalizeEvaluators,
216
251
  normalizeJsonMetricValue,
217
252
  sleep,
218
253
  stringify,
254
+ timestampMs,
219
255
  validateEvaluatorName,
220
256
  }
@@ -228,6 +228,7 @@ function getJsonStringValue (str, defaultValue) {
228
228
  function getModelMetadata (tags) {
229
229
  /** @type {Record<string, unknown>} */
230
230
  const modelMetadata = {}
231
+ let hasModelMetadata = false
231
232
  for (const tag of Object.keys(tags)) {
232
233
  const isModelMetadata = tag.startsWith(VERCEL_AI_MODEL_METADATA_PREFIX)
233
234
  if (isModelMetadata) {
@@ -235,6 +236,7 @@ function getModelMetadata (tags) {
235
236
  const metadataKey = lastCommaPosition === -1 ? tag : tag.slice(lastCommaPosition + 1)
236
237
  if (metadataKey && MODEL_METADATA_KEYS.has(metadataKey)) {
237
238
  modelMetadata[metadataKey] = tags[tag]
239
+ hasModelMetadata = true
238
240
  }
239
241
  } else {
240
242
  const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)
@@ -242,12 +244,13 @@ function getModelMetadata (tags) {
242
244
  const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
243
245
  if (metadataKey) {
244
246
  modelMetadata[metadataKey] = tags[tag]
247
+ hasModelMetadata = true
245
248
  }
246
249
  }
247
250
  }
248
251
  }
249
252
 
250
- return Object.keys(modelMetadata).length ? modelMetadata : null
253
+ return hasModelMetadata ? modelMetadata : null
251
254
  }
252
255
 
253
256
  /**
@@ -259,6 +262,7 @@ function getModelMetadata (tags) {
259
262
  function getGenerationMetadata (tags) {
260
263
  /** @type {Record<string, unknown>} */
261
264
  const metadata = {}
265
+ let hasMetadata = false
262
266
 
263
267
  for (const tag of Object.keys(tags)) {
264
268
  const isGenerationMetadata = tag.startsWith(VERCEL_AI_GENERATION_METADATA_PREFIX)
@@ -270,18 +274,20 @@ function getGenerationMetadata (tags) {
270
274
 
271
275
  const settingValue = tags[tag]
272
276
  metadata[settingKey] = settingValue
277
+ hasMetadata = true
273
278
  } else {
274
279
  const isTelemetryMetadata = tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)
275
280
  if (isTelemetryMetadata) {
276
281
  const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
277
282
  if (metadataKey) {
278
283
  metadata[metadataKey] = tags[tag]
284
+ hasMetadata = true
279
285
  }
280
286
  }
281
287
  }
282
288
  }
283
289
 
284
- return Object.keys(metadata).length ? metadata : null
290
+ return hasMetadata ? metadata : null
285
291
  }
286
292
 
287
293
  /**
@@ -307,6 +313,7 @@ function getGenerationMetadataFromEvent (event) {
307
313
  metadata[transformedKey] = value
308
314
  }
309
315
 
316
+ // eslint-disable-next-line no-restricted-syntax -- manual tracking would duplicate Object.assign semantics
310
317
  return Object.keys(metadata).length ? metadata : null
311
318
  }
312
319
 
@@ -432,6 +439,7 @@ function getLlmObsSpanName (operation, functionId) {
432
439
  */
433
440
  function getTelemetryMetadata (tags) {
434
441
  const metadata = {}
442
+ let hasMetadata = false
435
443
 
436
444
  for (const tag of Object.keys(tags)) {
437
445
  if (!tag.startsWith(VERCEL_AI_TELEMETRY_METADATA_PREFIX)) continue
@@ -439,10 +447,11 @@ function getTelemetryMetadata (tags) {
439
447
  const metadataKey = tag.slice(VERCEL_AI_TELEMETRY_METADATA_PREFIX.length)
440
448
  if (metadataKey) {
441
449
  metadata[metadataKey] = tags[tag]
450
+ hasMetadata = true
442
451
  }
443
452
  }
444
453
 
445
- return Object.keys(metadata).length ? metadata : null
454
+ return hasMetadata ? metadata : null
446
455
  }
447
456
 
448
457
  module.exports = {
@@ -84,9 +84,11 @@ class LLMObsPlugin extends TracingPlugin {
84
84
  }
85
85
 
86
86
  configure (config) {
87
- // we do not want to enable any LLMObs plugins if it is disabled on the tracer
87
+ // we do not want to enable any LLMObs plugins if it is disabled on the tracer, or if the
88
+ // integration opted out via `tracer.use(<name>, { llmobs: false })`. Opting out only disables
89
+ // the LLMObs layer: the integration keeps emitting APM spans and propagating trace context.
88
90
  const llmobsEnabled = this._tracerConfig.llmobs.DD_LLMOBS_ENABLED
89
- if (llmobsEnabled === false) {
91
+ if (llmobsEnabled === false || config?.llmobs === false) {
90
92
  config = typeof config === 'boolean' ? false : { ...config, enabled: false } // override to false
91
93
  }
92
94
  super.configure(config)
@@ -1,9 +1,9 @@
1
1
  'use strict'
2
2
 
3
- const { features } = require('../feature-registry')
4
3
  const NoopAppsecSdk = require('../appsec/sdk/noop')
5
4
  const NoopLLMObsSDK = require('../llmobs/noop')
6
5
  const NoopAIGuardSDK = require('../aiguard/noop')
6
+ const NoopFlaggingProvider = require('../openfeature/noop')
7
7
  const NoopDogStatsDClient = require('./dogstatsd')
8
8
  const NoopTracer = require('./tracer')
9
9
 
@@ -12,6 +12,7 @@ const noopAppsec = new NoopAppsecSdk()
12
12
  const noopDogStatsDClient = new NoopDogStatsDClient()
13
13
  const noopLLMObs = new NoopLLMObsSDK(noop)
14
14
  const noopAIGuard = new NoopAIGuardSDK()
15
+ const noopFlaggingProviderInstance = new NoopFlaggingProvider()
15
16
  const noopProfiling = {
16
17
  setCustomLabelKeys () {},
17
18
  runWithLabels (labels, fn) { return fn() },
@@ -25,9 +26,7 @@ class NoopProxy {
25
26
  this.dogstatsd = noopDogStatsDClient
26
27
  this.llmobs = noopLLMObs
27
28
  this.aiguard = noopAIGuard
28
- for (const { name, noop } of Object.values(features)) {
29
- this[name] = noop
30
- }
29
+ this.openfeature = noopFlaggingProviderInstance
31
30
  this.setBaggageItem = (key, value) => {}
32
31
  this.getBaggageItem = (key) => {}
33
32
  this.getAllBaggageItems = () => {}
@@ -1,14 +1,14 @@
1
1
  'use strict'
2
2
 
3
3
  const { channel } = require('dc-polyfill')
4
+
5
+ const { DatadogNodeServerProvider } = require('../../../../vendor/dist/@datadog/openfeature-node-server')
4
6
  const log = require('../log')
5
7
  const configurationSource = require('./configuration_source')
6
8
  const { EXPOSURE_CHANNEL } = require('./constants/constants')
7
9
  const EvalMetricsHook = require('./eval-metrics-hook')
8
10
  const SpanEnrichmentHook = require('./span-enrichment-hook')
9
11
 
10
- const { DatadogNodeServerProvider } = require('./require-provider')
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.
@@ -48,6 +48,22 @@ class FlaggingProvider extends DatadogNodeServerProvider {
48
48
  this.#configurationSource?.start()
49
49
  }
50
50
 
51
+ /**
52
+ * @param {import('@openfeature/core').EvaluationContext} [context]
53
+ * @returns {Promise<void>}
54
+ */
55
+ initialize (context) {
56
+ const promise = super.initialize(context)
57
+
58
+ // `DatadogNodeServerProvider#initialize` starts a timer that is never unref'd, which would
59
+ // otherwise keep an idle process (a short script, a serverless handler) alive for up to
60
+ // `initializationTimeoutMs` while waiting for configuration to arrive.
61
+ // TODO: remove once `@datadog/openfeature-node-server` unrefs this timer itself.
62
+ this.initController?.timeoutId?.unref?.()
63
+
64
+ return promise
65
+ }
66
+
51
67
  /**
52
68
  * Called when the provider is shut down.
53
69
  * Cleans up resources including channel subscriptions.
@@ -6,7 +6,7 @@ const RemoteConfigCapabilities = require('../remote_config/capabilities')
6
6
  * Configures remote config handlers for openfeature feature flagging
7
7
  *
8
8
  * @param {import('../remote_config')} rc - RemoteConfig instance
9
- * @param {() => import('./flagging_provider')} getOpenfeatureProxy
9
+ * @param {() => InstanceType<ReturnType<typeof import('./flagging_provider')>>} getOpenfeatureProxy
10
10
  * @param {boolean} subscribe - Whether Agent Remote Config owns UFC delivery
11
11
  */
12
12
  function enable (rc, getOpenfeatureProxy, subscribe) {
@@ -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) {
@@ -10,6 +10,8 @@ const MeterProvider = require('./meter_provider')
10
10
  const PeriodicMetricReader = require('./periodic_metric_reader')
11
11
  const OtlpHttpMetricExporter = require('./otlp_http_metric_exporter')
12
12
 
13
+ const RESERVED_TRACER_TAGS = new Set(['service', 'env', 'version', 'runtime_id', 'runtime-id'])
14
+
13
15
  /**
14
16
  * @typedef {import('../../config')} Config
15
17
  */
@@ -78,7 +80,16 @@ function initializeOpenTelemetryMetrics (config) {
78
80
  metrics.setGlobalMeterProvider(meterProvider)
79
81
  }
80
82
 
81
- function buildResourceAttributes (tags, { reportHostname, otelSemanticsEnabled, service, env, serviceVersion } = {}) {
83
+ /**
84
+ * @param {Record<string, unknown>} tags
85
+ * @param {object} [options]
86
+ * @param {boolean} [options.reportHostname]
87
+ * @param {string} [options.service]
88
+ * @param {string} [options.env]
89
+ * @param {string} [options.serviceVersion]
90
+ * @returns {import('@opentelemetry/api').Attributes}
91
+ */
92
+ function buildResourceAttributes (tags, { reportHostname, service, env, serviceVersion } = {}) {
82
93
  const attrs = {
83
94
  'telemetry.sdk.name': 'datadog',
84
95
  'telemetry.sdk.language': 'nodejs',
@@ -89,14 +100,19 @@ function buildResourceAttributes (tags, { reportHostname, otelSemanticsEnabled,
89
100
  if (env) attrs['deployment.environment.name'] = env
90
101
  if (reportHostname) attrs['host.name'] = os.hostname()
91
102
 
92
- if (!otelSemanticsEnabled) {
93
- if (tags?.['runtime-id']) attrs['datadog.runtime_id'] = tags['runtime-id']
94
- const processTagsObject = processTags.tagsObject
95
- if (processTagsObject) {
96
- for (const key of Object.keys(processTagsObject)) {
97
- attrs[`datadog.${key}`] = processTagsObject[key]
98
- }
99
- }
103
+ if (tags['runtime-id']) attrs['datadog.runtime_id'] = tags['runtime-id']
104
+ const tracerTags = []
105
+ for (const [key, value] of Object.entries(tags)) {
106
+ const valueType = typeof value
107
+ const supported = valueType === 'string' || valueType === 'boolean' ||
108
+ (valueType === 'number' && Number.isFinite(value))
109
+ if (!RESERVED_TRACER_TAGS.has(key) && supported) tracerTags.push(`${key}:${value}`)
110
+ }
111
+ if (tracerTags.length) attrs['datadog.tracer_tags'] = tracerTags
112
+ // Mirrors the legacy v0.6/stats ProcessTags shape (buildProcessTags().tagsArray); keep both in sync.
113
+ const processTagsArray = processTags.tagsArray
114
+ if (processTagsArray.length) {
115
+ attrs['datadog.process_tags'] = processTagsArray
100
116
  }
101
117
  return attrs
102
118
  }
@@ -106,7 +122,6 @@ function createOtlpSpanStatsExporter (config) {
106
122
  const protocol = config.OTEL_EXPORTER_OTLP_METRICS_PROTOCOL || 'http/json'
107
123
  const resourceAttributes = buildResourceAttributes(config.tags, {
108
124
  reportHostname: config.reportHostname,
109
- otelSemanticsEnabled: config.DD_TRACE_OTEL_SEMANTICS_ENABLED,
110
125
  service: config.service,
111
126
  env: config.env,
112
127
  serviceVersion: config.version,
@@ -115,8 +130,6 @@ function createOtlpSpanStatsExporter (config) {
115
130
  config.OTEL_EXPORTER_OTLP_METRICS_ENDPOINT,
116
131
  protocol,
117
132
  resourceAttributes,
118
- config.DD_TRACE_OTEL_SEMANTICS_ENABLED,
119
- config.service,
120
133
  config.OTEL_EXPORTER_OTLP_METRICS_HEADERS,
121
134
  config.OTEL_EXPORTER_OTLP_METRICS_TIMEOUT
122
135
  )
@@ -11,15 +11,12 @@ class OtlpStatsExporter extends OtlpHttpExporterBase {
11
11
  * @param {string} url
12
12
  * @param {string} protocol
13
13
  * @param {import('@opentelemetry/api').Attributes} resourceAttributes
14
- * @param {boolean} [otelSemanticsEnabled]
15
- * @param {string} [defaultService]
16
14
  * @param {Record<string, string>} [headers]
17
15
  * @param {number} [timeout]
18
16
  */
19
- constructor (url, protocol, resourceAttributes, otelSemanticsEnabled = false, defaultService = '',
20
- headers, timeout = 10_000) {
17
+ constructor (url, protocol, resourceAttributes, headers, timeout = 10_000) {
21
18
  super(url, headers, timeout, protocol, 'span-stats')
22
- this.#transformer = new OtlpStatsTransformer(resourceAttributes, protocol, otelSemanticsEnabled, defaultService)
19
+ this.#transformer = new OtlpStatsTransformer(resourceAttributes, protocol)
23
20
  }
24
21
 
25
22
  /**